From 903662def5336ee6eabccc89169326d849acc2d3 Mon Sep 17 00:00:00 2001 From: ihmily <961532186@qq.com> Date: Sat, 9 Mar 2024 14:34:14 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9AAdded=20recording=20support=20for?= =?UTF-8?q?=20five=20new=20live=20streaming=20platforms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added recording support for five new live streaming platforms: winktv, flextv, look live, popkontv, and twitcasting. - Implemented configuration for overseas platform accounts and passwords, enabling automatic login and cookie updates for overseas platforms. - Added display name for overseas platforms as account nickname + account ID. - Introduced custom configuration for platforms requiring proxy recording. - Added option to only push live broadcast notifications without recording. --- README.md | 44 +- config/config.ini | 72 +-- main.py | 491 ++++++++++++++++----- requirements.txt | 3 +- spider.py | 1067 +++++++++++++++++++++++++++++++++------------ web_rid.py | 68 +-- 6 files changed, 1289 insertions(+), 456 deletions(-) diff --git a/README.md b/README.md index ea3a0fb..fc3f5c4 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ [![Docker Pulls](https://img.shields.io/docker/pulls/ihmily/douyin-live-recorder?label=Docker%20Pulls&color=blue&logo=docker)](https://hub.docker.com/r/ihmily/douyin-live-recorder/tags) ![GitHub issues](https://img.shields.io/github/issues/ihmily/DouyinLiveRecorder.svg) [![Latest Release](https://img.shields.io/github/v/release/ihmily/DouyinLiveRecorder)](https://github.com/ihmily/DouyinLiveRecorder/releases/latest) -![Downloads](https://img.shields.io/github/downloads/ihmily/DouyinLiveRecorder/total) +[![Downloads](https://img.shields.io/github/downloads/ihmily/DouyinLiveRecorder/total)](https://github.com/ihmily/DouyinLiveRecorder/releases/latest) -一款简易的可循环值守的直播录制工具,基于FFmpeg实现多平台直播源录制,支持自定义配置录制以及直播状态推送。 +一款**简易**的可循环值守的直播录制工具,基于FFmpeg实现多平台直播源录制,支持自定义配置录制以及直播状态推送。 @@ -27,8 +27,13 @@ - [x] AfreecaTV - [x] 网易cc - [x] 千度热播 -- [x] pandaTV +- [x] PandaTV - [x] 猫耳FM +- [x] Look直播 +- [x] WinkTV +- [x] FlexTV +- [x] PopkonTV +- [x] TwitCasting - [ ] 更多平台正在更新中 @@ -38,7 +43,6 @@ ``` . └── DouyinLiveRecorder/ - ├── /api -> (get live stream api ) ├── /config -> (config record) ├── /logs -> (save runing log file) ├── /backup_config -> (backup file) @@ -76,7 +80,7 @@ - 如果要长时间挂着软件循环监测直播,最好循环时间设置长一点(咱也不差没录制到的那几分钟),避免因请求频繁导致被官方封禁IP 。 - 要停止直播录制,使用`Ctrl+C ` 或直接关闭程序即可。 -- 最后,欢迎右上角给本项目一个star,同时也非常乐意大家提交pr(请先询问我,避免做无用功)。 +- 最后,欢迎右上角给本项目一个star,同时也非常乐意大家提交pr。   @@ -107,7 +111,7 @@ B站: https://live.bilibili.com/320 小红书: -https://www.redelight.cn/hina/livestream/569077534207413574/1707413727088?appuid=5f3f478a00000000010005b3& +https://www.xiaohongshu.com/hina/livestream/569077534207413574/1707413727088?appuid=5f3f478a00000000010005b3& bigo直播: https://www.bigo.tv/cn/716418802 @@ -124,11 +128,26 @@ https://cc.163.com/583946984 千度热播: https://qiandurebo.com/web/video.php?roomnumber=33333 -pandaTV: +PandaTV: https://www.pandalive.co.kr/live/play/bara0109 猫耳FM: https://fm.missevan.com/live/868895007 + +Look直播: +https://look.163.com/live?id=65108820&position=3 + +WinkTV: +https://www.winktv.co.kr/live/play/anjer1004 + +FlexTV: +https://www.flextv.co.kr/channels/593127/live + +PopkonTV: +https://www.popkontv.com/live/view?castId=wjfal007&partnerCode=P-00117 + +TwitCasting: +https://twitcasting.tv/c:uonq ``` 直播间分享地址和网页端长地址都能正常进行录制(抖音尽量用长链接,避免因短链接转换失效导致不能正常录制,而且需要有nodejs环境,否则无法转换)。 @@ -152,7 +171,6 @@ https://fm.missevan.com/live/868895007 ```bash git clone https://github.com/ihmily/DouyinLiveRecorder.git - ``` 2.进入项目文件夹,安装依赖 @@ -256,10 +274,20 @@ docker-compose stop [![iridescentGray](https://github.com/iridescentGray.png?size=50)](https://github.com/iridescentGray) [![annidy](https://github.com/annidy.png?size=50)](https://github.com/annidy) [![wwkk2580](https://github.com/wwkk2580.png?size=50)](https://github.com/wwkk2580) +[![missuo](https://github.com/missuo.png?size=50)](https://github.com/missuo) +   ## ⏳提交日志 +- 20240309 + - 修复虎牙直播、小红书直播和B站直播录制 + - 新增5个直播平台录制,包括winktv、flextv、look、popkontv、twitcasting + - 新增部分海外平台账号密码配置,实现自动登录并更新配置文件中的cookie + - 新增自定义配置需要使用代理录制的平台 + - 新增只推送开播消息不进行录制设置 + - 修复了一些bug + - 20240209 - 优化AfreecaTV录制,新增账号密码登录获取cookie以及持久保存 - 修复了小红书直播因官方更新直播域名,导致无法录制直播的问题 diff --git a/config/config.ini b/config/config.ini index fb33ce1..4560b0b 100644 --- a/config/config.ini +++ b/config/config.ini @@ -1,45 +1,63 @@ [录制设置] -直播保存路径(不填则默认) = -视频保存格式ts|mkv|flv|mp4|ts音频|mkv音频 = mp4 +直播保存路径(不填则默认) = +视频保存格式ts|mkv|flv|mp4|ts音频|mkv音频 = ts 原画|超清|高清|标清 = 原画 是否使用代理ip(是/否) = 是 -代理地址 = +代理地址 = 同一时间访问网络的线程数 = 3 循环时间(秒) = 120 排队读取网址时间(秒) = 0 是否显示循环秒数 = 否 -分段录制是否开启 = 是 +分段录制是否开启 = 否 视频分段时间(秒) = 1800 -生成时间文件 = 否 -TS录制完成后自动转为mp4格式 = 否 -TS录制完成后自动增加生成m4a格式 = 否 +ts录制完成后自动转为mp4格式 = 否 +ts录制完成后自动增加生成m4a格式 = 否 追加格式后删除原文件 = 否 +生成时间文件 = 否 +使用代理录制的平台(逗号分隔) = tiktok, afreecatv, pandalive, winktv, flextv, popkontv +额外使用代理录制的平台(逗号分隔) = [推送配置] -直播状态通知(可选微信|钉钉|TG或者都填) = -钉钉推送接口链接 = -微信推送接口链接 = -钉钉通知@对象(填手机号) = -TGAPI令牌 = -TG聊天ID(个人或者群组ID) = +直播状态通知(可选微信|钉钉|tg或者都填) = +钉钉推送接口链接 = +微信推送接口链接 = +钉钉通知@对象(填手机号) = +tgapi令牌 = +tg聊天id(个人或者群组id) = +只推送通知不录制(是/否) = 否 +直播推送检测频率(秒) = 1800 [Cookie] 抖音cookie(录制抖音必须要有) = ttwid=1%7CB1qls3GdnZhUov9o2NxOMxxYS2ff6OSvEWbv0ytbES4%7C1680522049%7C280d802d6d478e3e78d0c807f7c487e7ffec0ae4e5fdd6a0fe74c3c6af149511; my_rd=1; passport_csrf_token=3ab34460fa656183fccfb904b16ff742; passport_csrf_token_default=3ab34460fa656183fccfb904b16ff742; d_ticket=9f562383ac0547d0b561904513229d76c9c21; n_mh=hvnJEQ4Q5eiH74-84kTFUyv4VK8xtSrpRZG1AhCeFNI; store-region=cn-fj; store-region-src=uid; LOGIN_STATUS=1; __security_server_data_status=1; FORCE_LOGIN=%7B%22videoConsumedRemainSeconds%22%3A180%7D; pwa2=%223%7C0%7C3%7C0%22; download_guide=%223%2F20230729%2F0%22; volume_info=%7B%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%2C%22volume%22%3A0.6%7D; strategyABtestKey=%221690824679.923%22; stream_recommend_feed_params=%22%7B%5C%22cookie_enabled%5C%22%3Atrue%2C%5C%22screen_width%5C%22%3A1536%2C%5C%22screen_height%5C%22%3A864%2C%5C%22browser_online%5C%22%3Atrue%2C%5C%22cpu_core_num%5C%22%3A8%2C%5C%22device_memory%5C%22%3A8%2C%5C%22downlink%5C%22%3A10%2C%5C%22effective_type%5C%22%3A%5C%224g%5C%22%2C%5C%22round_trip_time%5C%22%3A150%7D%22; VIDEO_FILTER_MEMO_SELECT=%7B%22expireTime%22%3A1691443863751%2C%22type%22%3Anull%7D; home_can_add_dy_2_desktop=%221%22; __live_version__=%221.1.1.2169%22; device_web_cpu_core=8; device_web_memory_size=8; xgplayer_user_id=346045893336; csrf_session_id=2e00356b5cd8544d17a0e66484946f28; odin_tt=724eb4dd23bc6ffaed9a1571ac4c757ef597768a70c75fef695b95845b7ffcd8b1524278c2ac31c2587996d058e03414595f0a4e856c53bd0d5e5f56dc6d82e24004dc77773e6b83ced6f80f1bb70627; __ac_nonce=064caded4009deafd8b89; __ac_signature=_02B4Z6wo00f01HLUuwwAAIDBh6tRkVLvBQBy9L-AAHiHf7; ttcid=2e9619ebbb8449eaa3d5a42d8ce88ec835; webcast_leading_last_show_time=1691016922379; webcast_leading_total_show_times=1; webcast_local_quality=sd; live_can_add_dy_2_desktop=%221%22; msToken=1JDHnVPw_9yTvzIrwb7cQj8dCMNOoesXbA_IooV8cezcOdpe4pzusZE7NB7tZn9TBXPr0ylxmv-KMs5rqbNUBHP4P7VBFUu0ZAht_BEylqrLpzgt3y5ne_38hXDOX8o=; msToken=jV_yeN1IQKUd9PlNtpL7k5vthGKcHo0dEh_QPUQhr8G3cuYv-Jbb4NnIxGDmhVOkZOCSihNpA2kvYtHiTW25XNNX_yrsv5FN8O6zm3qmCIXcEe0LywLn7oBO2gITEeg=; tt_scid=mYfqpfbDjqXrIGJuQ7q-DlQJfUSG51qG.KUdzztuGP83OjuVLXnQHjsz-BRHRJu4e986 -快手cookie = -tiktok_cookie = -虎牙cookie = -斗鱼cookie = -yy_cookie = -B站cookie = -小红书cookie = -bigo_cookie = -blued_cookie = -afreecatv_cookie = -netease_cookie = -千度热播_cookie = +快手cookie = +tiktok_cookie = +虎牙cookie = +斗鱼cookie = +yy_cookie = +b站cookie = +小红书cookie = +bigo_cookie = +blued_cookie = +afreecatv_cookie = +netease_cookie = +千度热播_cookie = pandatv_cookie = -猫耳FM_cookie = +猫耳fm_cookie = +winktv_cookie = +flextv_cookie = +look_cookie = +twitcasting_cookie = + +[Authorization] +popkontv_token = [账号密码] afreecatv账号 = -afreecatv密码 = \ No newline at end of file +afreecatv密码 = +flextv账号 = +flextv密码 = +popkontv账号 = +partner_code = P-00001 +popkontv密码 = +twitcasting账号 = +twitcasting密码 = \ No newline at end of file diff --git a/main.py b/main.py index e9e799e..b8609cb 100644 --- a/main.py +++ b/main.py @@ -4,26 +4,29 @@ Author: Hmily GitHub: https://github.com/ihmily Date: 2023-07-17 23:52:05 -Update: 2024-02-09 02:41:18 +Update: 2024-03-09 03:25:39 Copyright (c) 2023-2024 by Hmily, All Rights Reserved. Function: Record live stream video. """ -import random import os import sys -import urllib.parse -import urllib.request -import configparser import subprocess +import signal import threading -import datetime import time +import datetime import json import re import shutil -import signal +import random +import base64 +import hashlib +import urllib.parse +import urllib.request +from urllib.error import URLError, HTTPError from typing import Any, Union +import configparser from spider import ( get_douyin_stream_data, @@ -41,7 +44,12 @@ from spider import ( get_netease_stream_data, get_qiandurebo_stream_data, get_pandatv_stream_data, - get_maoerfm_stream_url + get_maoerfm_stream_url, + get_winktv_stream_data, + get_flextv_stream_data, + get_looklive_stream_url, + get_popkontv_stream_url, + get_twitcasting_stream_url ) from web_rid import ( @@ -54,8 +62,10 @@ from utils import ( ) from msg_push import dingtalk, xizhi, tg_bot -version = "v3.0.1-beta" -platforms = "抖音|TikTok|快手|虎牙|斗鱼|YY|B站|小红书|bigo直播|blued直播|AfreecaTV|网易CC|千度热播|pandaTV|猫耳FM" +version = "v3.0.2" +platforms = "\n国内站点:抖音|快手|虎牙|斗鱼|YY|B站|小红书|bigo直播|blued直播|网易CC|千度热播|猫耳FM|Look直播|TwitCasting" \ + "\n海外站点:TikTok|AfreecaTV|PandaTV|WinkTV|FlexTV|PopkonTV" + # --------------------------全局变量------------------------------------- recording = set() unrecording = set() @@ -78,7 +88,7 @@ config_file = './config/config.ini' url_config_file = './config/URL_config.ini' backup_dir = './backup_config' encoding = 'utf-8-sig' -rstr = r"[\/\\\:\*\?\"\<\>\|&u]" +rstr = r"[\/\\\:\*\?\"\<\>\|&.。]" ffmpeg_path = "ffmpeg" # ffmpeg文件路径 default_path = os.getcwd() + '/downloads' os.makedirs(default_path, exist_ok=True) @@ -162,7 +172,7 @@ def update_file(file_path: str, old_str: str, new_str: str, start_str: str = Non def converts_mp4(address: str): - if tsconvert_to_mp4: + if ts_to_mp4: _output = subprocess.check_output([ "ffmpeg", "-i", address, "-c:v", "copy", @@ -176,7 +186,7 @@ def converts_mp4(address: str): def converts_m4a(address: str): - if tsconvert_to_m4a: + if ts_to_m4a: _output = subprocess.check_output([ "ffmpeg", "-i", address, "-n", "-vn", @@ -297,13 +307,14 @@ def get_douyin_stream_url(json_data: dict, video_quality: str) -> dict: result['flv_url'] = flv_url result['is_live'] = True result['record_url'] = m3u8_url # 使用 m3u8 链接进行录制 - return result @trace_error_decorator def get_tiktok_stream_url(json_data: dict, video_quality: str) -> dict: # TODO: 获取tiktok直播源地址 + if not json_data: + return {"anchor_name": None, "is_live": False} def get_video_quality_url(stream, q_key): return { @@ -313,7 +324,7 @@ def get_tiktok_stream_url(json_data: dict, video_quality: str) -> dict: live_room = json_data['LiveRoom']['liveRoomUserInfo'] user = live_room['user'] - anchor_name = user['nickname'] + anchor_name = f"{user['nickname']}-{user['uniqueId']}" status = user.get("status", 4) result = { @@ -394,16 +405,53 @@ def get_huya_stream_url(json_data: dict, video_quality: str) -> dict: if stream_info_list: select_cdn = stream_info_list[0] - # flv_url = select_cdn.get('sFlvUrl') - flv_url = 'http://hw.flv.huya.com/src' # 能播放但无法录制,待修复 + flv_url = select_cdn.get('sFlvUrl') stream_name = select_cdn.get('sStreamName') flv_url_suffix = select_cdn.get('sFlvUrlSuffix') hls_url = select_cdn.get('sHlsUrl') hls_url_suffix = select_cdn.get('sHlsUrlSuffix') flv_anti_code = select_cdn.get('sFlvAntiCode') - flv_url = f'{flv_url}/{stream_name}.{flv_url_suffix}?{flv_anti_code}&ratio=' - m3u8_url = f'{hls_url}/{stream_name}.{hls_url_suffix}?{flv_anti_code}&ratio=' + def get_anti_code(old_anti_code): + + # js地址:https://hd.huya.com/cdn_libs/mobile/hysdk-m-202402211431.js + + params_t = 100 + sdk_version = 2403051612 + + # sdk_id是13位数毫秒级时间戳 + t13 = int(time.time()) * 1000 + sdk_sid = t13 + + # 计算uuid和uid参数值 + init_uuid = (int(t13 % 10**10 * 1000) + int(1000 * random.random())) % 4294967295 # 直接初始化 + uid = random.randint(1400000000000, 1400009999999) # 经过测试uid也可以使用init_uuid代替 + seq_id = uid + sdk_sid # 移动端请求的直播流地址中包含seqId参数 + + # 计算ws_time参数值(16进制) 可以是当前毫秒时间戳,当然也可以直接使用url_query['wsTime'][0] + # 原始最大误差不得慢240000毫秒 + target_unix_time = (t13+110624) // 1000 + ws_time = hex(target_unix_time)[2:].lower() + + # fm参数值是经过url编码然后base64编码得到的,解码结果类似 DWq8BcJ3h6DJt6TY_$0_$1_$2_$3 + # 具体细节在上面js中查看,大概在32657行代码开始,有base64混淆代码请自行替换 + url_query = urllib.parse.parse_qs(old_anti_code) + ws_secret_pf = base64.b64decode(urllib.parse.unquote(url_query['fm'][0]).encode()).decode().split("_")[0] + ws_secret_hash = hashlib.md5(f'{seq_id}|{url_query["ctype"][0]}|{params_t}'.encode()).hexdigest() + ws_secret = f'{ws_secret_pf}_{uid}_{stream_name}_{ws_secret_hash}_{ws_time}' + ws_secret_md5 = hashlib.md5(ws_secret.encode()).hexdigest() + + anti_code = ( + f'wsSecret={ws_secret_md5}&wsTime={ws_time}&seqid={seq_id}&ctype={url_query["ctype"][0]}&ver=1' + f'&fs={url_query["fs"][0]}&uuid={init_uuid}&u={uid}&t={params_t}&sv={sdk_version}' + f'&sphdcdn={url_query["sphdcdn"][0]}&sphdDC={url_query["sphdDC"][0]}&sphd={url_query["sphd"][0]}' + f'&exsphd={url_query["exsphd"][0]}&sdk_sid={sdk_sid}&codec=264' + ) + return anti_code + + new_anti_code = get_anti_code(flv_anti_code) + flv_url = f'{flv_url}/{stream_name}.{flv_url_suffix}?{new_anti_code}&ratio=' + m3u8_url = f'{hls_url}/{stream_name}.{hls_url_suffix}?{new_anti_code}&ratio=' quality_list = flv_anti_code.split('&exsphd=') if len(quality_list) > 1: @@ -430,12 +478,12 @@ def get_huya_stream_url(json_data: dict, video_quality: str) -> dict: result['flv_url'] = flv_url result['m3u8_url'] = m3u8_url result['is_live'] = True - result['record_url'] = flv_url # 虎牙使用flv视频流录制 + result['record_url'] = flv_url # m3u8经常会出现断流 return result @trace_error_decorator -def get_douyu_stream_url(json_data: dict, cookies: str, video_quality: str) -> dict: +def get_douyu_stream_url(json_data: dict, cookies: str, video_quality: str, proxy_address: str) -> dict: # TODO: 获取斗鱼直播源地址 video_quality_options = { "原画": '0', @@ -453,16 +501,16 @@ def get_douyu_stream_url(json_data: dict, cookies: str, video_quality: str) -> d "is_live": False, } # 如果status值为1,则正在直播 - # 这边有个bug,就是如果是直播回放,状态也是在直播 待修复 + # 这边有个bug,就是如果是直播回放,状态也是在直播 待优化 if status == 1: rid = str(room_info['rid']) rate = video_quality_options.get(video_quality, '0') # 默认为原画 - flv_data = get_douyu_stream_data(rid, rate, cookies) + flv_data = get_douyu_stream_data(rid, rate, cookies=cookies, proxy_addr=proxy_address) flv_url = flv_data['data'].get('url', None) if flv_url: result['flv_url'] = flv_url result['is_live'] = True - result['record_url'] = flv_url # 斗鱼目前只能使用flv视频流录制 + result['record_url'] = flv_url return result @@ -509,12 +557,16 @@ def get_bilibili_stream_url(json_data: dict, video_quality: str) -> dict: while len(accept_qn_list) < 4: accept_qn_list.append(accept_qn_list[-1]) base_url = stream_data['base_url'] + current_qn = stream_data['current_qn'] host = stream_data['url_info'][0]['host'] extra = stream_data['url_info'][0]['extra'] url_type = format_list[m] qn = str(accept_qn_list[n]) select_quality = quality_list[qn] - base_url = re.sub(r'_(\d+)' + f'(?={url_type}\\?)', select_quality, base_url) + + if current_qn != 10000: + base_url = re.sub(r'_(\d+)' + f'(?={url_type}\\?)', select_quality, base_url) + extra = re.sub('&qn=0', f'&qn={qn}', extra) return host + base_url + extra @@ -563,12 +615,50 @@ def get_netease_stream_url(json_data: dict, video_quality: str) -> dict: } +@trace_error_decorator +def get_winktv_stream_url(json_data: dict, video_quality: str) -> dict: + if not json_data['is_live']: + return json_data + + quality_length = len(json_data['play_url_list']) + quality_list = {'原画': 'hls', '蓝光': 'hls', '超清': 'hls2', '高清': 'hls3', '标清': 'hls4'} + for i in json_data['play_url_list']: + if i in 'hls' and i not in list(quality_list.values()): + json_data['play_url_list'][i] = json_data['play_url_list'][quality_length - 1] + + selected_quality = quality_list[video_quality] + flv_url = json_data['play_url_list'][selected_quality][0]['url'] + + return { + "is_live": True, + "anchor_name": json_data['anchor_name'], + "flv_url": flv_url, + "record_url": flv_url + } + + +def push_message(content: str): + push_pts = [] + if '微信' in live_status_push: + push_pts.append('微信') + xizhi(xizhi_api_url, content) + if '钉钉' in live_status_push: + push_pts.append('钉钉') + dingtalk(dingtalk_api_url, content, dingtalk_phone_num) + if 'TG' in live_status_push: + push_pts.append('TG') + tg_bot(tg_chat_id, tg_token, content) + push_pts = '、'.join(push_pts) if len(push_pts) > 0 else '' + return push_pts + + def start_record(url_data: tuple, count_variable: int = -1): global warning_count global video_save_path global live_list global not_record_list global recording_time_list + start_pushed = False while True: try: @@ -581,8 +671,23 @@ def start_record(url_data: tuple, count_variable: int = -1): count_time = time.time() retry = 0 record_quality, record_url, anchor_name = url_data - print(f"\r运行新线程,传入地址 {record_url}") + proxy_address = proxy_addr + if proxy_addr: + proxy_address = None + for platform in enable_proxy_platform_list: + if platform and platform.strip() in url: + proxy_address = proxy_addr + break + + if not proxy_address: + if extra_enable_proxy_platform_list: + for pt in extra_enable_proxy_platform_list: + if pt and pt.strip() in url: + proxy_address = proxy_addr_bak if proxy_addr_bak else None + + # print(f'\r代理地址:{proxy_address}') + print(f"\r运行新线程,传入地址 {record_url}") while True: try: port_info = [] @@ -590,93 +695,114 @@ def start_record(url_data: tuple, count_variable: int = -1): platform = '抖音直播' # 判断如果是浏览器长链接 with semaphore: - # 使用semaphore来控制同时访问资源的线程数量 - json_data = get_douyin_stream_data(record_url, cookies=dy_cookie) + json_data = get_douyin_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=dy_cookie) port_info = get_douyin_stream_url(json_data, record_quality) elif record_url.find("https://v.douyin.com/") > -1: platform = '抖音直播' # 判断如果是app分享链接 is_long_url = True - room_id, sec_user_id = get_sec_user_id(record_url) - web_rid = get_live_room_id(room_id, sec_user_id) + room_id, sec_user_id = get_sec_user_id(url=record_url, proxy_addr=proxy_address) + web_rid = get_live_room_id(room_id, sec_user_id, proxy_addr=proxy_address) if len(web_rid) == 0: print('web_rid 获取失败,若多次失败请联系作者修复或者使用浏览器打开后的长链接') new_record_url = "https://live.douyin.com/" + str(web_rid) not_record_list.append(new_record_url) with semaphore: - json_data = get_douyin_stream_data(new_record_url, cookies=dy_cookie) + json_data = get_douyin_stream_data( + url=new_record_url, + proxy_addr=proxy_address, + cookies=dy_cookie) port_info = get_douyin_stream_url(json_data, record_quality) elif record_url.find("https://www.tiktok.com/") > -1: platform = 'TikTok直播' with semaphore: - if use_proxy: - if global_proxy or proxy_addr != '': - json_data = get_tiktok_stream_data( - url=record_url, - proxy_addr=proxy_addr, - cookies=tiktok_cookie) - port_info = get_tiktok_stream_url(json_data, record_quality) + + if global_proxy or proxy_address: + json_data = get_tiktok_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=tiktok_cookie) + port_info = get_tiktok_stream_url(json_data, record_quality) + else: + logger.warning(f"错误信息: 网络异常,请检查网络是否能正常访问TikTok平台") elif record_url.find("https://live.kuaishou.com/") > -1: platform = '快手直播' with semaphore: - json_data = get_kuaishou_stream_data(record_url, cookies=ks_cookie) + json_data = get_kuaishou_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=ks_cookie) port_info = get_kuaishou_stream_url(json_data, record_quality) elif record_url.find("https://www.huya.com/") > -1: platform = '虎牙直播' with semaphore: - json_data = get_huya_stream_data(record_url, cookies=hy_cookie) + json_data = get_huya_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=hy_cookie) port_info = get_huya_stream_url(json_data, record_quality) elif record_url.find("https://www.douyu.com/") > -1: platform = '斗鱼直播' with semaphore: - json_data = get_douyu_info_data(record_url) + json_data = get_douyu_info_data(url=record_url, proxy_addr=proxy_address) port_info = get_douyu_stream_url( - json_data, cookies=douyu_cookie, + json_data, proxy_address=proxy_address, cookies=douyu_cookie, video_quality=record_quality ) elif record_url.find("https://www.yy.com/") > -1: platform = 'YY直播' with semaphore: - json_data = get_yy_stream_data(record_url, cookies=yy_cookie) + json_data = get_yy_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=yy_cookie) port_info = get_yy_stream_url(json_data) elif record_url.find("https://live.bilibili.com/") > -1: platform = 'B站直播' with semaphore: - json_data = get_bilibili_stream_data(record_url, cookies=bili_cookie) + json_data = get_bilibili_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=bili_cookie) port_info = get_bilibili_stream_url(json_data, record_quality) - elif record_url.find("https://www.redelight.cn/") > -1: + elif record_url.find("https://www.redelight.cn/") > -1 or \ + record_url.find("https://www.xiaohongshu.com/") > -1: platform = '小红书直播' if retry > 0: time.sleep(7200) retry = 0 with semaphore: - port_info = get_xhs_stream_url(record_url, cookies=xhs_cookie) + port_info = get_xhs_stream_url(url=record_url, proxy_addr=proxy_address, cookies=xhs_cookie) retry += 1 elif record_url.find("https://www.bigo.tv/") > -1: - platform = 'bigo直播' + platform = 'Bigo直播' with semaphore: - port_info = get_bigo_stream_url(record_url, cookies=bigo_cookie) + port_info = get_bigo_stream_url(record_url, proxy_addr=proxy_address, cookies=bigo_cookie) elif record_url.find("https://app.blued.cn/") > -1: - platform = 'blued直播' + platform = 'Blued直播' with semaphore: - port_info = get_blued_stream_url(record_url, cookies=blued_cookie) + port_info = get_blued_stream_url(record_url, proxy_addr=proxy_address, cookies=blued_cookie) elif record_url.find("afreecatv.com/") > -1: - platform = 'AfreecaTv直播' + platform = 'AfreecaTV' with semaphore: - port_info = get_afreecatv_stream_url( - url=record_url, proxy_addr=proxy_addr, - cookies=afreecatv_cookie - ) + if global_proxy or proxy_address: + port_info = get_afreecatv_stream_url( + url=record_url, proxy_addr=proxy_address, + cookies=afreecatv_cookie, + username=afreecatv_username, + password=afreecatv_password + ) + else: + logger.warning(f"错误信息: 网络异常,请检查本网络是否能正常访问AfreecaTV平台") elif record_url.find("cc.163.com/") > -1: platform = '网易CC直播' @@ -687,21 +813,85 @@ def start_record(url_data: tuple, count_variable: int = -1): elif record_url.find("qiandurebo.com/") > -1: platform = '千度热播' with semaphore: - port_info = get_qiandurebo_stream_data(url=record_url, cookies=qiandurebo_cookie) + port_info = get_qiandurebo_stream_data( + url=record_url, proxy_addr=proxy_address, cookies=qiandurebo_cookie) elif record_url.find("www.pandalive.co.kr/") > -1: - platform = 'pandaTV' + platform = 'PandaTV' with semaphore: - port_info = get_pandatv_stream_data( - url=record_url, - proxy_addr=proxy_addr, - cookies=pandatv_cookie - ) + if global_proxy or proxy_address: + port_info = get_pandatv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=pandatv_cookie + ) + else: + logger.warning(f"错误信息: 网络异常,请检查本网络是否能正常访问PandaTV直播平台") elif record_url.find("fm.missevan.com/") > -1: - platform = '猫耳FM' + platform = '猫耳FM直播' with semaphore: - port_info = get_maoerfm_stream_url(url=record_url, cookies=maoerfm_cookie) + port_info = get_maoerfm_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=maoerfm_cookie) + + elif record_url.find("www.winktv.co.kr/") > -1: + platform = 'WinkTV' + with semaphore: + if global_proxy or proxy_address: + json_data = get_winktv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=winktv_cookie) + port_info = get_winktv_stream_url(json_data, record_quality) + else: + logger.warning(f"错误信息: 网络异常,请检查本网络是否能正常访问WinkTV直播平台") + + elif record_url.find("www.flextv.co.kr/") > -1: + platform = 'FlexTV' + with semaphore: + if global_proxy or proxy_address: + port_info = get_flextv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=flextv_cookie, + username=flextv_username, + password=flextv_password + ) + else: + logger.warning(f"错误信息: 网络异常,请检查本网络是否能正常访问FlexTV直播平台") + + elif record_url.find("look.163.com/") > -1: + platform = 'Look直播' + with semaphore: + port_info = get_looklive_stream_url( + url=record_url, proxy_addr=proxy_address, cookies=look_cookie + ) + + elif record_url.find("www.popkontv.com/") > -1: + platform = 'PopkonTV' + with semaphore: + if global_proxy or proxy_address: + port_info = get_popkontv_stream_url( + url=record_url, + proxy_addr=proxy_address, + access_token=popkontv_access_token, + username=popkontv_username, + password=popkontv_password, + partner_code=popkontv_partner_code + ) + else: + logger.warning(f"错误信息: 网络异常,请检查本网络是否能正常访问PopkonTV直播平台") + + elif record_url.find("twitcasting.tv/") > -1: + platform = 'TwitCasting' + with semaphore: + port_info = get_twitcasting_stream_url( + url=record_url, + proxy_addr=proxy_address, + cookies=twitcasting_cookie, + username=twitcasting_username, + password=twitcasting_password + ) else: logger.warning(f'{record_url} 未知直播地址') @@ -736,19 +926,28 @@ def start_record(url_data: tuple, count_variable: int = -1): run_once = True if port_info['is_live'] is False: - print(f"{record_name} 等待直播... ") + print(f"\r{record_name} 等待直播... ") + + if start_pushed: + content = f"{record_name} 直播已结束!" + push_pts = push_message(content) + if push_pts: + print(f'提示信息:已经将[{record_name}]直播状态消息推送至你的{push_pts}') + start_pushed = False else: - content = f"{record_name} 正在直播中..." + content = f"\r{record_name} 正在直播中..." print(content) # 推送通知 - if live_status_push: - if '微信' in live_status_push: - xizhi(xizhi_api_url, content) - if '钉钉' in live_status_push: - dingtalk(dingtalk_api_url, content, dingtalk_phone_num) - if 'TG' in live_status_push: - tg_bot(tg_chat_id, tg_token, content) + if live_status_push and not start_pushed: + push_pts = push_message(f"{content.split('...')[0]},时间:{datetime.datetime.today()}") + if push_pts: + print(f'提示信息:已经将[{record_name}]直播状态消息推送至你的{push_pts}') + start_pushed = True + + if disable_record: + time.sleep(push_check_seconds) + continue real_url = port_info['record_url'] full_path = f'{default_path}/{platform}/{anchor_name}' @@ -776,36 +975,44 @@ def start_record(url_data: tuple, count_variable: int = -1): user_agent = ("Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (" "KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile " "Safari/537.36") + + analyzeduration = "20000000" + probesize = "10000000" + bufsize = "8000k" + max_muxing_queue_size = "1024" + for pt_host in overseas_platform_host: + if pt_host in record_url: + analyzeduration = "40000000" + probesize = "20000000" + bufsize = "15000k" + max_muxing_queue_size = "2048" + break + ffmpeg_command = [ ffmpeg_path, "-y", "-v", "verbose", - "-rw_timeout", "30000000", # 改为30s + "-rw_timeout", "30000000", "-loglevel", "error", "-hide_banner", "-user_agent", user_agent, "-protocol_whitelist", "rtmp,crypto,file,http,https,tcp,tls,udp,rtp", - "-thread_queue_size", "512", - "-analyzeduration", "5000000", - "-probesize", "10000000", + "-thread_queue_size", "1024", + "-analyzeduration", analyzeduration, + "-probesize", probesize, "-fflags", "+discardcorrupt", "-i", real_url, - "-bufsize", "9000k", # 适当增加输入缓冲区大小 + "-bufsize", bufsize, "-sn", "-dn", - "-reconnect_delay_max", "60", # 适当增加最大重连延迟 + "-reconnect_delay_max", "60", "-reconnect_streamed", "-reconnect_at_eof", - "-max_muxing_queue_size", "128", # 适当增加输出复用器的最大队列大小 + "-max_muxing_queue_size", max_muxing_queue_size, "-correct_ts_overflow", "1", ] # 添加代理参数 - need_proxy_url = ['tiktok', 'afreecatv', 'pandalive'] - for i in need_proxy_url: - if i in real_url: - if use_proxy and proxy_addr != '': - # os.environ["http_proxy"] = proxy_addr - ffmpeg_command.insert(1, "-http_proxy") - ffmpeg_command.insert(2, proxy_addr) - break + if proxy_address: + ffmpeg_command.insert(1, "-http_proxy") + ffmpeg_command.insert(2, proxy_address) recording.add(record_name) start_record_time = datetime.datetime.now() @@ -848,6 +1055,7 @@ def start_record(url_data: tuple, count_variable: int = -1): now = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) save_file_path = f"{full_path}/{anchor_name}_{now}_%03d.mkv" command = [ + "-flags", "global_header", "-c:v", "copy", "-c:a", "aac", "-map", "0", @@ -868,6 +1076,7 @@ def start_record(url_data: tuple, count_variable: int = -1): create_var[str(filename_short)].start() command = [ + "-flags", "global_header", "-map", "0", "-c:v", "copy", "-c:a", "copy", @@ -899,7 +1108,6 @@ def start_record(url_data: tuple, count_variable: int = -1): "-f", "segment", "-segment_time", split_time, "-segment_format", "mp4", - "-movflags", "+faststart", "-reset_timestamps", "1", save_file_path, ] @@ -943,7 +1151,7 @@ def start_record(url_data: tuple, count_variable: int = -1): ffmpeg_command.extend(command) _output = subprocess.check_output(ffmpeg_command, stderr=subprocess.STDOUT) - if tsconvert_to_m4a: + if ts_to_m4a: threading.Thread(target=converts_m4a, args=(save_file_path,)).start() except subprocess.CalledProcessError as e: logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") @@ -965,7 +1173,7 @@ def start_record(url_data: tuple, count_variable: int = -1): ffmpeg_command.extend(command) _output = subprocess.check_output(ffmpeg_command, stderr=subprocess.STDOUT) - if tsconvert_to_m4a: + if ts_to_m4a: threading.Thread(target=converts_m4a, args=(save_file_path,)).start() except subprocess.CalledProcessError as e: logger.warning(f"错误信息: {e} 发生错误的行数: {e.__traceback__.tb_lineno}") @@ -979,7 +1187,7 @@ def start_record(url_data: tuple, count_variable: int = -1): print(f'{rec_info}/{filename}') try: - if tsconvert_to_mp4: + if ts_to_mp4: save_path_name = f"{full_path}/{anchor_name}_{now}_%03d.mp4" audio_code = 'aac' segment_format = 'mp4' @@ -1034,9 +1242,9 @@ def start_record(url_data: tuple, count_variable: int = -1): ffmpeg_command.extend(command) _output = subprocess.check_output(ffmpeg_command, stderr=subprocess.STDOUT) - if tsconvert_to_mp4: + if ts_to_mp4: threading.Thread(target=converts_mp4, args=(save_file_path,)).start() - if tsconvert_to_m4a: + if ts_to_m4a: threading.Thread(target=converts_m4a, args=(save_file_path,)).start() except subprocess.CalledProcessError as e: @@ -1178,8 +1386,7 @@ if ffmepg_file_check.find("run") > -1: # print("ffmpeg存在") pass else: - print("重要提示:") - input("检测到ffmpeg不存在,请将ffmpeg.exe放到同目录,或者设置为环境变量,没有ffmpeg将无法录制") + input("重要提示:\n\r检测到ffmpeg不存在,请将ffmpeg.exe放到同目录,或者设置为环境变量,没有ffmpeg将无法录制") sys.exit(0) # --------------------------初始化程序------------------------------------- @@ -1187,9 +1394,9 @@ print("-----------------------------------------------------") print("| DouyinLiveRecorder |") print("-----------------------------------------------------") -print(f"版本号:{version}") -print("Github:https://github.com/ihmily/DouyinLiveRecorder") -print(f'支持平台:{platforms}') +print(f"版本号: {version}") +print(f"GitHub: https://github.com/ihmily/DouyinLiveRecorder") +print(f'支持平台: {platforms}') print('.....................................................') if not os.path.exists('./config'): @@ -1199,16 +1406,21 @@ if not os.path.exists('./config'): t3 = threading.Thread(target=backup_file_start, args=(), daemon=True) t3.start() -# 录制tiktok时,如果开启了电脑全局/规则代理,可以不用再在配置文件中填写代理地址 -# 但强烈建议还是配置一下代理地址,否则非常不稳定 + try: - # 检测电脑是否开启了全局/规则代理 + # 录制国外平台时,如果开启了电脑全局/规则代理,可以正常录制,但强烈建议还是配置一下代理地址,否则非常不稳定 + # 检测电脑是否开启了全局/规则代理(如果身处国外请忽略) print('系统代理检测中,请耐心等待...') response_g = urllib.request.urlopen("https://www.google.com/", timeout=15) global_proxy = True - print('全局/规则网络代理已开启√ 注意:配置文件中的代理设置也要开启才会生效哦!') -except Exception: - print('INFO:未检测到全局/规则网络代理,请检查代理配置(若无需录制TikTok/AfreecaTV直播请忽略此条提示)') + print('\r全局/规则网络代理已开启√') +except HTTPError as err: + print(f"HTTP error occurred: {err.code} - {err.reason}") +except URLError as err: + # print("URLError:", err.reason) + print('INFO:未检测到全局/规则网络代理,请检查代理配置(若无需录制海外直播请忽略此条提示)') +except Exception as err: + print("An unexpected error occurred:", err) def read_config_value(config_parser: configparser.RawConfigParser, section: str, option: str, default_value: Any) -> ( @@ -1222,6 +1434,10 @@ def read_config_value(config_parser: configparser.RawConfigParser, section: str, config_parser.add_section('推送配置') if 'Cookie' not in config_parser.sections(): config_parser.add_section('Cookie') + if 'Authorization' not in config_parser.sections(): + config_parser.add_section('Authorization') + if '账号密码' not in config_parser.sections(): + config_parser.add_section('账号密码') return config_parser.get(section, option) except (configparser.NoSectionError, configparser.NoOptionError): config_parser.set(section, option, str(default_value)) @@ -1255,29 +1471,47 @@ while True: file.write(input_url) video_save_path = read_config_value(config, '录制设置', '直播保存路径(不填则默认)', "") - video_save_type = read_config_value(config, '录制设置', '视频保存格式TS|MKV|FLV|MP4|TS音频|MKV音频', "mp4") + video_save_type = read_config_value(config, '录制设置', '视频保存格式TS|MKV|FLV|MP4|TS音频|MKV音频', "ts") video_record_quality = read_config_value(config, '录制设置', '原画|超清|高清|标清', "原画") use_proxy = options.get(read_config_value(config, '录制设置', '是否使用代理ip(是/否)', "是"), False) - proxy_addr = read_config_value(config, '录制设置', '代理地址', "") + proxy_addr_bak = read_config_value(config, '录制设置', '代理地址', "") + proxy_addr = None if not use_proxy else proxy_addr_bak max_request = int(read_config_value(config, '录制设置', '同一时间访问网络的线程数', 3)) semaphore = threading.Semaphore(max_request) delay_default = int(read_config_value(config, '录制设置', '循环时间(秒)', 120)) local_delay_default = int(read_config_value(config, '录制设置', '排队读取网址时间(秒)', 0)) loop_time = options.get(read_config_value(config, '录制设置', '是否显示循环秒数', "否"), False) split_video_by_time = options.get(read_config_value(config, '录制设置', '分段录制是否开启', "否"), False) - split_time = str(read_config_value(config, '录制设置', '视频分段时间(秒)', 3600)) - tsconvert_to_mp4 = options.get(read_config_value(config, '录制设置', 'TS录制完成后自动转为mp4格式', "否"), - False) - tsconvert_to_m4a = options.get(read_config_value(config, '录制设置', 'TS录制完成后自动增加生成m4a格式', "否"), - False) + split_time = str(read_config_value(config, '录制设置', '视频分段时间(秒)', 1800)) + ts_to_mp4 = options.get(read_config_value(config, '录制设置', 'TS录制完成后自动转为mp4格式', "否"), + False) + ts_to_m4a = options.get(read_config_value(config, '录制设置', 'TS录制完成后自动增加生成m4a格式', "否"), + False) delete_origin_file = options.get(read_config_value(config, '录制设置', '追加格式后删除原文件', "否"), False) create_time_file = options.get(read_config_value(config, '录制设置', '生成时间文件', "否"), False) + enable_proxy_platform = read_config_value(config, '录制设置', '使用代理录制的平台(逗号分隔)', + 'tiktok, afreecatv, pandalive, winktv, flextv, popkontv') + enable_proxy_platform_list = enable_proxy_platform.replace(',', ',').split(',') if enable_proxy_platform else None + extra_enable_proxy = read_config_value(config, '录制设置', '额外使用代理录制的平台(逗号分隔)', '') + extra_enable_proxy_platform_list = extra_enable_proxy.replace(',', ',').split(',') if extra_enable_proxy else None live_status_push = read_config_value(config, '推送配置', '直播状态通知(可选微信|钉钉|TG或者都填)', "") dingtalk_api_url = read_config_value(config, '推送配置', '钉钉推送接口链接', "") xizhi_api_url = read_config_value(config, '推送配置', '微信推送接口链接', "") dingtalk_phone_num = read_config_value(config, '推送配置', '钉钉通知@对象(填手机号)', "") tg_token = read_config_value(config, '推送配置', 'TGAPI令牌', "") tg_chat_id = read_config_value(config, '推送配置', 'TG聊天ID(个人或者群组ID)', "") + disable_record = options.get(read_config_value(config, '推送配置', '只推送通知不录制(是/否)', "否"), False) + push_check_seconds = int(read_config_value(config, '推送配置', '直播推送检测频率(秒)', 1800)) + afreecatv_username = read_config_value(config, '账号密码', 'afreecatv账号', '') + afreecatv_password = read_config_value(config, '账号密码', 'afreecatv密码', '') + flextv_username = read_config_value(config, '账号密码', 'flextv账号', '') + flextv_password = read_config_value(config, '账号密码', 'flextv密码', '') + popkontv_username = read_config_value(config, '账号密码', 'popkontv账号', '') + popkontv_partner_code = read_config_value(config, '账号密码', 'partner_code', 'P-00001') + popkontv_password = read_config_value(config, '账号密码', 'popkontv密码', '') + twitcasting_username = read_config_value(config, '账号密码', 'twitcasting账号', '') + twitcasting_password = read_config_value(config, '账号密码', 'twitcasting密码', '') + popkontv_access_token = read_config_value(config, 'Authorization', 'popkontv_token', '') dy_cookie = read_config_value(config, 'Cookie', '抖音cookie(录制抖音必须要有)', '') ks_cookie = read_config_value(config, 'Cookie', '快手cookie', '') tiktok_cookie = read_config_value(config, 'Cookie', 'tiktok_cookie', '') @@ -1293,6 +1527,10 @@ while True: qiandurebo_cookie = read_config_value(config, 'Cookie', '千度热播_cookie', '') pandatv_cookie = read_config_value(config, 'Cookie', 'pandatv_cookie', '') maoerfm_cookie = read_config_value(config, 'Cookie', '猫耳FM_cookie', '') + winktv_cookie = read_config_value(config, 'Cookie', 'winktv_cookie', '') + flextv_cookie = read_config_value(config, 'Cookie', 'flextv_cookie', '') + look_cookie = read_config_value(config, 'Cookie', 'look_cookie', '') + twitcasting_cookie = read_config_value(config, 'Cookie', 'twitcasting_cookie', '') if len(video_save_type) > 0: if video_save_type.upper().lower() == "FLV".lower(): @@ -1360,31 +1598,44 @@ while True: url = 'https://' + url url_host = url.split('/')[2] - host_list = [ + platform_host = [ 'live.douyin.com', 'v.douyin.com', - 'www.tiktok.com', 'live.kuaishou.com', 'www.huya.com', 'www.douyu.com', 'www.yy.com', 'live.bilibili.com', 'www.redelight.cn', + 'www.xiaohongshu.com', 'www.bigo.tv', 'app.blued.cn', - 'play.afreecatv.com', - 'm.afreecatv.com', 'cc.163.com', 'qiandurebo.com', + 'fm.missevan.com', + 'look.163.com', + 'twitcasting.tv', + ] + overseas_platform_host = [ + 'www.tiktok.com', + 'play.afreecatv.com', + 'm.afreecatv.com', 'www.pandalive.co.kr', - 'fm.missevan.com' + 'www.winktv.co.kr', + 'www.flextv.co.kr', + 'www.popkontv.com' ] - if url_host in host_list: + platform_host.extend(overseas_platform_host) + if url_host in platform_host: + if url_host in ['live.douyin.com', 'live.bilibili.com']: + update_file(url_config_file, url, url.split('?')[0]) + url = url.split('?')[0] + new_line = (quality, url, name) url_tuples_list.append(new_line) else: - print(f"{url} 未知链接.此条跳过") + print(f"\r{url} 未知链接.此条跳过") update_file(url_config_file, url, url, start_str='#') while len(name_list): @@ -1409,7 +1660,7 @@ while True: if url_tuple[1] not in runing_list: if not first_start: - print(f"新增链接: {url_tuple[1]}") + print(f"\r新增链接: {url_tuple[1]}") monitoring += 1 args = [url_tuple, monitoring] # TODO: 执行开始录制的操作 diff --git a/requirements.txt b/requirements.txt index 1952fe4..7fc0fd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests PyExecJS -loguru==0.7.2 \ No newline at end of file +loguru==0.7.2 +pycryptodome==3.20.0 \ No newline at end of file diff --git a/spider.py b/spider.py index b4071d7..e924eec 100644 --- a/spider.py +++ b/spider.py @@ -4,7 +4,7 @@ Author: Hmily GitHub:https://github.com/ihmily Date: 2023-07-15 23:15:00 -Update: 2024-02-09 03:33:50 +Update: 2024-03-09 01:39:17 Copyright (c) 2023 by Hmily, All Rights Reserved. Function: Get live stream data. """ @@ -21,7 +21,6 @@ import urllib.request from utils import ( trace_error_decorator, update_config, - read_config_value, dict_to_cookie_str ) import http.cookiejar @@ -31,8 +30,62 @@ no_proxy_handler = urllib.request.ProxyHandler({}) opener = urllib.request.build_opener(no_proxy_handler) +def get_req( + url: str, + proxy_addr: Union[str, None] = None, + headers: Union[dict, None] = None, + data: Union[dict, bytes, None] = None, + json_data: dict = None, + timeout: int = 20, + abroad: bool = False +) -> Union[str, Any]: + + if headers is None: + headers = {} + try: + if proxy_addr: + proxies = { + 'http': proxy_addr, + 'https': proxy_addr + } + if data or json_data: + response = requests.post(url, data=data, json=json_data, headers=headers, proxies=proxies, timeout=timeout) + else: + response = requests.get(url, headers=headers, proxies=proxies, timeout=timeout) + resp_str = response.text + else: + if data and not isinstance(data, bytes): + data = urllib.parse.urlencode(data).encode('utf-8') + if json_data and isinstance(json_data, dict): + data = json.dumps(json_data).encode('utf-8') + req = urllib.request.Request(url, data=data, headers=headers) + if abroad: + with urllib.request.urlopen(req, timeout=timeout) as response: + resp_str = response.read().decode('utf-8') + else: + with opener.open(req, timeout=timeout) as response: + resp_str = response.read().decode('utf-8') + + except Exception as e: + resp_str = str(e) + + return resp_str + + +def get_partner_code(url, params): + + parsed_url = urllib.parse.urlparse(url) + query_params = urllib.parse.parse_qs(parsed_url.query) + + if params in query_params: + return query_params[params][0] + else: + return None + + @trace_error_decorator -def get_douyin_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[str, Any]: +def get_douyin_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> \ + Dict[str, Any]: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', @@ -43,10 +96,7 @@ def get_douyin_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[s headers['Cookie'] = cookies try: - # 使用更底层的urllib内置库,防止开启代理时导致的抖音录制SSL 443报错 - req = urllib.request.Request(url, headers=headers) - response = opener.open(req, timeout=15) - html_str = response.read().decode('utf-8') + html_str = get_req(url=url, proxy_addr=proxy_addr, headers=headers) match_json_str = re.search(r'(\{\\"state\\":.*?)]\\n"]\)', html_str) if not match_json_str: match_json_str = re.search(r'(\{\\"common\\":.*?)]\\n"]\)
Dict[s except Exception as e: print(f'失败地址:{url} 准备切换解析方法{e}') web_rid = re.match('https://live.douyin.com/(\d+)', url).group(1) - headers['Cookie'] = 'sessionid=73d300f837f261eaa8ffc69d50162700' + headers['Cookie'] = 'sessionid=b03763e09810c59948fbd9c6ab5a667a' url2 = f'https://live.douyin.com/webcast/room/web/enter/?aid=6383&app_name=douyin_web&live_id=1&web_rid={web_rid}' - req = urllib.request.Request(url2, headers=headers) - response = opener.open(req, timeout=15) - json_str = response.read().decode('utf-8') + json_str = get_req(url=url2, proxy_addr=proxy_addr, headers=headers) json_data = json.loads(json_str)['data'] room_data = json_data['data'][0] room_data['anchor_name'] = json_data['user']['nickname'] @@ -74,39 +122,31 @@ def get_douyin_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[s @trace_error_decorator -def get_tiktok_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> Dict[ - str, Any]: +def get_tiktok_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> \ + Dict[str, Any]: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.79', 'Cookie': 'ttwid=1%7CM-rF193sJugKuNz2RGNt-rh6pAAR9IMceUSzlDnPCNI%7C1683274418%7Cf726d4947f2fc37fecc7aeb0cdaee52892244d04efde6f8a8edd2bb168263269; tiktok_webapp_theme=light; tt_chain_token=VWkygAWDlm1cFg/k8whmOg==; passport_csrf_token=6e422c5a7991f8cec7033a8082921510; passport_csrf_token_default=6e422c5a7991f8cec7033a8082921510; d_ticket=f8c267d4af4523c97be1ccb355e9991e2ae06; odin_tt=320b5f386cdc23f347be018e588873db7f7aea4ea5d1813681c3fbc018ea025dde957b94f74146dbc0e3612426b865ccb95ec8abe4ee36cca65f15dbffec0deff7b0e69e8ea536d46e0f82a4fc37d211; cmpl_token=AgQQAPNSF-RO0rT04baWtZ0T_jUjl4fVP4PZYM2QPw; uid_tt=319b558dbba684bb1557206c92089cd113a875526a89aee30595925d804b81c7; uid_tt_ss=319b558dbba684bb1557206c92089cd113a875526a89aee30595925d804b81c7; sid_tt=ad5e736f4bedb2f6d42ccd849e706b1d; sessionid=ad5e736f4bedb2f6d42ccd849e706b1d; sessionid_ss=ad5e736f4bedb2f6d42ccd849e706b1d; store-idc=useast5; store-country-code=us; store-country-code-src=uid; tt-target-idc=useast5; tt-target-idc-sign=qXNk0bb1pDQ0FbCNF120Pl9WWMLZg9Edv5PkfyCbS4lIk5ieW5tfLP7XWROnN0mEaSlc5hg6Oji1pF-yz_3ZXnUiNMrA9wNMPvI6D9IFKKVmq555aQzwPIGHv0aQC5dNRgKo5Z5LBkgxUMWEojTKclq2_L8lBciw0IGdhFm_XyVJtbqbBKKgybGDLzK8ZyxF4Jl_cYRXaDlshZjc38JdS6wruDueRSHe7YvNbjxCnApEFUv-OwJANSPU_4rvcqpVhq3JI2VCCfw-cs_4MFIPCDOKisk5EhAo2JlHh3VF7_CLuv80FXg_7ZqQ2pJeMOog294rqxwbbQhl3ATvjQV_JsWyUsMd9zwqecpylrPvtySI2u1qfoggx1owLrrUynee1R48QlanLQnTNW_z1WpmZBgVJqgEGLwFoVOmRzJuFFNj8vIqdjM2nDSdWqX8_wX3wplohkzkPSFPfZgjzGnQX28krhgTytLt7BXYty5dpfGtsdb11WOFHM6MZ9R9uLVB; sid_guard=ad5e736f4bedb2f6d42ccd849e706b1d%7C1690990657%7C15525213%7CMon%2C+29-Jan-2024+08%3A11%3A10+GMT; sid_ucp_v1=1.0.0-KGM3YzgwYjZhODgyYWI1NjIwNTA0NjBmOWUxMGRhMjIzYTI2YjMxNDUKGAiqiJ30keKD5WQQwfCppgYYsws4AkDsBxAEGgd1c2Vhc3Q1IiBhZDVlNzM2ZjRiZWRiMmY2ZDQyY2NkODQ5ZTcwNmIxZA; ssid_ucp_v1=1.0.0-KGM3YzgwYjZhODgyYWI1NjIwNTA0NjBmOWUxMGRhMjIzYTI2YjMxNDUKGAiqiJ30keKD5WQQwfCppgYYsws4AkDsBxAEGgd1c2Vhc3Q1IiBhZDVlNzM2ZjRiZWRiMmY2ZDQyY2NkODQ5ZTcwNmIxZA; tt_csrf_token=dD0EIH8q-pe3qDQsCyyD1jLN6KizJDRjOEyk; __tea_cache_tokens_1988={%22_type_%22:%22default%22%2C%22user_unique_id%22:%227229608516049831425%22%2C%22timestamp%22:1683274422659}; ttwid=1%7CM-rF193sJugKuNz2RGNt-rh6pAAR9IMceUSzlDnPCNI%7C1694002151%7Cd89b77afc809b1a610661a9d1c2784d80ebef9efdd166f06de0d28e27f7e4efe; msToken=KfJAVZ7r9D_QVeQlYAUZzDFbc1Yx-nZz6GF33eOxgd8KlqvTg1lF9bMXW7gFV-qW4MCgUwnBIhbiwU9kdaSpgHJCk-PABsHCtTO5J3qC4oCTsrXQ1_E0XtbqiE4OVLZ_jdF1EYWgKNPT2SnwGkQ=; msToken=KfJAVZ7r9D_QVeQlYAUZzDFbc1Yx-nZz6GF33eOxgd8KlqvTg1lF9bMXW7gFV-qW4MCgUwnBIhbiwU9kdaSpgHJCk-PABsHCtTO5J3qC4oCTsrXQ1_E0XtbqiE4OVLZ_jdF1EYWgKNPT2SnwGkQ=' } if cookies: headers['Cookie'] = cookies - - if proxy_addr: - - proxies = { - 'http': proxy_addr, - 'https': proxy_addr - } - - html = requests.get(url, headers=headers, proxies=proxies, timeout=15) - html_str = html.text - - else: - - req = urllib.request.Request(url, headers=headers) - response = urllib.request.urlopen(req, timeout=15) - html_str = response.read().decode('utf-8') - json_str = re.findall( - '', html_str).group(1) json_data = json.loads(json_str) @@ -272,10 +304,10 @@ def get_douyu_info_data(url: str) -> Dict[str, Any]: @trace_error_decorator -def get_douyu_stream_data(rid: str, rate: str = '-1', cookies: Union[str, None] = None) -> Dict[str, Any]: +def get_douyu_stream_data(rid: str, rate: str = '-1', proxy_addr: Union[str, None] = None, + cookies: Union[str, None] = None) -> Dict[str, Any]: did = '10000000000000000000000000003306' - params_list = get_token_js(rid, did) - + params_list = get_token_js(rid, did, proxy_addr=proxy_addr) headers = { 'User-Agent': 'Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36', 'Referer': 'https://m.douyu.com/3125893?rid=3125893&dyshid=0-96003918aa5365bc6dcb4933000316p1&dyshci=181', @@ -293,18 +325,16 @@ def get_douyu_stream_data(rid: str, rate: str = '-1', cookies: Union[str, None] 'rid': rid, 'rate': rate, # 0蓝光、3超清、2高清、-1默认 } - # 将数据转换为 URL 编码的字节格式 - data = urllib.parse.urlencode(data).encode('utf-8') + app_api = 'https://m.douyu.com/hgapi/livenc/room/getStreamUrl' - req = urllib.request.Request(app_api, data=data, headers=headers) - response = opener.open(req, timeout=15) - json_str = response.read().decode('utf-8') + json_str = get_req(url=app_api, proxy_addr=proxy_addr, headers=headers, data=data) json_data = json.loads(json_str) return json_data @trace_error_decorator -def get_yy_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[str, Any]: +def get_yy_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> \ + Dict[str, Any]: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', @@ -314,46 +344,43 @@ def get_yy_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[str, if cookies: headers['Cookie'] = cookies - req = urllib.request.Request(url, headers=headers) - response = opener.open(req, timeout=15) - html_str = response.read().decode('utf-8') + html_str = get_req(url=url, proxy_addr=proxy_addr, headers=headers) anchor_name = re.search('nick: "(.*?)",\n\s+logo', html_str).group(1) cid = re.search('sid : "(.*?)",\n\s+ssid', html_str, re.S).group(1) data = '{"head":{"seq":1701869217590,"appidstr":"0","bidstr":"121","cidstr":"' + cid + '","sidstr":"' + cid + '","uid64":0,"client_type":108,"client_ver":"5.17.0","stream_sys_ver":1,"app":"yylive_web","playersdk_ver":"5.17.0","thundersdk_ver":"0","streamsdk_ver":"5.17.0"},"client_attribute":{"client":"web","model":"web0","cpu":"","graphics_card":"","os":"chrome","osversion":"0","vsdk_version":"","app_identify":"","app_version":"","business":"","width":"1920","height":"1080","scale":"","client_type":8,"h265":0},"avp_parameter":{"version":1,"client_type":8,"service_type":0,"imsi":0,"send_time":1701869217,"line_seq":-1,"gear":4,"ssl":1,"stream_format":0}}' data_bytes = data.encode('utf-8') url2 = f'https://stream-manager.yy.com/v3/channel/streams?uid=0&cid={cid}&sid={cid}&appid=0&sequence=1701869217590&encode=json' - req = urllib.request.Request(url2, data=data_bytes, headers=headers) - response = opener.open(req, timeout=15) - json_str = response.read().decode('utf-8') + json_str = get_req(url=url2, data=data_bytes, proxy_addr=proxy_addr, headers=headers) json_data = json.loads(json_str) json_data['anchor_name'] = anchor_name return json_data @trace_error_decorator -def get_bilibili_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[str, Any]: +def get_bilibili_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> \ + Dict[str, Any]: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Referer': 'https://live.bilibili.com/?spm_id_from=333.1296.0.0', - 'Cookie': "buvid3=13436C33-39B8-C4D5-C5C6-3F31B85716A131745infoc; b_nut=1680525931; CURRENT_FNVAL=4048; _uuid=B10E775DC-168D-CA47-E1B8-CEF7C52FA84234052infoc; buvid_fp=a2f7f8f3977824b52ec75cf23e5b6754; CURRENT_PID=70fa2680-d21d-11ed-ba58-9979ebfa5794; rpdid=|(JYYJ|uuYm)0J'uY)|lklmRJ; buvid4=C29E3582-5740-8FF3-AFD1-98B345DDAF5393968-022082019-Vk7oLekZ8O%2FtgWtFEu98GQ%3D%3D; DedeUserID=623475372; DedeUserID__ckMd5=db79fcea5a8315aa; i-wanna-go-back=-1; b_ut=5; FEED_LIVE_VERSION=V8; header_theme_version=CLOSE; home_feed_column=5; browser_resolution=1483-722; SESSDATA=122468fe%2C1707184844%2C2c98c%2A827Ts7uT3NZIxeOzop88h3EdmSUIG9NhWF9VkiidKIkTgJkTbh5WcONjTKuaOwfeR9t6uUZAAASAA; bili_jct=b8479df41520c402eb0a1a7f37a26de8; bp_video_offset_623475372=827303476826472609; PVID=1; LIVE_BUVID=AUTO5816940041629512; GIFT_BLOCK_COOKIE=GIFT_BLOCK_COOKIE" + 'Cookie': "bilibili.com" } if cookies: headers['Cookie'] = cookies try: - req = urllib.request.Request(url, headers=headers) - response = opener.open(req, timeout=15) - html_str = response.read().decode('utf-8') + html_str = get_req(url=url, proxy_addr=proxy_addr, headers=headers) json_str = re.search('', html_str, - re.S).group(1) + + html_str = get_req(url=url, proxy_addr=proxy_addr, headers=headers) + json_str = re.search('', + html_str, re.S).group(1) json_data = json.loads(json_str) room_data = json_data['props']['pageProps']['roomInfoInitData'] live_data = room_data['live'] @@ -731,7 +719,8 @@ def get_netease_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[ @trace_error_decorator -def get_qiandurebo_stream_data(url: str, cookies: Union[str, None] = None) -> Dict[str, Any]: +def get_qiandurebo_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> \ + Dict[str, Any]: headers = { 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', @@ -741,18 +730,16 @@ def get_qiandurebo_stream_data(url: str, cookies: Union[str, None] = None) -> Di if cookies: headers['Cookie'] = cookies - req = urllib.request.Request(url, headers=headers) - response = opener.open(req, timeout=15) - html_str = response.read().decode('utf-8') + html_str = get_req(url=url, proxy_addr=proxy_addr, headers=headers) data = re.search('var user = (.*?)\r\n\s+user\.play_url', html_str, re.S).group(1) anchor_name = re.findall('"zb_nickname": "(.*?)",\r\n', data) - result = {"anchor": "", "is_live": False} + result = {"anchor_name": "", "is_live": False} if len(anchor_name) > 0: result['anchor_name'] = anchor_name[0] play_url = re.findall('"play_url": "(.*?)",\r\n', data) - if len(play_url) > 0: + if len(play_url) > 0 and 'common-text-center" style="display:block' not in html_str: result['anchor_name'] = anchor_name[0] result['flv_url'] = play_url[0] result['is_live'] = True @@ -761,8 +748,8 @@ def get_qiandurebo_stream_data(url: str, cookies: Union[str, None] = None) -> Di @trace_error_decorator -def get_pandatv_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> Dict[ - str, Any]: +def get_pandatv_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> \ + Dict[str, Any]: headers = { 'referer': 'https://www.pandalive.co.kr/', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.58', @@ -776,48 +763,28 @@ def get_pandatv_stream_data(url: str, proxy_addr: Union[str, None] = None, cooki 'userId': user_id, 'info': 'media fanGrade', } + room_password = get_partner_code(url, "pwd") + if not room_password: + room_password = '' data2 = { 'action': 'watch', 'userId': user_id, - 'password': '', + 'password': room_password, 'shareLinkType': '', } result = {"anchor_name": "", "is_live": False} - if proxy_addr: - proxies = { - 'http': proxy_addr, - 'https': proxy_addr - } - - response = requests.post('https://api.pandalive.co.kr/v1/member/bj', - headers=headers, proxies=proxies, data=data) - json_data = response.json() - anchor_name = json_data['bjInfo']['nick'] - result['anchor_name'] = anchor_name - live_status = 'media' in json_data - if live_status: - response = requests.post(url2, data=data2, headers=headers, proxies=proxies, timeout=15) - json_data = response.json() - - else: - - data = urllib.parse.urlencode(data).encode('utf-8') - req = urllib.request.Request('https://api.pandalive.co.kr/v1/member/bj', data=data, headers=headers) - response = urllib.request.urlopen(req, timeout=20) - json_str = response.read().decode('utf-8') - json_data = json.loads(json_str) - anchor_name = json_data['bjInfo']['nick'] - result['anchor_name'] = anchor_name - live_status = 'media' in json_data - if live_status: - data2 = urllib.parse.urlencode(data2).encode('utf-8') - req = urllib.request.Request(url2, data=data2, headers=headers) - response = urllib.request.urlopen(req, timeout=20) - json_str = response.read().decode('utf-8') - json_data = json.loads(json_str) + json_str = get_req('https://api.pandalive.co.kr/v1/member/bj', + proxy_addr=proxy_addr, headers=headers, data=data, abroad=True) + json_data = json.loads(json_str) + anchor_id = json_data['bjInfo']['id'] + anchor_name = f"{json_data['bjInfo']['nick']}-{anchor_id}" + result['anchor_name'] = anchor_name + live_status = 'media' in json_data if live_status: + json_str = get_req(url2, proxy_addr=proxy_addr, headers=headers, data=data2, abroad=True) + json_data = json.loads(json_str) play_url = json_data['PlayList']['hls'][0]['url'] result['m3u8_url'] = play_url result['is_live'] = True @@ -826,7 +793,8 @@ def get_pandatv_stream_data(url: str, proxy_addr: Union[str, None] = None, cooki @trace_error_decorator -def get_maoerfm_stream_url(url: str, cookies: Union[str, None] = None) -> Dict[str, Any]: +def get_maoerfm_stream_url(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> \ + Dict[str, Any]: headers = { 'accept': 'application/json, text/plain, */*', 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', @@ -839,9 +807,8 @@ def get_maoerfm_stream_url(url: str, cookies: Union[str, None] = None) -> Dict[s room_id = url.split('?')[0].rsplit('/', maxsplit=1)[1] url2 = f'https://fm.missevan.com/api/v2/live/{room_id}' - req = urllib.request.Request(url2, headers=headers) - response = opener.open(req, timeout=15) - json_str = response.read().decode('utf-8') + + json_str = get_req(url=url2, proxy_addr=proxy_addr, headers=headers) json_data = json.loads(json_str) anchor_name = json_data['info']['creator']['username'] @@ -864,6 +831,554 @@ def get_maoerfm_stream_url(url: str, cookies: Union[str, None] = None) -> Dict[s return result +@trace_error_decorator +def get_winktv_bj_info(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> \ + tuple[str, Any]: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'content-type': 'application/x-www-form-urlencoded', + 'referer': 'https://www.winktv.co.kr/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', + } + if cookies: + headers['Cookie'] = cookies + user_id = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + data = { + 'userId': user_id, + 'info': 'media', + } + + info_api = 'https://api.winktv.co.kr/v1/member/bj' + json_str = get_req(url=info_api, proxy_addr=proxy_addr, headers=headers, data=data, abroad=True) + json_data = json.loads(json_str) + live_status = 'media' in json_data + anchor_id = json_data['bjInfo']['id'] + anchor_name = f"{json_data['bjInfo']['nick']}-{anchor_id}" + return anchor_name, live_status + + +@trace_error_decorator +def get_winktv_stream_data(url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None) -> \ + Dict[str, Any]: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'content-type': 'application/x-www-form-urlencoded', + 'referer': 'https://www.winktv.co.kr/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', + + } + if cookies: + headers['Cookie'] = cookies + user_id = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + room_password = get_partner_code(url, "pwd") + if not room_password: + room_password = '' + data = { + 'action': 'watch', + 'userId': user_id, + 'password': room_password, + 'shareLinkType': '', + } + + anchor_name, live_status = get_winktv_bj_info(url=url, proxy_addr=proxy_addr, cookies=cookies) + result = {"anchor_name": anchor_name, "is_live": live_status} + if live_status: + play_api = 'https://api.winktv.co.kr/v1/live/play' + json_str = get_req(url=play_api, proxy_addr=proxy_addr, headers=headers, data=data, abroad=True) + json_data = json.loads(json_str) + result['play_url_list'] = json_data['PlayList'] + return result + + +@trace_error_decorator +def login_flextv(username: str, password: str, proxy_addr: Union[str, None] = None) -> Union[str, None]: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'content-type': 'application/json;charset=UTF-8', + 'referer': 'https://www.flextv.co.kr/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', + } + + data = { + 'loginId': username, + 'password': password, + 'loginKeep': True, + 'saveId': True, + 'device': 'PCWEB', + } + + url = 'https://api.flextv.co.kr/v2/api/auth/signin' + try: + if proxy_addr: + proxies = { + 'http': proxy_addr, + 'https': proxy_addr + } + + response = requests.post(url, json=data, headers=headers, proxies=proxies, timeout=20) + json_data = response.json() + cookie_dict = response.cookies.get_dict() + else: + + req_json_data = json.dumps(data).encode('utf-8') + cookie_jar = http.cookiejar.CookieJar() + login_opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie_jar)) + req = Request(url, data=req_json_data, headers=headers) + response = login_opener.open(req, timeout=20) + resp_str = response.read().decode('utf-8') + json_data = json.loads(resp_str) + cookie_dict = {cookie.name: cookie.value for cookie in cookie_jar} + + if "error" not in json_data: + cookie = dict_to_cookie_str(cookie_dict) + return cookie + except Exception as e: + print('FlexTV登录失败,请检查配置文件中的账号密码是否正确', e) + + +def get_flextv_stream_url( + url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None, + username: Union[str, None] = None, password: Union[str, None] = None +) -> Dict[str, Any]: + def fetch_data(cookie): + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://www.flextv.co.kr/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', + } + user_id = url.split('/live')[0].rsplit('/', maxsplit=1)[-1] + if cookie: + headers['Cookie'] = cookie + play_api = f'https://api.flextv.co.kr/api/channels/{user_id}/stream?option=all' + json_str = get_req(play_api, proxy_addr=proxy_addr, headers=headers, abroad=True) + if 'HTTP Error 400: Bad Request' in json_str: + raise ConnectionError('获取FlexTV直播数据失败,请切换代理重试') + return json.loads(json_str) + + json_data = fetch_data(cookies) + if "message" in json_data and json_data["message"] == "로그인후 이용이 가능합니다.": + print("FlexTV直播获取失败[未登录]: 19+直播需要登录后是成人才可观看") + print("正在尝试登录AfreecaTV直播平台,请确保已在配置文件中填写好您的账号和密码") + if len(username) < 6 or len(password) < 8: + raise RuntimeError('AfreecaTV登录失败!请在config.ini配置文件中填写正确的AfreecaTV平台的账号和密码') + print('FlexTV平台登录中...') + new_cookie = login_flextv(username, password, proxy_addr=proxy_addr) + if new_cookie and len(new_cookie) > 0: + print('FlexTV平台登录成功!开始获取直播数据...') + json_data = fetch_data(new_cookie) + update_config('./config/config.ini', 'Cookie', 'flextv_cookie', new_cookie) + else: + raise RuntimeError('AfreecaTV登录失败,请检查账号和密码是否正确') + + if 'sources' in json_data and len(json_data['sources']) > 0: + play_url = json_data['sources'][0]['url'] + return play_url + + +@trace_error_decorator +def get_flextv_stream_data( + url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None, + username: Union[str, None] = None, password: Union[str, None] = None +) -> Dict[str, Any]: + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'referer': 'https://www.flextv.co.kr/', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', + } + if cookies: + headers['Cookie'] = cookies + user_id = url.split('/live')[0].rsplit('/', maxsplit=1)[-1] + result = {"anchor_name": '', "is_live": False} + try: + url2 = f'https://www.flextv.co.kr/channels/{user_id}' + html_str = get_req(url2, proxy_addr=proxy_addr, headers=headers, abroad=True) + json_str = re.search('', html_str).group(1) + json_data = json.loads(json_str) + channel_data = json_data['props']['pageProps']['channel'] + live_status = channel_data['isInLive'] + anchor_id = channel_data['owner']['loginId'] + anchor_name = f"{channel_data['owner']['nickname']}-{anchor_id}" + result["anchor_name"] = anchor_name + if live_status: + result['is_live'] = True + play_url = get_flextv_stream_url( + url=url, proxy_addr=proxy_addr, cookies=cookies, username=username, password=password) + if play_url: + result['m3u8_url'] = play_url + result['record_url'] = play_url + except Exception as e: + print('FlexTV直播间数据获取失败', e) + return result + + +def get_looklive_secret_data(text): + # 本算法参考项目:https://github.com/785415581/MusicBox/blob/b8f716d43d/doc/analysis/analyze_captured_data.md + + modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee' \ + '341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe487' \ + '5d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' + nonce = b'0CoJUm6Qyw8W8jud' + public_key = '010001' + from Crypto.Cipher import AES + from Crypto.Util.Padding import pad + import base64 + import binascii + import secrets + + def create_secret_key(size: int) -> bytes: + charset = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-=[]{}|;:,.<>?' + return ''.join(secrets.choice(charset) for _ in range(size)).encode('utf-8') + + def aes_encrypt(text: Union[str, bytes], seckey: Union[str, bytes]) -> bytes: + if isinstance(text, str): + text = text.encode('utf-8') + if isinstance(seckey, str): + seckey = seckey.encode('utf-8') + seckey = seckey[:16] # 16 (AES-128), 24 (AES-192), or 32 (AES-256) bytes + iv = bytes('0102030405060708', 'utf-8') + encryptor = AES.new(seckey, AES.MODE_CBC, iv) + padded_text = pad(text, AES.block_size) + ciphertext = encryptor.encrypt(padded_text) + encoded_ciphertext = base64.b64encode(ciphertext) + return encoded_ciphertext + + def rsa_encrypt(text: Union[str, bytes], pub_key: str, mod: str) -> str: + if isinstance(text, str): + text = text.encode('utf-8') + text_reversed = text[::-1] + text_int = int(binascii.hexlify(text_reversed), 16) + encrypted_int = pow(text_int, int(pub_key, 16), int(mod, 16)) + return format(encrypted_int, 'x').zfill(256) + + sec_key = create_secret_key(16) + enc_text = aes_encrypt(aes_encrypt(json.dumps(text), nonce), sec_key) + enc_sec_key = rsa_encrypt(sec_key, public_key, modulus) + return enc_text.decode(), enc_sec_key + + +def get_looklive_stream_url( + url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None +) -> Dict[str, Any]: + """ + 通过PC网页端的接口获取完整直播源,只有params和encSecKey这两个加密请求参数。 + params: 由两次AES加密完成 + ncSecKey: 由一次自写的加密函数完成,值可固定 + """ + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0', + 'Accept': 'application/json, text/javascript', + 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': 'https://look.163.com/', + } + + if cookies: + headers['Cookie'] = cookies + + room_id = re.search('live\?id=(.*?)&', url).group(1) + params, secretkey = get_looklive_secret_data({"liveRoomNo": room_id}) + request_data = {'params': params, 'encSecKey': secretkey} + api = 'https://api.look.163.com/weapi/livestream/room/get/v3' + json_str = get_req(api, proxy_addr=proxy_addr, headers=headers, data=request_data) + json_data = json.loads(json_str) + anchor_name = json_data['data']['anchor']['nickName'] + live_status = json_data['data']['liveStatus'] + result = {"anchor_name": anchor_name, "is_live": False} + if live_status == 1: + result["is_live"] = True + if json_data['data']['roomInfo']['liveType'] == 1: + print('Look直播暂时只支持音频直播,不支持Look视频直播!') + else: + play_url_list = json_data['data']['roomInfo']['liveUrl'] + result["flv_url"] = play_url_list['httpPullUrl'] + result["m3u8_url"] = play_url_list['hlsPullUrl'] + result["record_url"] = play_url_list['hlsPullUrl'] + return result + + +@trace_error_decorator +def login_popkontv( + username: str, password: str, proxy_addr: Union[str, None] = None, code: Union[str, None] = 'P-00001' +) -> Union[tuple, None]: + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Authorization': 'Basic FpAhe6mh8Qtz116OENBmRddbYVirNKasktdXQiuHfm88zRaFydTsFy63tzkdZY0u', + 'Content-Type': 'application/json', + 'Origin': 'https://www.popkontv.com', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', + } + + data = { + 'partnerCode': code, + 'signId': username, + 'signPwd': password, + } + + url = 'https://www.popkontv.com/api/proxy/member/v1/login' + if proxy_addr: + proxies = { + 'http': proxy_addr, + 'https': proxy_addr + } + response = requests.post(url, json=data, headers=headers, proxies=proxies, timeout=20) + json_data = response.json() + + else: + req_json_data = json.dumps(data).encode('utf-8') + cookie_jar = http.cookiejar.CookieJar() + login_opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie_jar)) + req = Request(url, data=req_json_data, headers=headers) + response = login_opener.open(req, timeout=20) + resp_str = response.read().decode('utf-8') + json_data = json.loads(resp_str) + + login_status_code = json_data["statusCd"] + if login_status_code == 'E4010': + raise Exception('popkontv登录失败,请重新配置正确的登录账号或者密码!') + elif json_data["statusCd"] == 'S2000': + token = json_data['data']["token"] + partner_code = json_data['data']["partnerCode"] + return token, partner_code + else: + raise Exception(f'popkontv登录失败,{json_data["statusMsg"]}') + + +@trace_error_decorator +def get_popkontv_stream_data( + url: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None, + username: Union[str, None] = None, code: Union[str, None] = 'P-00001' +) -> Union[tuple, Any]: + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Content-Type': 'application/json', + 'Origin': 'https://www.popkontv.com', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', + } + if cookies: + headers['Cookie'] = cookies + + anchor_id = re.search('castId=(.*?)(?=&|$)', url).group(1) + data = { + 'partnerCode': code, + 'searchKeyword': anchor_id, + 'signId': username, + } + + api = 'https://www.popkontv.com/api/proxy/broadcast/v1/search/all' + json_str = get_req(api, proxy_addr=proxy_addr, headers=headers, json_data=data, abroad=True) + json_data = json.loads(json_str) + anchor_id = json_data['data']['broadCastList'][0]['mcSignId'] + anchor_name = f"{json_data['data']['broadCastList'][0]['nickName']}-{anchor_id}" + partner_code = json_data['data']['broadCastList'][0]['mcPartnerCode'] + live_list = json_data['data']['liveList'] + live_status = len(live_list) > 0 + + cast_start_date_code = live_list[0]['castStartDateCode'] if live_status else None + private = live_list[0]["isPrivate"] == "1" if live_list else None + return anchor_name, partner_code, cast_start_date_code, private + + +@trace_error_decorator +def get_popkontv_stream_url( + url: str, + proxy_addr: Union[str, None] = None, + access_token: Union[str, None] = None, + username: Union[str, None] = None, + password: Union[str, None] = None, + partner_code: Union[str, None] = 'P-00001' +) -> Dict[str, Any]: + headers = { + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'ClientKey': 'Client FpAhe6mh8Qtz116OENBmRddbYVirNKasktdXQiuHfm88zRaFydTsFy63tzkdZY0u', + 'Content-Type': 'application/json', + 'Origin': 'https://www.popkontv.com', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', + } + + if access_token: + headers['Authorization'] = f'Bearer {access_token}' + + anchor_id = re.search('castId=(.*?)(?=&|$)', url).group(1) + anchor_name, mc_partner_code, cast_start_date_code, is_private = get_popkontv_stream_data( + url, proxy_addr=proxy_addr, code=partner_code, username=username) + result = {"anchor_name": anchor_name, "is_live": False} + + if cast_start_date_code: + result["is_live"] = True + room_password = get_partner_code(url, "pwd") + if is_private and room_password: + raise RuntimeError(f"直播间数据获取失败,因为{anchor_name}直播间为私密房间,请配置房间密码后重试") + + def fetch_data(header: dict = None, code: str = None) -> str: + data = { + 'androidStore': 0, + 'castCode': f'{anchor_id}-{cast_start_date_code}', + 'castPartnerCode': mc_partner_code, + 'castSignId': anchor_id, + 'castType': '0', + 'commandType': 0, + 'exePath': 5, + 'isSecret': 0, + 'partnerCode': code, + 'password': room_password, + 'signId': username, + 'version': '4.6.2', + } + play_api = 'https://www.popkontv.com/api/proxy/broadcast/v1/castwatchonoff' + return get_req(play_api, proxy_addr=proxy_addr, json_data=data, headers=header, abroad=True) + + json_str = fetch_data(headers, partner_code) + + if 'HTTP Error 400' in json_str or 'statusCd":"E5000' in json_str: + print("popkontv直播获取失败[token不存在或者已过期]: 请登录后观看") + print("正在尝试登录popkontv直播平台,请确保已在配置文件中填写好您的账号和密码") + if len(username) < 4 or len(password) < 10: + raise RuntimeError('popkontv登录失败!请在config.ini配置文件中填写正确的popkontv平台的账号和密码') + print('popkontv平台登录中...') + new_access_token, new_partner_code = login_popkontv( + username=username, password=password, proxy_addr=proxy_addr, code=partner_code + ) + if new_access_token and len(new_access_token) == 640: + print('popkontv平台登录成功!开始获取直播数据...') + headers['Authorization'] = f'Bearer {new_access_token}' + json_str = fetch_data(headers, new_partner_code) + update_config('./config/config.ini', 'Authorization', 'popkontv_token', new_access_token) + else: + raise RuntimeError('popkontv登录失败,请检查账号和密码是否正确') + json_data = json.loads(json_str) + status_msg = json_data["statusMsg"] + if json_data['statusCd'] == "L000A": + print('获取直播源失败,', status_msg) + raise RuntimeError('你是未认证会员。登录popkontv官方网站后,在“我的页面”>“修改我的信息”底部进行手机认证后可用') + elif json_data['statusCd'] == "L0001": + cast_start_date_code = int(cast_start_date_code) - 1 + json_str = fetch_data(headers, partner_code) + json_data = json.loads(json_str) + m3u8_url = json_data['data']['castHlsUrl'] + result["m3u8_url"] = m3u8_url + result["record_url"] = m3u8_url + elif json_data['statusCd'] == "L0000": + m3u8_url = json_data['data']['castHlsUrl'] + result["m3u8_url"] = m3u8_url + result["record_url"] = m3u8_url + else: + raise RuntimeError('获取直播源失败,', status_msg) + return result + + +@trace_error_decorator +def login_twitcasting( + username: str, password: str, proxy_addr: Union[str, None] = None, cookies: Union[str, None] = None +) -> Union[str, None]: + + headers = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Referer': 'https://twitcasting.tv/indexcaslogin.php?redir=%2Findexloginwindow.php%3Fnext%3D%252F&keep=1', + 'Cookie': 'hl=zh; did=04fb08f1b15d248644f1dfa82816d323; _ga=GA1.1.1021187740.1709706998; keep=1; mfadid=yrQiEB26ruRg7mlMavABMBZWdOddzojW; _ga_X8R46Y30YM=GS1.1.1709706998.1.1.1709712274.0.0.0', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36 Edg/121.0.0.0', + } + + if cookies: + headers['Cookie'] = cookies + + login_url = 'https://twitcasting.tv/indexcaslogin.php?redir=%2F&keep=1' + html_str = get_req(login_url, proxy_addr=proxy_addr, headers=headers) + cs_session_id = re.search('', html_str).group(1) + + data = { + 'username': username, + 'password': password, + 'action': 'login', + 'cs_session_id': cs_session_id, + } + + login_api = 'https://twitcasting.tv/indexcaslogin.php?redir=/indexloginwindow.php?next=%2F&keep=1' + + try: + if proxy_addr: + proxies = { + 'http': proxy_addr, + 'https': proxy_addr + } + + response = requests.post(login_api, data=data, headers=headers, proxies=proxies, timeout=20) + cookie_dict = response.cookies.get_dict() + else: + req_json_data = urllib.parse.urlencode(data).encode('utf-8') + cookie_jar = http.cookiejar.CookieJar() + login_opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie_jar)) + req = Request(login_api, data=req_json_data, headers=headers) + _ = login_opener.open(req, timeout=20) + cookie_dict = {cookie.name: cookie.value for cookie in cookie_jar} + + if 'tc_ss' in cookie_dict: + cookie = dict_to_cookie_str(cookie_dict) + return cookie + except Exception as e: + print('TwitCasting登录出错,', e) + + +@trace_error_decorator +def get_twitcasting_stream_url( + url: str, + proxy_addr: Union[str, None] = None, + cookies: Union[str, None] = None, + username: Union[str, None] = None, + password: Union[str, None] = None, +) -> Dict[str, Any]: + + headers = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Referer': 'https://twitcasting.tv/?ch0', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0', + } + + anchor_id = url.split('/')[3] + + if cookies: + headers['Cookie'] = cookies + + def get_data(header): + html_str = get_req(url, proxy_addr=proxy_addr, headers=header) + anchor = re.search("(.*?)\(@(.*?)\) 's Live - TwitCasting",html_str) + status = re.search('data-is-onlive="(.*?)"\n\s+data-view-mode', html_str) + return f'{anchor.group(1).strip()}-{anchor.group(2)}', status.group(1) + + result = {"anchor_name": '', "is_live": False} + + try: + anchor_name, live_status = get_data(headers) + except AttributeError: + print('获取TwitCasting数据失败,正在尝试登录...') + new_cookie = login_twitcasting(username=username, password=password, proxy_addr=proxy_addr, cookies=cookies) + if not new_cookie: + raise RuntimeError('TwitCasting登录失败,请检查配置文件中的账号密码是否正确') + print('TwitCasting 登录成功!开始获取数据...') + headers['Cookie'] = new_cookie + update_config('./config/config.ini', 'Cookie', 'twitcasting_cookie', new_cookie) + anchor_name, live_status = get_data(headers) + + result["anchor_name"] = anchor_name + if live_status == 'true': + play_url = f'https://twitcasting.tv/{anchor_id}/metastream.m3u8/?video=1&mode=source' + result['m3u8_url'] = play_url + result['record_url'] = play_url + result['is_live'] = True + return result + + if __name__ == '__main__': # 尽量用自己的cookie,以避免默认的不可用导致无法获取数据 # 以下示例链接不保证时效性,请自行查看链接是否能正常访问 @@ -876,8 +1391,10 @@ if __name__ == '__main__': # room_url = 'https://www.douyu.com/3637778?dyshid' # room_url = 'https://www.yy.com/22490906/22490906' # YY直播 # room_url = 'https://live.bilibili.com/21593109' # b站直播 + # room_url = 'https://live.bilibili.com/23448867' # b站直播 # 小红书直播 # room_url = 'https://www.redelight.cn/hina/livestream/569077534207413574/1707413727088?appuid=5f3f478a00000000010005b3&' + # room_url = 'https://www.xiaohongshu.com/hina/livestream/569098486282043893/1708661048594?appuid=5f3f478a00000000010005b3&' # room_url = 'https://www.bigo.tv/cn/716418802' # bigo直播 # room_url = 'https://app.blued.cn/live?id=Mp6G2R' # blued直播 # room_url = 'https://play.afreecatv.com/sw7love' # afreecatv直播 @@ -885,22 +1402,32 @@ if __name__ == '__main__': # room_url = 'https://play.afreecatv.com/secretx' # afreecatv直播 # room_url = 'https://cc.163.com/583946984' # 网易cc直播 # room_url = 'https://qiandurebo.com/web/video.php?roomnumber=33333' # 千度热播 - # room_url = 'https://www.pandalive.co.kr/live/play/bara0109' # pandaTV + # room_url = 'https://www.pandalive.co.kr/live/play/bara0109' # PandaTV # room_url = 'https://fm.missevan.com/live/868895007' # 猫耳FM直播 + # room_url = 'https://www.winktv.co.kr/live/play/anjer1004' # WinkTV + # room_url = 'https://www.flextv.co.kr/channels/593127/live' # FlexTV + # room_url = 'https://look.163.com/live?id=65108820&position=3' # Look直播 + # room_url = 'https://www.popkontv.com/live/view?castId=wjfal007&partnerCode=P-00117' # popkontv + # room_url = 'https://twitcasting.tv/c:uonq' # TwitCasting - print(get_douyin_stream_data(room_url)) - # print(get_tiktok_stream_data(url,proxy_addr='')) - # print(get_kuaishou_stream_data(room_url)) - # print(get_huya_stream_data(room_url)) - # print(get_douyu_info_data(room_url)) - # print(get_douyu_stream_data("4921614",rate='-1')) - # print(get_yy_stream_data(room_url)) - # print(get_bilibili_stream_data(room_url)) - # print(get_xhs_stream_url(room_url)) - # print(get_bigo_stream_url(room_url)) - # print(get_blued_stream_url(room_url)) - # print(get_afreecatv_stream_url(room_url, proxy_addr='')) - # print(get_netease_stream_data(room_url)) - # print(get_qiandurebo_stream_data(room_url)) + print(get_douyin_stream_data(room_url, proxy_addr='')) + # print(get_tiktok_stream_data(room_url, proxy_addr='')) + # print(get_kuaishou_stream_data2(room_url, proxy_addr='')) + # print(get_huya_stream_data(room_url, proxy_addr='')) + # print(get_douyu_info_data(room_url, proxy_addr='')) + # print(get_douyu_stream_data("4921614", proxy_addr='')) + # print(get_yy_stream_data(room_url, proxy_addr='')) + # print(get_bilibili_stream_data(room_url, proxy_addr='')) + # print(get_xhs_stream_url(room_url, proxy_addr='')) + # print(get_bigo_stream_url(room_url, proxy_addr='')) + # print(get_blued_stream_url(room_url, proxy_addr='')) + # print(get_afreecatv_stream_url(room_url, proxy_addr='', username='', password='')) + # print(get_netease_stream_data(room_url, proxy_addr='')) + # print(get_qiandurebo_stream_data(room_url, proxy_addr='')) # print(get_pandatv_stream_data(room_url, proxy_addr='')) - # print(get_maoerfm_stream_url(room_url)) + # print(get_maoerfm_stream_url(room_url, proxy_addr='')) + # print(get_winktv_stream_data(room_url, proxy_addr='')) + # print(get_flextv_stream_data(room_url,proxy_addr='', username='', password='')) + # print(get_looklive_stream_url(room_url, proxy_addr='')) + # print(get_popkontv_stream_url(room_url, proxy_addr='', username='', password='')) + # print(get_twitcasting_stream_url(room_url, proxy_addr='', username='', password='')) diff --git a/web_rid.py b/web_rid.py index 3617827..03184d7 100644 --- a/web_rid.py +++ b/web_rid.py @@ -4,29 +4,29 @@ Author: Hmily Github:https://github.com/ihmily Date: 2023-07-17 23:52:05 -Update: 2023-09-07 23:35:00 +Update: 2024-03-06 23:35:00 Copyright (c) 2023 by Hmily, All Rights Reserved. """ import json import re import urllib.parse +from typing import Union import execjs # pip install PyExecJS import requests import urllib.request - no_proxy_handler = urllib.request.ProxyHandler({}) opener = urllib.request.build_opener(no_proxy_handler) headers = { - 'User-Agent': 'Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36', - 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', - 'Cookie': 's_v_web_id=verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf' - } + 'User-Agent': 'Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36', + 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', + 'Cookie': 's_v_web_id=verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf' +} # X-bogus算法 -def get_xbogus(url) -> str: +def get_xbogus(url: str) -> str: query = urllib.parse.urlparse(url).query xbogus = execjs.compile(open('./x-bogus.js').read()).call('sign', query, headers["User-Agent"]) # print(xbogus) @@ -34,38 +34,46 @@ def get_xbogus(url) -> str: # 获取房间ID和用户secID -def get_sec_user_id(url): - response = opener.open(url, timeout=15) +def get_sec_user_id(url: str, proxy_addr: Union[str, None] = None): + if proxy_addr: + proxies = { + 'http': proxy_addr, + 'https': proxy_addr + } + response = requests.get(url, headers=headers, proxies=proxies, timeout=15) + else: + response = opener.open(url, timeout=15) redirect_url = response.url - sec_user_id=re.search(r'sec_user_id=([\w\d_\-]+)&',redirect_url).group(1) - room_id=redirect_url.split('?')[0].rsplit('/',maxsplit=1)[1] - return room_id,sec_user_id + sec_user_id = re.search(r'sec_user_id=([\w\d_\-]+)&', redirect_url).group(1) + room_id = redirect_url.split('?')[0].rsplit('/', maxsplit=1)[1] + return room_id, sec_user_id # 获取直播间webID -def get_live_room_id(room_id,sec_user_id): - url= f'https://webcast.amemv.com/webcast/room/reflow/info/?verifyFp=verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf&type_id=0&live_id=1&room_id={room_id}&sec_user_id={sec_user_id}&app_id=1128&msToken=wrqzbEaTlsxt52-vxyZo_mIoL0RjNi1ZdDe7gzEGMUTVh_HvmbLLkQrA_1HKVOa2C6gkxb6IiY6TY2z8enAkPEwGq--gM-me3Yudck2ailla5Q4osnYIHxd9dI4WtQ==' +def get_live_room_id(room_id: str, sec_user_id: str, proxy_addr: Union[str, None] = None) -> str: + url = f'https://webcast.amemv.com/webcast/room/reflow/info/?verifyFp=verify_lk07kv74_QZYCUApD_xhiB_405x_Ax51_GYO9bUIyZQVf&type_id=0&live_id=1&room_id={room_id}&sec_user_id={sec_user_id}&app_id=1128&msToken=wrqzbEaTlsxt52-vxyZo_mIoL0RjNi1ZdDe7gzEGMUTVh_HvmbLLkQrA_1HKVOa2C6gkxb6IiY6TY2z8enAkPEwGq--gM-me3Yudck2ailla5Q4osnYIHxd9dI4WtQ==' xbogus = get_xbogus(url) # 获取X-Bogus算法 url = url + "&X-Bogus=" + xbogus - # response = requests.get(url,headers=headers) - # json_data=response.json() - # 通通改成用urlib库,防止同时录制Tiktok直播时,代理影响requests请求出错 - req = urllib.request.Request(url, headers=headers) - response = opener.open(req, timeout=15) - html_str = response.read().decode('utf-8') - json_data = json.loads(html_str) - web_rid=json_data['data']['room']['owner']['web_rid'] - return web_rid + + if proxy_addr: + proxies = { + 'http': proxy_addr, + 'https': proxy_addr + } + response = requests.get(url, headers=headers, proxies=proxies, timeout=15) + json_str = response.text + else: + req = urllib.request.Request(url, headers=headers) + response = opener.open(req, timeout=15) + json_str = response.read().decode('utf-8') + json_data = json.loads(json_str) + return json_data['data']['room']['owner']['web_rid'] if __name__ == '__main__': - url="https://v.douyin.com/iQLgKSj/" + url = "https://v.douyin.com/iQLgKSj/" # url="https://v.douyin.com/iQFeBnt/" # url="https://v.douyin.com/iehvKttp/" - room_id,sec_user_id = get_sec_user_id(url) - web_rid=get_live_room_id(room_id,sec_user_id) + room_id, sec_user_id = get_sec_user_id(url) + web_rid = get_live_room_id(room_id, sec_user_id) print(web_rid) - - - -