Files
gyj07 a794bbcfa1 1
2026-05-21 16:39:48 +08:00

444 lines
18 KiB
Python

# -*- 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}&timestamp={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&timestamp=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