# -*- coding: utf-8 -*- # 咪咕直播 - OK影视插件(完整频道版) import base64 import sys import time import json import requests import hashlib import random import os from urllib.parse import urlparse, parse_qs sys.path.append('..') from base.spider import Spider class Spider(Spider): def getName(self): return "咪咕直播" def init(self, extend): self.extend = extend try: self.extendDict = json.loads(extend) except: self.extendDict = {} proxy = self.extendDict.get('proxy', None) if proxy is None: self.is_proxy = False else: self.proxy = proxy self.is_proxy = True # 缓存目录 self.cache_dir = os.path.join(os.path.dirname(__file__), 'migucache') if not os.path.exists(self.cache_dir): try: os.makedirs(self.cache_dir, 0o775, True) except: pass def getDependence(self): return [] def isVideoFormat(self, url): pass def manualVideoCheck(self): pass # ==================== 咪咕核心函数 ==================== def _cache_path(self, key): return os.path.join(self.cache_dir, f"migu_cache_{hashlib.md5(key.encode()).hexdigest()}.json") def _get_cache(self, key): path = self._cache_path(key) if not os.path.exists(path): return None try: with open(path, 'r', encoding='utf-8') as f: data = json.load(f) if time.time() - data.get('time', 0) > data.get('ttl', 0): os.unlink(path) return None return data.get('url') except: return None def _set_cache(self, key, url, ttl_seconds): path = self._cache_path(key) try: with open(path, 'w', encoding='utf-8') as f: json.dump({'url': url, 'time': time.time(), 'ttl': ttl_seconds}, f) except: pass def _get_sign_config(self, contId): appVersion = '2600033500' saltValue = '16d4328df21a4138859388418bd252c2' timestampMs = str(int(round(time.time() * 1000))) ver8 = appVersion[:8] md5string = hashlib.md5(f"{timestampMs}{contId}{ver8}".encode()).hexdigest() prefix = random.randint(0, 999999) salt = f"{prefix:06d}80" text = md5string + saltValue + 'migu' + salt[:4] sign = hashlib.md5(text.encode()).hexdigest() return timestampMs, [salt, sign] def _send_get_request(self, url, headers): try: if self.is_proxy: response = requests.get(url, headers=headers, timeout=10, proxies=self.proxy) else: response = requests.get(url, headers=headers, timeout=10) if response.status_code != 200: return None return response.text except: return None def _migu_encrypted_url(self, rawUrl): factorOfEncryption = [8, 3, 7, 6, 6] parsed = urlparse(rawUrl) if parsed is None: return rawUrl queryParams = parse_qs(parsed.query) for k, v in queryParams.items(): if v and isinstance(v, list): queryParams[k] = v[0] puData = queryParams.get('puData', '') if puData == '': return rawUrl paramsToAppend = [] ddCalcuExists = queryParams.get('ddCalcu', '') != '' if not ddCalcuExists: userid = queryParams.get('userid', '') if userid == '': userid = 'eeeeeeeee' timestamp = queryParams.get('timestamp', '') if timestamp == '': timestamp = 'tttttttttttttt' programId = queryParams.get('ProgramID', '') if programId == '': programId = 'ccccccccc' channelId = queryParams.get('Channel_ID', '') if channelId == '': channelId = 'nnnnnnnnnnnnnnnn' useridChars = list(userid) timestampChars = list(timestamp) programIdChars = list(programId) channelIdChars = list(channelId) ddCalcu = '' puLen = len(puData) halfLen = puLen // 2 for i in range(halfLen): ddCalcu += puData[puLen - 1 - i] ddCalcu += puData[i] if i == 1: idx = factorOfEncryption[0] - 1 charToEncrypt = 'e' if idx < len(useridChars): charToEncrypt = useridChars[idx] codePoint = ord(charToEncrypt) if charToEncrypt else 0 encryptedVal = (codePoint ^ factorOfEncryption[4]) % 26 + 97 ddCalcu += chr(encryptedVal) elif i == 2: idx = factorOfEncryption[1] - 1 charToEncrypt = 't' if idx < len(timestampChars): charToEncrypt = timestampChars[idx] codePoint = ord(charToEncrypt) if charToEncrypt else 0 encryptedVal = (codePoint ^ factorOfEncryption[4]) % 26 + 97 ddCalcu += chr(encryptedVal) elif i == 3: idx = factorOfEncryption[2] - 1 charToEncrypt = 'c' if idx < len(programIdChars): charToEncrypt = programIdChars[idx] codePoint = ord(charToEncrypt) if charToEncrypt else 0 encryptedVal = (codePoint ^ factorOfEncryption[4]) % 26 + 97 ddCalcu += chr(encryptedVal) elif i == 4: idx = factorOfEncryption[3] - 1 charToEncrypt = 'n' if idx < len(channelIdChars): charToEncrypt = channelIdChars[idx] codePoint = ord(charToEncrypt) if charToEncrypt else 0 encryptedVal = (codePoint ^ factorOfEncryption[4]) % 26 + 97 ddCalcu += chr(encryptedVal) if puLen % 2 == 1: ddCalcu += puData[halfLen] paramsToAppend.append(f'ddCalcu={ddCalcu}') sv = queryParams.get('sv', '') if sv == '': paramsToAppend.append('sv=10004') ct = queryParams.get('ct', '') if ct == '': paramsToAppend.append('ct=android') if paramsToAppend: if '?' in rawUrl: if rawUrl[-1] not in ['?', '&']: rawUrl += '&' else: rawUrl += '?' rawUrl += '&'.join(paramsToAppend) return rawUrl def _get_channel_stream(self, channel_id): """获取单个频道的直播流地址""" cached = self._get_cache(channel_id) if cached: return cached try: tm, saltSign = self._get_sign_config(channel_id) salt = saltSign[0] sign = saltSign[1] url = f"https://play.miguvideo.com/playurl/v1/play/playurl?contId={channel_id}&dolby=true&isMultiView=true&xh265=true&os=13&ott=false&rateType=3&salt={salt}&sign={sign}×tamp={tm}&ua=oneplus-12&vr=true" headers = { "Host": "play.miguvideo.com", "appId": "miguvideo", "terminalId": "android", "User-Agent": "Dalvik/2.1.0+(Linux;+U;+Android+13;+oneplus-13+Build/TP1A.220624.014)", "MG-BH": "true", "appVersionName": "6.3.35", "appVersion": "2600033500", "Phone-Info": "oneplus-13|13", "X-UP-CLIENT-CHANNEL-ID": "2600033500-99000-201600010010028", "APP-VERSION-CODE": "260335005", "Accept": "*/*", "Connection": "keep-alive", } body = self._send_get_request(url, headers) if body is None: return None data = json.loads(body) rawUrl = data.get("body", {}).get("urlInfo", {}).get("url", "") if not rawUrl: return None ottUrl = self._migu_encrypted_url(rawUrl) if ottUrl: self._set_cache(channel_id, ottUrl, 1800) return ottUrl except: pass return None # ==================== 完整频道列表(从咪咕直播.txt提取)==================== def _get_channel_list(self): """返回所有频道列表""" channels = [ # ========== 央视频道 ========== ("CCTV1综合", "608807420", "央视频道"), ("CCTV2财经", "631780532", "央视频道"), ("CCTV3综艺", "624878271", "央视频道"), ("CCTV4中文国际", "631780421", "央视频道"), ("CCTV5体育", "641886683", "央视频道"), ("CCTV5+体育赛事", "641886773", "央视频道"), ("CCTV6电影", "624878396", "央视频道"), ("CCTV7国防军事", "673168121", "央视频道"), ("CCTV8电视剧", "624878356", "央视频道"), ("CCTV9纪录", "673168140", "央视频道"), ("CCTV10科教", "624878405", "央视频道"), ("CCTV11戏曲", "667987558", "央视频道"), ("CCTV12社会与法", "673168185", "央视频道"), ("CCTV13新闻", "608807423", "央视频道"), ("CCTV14少儿", "624878440", "央视频道"), ("CCTV15音乐", "673168223", "央视频道"), ("CCTV17农业农村", "673168256", "央视频道"), ("CCTV4欧洲", "608807419", "央视频道"), ("CCTV4美洲", "608807416", "央视频道"), ("CGTN", "609017205", "央视频道"), ("CGTN外语纪录", "609006487", "央视频道"), ("CGTN阿拉伯语", "609154345", "央视频道"), ("CGTN西班牙语", "609006450", "央视频道"), ("CGTN法语", "609006476", "央视频道"), ("CGTN俄语", "609006446", "央视频道"), ("老故事", "884121956", "央视频道"), ("中学生", "708869532", "央视频道"), # ========== 卫视频道 ========== ("东方卫视", "651632648", "卫视频道"), ("江苏卫视", "623899368", "卫视频道"), ("广东卫视", "608831231", "卫视频道"), ("江西卫视", "783847495", "卫视频道"), ("河南卫视", "790187291", "卫视频道"), ("陕西卫视", "738910838", "卫视频道"), ("大湾区卫视", "608917627", "卫视频道"), ("湖北卫视", "947472496", "卫视频道"), ("吉林卫视", "947472500", "卫视频道"), ("青海卫视", "947472506", "卫视频道"), ("东南卫视", "849116810", "卫视频道"), ("海南卫视", "947472502", "卫视频道"), ("海峡卫视", "849119120", "卫视频道"), ("中国农林卫视", "956904896", "卫视频道"), ("兵团卫视", "956923145", "卫视频道"), ("辽宁卫视", "630291707", "卫视频道"), ("湖南卫视", "608799681", "卫视频道"), ("北京卫视", "608799545", "卫视频道"), ("浙江卫视", "608858086", "卫视频道"), ("深圳卫视", "608858094", "卫视频道"), # ========== 地方频道 ========== ("上海新闻综合", "651632657", "地方频道"), ("上视东方影视", "617290047", "地方频道"), ("南京新闻综合", "838109047", "地方频道"), ("南京教科频道", "838153729", "地方频道"), ("南京十八频道", "838151753", "地方频道"), ("江苏城市频道", "626064714", "地方频道"), ("江苏国际", "626064674", "地方频道"), ("江苏教育", "628008321", "地方频道"), ("江苏影视", "626064697", "地方频道"), ("江苏综艺", "626065193", "地方频道"), ("公共新闻频道", "626064693", "地方频道"), ("盐城新闻综合", "639731825", "地方频道"), ("淮安新闻综合", "639731826", "地方频道"), ("泰州新闻综合", "639731818", "地方频道"), ("连云港新闻综合", "639731715", "地方频道"), ("宿迁新闻综合", "639731832", "地方频道"), ("徐州新闻综合", "639731747", "地方频道"), ("优漫卡通", "626064703", "地方频道"), ("江阴新闻综合", "955227979", "地方频道"), ("南通新闻综合", "955227985", "地方频道"), ("宜兴新闻综合", "955227996", "地方频道"), ("溧水新闻综合", "639737327", "地方频道"), ("陕西银龄频道", "956909362", "地方频道"), ("陕西都市青春", "956909358", "地方频道"), ("陕西体育休闲", "956909356", "地方频道"), ("陕西秦腔频道", "956909303", "地方频道"), ("陕西新闻资讯", "956909289", "地方频道"), ("财富天下", "956923159", "地方频道"), # ========== 影视频道 ========== ("经典香港电影", "625703337", "影视频道"), ("抗战经典影片", "617432318", "影视频道"), ("新片放映厅", "619495952", "影视频道"), ("CHC影迷电影", "952383261", "影视频道"), ("和美乡途轮播台", "713591450", "影视频道"), ("高清大片", "629943678", "影视频道"), ("南方影视", "614961829", "影视频道"), ("血色山河·抗日战争影像志", "713600957", "影视频道"), # ========== 熊猫频道 ========== ("熊猫频道01高清", "609158151", "熊猫频道"), ("熊猫频道1", "608933610", "熊猫频道"), ("熊猫频道2", "608933640", "熊猫频道"), ("熊猫频道3", "608934619", "熊猫频道"), ("熊猫频道4", "608934721", "熊猫频道"), ("熊猫频道5", "608935104", "熊猫频道"), ("熊猫频道6", "608935797", "熊猫频道"), ("熊猫频道7", "609169286", "熊猫频道"), ("熊猫频道8", "609169287", "熊猫频道"), ("熊猫频道9", "609169226", "熊猫频道"), ("熊猫频道10", "609169285", "熊猫频道"), # ========== 其他频道 ========== ("最强综艺趴", "629942228", "其他频道"), ("嘉佳卡通", "614952364", "其他频道"), ("经典动画大集合", "629942219", "其他频道"), ("新动力量创一流", "713589837", "其他频道"), ("环球旅游", "958475356", "其他频道"), ("钱塘江", "647370520", "其他频道"), ("五环传奇", "707671890", "其他频道"), ("赛事最经典", "646596895", "其他频道"), ("掼蛋精英赛", "631354620", "其他频道"), ("体坛名栏汇", "629943305", "其他频道"), ("四海钓鱼", "637444975", "其他频道"), ("咪咕24小时体育台", "654102378", "其他频道"), ("24小时城市联赛轮播台", "915512915", "其他频道"), ("武术世界", "958475359", "其他频道"), ("CETV1", "923287154", "其他频道"), ("CETV2", "923287211", "其他频道"), ("CETV4", "923287339", "其他频道"), ("山东教育", "609154353", "其他频道"), ] return channels # ==================== OK影视入口函数 ==================== def liveContent(self, url): """返回M3U频道列表""" channels = self._get_channel_list() m3u_lines = ['#EXTM3U'] for name, cid, group in channels: stream_url = self._get_channel_stream(cid) if stream_url: extinf = f'#EXTINF:-1 tvg-name="{name}" group-title="{group}",{name}' m3u_lines.append(extinf) m3u_lines.append(stream_url) result = '\n'.join(m3u_lines) # 如果全部失败,返回测试频道 if len(m3u_lines) <= 1: return '''#EXTM3U #EXTINF:-1 tvg-name="CCTV1综合" group-title="央视频道",CCTV1综合 http://gslbmgsplive.miguvideo.com/wd_r2/cctv/cctv1hd/600/index.m3u8?msisdn=2026042617575278c8eadc36de41339f0cd48295893350&mdspid=&spid=699004&netType=0&sid=2201057821&pid=2028597139×tamp=20260426175752&Channel_ID=0116_2600033500-99000-201600010010028&ProgramID=608807420&ParentNodeID=-99&assertID=2201057821&client_ip=112.3.44.29&SecurityKey=20260426175752&promotionId=&mvid=2201057821&mcid=500020&playurlVersion=ZQ-A1-9.3.2-RELEASE&userid=&jmhm=&videocodec=h264&appCode=miguvideo_android&bean=mgspad&tid=android&conFee=0&puData=1cc31b33cb71e961afdb65232c779f86&ddCalcu=618cvfca93y71a7bc3233c2b5761bed9f6a1&sv=10004&ct=android''' return result def getLives(self): return self.liveContent("") # ==================== 空方法 ==================== def homeContent(self, filter): return {} def homeVideoContent(self): return {} def categoryContent(self, cid, page, filter, ext): return {} def detailContent(self, did): return {} def searchContent(self, key, quick, page='1'): return {} def searchContentPage(self, keywords, quick, page): return {} def playerContent(self, flag, pid, vipFlags): return {} def localProxy(self, params): return [302, "text/plain", None, {'Location': 'https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4'}] def destroy(self): return 'Destroy' if __name__ == '__main__': pass