diff --git a/README.md b/README.md index 389fc75..790c180 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ - [x] 百度直播 - [x] 微博直播 - [x] 酷狗直播 +- [x] TwitchTV - [ ] 更多平台正在更新中 @@ -161,6 +162,9 @@ https://weibo.com/l/wblive/p/show/1022:2321325026370190442592 酷狗直播: https://fanxing2.kugou.com/50428671?refer=2177&sourceFrom= + +TwitchTV: +https://www.twitch.tv/gamerbee ``` 直播间分享地址和网页端长地址都能正常进行录制(抖音尽量用长链接,避免因短链接转换失效导致不能正常录制,而且需要有nodejs环境,否则无法转换)。 @@ -293,9 +297,12 @@ docker-compose stop ## ⏳提交日志 +- 20240425 + - 新增TwitchTV直播录制 + - 20240424 - 新增酷狗直播录制、优化PopkonTV直播录制 - + - 20240423 - 新增百度直播录制、微博直播录制 diff --git a/config/config.ini b/config/config.ini index 9cc4770..c975773 100644 --- a/config/config.ini +++ b/config/config.ini @@ -17,7 +17,7 @@ ts录制完成后自动增加生成m4a格式 = 否 音频录制完成后自动转为mp3格式 = 否 追加格式后删除原文件 = 否 生成时间文件 = 否 -使用代理录制的平台(逗号分隔) = tiktok, afreecatv, pandalive, winktv, flextv, popkontv +使用代理录制的平台(逗号分隔) = tiktok, afreecatv, pandalive, winktv, flextv, popkontv, twitch 额外使用代理录制的平台(逗号分隔) = [推送配置] @@ -55,6 +55,7 @@ twitcasting_cookie = baidu_cookie = weibo_cookie = kugou_cookie = +twitch_cookie = [Authorization] popkontv_token = diff --git a/main.py b/main.py index c8076b0..4803b6d 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ Author: Hmily GitHub: https://github.com/ihmily Date: 2023-07-17 23:52:05 -Update: 2024-04-23 22:02:49 +Update: 2024-04-25 19:07:49 Copyright (c) 2023-2024 by Hmily, All Rights Reserved. Function: Record live stream video. """ @@ -52,7 +52,8 @@ from spider import ( get_twitcasting_stream_url, get_baidu_stream_data, get_weibo_stream_url, - get_kugou_stream_url + get_kugou_stream_url, + get_twitchtv_stream_data, ) from web_rid import ( @@ -67,7 +68,7 @@ from msg_push import dingtalk, xizhi, tg_bot version = "v3.0.3" platforms = "\n国内站点:抖音|快手|虎牙|斗鱼|YY|B站|小红书|bigo|blued|网易CC|千度热播|猫耳FM|Look|TwitCasting|百度|微博|酷狗" \ - "\n海外站点:TikTok|AfreecaTV|PandaTV|WinkTV|FlexTV|PopkonTV" + "\n海外站点:TikTok|AfreecaTV|PandaTV|WinkTV|FlexTV|PopkonTV|TwitchTV" # --------------------------全局变量------------------------------------- recording = set() @@ -727,6 +728,26 @@ def get_baidu_stream_url(json_data: dict, video_quality: str) -> dict: } +def get_twitchtv_stream_url(json_data: dict, video_quality: str) -> dict: + if not json_data['is_live']: + return json_data + + play_url_list = json_data['play_url_list'] + quality_list = {'原画': 0, '蓝光': 0, '超清': 1, '高清': 2, '标清': 3} + while len(play_url_list) < 4: + play_url_list.append(play_url_list[-1]) + + selected_quality = quality_list[video_quality] + m3u8_url = play_url_list[selected_quality] + + return { + "anchor_name": json_data['anchor_name'], + "is_live": True, + "m3u8_url": json_data['m3u8_url'], + "record_url": m3u8_url + } + + def push_message(content: str): push_pts = [] if '微信' in live_status_push: @@ -1007,6 +1028,19 @@ def start_record(url_data: tuple, count_variable: int = -1): port_info = get_kugou_stream_url( url=record_url, proxy_addr=proxy_address, cookies=kugou_cookie) + elif record_url.find("www.twitch.tv/") > -1: + platform = 'TwitchTV' + with semaphore: + if global_proxy or proxy_address: + json_data = get_twitchtv_stream_data( + url=record_url, + proxy_addr=proxy_address, + cookies=twitch_cookie + ) + port_info = get_twitchtv_stream_url(json_data, record_quality) + else: + logger.error(f"错误信息: 网络异常,请检查本网络是否能正常访问TwitchTV直播平台") + else: logger.error(f'{record_url} 未知直播地址') return @@ -1704,6 +1738,7 @@ while True: baidu_cookie = read_config_value(config, 'Cookie', 'baidu_cookie', '') weibo_cookie = read_config_value(config, 'Cookie', 'weibo_cookie', '') kugou_cookie = read_config_value(config, 'Cookie', 'kugou_cookie', '') + twitch_cookie = read_config_value(config, 'Cookie', 'twitch_cookie', '') if len(video_save_type) > 0: if video_save_type.upper().lower() == "FLV".lower(): @@ -1800,7 +1835,8 @@ while True: 'www.pandalive.co.kr', 'www.winktv.co.kr', 'www.flextv.co.kr', - 'www.popkontv.com' + 'www.popkontv.com', + 'www.twitch.tv', ] platform_host.extend(overseas_platform_host) @@ -1860,4 +1896,4 @@ while True: first_run = False - time.sleep(3) \ No newline at end of file + time.sleep(3) diff --git a/spider.py b/spider.py index 0d7a160..cc36c22 100644 --- a/spider.py +++ b/spider.py @@ -4,20 +4,20 @@ Author: Hmily GitHub:https://github.com/ihmily Date: 2023-07-15 23:15:00 -Update: 2024-04-23 23:42:27 +Update: 2024-04-25 19:05:11 Copyright (c) 2023 by Hmily, All Rights Reserved. Function: Get live stream data. """ import hashlib import random -import ssl import time import urllib.parse import urllib.error from urllib.request import Request from typing import Union, Dict, Any, Tuple import requests +import ssl import re import json import execjs @@ -42,7 +42,7 @@ def get_req( proxy_addr: Union[str, None] = None, headers: Union[dict, None] = None, data: Union[dict, bytes, None] = None, - json_data: dict = None, + json_data: Union[dict, list, None] = None, timeout: int = 20, abroad: bool = False ) -> Union[str, Any]: @@ -63,7 +63,7 @@ def get_req( else: if data and not isinstance(data, bytes): data = urllib.parse.urlencode(data).encode('utf-8') - if json_data and isinstance(json_data, dict): + if json_data and isinstance(json_data, (dict, list)): data = json.dumps(json_data).encode('utf-8') req = urllib.request.Request(url, data=data, headers=headers) @@ -116,8 +116,8 @@ def jsonp_to_json(jsonp_str): return None -def get_play_url_list(m3u8: str, proxy: Union[str, None] = None, header: Union[dict, None] = None) -> list: - resp = get_req(url=m3u8, proxy_addr=proxy, headers=header, abroad=True) +def get_play_url_list(m3u8: str, proxy: Union[str, None] = None, header: Union[dict, None] = None, abroad: bool = False) -> list: + resp = get_req(url=m3u8, proxy_addr=proxy, headers=header, abroad=abroad) play_url_list = [] for i in resp.split('\n'): if i.startswith('https://'): @@ -874,7 +874,7 @@ def get_pandatv_stream_data(url: str, proxy_addr: Union[str, None] = None, cooki play_url = json_data['PlayList']['hls'][0]['url'] result['m3u8_url'] = play_url result['is_live'] = True - result['play_url_list'] = get_play_url_list(m3u8=play_url, proxy=proxy_addr, header=headers) + result['play_url_list'] = get_play_url_list(m3u8=play_url, proxy=proxy_addr, header=headers, abroad=True) return result @@ -981,7 +981,7 @@ def get_winktv_stream_data(url: str, proxy_addr: Union[str, None] = None, cookie raise RuntimeError(json_data['errorData']['code'], json_data['message']) m3u8_url = json_data['PlayList']['hls'][0]['url'] result['m3u8_url'] = m3u8_url - result['play_url_list'] = get_play_url_list(m3u8=m3u8_url, proxy=proxy_addr, header=headers) + result['play_url_list'] = get_play_url_list(m3u8=m3u8_url, proxy=proxy_addr, header=headers, abroad=True) return result @@ -1104,7 +1104,7 @@ def get_flextv_stream_data( url=url, proxy_addr=proxy_addr, cookies=cookies, username=username, password=password) if play_url: result['m3u8_url'] = play_url - result['play_url_list'] = get_play_url_list(m3u8=play_url, proxy=proxy_addr, header=headers) + result['play_url_list'] = get_play_url_list(m3u8=play_url, proxy=proxy_addr, header=headers, abroad=True) except Exception as e: print('FlexTV直播间数据获取失败', e) return result @@ -1377,16 +1377,12 @@ def get_popkontv_stream_url( 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'] == "L00A1": - raise RuntimeError('获取直播源失败,该直播间需要赠送礼物才可观看') - + 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) @@ -1394,7 +1390,6 @@ def get_popkontv_stream_url( 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 @@ -1657,6 +1652,106 @@ def get_kugou_stream_url(url: str, proxy_addr: Union[str, None] = None, cookies: return result +def get_twitchtv_room_info(url: str, token: str, proxy_addr: Union[str, None] = None, + cookies: Union[str, None] = None): + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', + 'Accept-Language': 'zh-CN', + 'Referer': 'https://www.twitch.tv/', + 'Client-Id': 'kimne78kx3ncx6brgo4mv6wki5h1ko', + 'Client-Integrity': token, + 'Content-Type': 'text/plain;charset=UTF-8', + } + if cookies: + headers['Cookie'] = cookies + uid = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + + data = [ + { + "operationName": "ChannelShell", + "variables": { + "login": uid + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "580ab410bcd0c1ad194224957ae2241e5d252b2c5173d8e0cce9d32d5bb14efe" + } + } + }, + ] + + json_str = get_req('https://gql.twitch.tv/gql', proxy_addr=proxy_addr, headers=headers, json_data=data, abroad=True) + json_data = json.loads(json_str) + nickname = json_data[0]['data']['userOrError']['displayName'] + status = True if json_data[0]['data']['userOrError']['stream'] else False + return nickname, status + + +@trace_error_decorator +def get_twitchtv_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/124.0.0.0 Safari/537.36', + 'Accept-Language': 'en-US', + 'Referer': 'https://www.twitch.tv/', + 'Client-ID': 'kimne78kx3ncx6brgo4mv6wki5h1ko', + } + + if cookies: + headers['Cookie'] = cookies + uid = url.split('?')[0].rsplit('/', maxsplit=1)[-1] + + data = { + "operationName": "PlaybackAccessToken_Template", + "query": "query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isLive) { value signature authorization { isForbidden forbiddenReasonCode } __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}", + "variables": { + "isLive": True, + "login": uid, + "isVod": False, + "vodID": "", + "playerType": "site" + } + } + + json_str = get_req('https://gql.twitch.tv/gql', proxy_addr=proxy_addr, headers=headers, json_data=data, abroad=True) + json_data = json.loads(json_str) + token = json_data['data']['streamPlaybackAccessToken']['value'] + sign = json_data['data']['streamPlaybackAccessToken']['signature'] + + anchor_name, live_status = get_twitchtv_room_info(url=url, token=token, proxy_addr=proxy_addr, cookies=cookies) + result = {"anchor_name": anchor_name, "is_live": live_status} + if live_status: + play_session_id = random.choice(["bdd22331a986c7f1073628f2fc5b19da", "064bc3ff1722b6f53b0b5b8c01e46ca5"]) + params = { + "acmb": "e30=", + "allow_sourc": "true", + "browser_family": "firefox", + "browser_version": "124.0", + "cdm": "wv", + "fast_bread": "true", + "os_name": "Windows", + "os_version": "NT%2010.0", + "p": "3553732", + "platform": "web", + "play_session_id": play_session_id, + "player_backend": "mediaplayer", + "player_version": "1.28.0-rc.1", + "playlist_include_framerate": "true", + "reassignments_supported": "true", + "sig": sign, + "token": token, + "transcode_mode": "cbr_v1" + } + access_key = urllib.parse.urlencode(params) + m3u8_url = f'https://usher.ttvnw.net/api/channel/hls/{uid}.m3u8?{access_key}' + play_url_list = get_play_url_list(m3u8=m3u8_url, proxy=proxy_addr, header=headers, abroad=True) + result['m3u8_url'] = m3u8_url + result['play_url_list'] = play_url_list + + return result + + if __name__ == '__main__': # 尽量用自己的cookie,以避免默认的不可用导致无法获取数据 # 以下示例链接不保证时效性,请自行查看链接是否能正常访问 @@ -1691,6 +1786,7 @@ if __name__ == '__main__': # room_url = 'https://live.baidu.com/m/media/pclive/pchome/live.html?room_id=9175031377&tab_category' # 百度直播 # room_url = 'https://weibo.com/l/wblive/p/show/1022:2321325026370190442592' # 微博直播 # room_url = 'https://fanxing2.kugou.com/50428671?refer=2177&sourceFrom=' # 酷狗直播 + # room_url = 'https://www.twitch.tv/gamerbee' # TwitchTV print(get_douyin_stream_data(room_url, proxy_addr='')) # print(get_tiktok_stream_data(room_url, proxy_addr='')) @@ -1715,4 +1811,5 @@ if __name__ == '__main__': # print(get_twitcasting_stream_url(room_url, proxy_addr='', username='', password='')) # print(get_baidu_stream_data(room_url, proxy_addr='')) # print(get_weibo_stream_url(room_url, proxy_addr='')) - # print(get_kugou_stream_url(room_url, proxy_addr='')) \ No newline at end of file + # print(get_kugou_stream_url(room_url, proxy_addr='')) + # print(get_twitchtv_stream_data(room_url, proxy_addr=''))