1
This commit is contained in:
+444
@@ -0,0 +1,444 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user