444 lines
18 KiB
Python
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}×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 |