├── .gitignore ├── LICENSE ├── README.md ├── bilibili.py ├── douyu.py ├── huya.py ├── real-url-proxy-server.py ├── stream_tester.py └── youtube.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | /.vscode 3 | /.idea 4 | /__pycache__ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # real-url-proxy-server 2 | 3 | ## 说明 4 | 通过斗鱼、虎牙及Bilibili房间号直接访问直播源的代理服务器。 5 | 6 | ## 运行 7 | python3 real-url-proxy-server.py [-h] -p PORT -r REFRESH [-l LOG_FILE]
8 | PORT: 端口号,服务器将监听于 0.0.0.0:PORT。
9 | REFRESH: 自动刷新间隔(秒),0表示禁止自动刷新。
10 | LOG_FILE: 日志文件路径,为空则仅输出至控制台。
11 | 12 | ## 访问 13 | **斗鱼**
14 |   8M:http://xxx.xxx.xxx.xxx:xxxx/douyu/房间号
15 |   4M:http://xxx.xxx.xxx.xxx:xxxx/douyu/房间号/4000
16 |   2M:http://xxx.xxx.xxx.xxx:xxxx/douyu/房间号/2000
17 | 18 | **虎牙**
19 |   4M:http://xxx.xxx.xxx.xxx:xxxx/huya/房间号
20 |   2M:http://xxx.xxx.xxx.xxx:xxxx/huya/房间号/2000p
21 | 22 | **Bilibili**
23 |   http://xxx.xxx.xxx.xxx:xxxx/bilibili/房间号
24 | 25 | ## 刷新 26 | 程序首次获取到实际直播源地址后会缓存下来,后续访问会使用缓存地址。在指定的时间间隔后会自动刷新实际直播源地址,或者通过访问 http://xxx.xxx.xxx.xxx:xxxx/douyu/房间号/refresh 或 http://xxx.xxx.xxx.xxx:xxxx/huya/房间号/refresh 来手动刷新。 27 | 28 | ## 其它 29 | 获取到的实际地址会以301跳转或EXTM3U形式返回,播放端得到播放地址后,后续正常播放过程中不会再次访问代理服务器,因此服务器负载和流量均很低。本人在家中将其部署于刷了Padavan的小米路由器上,并通过OTT盒子进行观看。(斗鱼,bilibili是301跳转,虎牙会一直访问代理服务器返回#EXTM3U) 30 | 31 | ## 感谢 32 | 获取直播源地址使用的douyu.py、huya.py及bilibili.py代码来自于Real-Url项目,在此表示由衷的感谢! 33 | -------------------------------------------------------------------------------- /bilibili.py: -------------------------------------------------------------------------------- 1 | # 获取哔哩哔哩直播的真实流媒体地址,默认获取直播间提供的最高画质 2 | # qn=150高清 3 | # qn=250超清 4 | # qn=400蓝光 5 | # qn=10000原画 6 | # 获取方法参考:https://blog.csdn.net/zy1281539626/article/details/112451021 7 | 8 | import requests 9 | import json 10 | 11 | class BiliBili: 12 | 13 | def __init__(self, rid): 14 | self.rid = rid 15 | 16 | def get_real_url(self): 17 | header = { 18 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' 19 | } 20 | 21 | # 先获取直播状态和真实房间号 22 | r_url = 'https://api.live.bilibili.com/room/v1/Room/room_init?id={}'.format(self.rid) 23 | with requests.Session() as s: 24 | res = s.get(r_url, headers=header, timeout=30).json() 25 | code = res['code'] 26 | if code == 0: 27 | live_status = res['data']['live_status'] 28 | if live_status == 1: 29 | room_id = res['data']['room_id'] 30 | 31 | urls = {} 32 | f_url = 'https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo' 33 | params = { 34 | 'room_id': room_id, 35 | 'no_playurl': 0, 36 | 'mask': 0, 37 | 'qn': 10000, 38 | 'platform': 'web', 39 | 'protocol': '0,1', # http_stream: 0, http_hls: 1 40 | 'format': '0,1,2', # flv: 0, ts: 1, fmp4: 2 41 | 'codec': '0,1' # avc: 0, hevc: 1 42 | } 43 | resp = s.get(f_url, params=params, headers=header, timeout=30).json() 44 | try: 45 | streams = resp['data']['playurl_info']['playurl']['stream'] 46 | for stream in streams: 47 | protocol_name = stream['protocol_name'] 48 | for format in stream['format']: 49 | format_name = format['format_name'] 50 | for codec in format['codec']: 51 | codec_name = codec['codec_name'] 52 | for url_info in codec['url_info']: 53 | urls[protocol_name + '_' + format_name + '_' + codec_name] = url_info['host'] + codec['base_url'] + url_info['extra'] 54 | 55 | return {k: urls[k] for k in sorted(urls.keys())} 56 | except KeyError or IndexError: 57 | raise Exception('获取失败') 58 | 59 | else: 60 | raise Exception('未开播') 61 | else: 62 | raise Exception('房间不存在') 63 | 64 | 65 | def get_real_url(rid): 66 | try: 67 | bilibili = BiliBili(rid) 68 | return bilibili.get_real_url() 69 | except Exception as e: 70 | print('Exception:', e) 71 | return False 72 | 73 | 74 | if __name__ == '__main__': 75 | r = input('请输入bilibili直播房间号:\n') 76 | print(json.dumps(get_real_url(r), indent=4)) 77 | -------------------------------------------------------------------------------- /douyu.py: -------------------------------------------------------------------------------- 1 | # 获取斗鱼直播间的真实流媒体地址,默认最高画质 2 | import hashlib 3 | import re 4 | import time 5 | 6 | import requests 7 | 8 | try: 9 | import quickjs 10 | use_quickjs = True 11 | except ImportError: 12 | import execjs 13 | use_quickjs = False 14 | 15 | 16 | class DouYu: 17 | def __init__(self, rid): 18 | """ 19 | 房间号通常为1~8位纯数字,浏览器地址栏中看到的房间号不一定是真实rid. 20 | Args: 21 | rid: 22 | """ 23 | self.did = '10000000000000000000000000001501' 24 | 25 | self.s = requests.Session() 26 | self.res = self.s.get('https://m.douyu.com/' + str(rid), timeout=30).text 27 | result = re.search(r'rid":(\d{1,8}),"vipId', self.res) 28 | 29 | if result: 30 | self.rid = result.group(1) 31 | else: 32 | raise Exception('房间号错误') 33 | 34 | @staticmethod 35 | def md5(data): 36 | return hashlib.md5(data.encode('utf-8')).hexdigest() 37 | 38 | def get_pre(self): 39 | url = 'https://playweb.douyucdn.cn/lapi/live/hlsH5Preview/' + self.rid 40 | data = { 41 | 'rid': self.rid, 42 | 'did': self.did 43 | } 44 | t13 = str(int((time.time() * 1000))) 45 | auth = DouYu.md5(self.rid + t13) 46 | headers = { 47 | 'rid': self.rid, 48 | 'time': t13, 49 | 'auth': auth 50 | } 51 | res = self.s.post(url, headers=headers, data=data, timeout=30).json() 52 | error = res['error'] 53 | data = res['data'] 54 | key = '' 55 | url = '' 56 | if data: 57 | rtmp_live = data['rtmp_live'] 58 | url = data['rtmp_url'] + '/' + rtmp_live 59 | key = re.search(r'(\d{1,8}[0-9a-zA-Z]+)_?\d{0,4}p?(.m3u8|/playlist)', rtmp_live).group(1) 60 | return error, key, url 61 | 62 | def get_js(self): 63 | result = re.search(r'(function ub98484234.*)\s(var.*)', self.res).group() 64 | func_ub9 = re.sub(r'eval.*;}', 'strc;}', result) 65 | if use_quickjs: 66 | js_func = quickjs.Function('ub98484234', func_ub9) 67 | res = js_func() 68 | else: 69 | js = execjs.compile(func_ub9) 70 | res = js.call('ub98484234') 71 | 72 | v = re.search(r'v=(\d+)', res).group(1) 73 | t10 = str(int(time.time())) 74 | rb = DouYu.md5(self.rid + self.did + t10 + v) 75 | 76 | func_sign = re.sub(r'return rt;}\);?', 'return rt;}', res) 77 | func_sign = func_sign.replace('(function (', 'function sign(') 78 | func_sign = func_sign.replace('CryptoJS.MD5(cb).toString()', '"' + rb + '"') 79 | 80 | if use_quickjs: 81 | js_func = quickjs.Function('sign', func_sign) 82 | params = js_func(self.rid, self.did, t10) 83 | else: 84 | js = execjs.compile(func_sign) 85 | params = js.call('sign', self.rid, self.did, t10) 86 | 87 | params += '&ver=219032101&rid={}&rate=-1'.format(self.rid) 88 | 89 | url = 'https://m.douyu.com/api/room/ratestream' 90 | res = self.s.post(url, params=params, timeout=30).json()['data'] 91 | key = re.search(r'(\d{1,8}[0-9a-zA-Z]+)_?\d{0,4}p?(.m3u8|/playlist)', res['url']).group(1) 92 | 93 | return key, res['url'] 94 | 95 | def get_pc_js(self, cdn='ws-h5', rate=0): 96 | """ 97 | 通过PC网页端的接口获取完整直播源。 98 | :param cdn: 主线路ws-h5、备用线路tct-h5 99 | :param rate: 1流畅;2高清;3超清;4蓝光4M;0蓝光8M或10M 100 | :return: JSON格式 101 | """ 102 | res = self.s.get('https://www.douyu.com/' + str(self.rid), timeout=30).text 103 | result = re.search(r'(vdwdae325w_64we[\s\S]*function ub98484234[\s\S]*?)function', res).group(1) 104 | func_ub9 = re.sub(r'eval.*?;}', 'strc;}', result) 105 | if use_quickjs: 106 | js_func = quickjs.Function('ub98484234', func_ub9) 107 | res = js_func() 108 | else: 109 | js = execjs.compile(func_ub9) 110 | res = js.call('ub98484234') 111 | 112 | v = re.search(r'v=(\d+)', res).group(1) 113 | t10 = str(int(time.time())) 114 | rb = DouYu.md5(self.rid + self.did + t10 + v) 115 | 116 | func_sign = re.sub(r'return rt;}\);?', 'return rt;}', res) 117 | func_sign = func_sign.replace('(function (', 'function sign(') 118 | func_sign = func_sign.replace('CryptoJS.MD5(cb).toString()', '"' + rb + '"') 119 | 120 | if use_quickjs: 121 | js_func = quickjs.Function('sign', func_sign) 122 | params = js_func(self.rid, self.did, t10) 123 | else: 124 | js = execjs.compile(func_sign) 125 | params = js.call('sign', self.rid, self.did, t10) 126 | 127 | params += '&cdn={}&rate={}'.format(cdn, rate) 128 | url = 'https://www.douyu.com/lapi/live/getH5Play/{}'.format(self.rid) 129 | res = self.s.post(url, params=params, timeout=30).json()['data'] 130 | 131 | return res['rtmp_url'] + '/' + res['rtmp_live'] 132 | 133 | def get_real_url(self): 134 | ret = {} 135 | error, key, url = self.get_pre() 136 | if error == 0: 137 | ret['900p'] = url 138 | elif error == 102: 139 | raise Exception('房间不存在') 140 | elif error == 104: 141 | raise Exception('房间未开播') 142 | key, url = self.get_js() 143 | ret['2000p'] = url 144 | #ret['flv'] = "http://openhls-tct.douyucdn2.cn/live/{}.flv?uuid=".format(key) 145 | return ret 146 | 147 | 148 | if __name__ == '__main__': 149 | r = input('输入斗鱼直播间号:\n') 150 | s = DouYu(r) 151 | print(s.get_real_url()) 152 | -------------------------------------------------------------------------------- /huya.py: -------------------------------------------------------------------------------- 1 | # 获取虎牙直播的真实流媒体地址。 2 | # 虎牙"一起看"频道的直播间可能会卡顿 3 | import requests 4 | import re 5 | import base64 6 | import urllib.parse 7 | import hashlib 8 | import time 9 | import json 10 | import html 11 | 12 | class huya: 13 | def __init__(self, rid, uid = 0, mode = 2): 14 | self.room_id = rid 15 | self.user_id = uid 16 | self.mode = mode 17 | self.live_url_infos = {} 18 | self.update_live_url_info() 19 | 20 | def decode_live_url_info(self, srcAntiCode): 21 | srcAntiCode = html.unescape(srcAntiCode) 22 | c = srcAntiCode.split('&') 23 | c = [i for i in c if i != ''] 24 | n = {i.split('=')[0]: i.split('=')[1] for i in c} 25 | fm = urllib.parse.unquote(n['fm']) 26 | u = base64.b64decode(fm).decode('utf-8') 27 | live_url_info = {} 28 | live_url_info['hash_prefix'] = u.split('_')[0] 29 | live_url_info['uuid'] = n.get('uuid', '') 30 | live_url_info['ctype'] = n.get('ctype', '') 31 | live_url_info['txyp'] = n.get('txyp', '') 32 | live_url_info['fs'] = n.get('fs', '') 33 | live_url_info['t'] = n.get('t', '') 34 | return live_url_info 35 | 36 | def clear_live_url_infos(self): 37 | self.live_url_infos = {} 38 | 39 | def update_live_url_info(self): 40 | try: 41 | if self.mode == 0: 42 | room_url = 'https://m.huya.com/' + str(self.room_id) 43 | header = { 44 | 'Content-Type': 'application/x-www-form-urlencoded', 45 | 'User-Agent': 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 ' 46 | '(KHTML, like Gecko) Chrome/75.0.3770.100 Mobile Safari/537.36 ' 47 | } 48 | response = requests.get(url=room_url, headers=header, timeout=30) 49 | if response.status_code == 200: 50 | self.clear_live_url_infos() 51 | livelineurl_base64 = re.findall(r'"liveLineUrl":"([\s\S]*?)"', response.text)[0] 52 | if livelineurl_base64: 53 | try: 54 | livelineurl = str(base64.b64decode(livelineurl_base64), "utf-8") 55 | except Exception: 56 | livelineurl = livelineurl_base64 57 | if 'replay' not in livelineurl: 58 | url, anti_code = livelineurl.split('?') 59 | live_url_info = {} 60 | live_url_info['stream_name'] = re.sub(r'.(flv|m3u8)', '', url.split('/')[-1]) 61 | live_url_info['base_url'] = 'http:' + url.split('/' + live_url_info['stream_name'])[0] 62 | live_url_info['hls_url'] = 'http:' + url 63 | live_url_info.update(self.decode_live_url_info(anti_code)) 64 | self.live_url_infos['TX'] = live_url_info 65 | elif self.mode == 1: 66 | room_url = 'https://www.huya.com/' + str(self.room_id) 67 | header = { 68 | 'Content-Type': 'application/x-www-form-urlencoded', 69 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' 70 | } 71 | response = requests.get(url=room_url, headers=header, timeout=30) 72 | if response.status_code == 200: 73 | self.clear_live_url_infos() 74 | liveData = None 75 | streamInfo = re.findall(r'stream: ([\s\S]*?)\n', response.text) 76 | if (len(streamInfo) > 0): 77 | liveData = json.loads(streamInfo[0]) 78 | else: 79 | streamInfo = re.findall(r'"stream": "([\s\S]*?)"', response.text) 80 | if (len(streamInfo) > 0): 81 | liveDataBase64 = streamInfo[0] 82 | liveData = json.loads(str(base64.b64decode(liveDataBase64), 'utf-8')) 83 | if liveData is not None: 84 | streamInfoList = liveData['data'][0]['gameStreamInfoList'] 85 | for streamInfo in streamInfoList: 86 | live_url_info = {} 87 | sCdnType = streamInfo['sCdnType'] 88 | live_url_info['stream_name'] = streamInfo['sStreamName'] 89 | live_url_info['base_url'] = streamInfo['sHlsUrl'] 90 | live_url_info['hls_url'] = streamInfo['sHlsUrl'] + '/' + streamInfo['sStreamName'] + '.' + streamInfo['sHlsUrlSuffix'] 91 | sHlsAntiCode = streamInfo['sHlsAntiCode'] 92 | live_url_info.update(self.decode_live_url_info(sHlsAntiCode)) 93 | self.live_url_infos[sCdnType] = live_url_info 94 | elif self.mode == 2: 95 | room_url = 'https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid=' + str(self.room_id) 96 | header = { 97 | 'Content-Type': 'application/x-www-form-urlencoded', 98 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36' 99 | } 100 | response = requests.get(url=room_url, headers=header, timeout=30) 101 | if response.status_code == 200: 102 | self.clear_live_url_infos() 103 | liveData = json.loads(response.text) 104 | if 'data' in liveData.keys() and 'stream' in liveData['data'].keys() and 'baseSteamInfoList' in liveData['data']['stream'].keys(): 105 | streamInfoList = liveData['data']['stream']['baseSteamInfoList'] 106 | for streamInfo in streamInfoList: 107 | live_url_info = {} 108 | sCdnType = streamInfo['sCdnType'] 109 | live_url_info['stream_name'] = streamInfo['sStreamName'] 110 | live_url_info['base_url'] = streamInfo['sHlsUrl'] 111 | live_url_info['hls_url'] = streamInfo['sHlsUrl'] + '/' + streamInfo['sStreamName'] + '.' + streamInfo['sHlsUrlSuffix'] 112 | sHlsAntiCode = streamInfo['sHlsAntiCode'] 113 | live_url_info.update(self.decode_live_url_info(sHlsAntiCode)) 114 | self.live_url_infos[sCdnType] = live_url_info 115 | except: 116 | pass 117 | 118 | def get_real_url(self, ratio = None): 119 | urls = [] 120 | seqid = str(int(time.time() * 1e3 + self.user_id)) 121 | wsTime = hex(int(time.time()) + 3600).replace('0x', '') 122 | for live_url_info in self.live_url_infos.values(): 123 | hash0 = hashlib.md5((seqid + '|' + live_url_info['ctype'] + '|' + live_url_info['t']).encode('utf-8')).hexdigest() 124 | hash1 = hashlib.md5('_'.join([live_url_info['hash_prefix'], str(self.user_id), live_url_info['stream_name'], hash0, wsTime]).encode('utf-8')).hexdigest() 125 | if ratio is None: 126 | ratio = '' 127 | if 'mobile' in live_url_info['ctype']: 128 | url = "{}?wsSecret={}&wsTime={}&uuid={}&uid={}&seqid={}&ratio={}&txyp={}&fs={}&ctype={}&ver=1&t={}".format( 129 | live_url_info['hls_url'], hash1, wsTime, live_url_info['uuid'], self.user_id, seqid, ratio, live_url_info['txyp'], 130 | live_url_info['fs'], live_url_info['ctype'], live_url_info['t']) 131 | else: 132 | url = "{}?wsSecret={}&wsTime={}&seqid={}&ctype={}&ver=1&txyp={}&fs={}&ratio={}&u={}&t={}&sv=2107230339".format( 133 | live_url_info['hls_url'], hash1, wsTime, seqid, live_url_info['ctype'], live_url_info['txyp'], live_url_info['fs'], ratio, self.user_id, live_url_info['t']) 134 | urls.append(url) 135 | return urls 136 | 137 | 138 | if __name__ == '__main__': 139 | rid = input('输入虎牙直播间号:\n') 140 | real_url = huya(rid, 1463993859134, 1).get_real_url() 141 | if real_url is not None: 142 | print(real_url) 143 | else: 144 | print('未开播或直播间不存在') 145 | -------------------------------------------------------------------------------- /real-url-proxy-server.py: -------------------------------------------------------------------------------- 1 | #!/opt/bin/python3 2 | 3 | #------------------------------------------------------------------------------- 4 | # Name: real-url-proxy-server 5 | # Purpose: A proxy server to extract real url of DouYu and HuYa live room 6 | # 7 | # Author: RAiN 8 | # 9 | # Created: 05-03-2020 10 | # Copyright: (c) RAiN 2020 11 | # Licence: GPL 12 | #------------------------------------------------------------------------------- 13 | 14 | import sys 15 | from abc import ABCMeta, abstractmethod 16 | from http.server import SimpleHTTPRequestHandler 17 | from http.server import HTTPServer 18 | from socketserver import ThreadingMixIn 19 | import functools 20 | from threading import Timer, Lock 21 | import argparse 22 | from datetime import datetime 23 | from time import sleep 24 | from douyu import DouYu 25 | from huya import huya 26 | from bilibili import BiliBili 27 | from youtube import youtube 28 | import requests 29 | import re 30 | 31 | import logging 32 | from logging import handlers 33 | 34 | class Logger(object): 35 | level_relations = { 36 | 'debug': logging.DEBUG, 37 | 'info': logging.INFO, 38 | 'warning': logging.WARNING, 39 | 'error': logging.ERROR, 40 | 'crit': logging.CRITICAL 41 | } 42 | 43 | def __init__(self, filename=None, level='info', when='D', backCount=3, fmt='%(asctime)s - %(levelname)s: %(message)s'): 44 | self.logger = logging.getLogger('real-url-proxy-server') 45 | format_str = logging.Formatter(fmt) 46 | self.logger.setLevel(self.level_relations.get(level)) 47 | sh = logging.StreamHandler() 48 | sh.setFormatter(format_str) 49 | self.logger.addHandler(sh) 50 | 51 | if filename is not None: 52 | th = handlers.TimedRotatingFileHandler( 53 | filename=filename, when=when, backupCount=backCount, encoding='utf-8') 54 | th.setFormatter(format_str) 55 | self.logger.addHandler(th) 56 | 57 | log = None 58 | 59 | class RealUrlExtractor: 60 | __metaclass__ = ABCMeta 61 | lock = Lock() 62 | 63 | def __init__(self, room, auto_refresh_interval): 64 | self.room = room 65 | self.real_url = None 66 | self.last_valid_real_url = None 67 | self._extracting_real_url = False 68 | self.auto_refresh_interval = auto_refresh_interval 69 | self.last_refresh_time = datetime.min 70 | if self.auto_refresh_interval > 0: 71 | self.refresh_timer = Timer(self.auto_refresh_interval, self.refresh_real_url) 72 | 73 | def reset_refresh_timer(self, failover): 74 | if self.auto_refresh_interval > 0: 75 | self.refresh_timer.cancel() 76 | if failover: 77 | refresh_interval = self.auto_refresh_interval / 2 78 | else: 79 | refresh_interval = self.auto_refresh_interval 80 | self.refresh_timer = Timer(refresh_interval, self.refresh_real_url) 81 | self.refresh_timer.start() 82 | 83 | def refresh_real_url(self): 84 | RealUrlExtractor.lock.acquire() 85 | try: 86 | self._extract_real_url() 87 | except: 88 | pass 89 | RealUrlExtractor.lock.release() 90 | 91 | @abstractmethod 92 | def _extract_real_url(self): 93 | failover = True 94 | if self._is_url_valid(self.real_url): 95 | self.last_valid_real_url = self.real_url 96 | failover = False 97 | elif self.last_valid_real_url is not None: 98 | self.real_url = self.last_valid_real_url 99 | 100 | self.last_refresh_time = datetime.now() 101 | self.reset_refresh_timer(failover) 102 | if failover: 103 | log.logger.info('failed to extract real url') 104 | else: 105 | log.logger.info('extracted url: %s', self.real_url) 106 | 107 | @abstractmethod 108 | def _is_url_valid(self, url): 109 | return False 110 | 111 | def get_real_url(self, bit_rate): 112 | if self.real_url is None or bit_rate == 'refresh': 113 | if not self._extracting_real_url: 114 | RealUrlExtractor.lock.acquire() 115 | self._extracting_real_url = True 116 | try: 117 | self._extract_real_url() 118 | except: 119 | pass 120 | self._extracting_real_url = False 121 | RealUrlExtractor.lock.release() 122 | else: 123 | while self._extracting_real_url: 124 | sleep(100) 125 | 126 | class HuYaRealUrlExtractor(RealUrlExtractor): 127 | def __init__(self, room, auto_refresh_interval): 128 | super().__init__(room, auto_refresh_interval) 129 | self.huya = huya(self.room, 1463993859134, 1) 130 | self.cdn_count = 0 131 | self.cdn_index = 0 132 | self.last_real_urls = None 133 | self.last_get_real_url_time = datetime.min 134 | 135 | def _extract_real_url(self): 136 | self.huya.update_live_url_info() 137 | self.cdn_count = len(self.huya.live_url_infos) 138 | if self.cdn_index >= self.cdn_count: 139 | self.cdn_index = 0 140 | if self.cdn_count > 0: 141 | self.real_url = list(self.huya.live_url_infos.values())[self.cdn_index]['hls_url'] 142 | else: 143 | self.real_url = None 144 | super()._extract_real_url() 145 | 146 | def _is_url_valid(self, url): 147 | return url is not None 148 | 149 | def get_real_url(self, bit_rate): 150 | super().get_real_url(bit_rate) 151 | 152 | if bit_rate == 'refresh': 153 | bit_rate = None 154 | 155 | if bit_rate == 'switch_cdn': 156 | self.cdn_index += 1 157 | 158 | if self.last_real_urls is None or (datetime.now() - self.last_get_real_url_time).total_seconds() > 120: 159 | urls = self.huya.get_real_url(bit_rate) 160 | self.last_real_urls = urls 161 | self.last_get_real_url_time = datetime.now() 162 | else: 163 | urls = self.last_real_urls 164 | 165 | if len(urls) > 0: 166 | if self.cdn_index >= len(urls): 167 | self.cdn_index = 0 168 | return urls[self.cdn_index] 169 | return None 170 | 171 | def reset_last_get_real_url_time(self): 172 | self.last_get_real_url_time = datetime.min 173 | 174 | def stream_name(self): 175 | return list(self.huya.live_url_infos.values())[self.cdn_index]['stream_name'] 176 | 177 | def base_url(self): 178 | return list(self.huya.live_url_infos.values())[self.cdn_index]['base_url'] 179 | 180 | 181 | class DouYuRealUrlExtractor(RealUrlExtractor): 182 | def _extract_real_url(self): 183 | try: 184 | self.real_url = DouYu(self.room).get_real_url() 185 | except: 186 | self.real_url = 'None' 187 | super()._extract_real_url() 188 | 189 | def _is_url_valid(self, url): 190 | return url is not None and url != 'None' 191 | 192 | def get_real_url(self, bit_rate): 193 | super().get_real_url(bit_rate) 194 | 195 | if bit_rate == 'refresh': 196 | bit_rate = None 197 | 198 | if not self._is_url_valid(self.real_url): 199 | return None 200 | if bit_rate is None or len(bit_rate) == 0: 201 | if 'flv' in self.real_url: 202 | return self.real_url['flv'] 203 | elif '2000p' in self.real_url: 204 | return self.real_url['2000p'] 205 | else: 206 | return self.real_url['900p'] 207 | if bit_rate in self.real_url.keys(): 208 | return self.real_url[bit_rate] 209 | 210 | class BilibiliRealUrlExtractor(RealUrlExtractor): 211 | def _extract_real_url(self): 212 | try: 213 | self.real_url = BiliBili(self.room).get_real_url() 214 | except: 215 | self.real_url = 'None' 216 | super()._extract_real_url() 217 | 218 | def _is_url_valid(self, url): 219 | return url is not None and len(url) > 0 220 | 221 | def get_real_url(self, bit_rate): 222 | super().get_real_url(bit_rate) 223 | 224 | if bit_rate == 'refresh': 225 | bit_rate = None 226 | 227 | if not self._is_url_valid(self.real_url): 228 | return None 229 | return list(self.real_url.values())[0] 230 | 231 | class YoutubeRealUrlExtractor(RealUrlExtractor): 232 | def __init__(self, room, auto_refresh_interval, cookie_file): 233 | super().__init__(room, auto_refresh_interval) 234 | self.youtube = youtube(self.room, cookie_file) 235 | 236 | def _extract_real_url(self): 237 | try: 238 | self.real_url = self.youtube.get_real_url() 239 | except: 240 | self.real_url = 'None' 241 | super()._extract_real_url() 242 | 243 | def _is_url_valid(self, url): 244 | return url is not None and len(url) > 0 245 | 246 | def get_real_url(self, bit_rate): 247 | super().get_real_url(bit_rate) 248 | 249 | if bit_rate == 'refresh': 250 | bit_rate = None 251 | 252 | if not self._is_url_valid(self.real_url): 253 | return None 254 | return self.real_url[0] 255 | 256 | class RealUrlRequestHandler(SimpleHTTPRequestHandler): 257 | def __init__(self, *args, processor_maps, auto_refresh_interval, youtube_cookie_file, **kwargs): 258 | self.processor_maps = processor_maps 259 | self.auto_refresh_interval = auto_refresh_interval 260 | self.youtube_cookie_file = youtube_cookie_file 261 | super().__init__(*args, **kwargs) 262 | 263 | def _send_cors_headers(self): 264 | self.send_header('Access-Control-Allow-Origin', '*') 265 | self.send_header('Access-Control-Allow-Methods', 'GET') 266 | self.send_header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization') 267 | 268 | def do_GET(self): 269 | s = self.path[1:].split('/') 270 | if len(s) >= 2: 271 | provider = s[0] 272 | room = s[1] 273 | if len(s) > 2: 274 | bit_rate = s[2] 275 | else: 276 | bit_rate = None 277 | log.logger.info('provider: %s, room: %s, bit_rate: %s', provider, room, bit_rate) 278 | 279 | if provider == 'douyu': 280 | if provider not in self.processor_maps.keys(): 281 | self.processor_maps[provider] = {} 282 | douyu_processor_map = self.processor_maps[provider] 283 | 284 | try: 285 | if room not in douyu_processor_map.keys(): 286 | douyu_processor_map[room] = DouYuRealUrlExtractor(room, self.auto_refresh_interval) 287 | 288 | real_url = douyu_processor_map[room].get_real_url(bit_rate) 289 | if real_url is not None: 290 | self.send_response(301) 291 | self._send_cors_headers() 292 | self.send_header('Location', real_url) 293 | self.end_headers() 294 | return 295 | except Exception as e: 296 | log.logger.error("Failed to extract douyu real url! Error: %s", str(e)) 297 | elif provider == 'bilibili': 298 | if provider not in self.processor_maps.keys(): 299 | self.processor_maps[provider] = {} 300 | bilibili_processor_map = self.processor_maps[provider] 301 | 302 | try: 303 | if room not in bilibili_processor_map.keys(): 304 | bilibili_processor_map[room] = BilibiliRealUrlExtractor(room, self.auto_refresh_interval) 305 | 306 | real_url = bilibili_processor_map[room].get_real_url(bit_rate) 307 | if real_url is not None: 308 | self.send_response(301) 309 | self._send_cors_headers() 310 | self.send_header('Location', real_url) 311 | self.end_headers() 312 | return 313 | except Exception as e: 314 | log.logger.error("Failed to extract bilibili real url! Error: %s", str(e)) 315 | elif provider == 'youtube': 316 | if provider not in self.processor_maps.keys(): 317 | self.processor_maps[provider] = {} 318 | youtube_processor_map = self.processor_maps[provider] 319 | 320 | try: 321 | if room not in youtube_processor_map.keys(): 322 | youtube_processor_map[room] = YoutubeRealUrlExtractor(room, self.auto_refresh_interval, self.youtube_cookie_file) 323 | 324 | real_url = youtube_processor_map[room].get_real_url(bit_rate) 325 | if real_url is not None: 326 | self.send_response(301) 327 | self._send_cors_headers() 328 | self.send_header('Location', real_url) 329 | self.end_headers() 330 | return 331 | except Exception as e: 332 | log.logger.error("Failed to extract youtube real url! Error: %s", str(e)) 333 | elif provider == 'huya': 334 | if provider not in self.processor_maps.keys(): 335 | self.processor_maps[provider] = {} 336 | huya_processor_map = self.processor_maps[provider] 337 | 338 | try: 339 | if room not in huya_processor_map.keys(): 340 | huya_processor_map[room] = HuYaRealUrlExtractor(room, self.auto_refresh_interval) 341 | 342 | real_url = huya_processor_map[room].get_real_url(bit_rate) 343 | status_code = 200 344 | for i in range(huya_processor_map[room].cdn_count): 345 | if real_url is not None: 346 | try: 347 | header = { 348 | 'Content-Type': 'application/x-www-form-urlencoded', 349 | 'User-Agent': 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 ' 350 | '(KHTML, like Gecko) Chrome/75.0.3770.100 Mobile Safari/537.36 ' 351 | } 352 | response = requests.get(url=real_url, headers=header, timeout=30) 353 | status_code = response.status_code 354 | m3u8_content = response.text 355 | if status_code == 403 and m3u8_content == 'Unauthorized': 356 | huya_processor_map[room].cdn_index += 1 357 | real_url = huya_processor_map[room].get_real_url(bit_rate) 358 | continue 359 | m3u8_content = re.sub(r'(^.*?\.ts)', huya_processor_map[room].base_url() + r'/\1', m3u8_content, flags=re.M) 360 | break 361 | except: 362 | huya_processor_map[room].cdn_index += 1 363 | real_url = huya_processor_map[room].get_real_url(bit_rate) 364 | if status_code == 403: 365 | huya_processor_map[room].reset_last_get_real_url_time() 366 | self.send_response(status_code) 367 | self._send_cors_headers() 368 | self.send_header('Content-type', "application/vnd.apple.mpegurl") 369 | self.send_header("Content-Length", str(len(m3u8_content))) 370 | self.end_headers() 371 | self.wfile.write(m3u8_content.encode('utf-8')) 372 | return 373 | except Exception as e: 374 | log.logger.error("Failed to proxy huya hls stream! Error: %s", str(e)) 375 | 376 | rsp = "Not Found" 377 | rsp = rsp.encode("gb2312") 378 | 379 | self.send_response(404) 380 | self._send_cors_headers() 381 | self.send_header("Content-type", "text/html; charset=gb2312") 382 | self.send_header("Content-Length", str(len(rsp))) 383 | self.end_headers() 384 | self.wfile.write(rsp) 385 | 386 | class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): 387 | pass 388 | 389 | if __name__ == '__main__': 390 | parser = argparse.ArgumentParser(description='A proxy server to get real url of live providers.') 391 | parser.add_argument('-p', '--port', type=int, required=True, help='Binding port of HTTP server.') 392 | parser.add_argument('-r', '--refresh', type=int, default=7200, help='Auto refresh interval in seconds, 0 means disable auto refresh.') 393 | parser.add_argument('-k', '--youtube_cookie_file', type=str, default=None, help='Cookie file for youtube.') 394 | parser.add_argument('-l', '--log', type=str, default=None, help='Log file path name.') 395 | args = parser.parse_args() 396 | 397 | log = Logger(args.log) 398 | 399 | processor_maps = {} 400 | HandlerClass = functools.partial(RealUrlRequestHandler, processor_maps=processor_maps, auto_refresh_interval=args.refresh, youtube_cookie_file=args.youtube_cookie_file) 401 | ServerClass = ThreadingHTTPServer 402 | #Protocol = "HTTP/1.0" 403 | 404 | server_address = ('0.0.0.0', args.port) 405 | 406 | #HandlerClass.protocol_version = Protocol 407 | httpd = ServerClass(server_address, HandlerClass) 408 | 409 | sa = httpd.socket.getsockname() 410 | log.logger.info('Serving HTTP on %s port %d...', sa[0], sa[1]) 411 | 412 | try: 413 | httpd.serve_forever() 414 | except KeyboardInterrupt: 415 | pass 416 | 417 | httpd.server_close() 418 | log.logger.info('Server stopped.') 419 | -------------------------------------------------------------------------------- /stream_tester.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import subprocess 4 | import json 5 | import argparse 6 | import queue 7 | import threading 8 | import re 9 | from urllib.parse import unquote 10 | 11 | def url_to_filename(url): 12 | # 先解码URL 13 | decoded_url = unquote(url) 14 | # 去除协议部分(如http://或https://) 15 | cleaned_url = re.sub(r'^https?://', '', decoded_url) 16 | # 替换非法字符为下划线 17 | filename = re.sub(r'[/\\?%*:|"<>]', '_', cleaned_url) 18 | return filename 19 | 20 | def ffprobe(media_path): 21 | if os.path.isfile(media_path) or media_path.find('://') > 0: 22 | cmd = ['ffprobe', '-v', 'quiet', '-show_streams', '-of', 'json', media_path] 23 | try: 24 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) 25 | out, err = p.communicate(timeout=10) 26 | return json.loads(out) 27 | except: 28 | p.kill() 29 | return None 30 | else: 31 | raise IOError('No such media file or not supported: ' + media_path) 32 | 33 | def grab_thumbnail(url, output_dir): 34 | cmd = ['ffmpeg', '-i', url, '-vframes', '1', '-f', 'image2', os.path.join(output_dir, url_to_filename(url)) + '.jpg', '-y'] 35 | try: 36 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) 37 | p.wait(10) 38 | if p.poll() is None: 39 | p.kill() 40 | except: 41 | pass 42 | 43 | lock = threading.Lock() 44 | 45 | def prober(urls, dump_stream_info, thumbnail_dir): 46 | while not urls.empty(): 47 | url = urls.get() 48 | try: 49 | meta = ffprobe(url) 50 | if (meta is not None and len(meta) > 0 and 'streams' in meta and len(meta['streams']) > 0): 51 | lock.acquire() 52 | print(url) 53 | if (dump_stream_info): 54 | print(json.dumps(meta, indent=4)) 55 | print() 56 | lock.release() 57 | if (thumbnail_dir is not None): 58 | grab_thumbnail(url, thumbnail_dir) 59 | except: 60 | pass 61 | 62 | if __name__ == '__main__': 63 | parser = argparse.ArgumentParser(description='Stream tester.') 64 | parser.add_argument('-u', '--url', type=str, nargs='?', help='The stream url to test.') 65 | parser.add_argument('--start', type=int, help='The start number for wildcard in url.') 66 | parser.add_argument('--end', type=int, help='The end number for wildcard in url.') 67 | parser.add_argument('-f', '--url_file', type=str, nargs='?', help='The file contains stream urls to test.') 68 | parser.add_argument('-t', '--thread', type=int, default=4, help='The parallel threads.') 69 | parser.add_argument('--thumb_dir', type=str, nargs='?', help='The folder to save thumbnail files.') 70 | parser.add_argument('-d', '--dump', default=False, action='store_true', help='Dump stream info.') 71 | args = parser.parse_args() 72 | 73 | if args.url is None and args.url_file is None: 74 | parser.print_usage() 75 | exit(-1) 76 | 77 | urls = queue.Queue() 78 | if args.url is not None: 79 | if '%' in args.url and args.start is not None and args.end is not None: 80 | for i in range(args.start, args.end + 1): 81 | urls.put(args.url % i) 82 | else: 83 | urls.put(args.url) 84 | else: 85 | try: 86 | url_file = open(args.url_file, 'r', encoding='utf-8') 87 | lines = url_file.readlines() 88 | url_file.close() 89 | for line in lines: 90 | line = line.rstrip() 91 | urls.put(line) 92 | except Exception as e: 93 | print("Failed to open url file, error: ", e) 94 | exit(-2) 95 | 96 | for i in range(args.thread): 97 | t = threading.Thread(target=prober, args=[urls, args.dump, args.thumb_dir]) 98 | t.start() 99 | -------------------------------------------------------------------------------- /youtube.py: -------------------------------------------------------------------------------- 1 | # 获取youtube直播的真实流媒体地址。 2 | import yt_dlp 3 | 4 | class youtube: 5 | def __init__(self, url:str, cookiefile:str = None, proxy:str = None): 6 | if url.startswith('http'): 7 | self.youtube_url = url 8 | else: 9 | self.youtube_url = 'https://www.youtube.com/watch?v=' + url 10 | 11 | self.ydl_opts = {'quiet': True, 'extractor-args': 'youtube:player-client=web;formats=incomplete'} 12 | if cookiefile is not None: 13 | self.ydl_opts['cookiefile'] = cookiefile 14 | if proxy is not None: 15 | self.ydl_opts['proxy'] = proxy 16 | self.ydl = yt_dlp.YoutubeDL(self.ydl_opts) 17 | 18 | def get_real_url(self): 19 | try: 20 | info = self.ydl.extract_info(self.youtube_url, download=False) 21 | if 'manifest_url' in info: 22 | return [info['manifest_url']] 23 | if 'url' in info: 24 | return [info['url']] 25 | return [] 26 | except Exception as e: 27 | print('Error: ' + str(e)) 28 | return [] 29 | 30 | 31 | if __name__ == '__main__': 32 | import argparse 33 | parser = argparse.ArgumentParser(description='Stream tester.') 34 | parser.add_argument('-p', '--proxy', type=str, nargs='?', default=None, help='Set the proxy server to use.') 35 | parser.add_argument('-k', '--cookie', type=str, nargs='?', default=None, help='Set the cookie file to use.') 36 | args = parser.parse_args() 37 | 38 | url = input('输入youtube直播地址:\n') 39 | real_url = youtube(url, args.cookie, args.proxy).get_real_url() 40 | if real_url is not None: 41 | print(real_url) 42 | else: 43 | print('未开播或直播间不存在') 44 | --------------------------------------------------------------------------------