├── LICENSE.txt ├── README.md ├── live_recorder ├── __init__.py ├── __main__.py ├── version.py └── you_live │ ├── __init__.py │ ├── _base_recorder.py │ ├── _recorder.py │ ├── acfun_recorder.py │ ├── bili_recorder.py │ ├── douyu_recorder.py │ ├── flv_checker.py │ ├── kuaishou_recorder.py │ ├── live_thread │ ├── __init__.py │ ├── download.py │ └── monitoring.py │ └── resources │ ├── __init__.py │ └── crypto.py ├── setup.py └── test └── record_test.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ============================================== 2 | This is a copy of the MIT license. 3 | ============================================== 4 | Copyright (C) 2020 NiceLee 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | you-live 2 | =========================== 3 | ![](https://img.shields.io/badge/Python-3-green.svg) ![](https://img.shields.io/badge/require-requests-green.svg)![](https://img.shields.io/badge/require-PyExecJS-green.svg) 4 | ### Live Recorder 5 | A live recorder focus on China mainland livestream sites. 6 | Brother Repo of [BilibiliLiveRecorder](https://github.com/nICEnnnnnnnLee/BilibiliLiveRecorder)(java) 7 | 8 | 9 | **** 10 | ## :dolphin:Installation 11 | ``` 12 | Linux/debian: 13 | 14 | sudo apt-get install python3-pip 15 | pip3 install you-live --upgrade --user 16 | add ~/.local/bin to your PATH 17 | 18 | Windows: 19 | 20 | install python3 from python.org 21 | pip install --upgrade you-live 22 | 23 | Other Linux: please follow debian 24 | 25 | Other OS: please DIY. 26 | ``` 27 | 28 | ## :dolphin:Usage 29 | ``` 30 | you-live [-h] [-qn QN] [-debug] [-check] [-delete] [-save_path SAVE_PATH] [-check_path CHECK_PATH] 31 | [-format FORMAT] [-time_format TIME_FORMAT] [-cookies COOKIES] [-cookies_path COOKIES_PATH] 32 | liver id 33 | 34 | B站/斗鱼/快手 直播视频录制 35 | 36 | positional arguments: 37 | liver 要录制的直播源,如 bili,douyu,kuaishou,acfun 38 | id 要录制的房间号,可以从url中直接获取 39 | 40 | optional arguments: 41 | -h, --help show this help message and exit 42 | -qn QN, -q QN 录制的清晰度,可以后续输入 43 | -only_url, -ou 仅输出录制链接,然后退出 44 | -debug debug模式 45 | -check 校准时间戳 46 | -delete, -d 删除原始文件 47 | -save_path SAVE_PATH, -sp SAVE_PATH 48 | 源文件保存路径 49 | -check_path CHECK_PATH, -chp CHECK_PATH 50 | 校正后的FLV文件保存路径 51 | -format FORMAT, -f FORMAT 52 | 文件命名格式 53 | -time_format TIME_FORMAT, -tf TIME_FORMAT 54 | 时间格式 55 | -cookies COOKIES, -c COOKIES 56 | cookie, 当cookies_path未指定时生效 57 | -cookies_path COOKIES_PATH, -cp COOKIES_PATH 58 | 指定cookie文件位置 59 | ``` 60 | 61 | ### Example0 62 | Record a live from 63 | ``` 64 | you-live bili 6 65 | ``` 66 | 67 | ### Example1 68 | Record a live from , correct the timestamp error and delete the origin files. 69 | ``` 70 | you-live -check -d douyu 593392 71 | ``` 72 | **Notice**:The record on this site(douyu) uses PyExecJS. 73 | You may need some extra installation about the JS Environment for linux OS. 74 | Here’s the guide for [Node.js installation](https://github.com/nodesource/distributions) 75 | 76 | 77 | **Notice**:You may need logged-in cookies to get high quality videos 78 | 79 | ### Example2 80 | Record a live from , speicify the file name you want. 81 | ``` 82 | you-live -format "{name}-{shortId} 的{liver}直播{startTime}" -cookies "clientid=3; did=web_0000000000000000000000000000000; client_key=00000000; xxx=xxx; ..." kuaishou ZFYS8888 83 | ``` 84 | **Notice**:You must need cookies(may not logged-in, just skip the captha test) to get room detail information 85 | 86 | 87 | ## :dolphin:LICENSE 88 | MIT 89 | 90 | 91 | -------------------------------------------------------------------------------- /live_recorder/__init__.py: -------------------------------------------------------------------------------- 1 | from . import you_live 2 | 3 | -------------------------------------------------------------------------------- /live_recorder/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from live_recorder import you_live 3 | from live_recorder import version 4 | 5 | args = None 6 | 7 | def arg_parser(): 8 | parser = argparse.ArgumentParser(prog='you-live', description="version %s : %s"%(version.__version__, version.__descriptrion__)) 9 | parser.add_argument("liver", help="要录制的直播源,如 bili,douyu,kuaishou,acfun") 10 | parser.add_argument("id", help="要录制的房间号,可以从url中直接获取") 11 | parser.add_argument("-qn", "-q", help="录制的清晰度,可以后续输入", required=False, default=None) 12 | parser.add_argument("-only_url", "-ou", help="仅输出录制链接,然后退出", required=False, action='store_true', default=False) 13 | parser.add_argument("-debug", help="debug模式", required=False, action='store_true', default=False) 14 | parser.add_argument("-check", help="校准时间戳", required=False, action='store_true', default=False) 15 | parser.add_argument("-delete", '-d', help="删除原始文件", required=False, action='store_true', default=False) 16 | parser.add_argument("-save_path", '-sp', help="源文件保存路径", required=False, default='./download') 17 | parser.add_argument("-check_path", '-chp', help="校正后的FLV文件保存路径", required=False, default=None) 18 | parser.add_argument("-format", '-f', help="文件命名格式", required=False, default='{name}-{shortId} 的{liver}直播{startTime}-{endTime}') 19 | parser.add_argument("-time_format", '-tf', help="时间格式", required=False, default='%Y%m%d_%H-%M') 20 | parser.add_argument("-cookies", '-c', help="cookie, 当cookies_path未指定时生效", required=False, default=None) 21 | parser.add_argument("-cookies_path", '-cp', help="指定cookie文件位置", required=False, default=None) 22 | 23 | global args 24 | args = parser.parse_args() 25 | # args = parser.parse_args(('-delete bili 6').split()) 26 | # print(args); 27 | 28 | 29 | def main(): 30 | arg_parser() 31 | liver = args.liver 32 | debug = args.debug 33 | params = {} 34 | params['save_folder'] = args.save_path 35 | params['flv_save_folder'] = args.check_path 36 | params['delete_origin_file'] = args.delete 37 | params['check_flv'] = args.check 38 | params['file_name_format'] = args.format 39 | params['time_format'] = args.time_format 40 | params['cookies'] = args.cookies 41 | params['debug'] = args.debug 42 | if args.cookies_path: 43 | try: 44 | with open(args.cookies_path,"r", encoding='utf-8') as f: 45 | params['cookies'] = f.read() 46 | except: 47 | print(args.cookies_path) 48 | print('指定cookie路径不存在') 49 | 50 | recorder = you_live.Recorder.createRecorder(liver, args.id, **params) 51 | 52 | # 获取房间信息 53 | roomInfo = recorder.getRoomInfo() 54 | if debug: 55 | print(roomInfo) 56 | 57 | # 获取如果在直播,那么录制 58 | if roomInfo['live_status'] == '1': 59 | print(roomInfo['live_rates']) 60 | if args.qn: 61 | qn = args.qn 62 | else: 63 | qn = input("输入要录制的清晰度\r\n") 64 | 65 | live_url = recorder.getLiveUrl(qn = qn) 66 | if args.only_url: 67 | print("以下为录制链接:") 68 | print(live_url) 69 | exit(0) 70 | if debug: 71 | print(live_url) 72 | download_thread = you_live.DownloadThread(recorder) 73 | monitoring_thread = you_live.MonitoringThread(recorder) 74 | 75 | download_thread.start() 76 | monitoring_thread.start() 77 | 78 | while recorder.downloadFlag: 79 | todo = input("输入q或stop停止录制\r\n") 80 | if todo == "q" or todo == "stop": 81 | recorder.downloadFlag = False 82 | else: 83 | print("请输入合法命令!!!") 84 | else: 85 | print("主播当前不在线!!") 86 | 87 | if __name__ == '__main__': 88 | main() -------------------------------------------------------------------------------- /live_recorder/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __version__ = "1.1.5" 5 | __descriptrion__ = "A站/B站/斗鱼/快手 直播视频录制" -------------------------------------------------------------------------------- /live_recorder/you_live/__init__.py: -------------------------------------------------------------------------------- 1 | from .flv_checker import Flv 2 | from .live_thread.download import DownloadThread 3 | from .live_thread.monitoring import MonitoringThread 4 | from . import _recorder as Recorder 5 | import pkgutil 6 | import inspect 7 | import os 8 | 9 | filepath, tmpfilename = os.path.split(inspect.getfile(Recorder)) 10 | 11 | for filefiner, name, ispkg in pkgutil.iter_modules([filepath]): 12 | if not ispkg and not name.startswith("_") and name.endswith("_recorder"): 13 | __import__(__name__ + "." + name) -------------------------------------------------------------------------------- /live_recorder/you_live/_base_recorder.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import requests 3 | import os 4 | import time 5 | import re 6 | from .flv_checker import Flv 7 | 8 | recorders = {} 9 | 10 | 11 | def recorder(liver): 12 | assert liver != None 13 | def clazz(cls): 14 | recorders[liver] = cls 15 | cls.liver = liver 16 | return cls 17 | return clazz 18 | 19 | class BaseRecorder: 20 | 21 | def __init__(self, short_id, cookies = None, \ 22 | save_folder = '../download', \ 23 | flv_save_folder = None, \ 24 | delete_origin_file = False, check_flv = True,\ 25 | file_name_format = "{name}-{shortId} 的{liver}直播{startTime}-{endTime}",\ 26 | time_format = "%Y%m%d_%H-%M",\ 27 | debug = False): 28 | self.short_id = str(short_id) 29 | self.cookies = cookies 30 | self.delete_origin_file = delete_origin_file 31 | self.check_flv = check_flv 32 | 33 | self.save_folder = save_folder.rstrip('\\').rstrip('/') 34 | self.flv_save_folder = flv_save_folder 35 | self.file_name_format = file_name_format 36 | self.time_format = time_format 37 | self.debug = debug 38 | 39 | 40 | 41 | self.downloaded = 0 42 | self.downloadFlag = True 43 | 44 | # def getRoomInfo(self): 45 | # roomInfo = {} 46 | # roomInfo['short_id'] = self.short_id 47 | # roomInfo['room_id'] = searchObj.group(1) 48 | # roomInfo['live_status'] = searchObj.group(1) 49 | # roomInfo['room_title'] = searchObj.group(1) 50 | # roomInfo['room_description'] = searchObj.group(1) 51 | # roomInfo['room_owner_id'] = searchObj.group(1) 52 | # roomInfo['room_owner_name'] = searchObj.group(1) 53 | # if roomInfo['live_status'] == '1': 54 | # roomInfo['live_rates'] = quality 55 | # return roomInfo 56 | 57 | # def getLiveUrl(self, qn): 58 | # if not hasattr(self, 'roomInfo'): 59 | # self.getRoomInfo() 60 | # if self.roomInfo['live_status'] != '1': 61 | # print('当前没有在直播') 62 | # return None 63 | # self.live_url = "" 64 | # self.live_qn = "" 65 | # return self.live_url 66 | 67 | def startRecord(self, path = None, qn = 0, headers = None): 68 | try: 69 | if not hasattr(self, 'live_url'): 70 | self.getLiveUrl(qn) 71 | if hasattr(self, 'download_headers'): 72 | headers = self.download_headers 73 | 74 | if path == None: 75 | # 如果没有指定path,根据自定义文件名来生成 76 | roomInfo = self.roomInfo 77 | filename = self.file_name_format.replace("{name}", roomInfo['room_owner_name']) 78 | filename = filename.replace("{shortId}", roomInfo['short_id']) 79 | filename = filename.replace("{roomId}", roomInfo['room_id']) 80 | filename = filename.replace("{liver}", self.liver) 81 | filename = filename.replace("{seq}", '0') 82 | current_time = time.strftime(self.time_format, time.localtime()) 83 | filename = filename.replace("{startTime}", current_time) 84 | filename = re.sub(r"[\/\\\:\?\"\<\>\|\t']", '_', filename) 85 | 86 | if not os.path.exists(self.save_folder): 87 | os.makedirs(self.save_folder) 88 | 89 | path = os.path.abspath('{}/{}.flv'.format(self.save_folder, filename)) 90 | 91 | with open(path,"wb") as file: 92 | response = requests.get(self.live_url, stream=True, headers=headers, timeout=120) 93 | for data in response.iter_content(chunk_size=1024*1024): 94 | if not self.downloadFlag: 95 | break 96 | if data: 97 | file.write(data) 98 | self.downloaded += len(data) 99 | response.close() 100 | 101 | if '{endTime}' in path: 102 | current_time = time.strftime(self.time_format, time.localtime()) 103 | filename = filename.replace("{endTime}", current_time) 104 | filename = re.sub(r"[\/\\\:\*\?\"\<\>\|\s']", '_', filename) 105 | new_path = os.path.abspath('{}/{}.flv'.format(self.save_folder, filename)) 106 | os.rename(path, new_path) 107 | path = new_path 108 | 109 | if self.check_flv: 110 | print("正在校准时间戳") 111 | flv = Flv(path, self.flv_save_folder, self.debug) 112 | flv.check() 113 | if self.delete_origin_file: 114 | os.remove(path) 115 | 116 | self.downloadFlag = False 117 | 118 | except Exception as e: 119 | print(e) 120 | self.downloadFlag = False 121 | raise e 122 | 123 | def stopRecord(self): 124 | self.downloadFlag = False 125 | 126 | -------------------------------------------------------------------------------- /live_recorder/you_live/_recorder.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from ._base_recorder import recorders 3 | 4 | 5 | def createRecorder(liver, short_id, **args): 6 | if liver in recorders: 7 | return recorders[liver](short_id, **args) 8 | else: 9 | return None 10 | -------------------------------------------------------------------------------- /live_recorder/you_live/acfun_recorder.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import requests 3 | import re 4 | import json 5 | from ._base_recorder import BaseRecorder, recorder 6 | 7 | @recorder(liver = 'acfun') 8 | class AcfunRecorder(BaseRecorder): 9 | 10 | def __init__(self, short_id, **args): 11 | BaseRecorder.__init__(self, short_id, **args) 12 | 13 | 14 | def getRoomInfo(self): 15 | roomInfo = {} 16 | roomInfo['short_id'] = self.short_id 17 | roomInfo['room_id'] = self.short_id 18 | roomInfo['room_owner_id'] = self.short_id 19 | h_session = requests.session() 20 | 21 | # 先访问获取cookie 22 | common_url = "https://m.acfun.cn/live/detail/%s"%self.short_id 23 | common_headers = { 24 | 'Host': "m.acfun.cn", 25 | 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 26 | 'Accept-Encoding': 'gzip, deflate, br', 27 | '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', 28 | 'User-Agent': 'Mozilla/5.0 (Android 9.0; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0', 29 | } 30 | html = h_session.get(common_url, timeout=10, headers=common_headers).text 31 | # with open('html.txt', 'w', encoding='utf-8') as f: 32 | # f.write(html) 33 | searchObj = re.search(r"(.*?)正在直播", html) 34 | roomInfo['room_owner_name'] = searchObj.group(1) 35 | 36 | if "直播已结束" in html: 37 | roomInfo['live_status'] = '0' 38 | else: 39 | roomInfo['live_status'] = '1' 40 | 41 | if roomInfo['live_status'] == '1': 42 | searchObj = re.search(r'<h1 class="live-content-title-text">(.*?)</h1>', html) 43 | roomInfo['room_title'] = searchObj.group(1) 44 | roomInfo['room_description'] = roomInfo['room_title'] 45 | 46 | # 从cookie获取did参数 47 | _did = dict(h_session.cookies)['_did'] 48 | 49 | # 游客登录,获取参数 50 | login_url = "https://id.app.acfun.cn/rest/app/visitor/login"; 51 | login_param = {'sid':'acfun.api.visitor'} 52 | login_headers = { 53 | 'Host': "id.app.acfun.cn", 54 | 'Accept': "application/json, text/plain, */*", 55 | 'Content-Type': "application/x-www-form-urlencoded", 56 | 'Origin': "https://m.acfun.cn", 57 | 'Referer': "https://m.acfun.cn", 58 | 'Accept-Encoding': 'gzip, deflate, br', 59 | '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', 60 | 'User-Agent': 'Mozilla/5.0 (Android 9.0; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0', 61 | } 62 | data_json = h_session.post(login_url, login_param, timeout=10, headers=login_headers).json() 63 | userId = data_json['userId'] 64 | api_st = data_json['acfun.api.visitor_st'] 65 | 66 | #根据参数组装,获取直播可提供的清晰度 67 | self.query_url = "https://api.kuaishouzt.com/rest/zt/live/web/startPlay?subBiz=mainApp&kpn=ACFUN_APP&userId=%s&did=%s&acfun.api.visitor_st=%s"%(userId, _did, api_st) 68 | self.query_param = {'authorId':self.short_id} 69 | self.query_headers = { 70 | 'Host': "api.kuaishouzt.com", 71 | 'Accept': "application/json, text/plain, */*", 72 | 'Content-Type': "application/x-www-form-urlencoded", 73 | 'Origin': "https://m.acfun.cn", 74 | 'Referer': "https://m.acfun.cn/live/detail/%s"%self.short_id, 75 | 'Accept-Encoding': 'gzip, deflate, br', 76 | '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', 77 | 'User-Agent': 'Mozilla/5.0 (Android 9.0; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0', 78 | } 79 | data_json = h_session.post(self.query_url, self.query_param, timeout=10, headers=self.query_headers).json()["data"] 80 | # 81 | qnArray = json.loads(data_json['videoPlayRes'])['liveAdaptiveManifest'][0]['adaptationSet']['representation'] 82 | quality = {} 83 | for rate in qnArray: 84 | quality[str(rate['id'])] = rate['name'] 85 | roomInfo['live_rates'] = quality 86 | # self.headers = headers 87 | self.roomInfo = roomInfo 88 | self.session = h_session 89 | return roomInfo 90 | 91 | def getLiveUrl(self, qn): 92 | if not hasattr(self, 'roomInfo'): 93 | self.getRoomInfo() 94 | if self.roomInfo['live_status'] != '1': 95 | print('当前没有在直播') 96 | return None 97 | 98 | data_json = self.session.post(self.query_url, self.query_param, timeout=10, headers=self.query_headers).json()["data"] 99 | # 100 | qnArray = json.loads(data_json['videoPlayRes'])['liveAdaptiveManifest'][0]['adaptationSet']['representation'] 101 | print(qnArray[0]) 102 | for rate in qnArray: 103 | if qn == str(rate['id']): 104 | self.live_url = rate['url'] 105 | self.live_qn = rate['id'] 106 | break 107 | 108 | if not hasattr(self, 'live_url'): 109 | self.live_url = qnArray[0]['url'] 110 | self.live_qn = qnArray[0]['id'] 111 | 112 | print("申请清晰度 %s的链接,得到清晰度 %d的链接"%(qn, self.live_qn)) 113 | self.download_headers = { 114 | 'User-Agent': 'Mozilla/5.0 (Android 9.0; Mobile; rv:68.0) Gecko/68.0 Firefox/68.0', 115 | } 116 | return self.live_url 117 | -------------------------------------------------------------------------------- /live_recorder/you_live/bili_recorder.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import requests 3 | from ._base_recorder import BaseRecorder, recorder 4 | 5 | @recorder(liver = 'bili') 6 | class BiliRecorder(BaseRecorder): 7 | 8 | def __init__(self, short_id, **args): 9 | BaseRecorder.__init__(self, short_id, **args) 10 | 11 | 12 | def getRoomInfo(self): 13 | roomInfo = {} 14 | roomInfo['short_id'] = self.short_id 15 | 16 | url = "https://api.live.bilibili.com/room/v1/Room/get_info?id=%s&from=room"%self.short_id 17 | headers = { 18 | 'Accept': 'application/json, text/plain, */*', 19 | #'Accept-Encoding': 'gzip, deflate, br', 20 | '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', 21 | 'Origin': 'https://live.bilibili.com', 22 | 'Referer': 'https://live.bilibili.com/blanc/%s'%self.short_id, 23 | 'X-Requested-With': 'ShockwaveFlash/28.0.0.137', 24 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0', 25 | } 26 | data_json = requests.get(url, timeout=10, headers=headers).json()['data'] 27 | roomInfo['room_id'] = str(data_json['room_id']) 28 | roomInfo['live_status'] = str(data_json['live_status']) 29 | roomInfo['room_title'] = data_json['title'] 30 | roomInfo['room_description'] = data_json['description'] 31 | roomInfo['room_owner_id'] = data_json['uid'] 32 | 33 | if roomInfo['live_status'] == '1': 34 | url = "https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room?roomid=%s"%roomInfo['room_id'] 35 | data_json = requests.get(url, timeout=10, headers=headers).json()['data'] 36 | roomInfo['room_owner_name'] = data_json['info']['uname'] 37 | 38 | quality = {} 39 | url = "https://api.live.bilibili.com/room/v1/Room/playUrl?cid=%s&quality=%s&platform=web"%(roomInfo['room_id'], 0) 40 | multirates = requests.get(url, timeout=10, headers=headers).json()['data']['quality_description'] 41 | for rate in multirates: 42 | quality[str(rate['qn'])] = rate['desc'] 43 | roomInfo['live_rates'] = quality 44 | self.headers = headers 45 | self.roomInfo = roomInfo 46 | return roomInfo 47 | 48 | def getLiveUrl(self, qn): 49 | if not hasattr(self, 'roomInfo'): 50 | self.getRoomInfo() 51 | if self.roomInfo['live_status'] != '1': 52 | print('当前没有在直播') 53 | return None 54 | 55 | url = "https://api.live.bilibili.com/room/v1/Room/playUrl?cid=%s&quality=%s&platform=web"%(self.roomInfo['room_id'], qn) 56 | data_json = requests.get(url, timeout=10, headers=self.headers).json()['data'] 57 | # print(data_json) 58 | self.live_url = data_json['durl'][0]['url'] 59 | # self.live_qn = data_json['current_quality'] 60 | self.live_qn = data_json['current_qn'] 61 | print("申请清晰度 %s的链接,得到清晰度 %d的链接"%(qn, self.live_qn)) 62 | self.download_headers = { 63 | 'Accept': 'application/json, text/plain, */*', 64 | 'Accept-Encoding': 'gzip, deflate, br', 65 | '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', 66 | 'Origin': 'https://live.bilibili.com', 67 | 'Referer': 'https://live.bilibili.com/%s'%self.short_id, 68 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0', 69 | } 70 | return self.live_url -------------------------------------------------------------------------------- /live_recorder/you_live/douyu_recorder.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import requests 3 | import execjs 4 | import time 5 | import re 6 | import random 7 | from ._base_recorder import BaseRecorder, recorder 8 | from .resources import crypto_js 9 | 10 | @recorder(liver = 'douyu') 11 | class DouyuRecorder(BaseRecorder): 12 | 13 | def __init__(self, short_id, **args): 14 | BaseRecorder.__init__(self, short_id, **args) 15 | if self.cookies == None: 16 | self.dy_did = ''.join(random.sample('1234567890qwertyuiopasdfghjklzxcvbnm', 32)) 17 | else: 18 | searchObj = re.search("dy_did=([^&; ]+)", self.cookies) 19 | self.dy_did = searchObj.group(1) 20 | 21 | def getRoomInfo(self): 22 | roomInfo = {} 23 | roomInfo['short_id'] = self.short_id 24 | 25 | url = "https://www.douyu.com/%s"%self.short_id 26 | headers = { 27 | 'Origin': 'https://www.douyu.com', 28 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0', 29 | 'Accept': 'application/json, text/plain, */*', 30 | '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', 31 | 'Accept-Encoding': 'gzip, deflate, br', 32 | } 33 | if not self.cookies is None: 34 | headers['Cookie'] = self.cookies 35 | 36 | http_result = requests.get(url, timeout=10, headers=headers) 37 | # print(http_result.text) 38 | searchObj = re.search( r'\$ROOM.room_id ?= ?([0-9]+);', http_result.text) 39 | 40 | roomInfo['room_id'] = searchObj.group(1) 41 | searchObj = re.search( r'\$ROOM.show_status ?= ?([0-9]+);', http_result.text) 42 | roomInfo['live_status'] = searchObj.group(1) 43 | searchObj = re.search( r'<h[0-9] class=\"Title-headlineH2\">([^/]*)</h[0-9]>', http_result.text) 44 | if searchObj: 45 | roomInfo['room_title'] = searchObj.group(1) 46 | else: 47 | searchObj = re.search( r'<title>([^/]*)', http_result.text) 48 | roomInfo['room_title'] = searchObj.group(1) 49 | searchObj = re.search( r'

([^/]*)

', http_result.text) 50 | if searchObj: 51 | roomInfo['room_description'] = searchObj.group(1) 52 | else: 53 | roomInfo['room_description'] = '无' 54 | searchObj = re.search( r'\$ROOM.owner_uid ?= ?([0-9]+);', http_result.text) 55 | roomInfo['room_owner_id'] = searchObj.group(1) 56 | searchObj = re.search( r'", begin) 72 | js_code = crypto_js + '\r\n' 73 | js_code += http_result.text[begin:end] 74 | self.js_code = js_code 75 | 76 | ctx = execjs.compile(js_code) 77 | param = ctx.call("ub98484234", roomInfo['room_id'], self.dy_did, int(time.time())) 78 | param += "&cdn=&rate=%d&ver=%s&iar=0&ive=1"%(0, "Douyu_219052705") 79 | 80 | self.api_headers = { 81 | 'Accept': 'application/json, text/plain, */*', 82 | 'Accept-Encoding': 'gzip, deflate, br', 83 | 'Accept-Language': 'zh-CN,zh;q=0.8', 84 | 'content-type': 'application/x-www-form-urlencoded', 85 | 'x-requested-with': 'XMLHttpRequest', 86 | 'Origin': 'https://www.douyu.com', 87 | 'Referer': "https://www.douyu.com/topic/xyb01?rid=%s"%self.short_id, 88 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0', 89 | } 90 | if not self.cookies is None: 91 | self.api_headers['Cookie'] = self.cookies 92 | 93 | http_result = requests.post(self.api_url, timeout=10, headers=self.api_headers, data=param) 94 | multirates = http_result.json()['data']['multirates'] 95 | for rate in multirates: 96 | quality[str(rate['rate'])] = rate['name'] 97 | 98 | roomInfo['live_rates'] = quality 99 | 100 | self.roomInfo = roomInfo 101 | return roomInfo 102 | 103 | def getLiveUrl(self, qn): 104 | if not hasattr(self, 'roomInfo'): 105 | self.getRoomInfo() 106 | if self.roomInfo['live_status'] != '1': 107 | print('当前没有在直播') 108 | return None 109 | 110 | ctx = execjs.compile(self.js_code) 111 | param = ctx.call("ub98484234", self.roomInfo['room_id'], self.dy_did, int(time.time())) 112 | param += "&cdn=&rate=%s&ver=%s&iar=0&ive=1"%(qn, "Douyu_219052705") 113 | json_result = requests.post(self.api_url, timeout=10, headers=self.api_headers, data=param).json() 114 | 115 | print("申请清晰度 %s的链接,得到清晰度 %d的链接"%(qn, json_result['data']['rate'])) 116 | header = json_result['data']['rtmp_url'] 117 | tail = json_result['data']['rtmp_live'] 118 | 119 | self.live_url = header + "/" + tail 120 | self.live_qn = json_result['data']['rate'] 121 | return self.live_url 122 | 123 | -------------------------------------------------------------------------------- /live_recorder/you_live/flv_checker.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | import struct 4 | 5 | class Flv(object): 6 | 7 | def __init__(self, path, dest_folder = None, debug = False): 8 | self.path = path 9 | self.debug = debug 10 | if dest_folder != None: 11 | self.dest_folder = dest_folder.rstrip('\\').rstrip('/') 12 | else: 13 | self.dest_folder = None 14 | 15 | 16 | 17 | def check(self): 18 | if self.dest_folder == None: 19 | file_folder = os.path.dirname(os.path.realpath(self.path)) 20 | else: 21 | file_folder = self.dest_folder 22 | file_name = os.path.basename(os.path.realpath(self.path)) 23 | file_short_name, file_extension = os.path.splitext(file_name) 24 | 25 | path_new = os.path.join(file_folder, file_short_name + '-checked0.flv') 26 | print(path_new) 27 | with open(self.path,"rb") as origin: 28 | with open(path_new,"wb+") as dest: 29 | # 复制头部 30 | data = origin.read(9) 31 | dest.write(data) 32 | # 处理Tag内容 33 | self.checkTag(origin, dest) 34 | 35 | self.changeDuration(path_new, float(self.lastTimestampWrite[b'\x08']) / 1000) 36 | 37 | def checkTag(self, origin, dest): 38 | currentLength = 9 39 | latsValidLength = currentLength 40 | 41 | self.lastTimestampRead = { b'\x08':-1, b'\x09':-1 } 42 | self.lastTimestampWrite = { b'\x08':-1, b'\x09':-1 } 43 | 44 | isFirstScriptTag = True 45 | remain = 10 46 | while True:# and remain >0: 47 | remain -=1 48 | # 读取前一个tag size 49 | data = origin.read(4) 50 | # predataSize = int.from_bytes(data, byteorder='big', signed=False) 51 | # print("前一个 tagSize:", predataSize) 52 | dest.write(data) 53 | # 记录当前新文件位置,若下一tag无效,则需要回退 54 | latsValidLength = currentLength 55 | currentLength = dest.tell() 56 | 57 | # 读取tag类型 58 | tagType = origin.read(1) 59 | # print("当前tag 类型为:", tagType) 60 | if tagType == b'\x08' or tagType == b'\x09': # 8/9 audio/video 61 | dest.write(tagType) 62 | # tag data size 3个字节。表示tag data的长度。从streamd id 后算起。 63 | data = origin.read(3) 64 | dest.write(data) 65 | dataSize = int.from_bytes(data, byteorder='big', signed=False) 66 | # print("当前tag data 长度为:", dataSize) 67 | 68 | # 时间戳 3 + 1 69 | timeData = origin.read(3) 70 | timeDataEx = origin.read(1) 71 | timestamp = int.from_bytes(timeData, byteorder='big', signed=False) 72 | timestamp |= (int.from_bytes(timeDataEx, byteorder='big', signed=False) << 24) 73 | self.dealTimeStamp(dest, timestamp, tagType) 74 | # print("当前timestamp 长度为:", timestamp) 75 | 76 | # 数据 77 | data = origin.read(3 + dataSize) 78 | dest.write(data) 79 | elif tagType == b'\x12': # scripts 80 | # 如果是scripts脚本,默认为第一个tag,此时将前一个tag Size 置零 81 | dest.seek(dest.tell() - 4) 82 | dest.write(b'\x00\x00\x00\x00') 83 | dest.write(tagType) 84 | isFirstScriptTag = False 85 | # tag data size 3个字节。表示tag data的长度。从streamd id 后算起。 86 | data = origin.read(3) 87 | dest.write(data) 88 | dataSize = int.from_bytes(data, byteorder='big', signed=False) 89 | # print("当前tag data 长度为:" , dataSize) 90 | # 时间戳 0 91 | origin.read(4) 92 | dest.write(b'\x00\x00\x00\x00') 93 | # 数据 94 | data = origin.read(3 + dataSize) 95 | dest.write(data) 96 | else: 97 | if self.debug: 98 | print("未知类型", tagType) 99 | dest.truncate(latsValidLength) 100 | break 101 | 102 | def dealTimeStamp(self, dest, timestamp, tagType): 103 | # print("上一帧读取timestamps 为:" , self.lastTimestampRead[tagType]) 104 | # print("上一帧写入timestamps 为:" , self.lastTimestampWrite[tagType]) 105 | # 如果是首帧 106 | if self.lastTimestampRead[tagType] == -1: 107 | self.lastTimestampWrite[tagType] = 0 108 | elif timestamp >= self.lastTimestampRead[tagType]: # 如果时序正常 109 | # 间隔十分巨大(1s),那么重新开始即可 110 | if timestamp > self.lastTimestampRead[tagType] + 1000: 111 | self.lastTimestampWrite[tagType] += 10 112 | if self.debug: 113 | print("---") 114 | else: 115 | self.lastTimestampWrite[tagType] = timestamp - self.lastTimestampRead[tagType] + self.lastTimestampWrite[tagType] 116 | else: #如果出现倒序时间戳 117 | # 如果间隔不大,那么如实反馈 118 | if self.lastTimestampRead[tagType] - timestamp < 5 * 1000: 119 | tmp = timestamp - self.lastTimestampRead[tagType] + self.lastTimestampWrite[tagType] 120 | if tmp < 0 : tmp = 1 121 | self.lastTimestampWrite[tagType] = tmp 122 | else: # 间隔十分巨大,那么重新开始即可 123 | self.lastTimestampWrite[tagType] += 10 124 | if self.debug: 125 | print("---rewind") 126 | self.lastTimestampRead[tagType] = timestamp 127 | 128 | # 低于0xffffff部分 129 | lowCurrenttime = self.lastTimestampWrite[tagType] & 0xffffff 130 | dest.write(lowCurrenttime.to_bytes(3,byteorder='big')) 131 | # 高于0xffffff部分 132 | highCurrenttime = self.lastTimestampWrite[tagType] >> 24 133 | dest.write(highCurrenttime.to_bytes(1,byteorder='big')) 134 | if self.debug: 135 | print(" 读取timestamps 为:%s, 写入timestamps 为:%s"%(timestamp, self.lastTimestampWrite[tagType])) 136 | 137 | 138 | def changeDuration(self, path, duration): 139 | if self.debug: 140 | print(duration) 141 | durationHeader = b"\x08\x64\x75\x72\x61\x74\x69\x6f\x6e" 142 | pointer = 0 143 | # 先找到 08 64 75 72 61 74 69 6f 6e所在位置 144 | with open(path,"rb+") as file: 145 | data = file.read(1024*20) 146 | i = 0 147 | findHeader = False 148 | while i < len(data): 149 | if data[i] == durationHeader[pointer]: 150 | pointer += 1 151 | # 如果完全包含durationHeader头部,则可以返回 152 | if pointer == len(durationHeader): 153 | findHeader = True 154 | break; 155 | else: 156 | pointer = 0 157 | i += 1 158 | 159 | if findHeader: 160 | file.seek(i + 1); 161 | file.write(b"\x00") 162 | file.write(struct.pack('>d', duration)) 163 | else: 164 | if self.debug: 165 | print("没有找到duration标签") 166 | 167 | 168 | if __name__ == '__main__': 169 | flv = Flv(r"D:\Workspace\PythonWork\LiveRecorder\live\test-checked0.flv") 170 | flv = Flv(r"test.flv") 171 | flv.check() 172 | # flv.changeDuration(flv.path, 123) 173 | -------------------------------------------------------------------------------- /live_recorder/you_live/kuaishou_recorder.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import requests, re, json 3 | from ._base_recorder import BaseRecorder, recorder 4 | 5 | @recorder(liver = 'kuaishou') 6 | class KuaishouRecorder(BaseRecorder): 7 | 8 | def __init__(self, short_id, **args): 9 | BaseRecorder.__init__(self, short_id, **args) 10 | 11 | def getLiveInfo(self): 12 | url = "https://live.kuaishou.com/u/" + self.short_id 13 | headers = { 14 | 'Accept': 'application/json, text/plain, */*', 15 | 'Accept-Encoding': 'gzip, deflate, br', 16 | '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', 17 | 'content-type': 'application/json', 18 | 'Origin': 'https://live.kuaishou.com', 19 | 'Referer': 'https://live.kuaishou.com/u/%s'%self.short_id, 20 | 'X-Requested-With': 'ShockwaveFlash/28.0.0.137', 21 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0', 22 | } 23 | self.headers = headers 24 | if not self.cookies is None: 25 | headers['Cookie'] = self.cookies 26 | 27 | html = requests.get(url, timeout=10, headers=headers).text 28 | searchObj = re.search("window\\.__INITIAL_STATE__ *= *(\\{.*?\\}) *; *\\(function\\(\\)", html) 29 | json_raw = searchObj.group(1) 30 | #print(json_raw) 31 | json_str = json_raw.replace("undefined", "null") 32 | return json.loads(json_str)["liveroom"]["playList"][0] 33 | 34 | def getRoomInfo(self): 35 | roomInfo = {} 36 | roomInfo['short_id'] = self.short_id 37 | roomInfo['room_id'] = self.short_id 38 | 39 | if self.cookies is None: 40 | print('缺少cookies(无需登录)') 41 | return None 42 | 43 | room_json = self.getLiveInfo() 44 | live_data_json = room_json["liveStream"] 45 | user_data_json = room_json["author"] 46 | if live_data_json and ("h264" in live_data_json["playUrls"]) and ("adaptationSet" in live_data_json["playUrls"]["h264"]): 47 | roomInfo['live_status'] = '1' 48 | roomInfo['room_title'] = live_data_json.get('caption', '空') 49 | roomInfo['live_rates'] = {} 50 | i = 0 51 | for rate in live_data_json['playUrls']["h264"]["adaptationSet"]["representation"]: 52 | key = int(rate["id"]) 53 | roomInfo['live_rates'][key] = rate['name'] 54 | i += 1 55 | else: 56 | roomInfo['live_status'] = '0' 57 | 58 | roomInfo['room_owner_id'] = user_data_json['originUserId'] 59 | roomInfo['room_owner_name'] = user_data_json['name'] 60 | roomInfo['room_description'] = user_data_json['description'] 61 | 62 | 63 | self.roomInfo = roomInfo 64 | return roomInfo 65 | 66 | def getLiveUrl(self, qn): 67 | qn = int(qn) 68 | if not hasattr(self, 'roomInfo'): 69 | self.getRoomInfo() 70 | if self.roomInfo['live_status'] != '1': 71 | print('当前没有在直播') 72 | return None 73 | 74 | live_data_json = self.getLiveInfo() 75 | self.live_url = live_data_json["liveStream"]['playUrls']["h264"]["adaptationSet"]["representation"][qn]['url'] 76 | self.live_qn = qn 77 | print("申请清晰度 %s的链接,得到清晰度 %d的链接"%(qn, self.live_qn)) 78 | # self.download_headers = { 79 | # 'Accept': 'application/json, text/plain, */*', 80 | # 'Accept-Encoding': 'gzip, deflate, br', 81 | # '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', 82 | # 'Origin': 'https://live.bilibili.com', 83 | # 'Referer': 'https://live.bilibili.com/%s'%self.short_id, 84 | # 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0', 85 | # } 86 | return self.live_url -------------------------------------------------------------------------------- /live_recorder/you_live/live_thread/__init__.py: -------------------------------------------------------------------------------- 1 | from .download import DownloadThread 2 | from .monitoring import MonitoringThread -------------------------------------------------------------------------------- /live_recorder/you_live/live_thread/download.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import threading 3 | 4 | class DownloadThread(threading.Thread): 5 | 6 | 7 | def __init__(self, live_recorder, path = None, qn = '0'): 8 | threading.Thread.__init__(self) 9 | self.live_recorder = live_recorder 10 | self.path = path 11 | self.qn = qn 12 | 13 | def run(self): 14 | self.live_recorder.startRecord(self.path, qn = self.qn) 15 | print("下载线程已经结束") 16 | 17 | -------------------------------------------------------------------------------- /live_recorder/you_live/live_thread/monitoring.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import threading 3 | import time 4 | import sys 5 | 6 | class MonitoringThread(threading.Thread): 7 | 8 | 9 | def __init__(self, live_recorder): 10 | threading.Thread.__init__(self) 11 | self.live_recorder = live_recorder 12 | 13 | def run(self): 14 | self.begin_time = time.time() 15 | while self.live_recorder.downloadFlag : 16 | time.sleep(10) 17 | self.current_time = time.time() 18 | print("当前已经录制了%s, 录制文件大小为%s"%(\ 19 | self.formatTime(self.current_time - self.begin_time),\ 20 | self.formatSize(self.live_recorder.downloaded))) 21 | print("监控线程已经结束") 22 | sys.exit() 23 | 24 | def formatTime(self, time): 25 | time = int(time) 26 | seconds = time % 60 27 | time = int(time / 60) 28 | minutes = time % 60 29 | hours = int(time / 60) 30 | if hours > 0: 31 | return "%dh %dmin %ds"%(hours, minutes, seconds) 32 | elif minutes > 0: 33 | return "%dmin %ds"%(minutes, seconds) 34 | else: 35 | return "%ds"%seconds 36 | 37 | KB = 1024; 38 | MB = KB * 1024; 39 | GB = MB * 1024; 40 | def formatSize(self, size): 41 | if size > MonitoringThread.GB: 42 | return "%.2fGB"%(size/MonitoringThread.GB) 43 | elif size > MonitoringThread.MB: 44 | return "%.1fMB"%(size/MonitoringThread.MB) 45 | else: 46 | return "%.1fKB"%(size/MonitoringThread.KB) -------------------------------------------------------------------------------- /live_recorder/you_live/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from .crypto import crypto_js -------------------------------------------------------------------------------- /live_recorder/you_live/resources/crypto.py: -------------------------------------------------------------------------------- 1 | 2 | crypto_js = ''' 3 | /* 4 | CryptoJS v3.1.2 5 | code.google.com/p/crypto-js 6 | (c) 2009-2013 by Jeff Mott. All rights reserved. 7 | code.google.com/p/crypto-js/wiki/License 8 | */ 9 | var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, 10 | r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< 11 | 32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>3]|=parseInt(a.substr(j, 12 | 2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}}, 13 | q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w< 17 | l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); 18 | (function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])}, 19 | _doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]), 20 | f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f, 21 | m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m, 22 | E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/ 23 | 4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math); 24 | (function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a, 30 | this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684, 31 | 1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})}, 32 | decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d, 33 | b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}(); 34 | (function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8, 35 | 16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;dd||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>> 36 | 8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t= 37 | d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})();function _k(h){var e=h+"0123456789abcdef";return e.substring(0, 16);} 38 | CryptoJS.e = function (d,p) { 39 | var key = CryptoJS.enc.Utf8.parse(_k(p)); 40 | var encrypted = CryptoJS.AES.encrypt(d, key, { 41 | iv: key, 42 | mode: CryptoJS.mode.CBC, 43 | padding: CryptoJS.pad.Pkcs7 44 | }); 45 | return encrypted.toString(); 46 | }; 47 | 48 | function doencodepsw(psw, code, acc) { 49 | return "[p]" + CryptoJS.e(CryptoJS.MD5(CryptoJS.MD5(CryptoJS.MD5(psw).toString() + code).toString()).toString() + "@" + acc, code); 50 | } 51 | 52 | function doencodeacc(acc, code) { 53 | return "[p]" + CryptoJS.e(acc, code); 54 | } 55 | ''' -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import os 3 | from live_recorder import version 4 | 5 | with open("README.md", "r", encoding = 'utf-8') as fh: 6 | long_description = fh.read() 7 | 8 | def find_packages(*tops): 9 | packages = [] 10 | for d in tops: 11 | for root, dirs, files in os.walk(d, followlinks=True): 12 | if '__init__.py' in files: 13 | packages.append(root) 14 | return packages 15 | 16 | REQ = ['PyExecJS', 'requests'] 17 | 18 | setuptools.setup( 19 | name = "you-live", 20 | version = version.__version__, 21 | description = version.__descriptrion__, 22 | author = "NiceLee", 23 | author_email = "lijia0732@sina.com", 24 | license = "MIT", 25 | long_description = long_description, 26 | long_description_content_type = "text/markdown", 27 | url = "https://github.com/nICEnnnnnnnLee/LiveRecorder", 28 | requires = REQ, 29 | install_requires = REQ, 30 | zip_safe = True, 31 | packages = find_packages('live_recorder'), 32 | classifiers = [ 33 | "Development Status :: 4 - Beta", 34 | "Intended Audience :: Developers", 35 | "Intended Audience :: End Users/Desktop", 36 | "Environment :: Console", 37 | "License :: OSI Approved :: MIT License", 38 | "Operating System :: OS Independent", 39 | "Programming Language :: Python", 40 | "Programming Language :: Python :: 3", 41 | "Programming Language :: Python :: 3 :: Only", 42 | "Programming Language :: Python :: 3.0", 43 | "Programming Language :: Python :: 3.1", 44 | "Programming Language :: Python :: 3.2", 45 | "Programming Language :: Python :: 3.3", 46 | "Programming Language :: Python :: 3.4", 47 | "Programming Language :: Python :: 3.5", 48 | "Programming Language :: Python :: 3.6", 49 | "Programming Language :: Python :: 3.7", 50 | "Topic :: Internet", 51 | "Topic :: Internet :: WWW/HTTP", 52 | "Topic :: Multimedia", 53 | "Topic :: Multimedia :: Sound/Audio", 54 | "Topic :: Multimedia :: Video" 55 | ], 56 | entry_points={ 57 | "console_scripts": ["you-live=live_recorder.__main__:main"] 58 | }, 59 | ) 60 | -------------------------------------------------------------------------------- /test/record_test.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import sys 3 | sys.path[1] = r"D:\Workspace\javaweb-springboot\LiveRecorder\\" 4 | from live_recorder import you_live 5 | 6 | if __name__ == '__main__': 7 | # recorder = you_live.Recorder.createRecorder('bili', 7734200, check_flv = False,\ 8 | # save_folder = '../download', delete_origin_file = True) 9 | # recorder = you_live.Recorder.createRecorder('douyu', 312212, check_flv = False, cookies = "acf_did=1474ee6ad3fff7616c76b88200011501; dy_did=1474ee6ad3fff7616c76b88200011501; Hm_lvt_e99aee90ec1b2106afe7ec3b199020a7=1615709433,1616551448,1616722268,1617427549; acf_avatar=https%3A%2F%2Fapic.douyucdn.cn%2Fupload%2Favatar_v3%2F201812%2F069809f62815f6d7ca106f786a5c89b3_; Hm_lpvt_e99aee90ec1b2106afe7ec3b199020a7=1617427557; PHPSESSID=mrifqo3j1dk659lohpsapi6ci2; acf_auth=5699TLLSYBQVjqW5MRw6CdV%2FO4QZzytmXVxowZHfDHvS7uRukRVPaMcRBtWOf4QmJUbKywTltReQ%2FiOXBjRCM1TMcZurTfn7nqfNGPdrN67BizE6J7Nrj8U; dy_auth=b69cbCbSelxAo%2BT8suQjk43vWuiT9MnVIzZPWf8linIrX%2BT7iwnlyFxZTML4p%2BrFbHEeZyrc0Ceufk%2F07C4bGbNNHthKR1nrhJljaSWn4BZPt4rBji5Vy8g; wan_auth37wan=45e5ec57d6eecAERPwNXJdwb2gHcoDtFVrz9qb5%2BGD3xLC7b69INuVLGMyRLiqaW7CwKzmFAJ3DjHGWQsevZeHAXYxwYjE9QMEhEU5%2FWsCpgow3llQo; acf_uid=224961119; acf_username=224961119; acf_nickname=%E6%9F%B4%E5%8F%AF%E5%A4%AB%E8%80%81%E5%8F%B8%E5%9F%BA; acf_own_room=0; acf_groupid=1; acf_phonestatus=1; acf_ct=0; acf_ltkid=67003147; acf_biz=1; acf_stk=8719110960b818fc") 10 | recorder = you_live.Recorder.createRecorder('kuaishou', 'zxc774882278', check_flv = False, cookies = None) 11 | # recorder = you_live.Recorder.createRecorder('acfun', '40909488', check_flv = True, cookies = None) 12 | # recorder = you_live.DouyuRecorder(312212, check_flv = False) 13 | 14 | # 获取房间信息 15 | roomInfo = recorder.getRoomInfo() 16 | print(roomInfo) 17 | 18 | # 获取如果在直播,那么录制 19 | if roomInfo['live_status'] == '1': 20 | print(roomInfo['live_rates']) 21 | qn = input("输入要录制的清晰度") 22 | live_url = recorder.getLiveUrl(qn = qn) #请查看roomInfo['live_rates'] 23 | print(live_url) 24 | download_thread = you_live.DownloadThread(recorder) 25 | monitoring_thread = you_live.MonitoringThread(recorder) 26 | 27 | download_thread.start() 28 | monitoring_thread.start() 29 | 30 | while recorder.downloadFlag: 31 | todo = input("输入q或stop停止录制\r\n") 32 | if todo == "q" or todo == "stop": 33 | recorder.downloadFlag = False 34 | else: 35 | print("请输入合法命令!!!") 36 | else: 37 | print("主播当前不在线!!") --------------------------------------------------------------------------------