Compare commits

...

9 Commits
main ... old

Author SHA1 Message Date
Tontonnow ef4fb03931
Add files via upload 11 months ago
Tontonnow 93e9105eb2
Add files via upload 11 months ago
Tontonnow d582ce9e02
Add files via upload 11 months ago
Tontonnow 688d577d83
Create README.md 11 months ago
Tontonnow 640ab216ce
增加爱奇艺4k 11 months ago
Tontonnow 581b753984
增加杜比视界 11 months ago
Tontonnow e26bc70c5b
Add files via upload 11 months ago
Tontonnow 210df982c5
Add files via upload 11 months ago
Tontonnow 4ab9965c21
Add files via upload 11 months ago
  1. 59
      README.md
  2. 31
      iqy.py
  3. 10
      main.py
  4. 73
      pywidevine/L3/cdm/cdm.py
  5. 22
      tx.py
  6. 12
      yk.py

@ -0,0 +1,59 @@
爱优腾视频下载器(web端)
## 基本说明
#### 支持内容
1.爱奇艺普通视频以及wvdrm内容
2.腾讯普通视频
3.优酷普通视频,wv加密内容以及自研加密内容
#### 暂不支持
腾讯wv(我网页都播放不了),chacha20(超前买不起)
### 使用说明
只提供win10文件,其余系统自行编译,下载相关依赖
本软件支持解析,不支持破解
1.下载链接中全部文件,放到同一个文件夹
2.第一次运行需要输入三大网站cookie,必须输,**没有会员也要输,除非自己编译**
3.输入链接,然后不报错的话,会生成bat文件或者txt,
bat文件直接运行,
txt需要下载https://www.52pojie.cn/thread-1631141-1-1.html,直接拖进下载器就行
报错的话,根据源码自行修改
## 运行截图
![img](https://s3.ananas.chaoxing.com/sv-w8/doc/c9/08/85/36834fabf123a4ce08f07dfc736d9d50/thumb/1.png)
![img](https://s3.ananas.chaoxing.com/sv-w8/doc/c9/08/85/36834fabf123a4ce08f07dfc736d9d50/thumb/2.png)
![img](https://s3.ananas.chaoxing.com/sv-w8/doc/c9/08/85/36834fabf123a4ce08f07dfc736d9d50/thumb/3.png)
![img](https://s3.ananas.chaoxing.com/sv-w8/doc/c9/08/85/36834fabf123a4ce08f07dfc736d9d50/thumb/4.png)
## 郑重声明
**本软件仅限研究web视频下载技术,所以开源了,软件本身并不重要,其中的部分参数如何逆向才重要**
**比如爱奇艺的vf,虽然只是一个md5,但混淆的相当复杂,以及优酷解密key的jsvmp,还有腾讯的ckey:vmp+wasm以及响应的解密也是一个jsvmp**
## 2023-10-31更新
1.增加腾讯杜比
2.增加爱奇艺4k
3.下载器修复,优酷存在多个map导致下载只有6s,修复爱奇艺wv加密,读取不到init.mp4

@ -4,14 +4,14 @@ import time
from urllib import parse
import requests
from tabulate import tabulate
from pywidevineb.L3.cdm import deviceconfig
from pywidevineb.L3.decrypt.wvdecryptcustom import WvDecrypt
from pywidevine.L3.cdm import deviceconfig
from pywidevine.L3.decrypt.wvdecryptcustom import WvDecrypt
from tools import dealck, md5, get_size, get_pssh
def get_key(pssh):
LicenseUrl = "https://drml.video.iqiyi.com/drm/widevine?ve=0"
wvdecrypt = WvDecrypt(init_data_b64=pssh, device=deviceconfig.device_android_generic)
wvdecrypt = WvDecrypt(init_data_b64=pssh, cert_data_b64="",device=deviceconfig.device_android_generic)
widevine_license = requests.post(url=LicenseUrl, data=wvdecrypt.get_challenge())
license_b64 = base64.b64encode(widevine_license.content)
wvdecrypt.update_license(license_b64)
@ -31,10 +31,8 @@ class iqy:
self.dfp = ckjson.get('__dfp', "").split("@")[0]
self.QC005 = ckjson.get('QC005', "")
self.requests = requests.Session()
self.requests.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Cookie": self.ck,
})
self.bop = f"{{\"version\":\"10.0\",\"dfp\":\"{self.dfp}\",\"b_ft1\":8}}"
@ -136,34 +134,33 @@ class iqy:
authKey = md5("d41d8cd98f00b204e9800998ecf8427e" + tm + str(tvid))
params = {
"tvid": tvid,
"bid": "600",
"vid": "",
"bid": "800",
"src": "01010031010000000000",
"uid": self.P00003,
"k_uid": self.QC005,
"authKey": authKey,
"dfp": self.dfp,
"pck": self.pck,
"vid": "",
"tm": tm,
"vt": "0",
"rs": "1",
"uid": self.P00003,
"ori": "pcw",
"ps": "0",
"k_uid": "dc7c8156286e94182d2843ada4ef6050",
"ps": "1",
"pt": "0",
"d": "0",
"s": "",
"lid": "0",
"cf": "0",
"ct": "0",
"authKey": authKey,
"k_tag": "1",
"dfp": self.dfp,
"locale": "zh_cn",
"pck": self.pck,
"k_err_retries": "0",
"up": "",
"sr": "1",
"qd_v": "5",
"tm": tm,
"qdy": "u",
"qds": "0",
"ppt": "0",
"k_ft1": "706436220846084",
"k_ft4": "1162321298202628",
"k_ft2": "262335",
@ -278,6 +275,10 @@ class iqy:
pssh = get_pssh(init)
key_string = get_key(pssh)
cmd = f"N_m3u8DL-RE.exe \"{file} \" --tmp-dir ./cache --save-name \"{name}\" --save-dir \"{savepath}\" --thread-count 16 --download-retry-count 30 --auto-select --check-segments-count " + key_string + " --decryption-binary-path ./mp4decrypt.exe -M format=mp4"
if m3u8data.startswith('<?xml'):
pssh = m3u8data.split('<cenc:pssh>')[1].split('</cenc:pssh>')[0]
key_string = get_key(pssh)
cmd = f"N_m3u8DL-RE.exe \"{file} \" --tmp-dir ./cache --save-name \"{name}\" --save-dir \"{savepath}\" --thread-count 16 --download-retry-count 30 --auto-select --check-segments-count " + key_string + " --decryption-binary-path ./mp4decrypt.exe -M format=mp4"
else:
cmd = f"N_m3u8DL-RE.exe \"{file} \" --tmp-dir ./cache --save-name \"{name}\" --save-dir \"{savepath}\" --thread-count 16 --download-retry-count 30 --auto-select --check-segments-count "
with open(f"{ctitle}.bat", 'a', encoding='gbk') as f:

@ -38,9 +38,13 @@ if __name__ == '__main__':
txck = config["txck"]
yk = config["yk"]
aqy = config["aqy"]
tx = TX(txck)
iq = iqy(aqy)
youku = YouKu(yk)
try:
tx = TX(txck)
iq = iqy(aqy)
youku = YouKu(yk)
except Exception as e:
print("配置文件有误,请检查")
print(e)
while True:
url = input("请输入视频链接:")
if "v.qq.com" in url:

@ -7,9 +7,9 @@ import binascii
from google.protobuf.message import DecodeError
from google.protobuf import text_format
from pywidevine.L3.cdm.formats import wv_proto2_pb2 as wv_proto2
from pywidevine.L3.cdm.session import Session
from pywidevine.L3.cdm.key import Key
from pywidevineb.L3.cdm.formats import wv_proto2_pb2 as wv_proto2
from pywidevineb.L3.cdm.session import Session
from pywidevineb.L3.cdm.key import Key
from Cryptodome.Random import get_random_bytes
from Cryptodome.Random import random
from Cryptodome.Cipher import PKCS1_OAEP, AES
@ -19,18 +19,19 @@ from Cryptodome.Signature import pss
from Cryptodome.Util import Padding
import logging
class Cdm:
def __init__(self):
self.logger = logging.getLogger(__name__)
self.sessions = {}
def open_session(self, init_data_b64, device, raw_init_data = None, offline=False):
def open_session(self, init_data_b64, device, raw_init_data=None, offline=False):
self.logger.debug("open_session(init_data_b64={}, device={}".format(init_data_b64, device))
self.logger.info("opening new cdm session")
if device.session_id_type == 'android':
# format: 16 random hexdigits, 2 digit counter, 14 0s
rand_ascii = ''.join(random.choice('ABCDEF0123456789') for _ in range(16))
counter = '01' # this resets regularly so its fine to use 01
counter = '01' # this resets regularly so its fine to use 01
rest = '00000000000000'
session_id = rand_ascii + counter + rest
session_id = session_id.encode('ascii')
@ -145,37 +146,32 @@ class Cdm:
else:
license_request = wv_proto2.SignedLicenseRequest()
client_id = wv_proto2.ClientIdentification()
if not os.path.exists(session.device_config.device_client_id_blob_filename):
self.logger.error("no client ID blob available for this device")
try:
cid_bytes = client_id.ParseFromString(base64.b64decode(
"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"))
except DecodeError:
self.logger.error("client id failed to parse as protobuf")
return 1
with open(session.device_config.device_client_id_blob_filename, "rb") as f:
try:
cid_bytes = client_id.ParseFromString(f.read())
except DecodeError:
self.logger.error("client id failed to parse as protobuf")
return 1
self.logger.debug("building license request")
if not self.raw_pssh:
license_request.Type = wv_proto2.SignedLicenseRequest.MessageType.Value('LICENSE_REQUEST')
license_request.Msg.ContentId.CencId.Pssh.CopyFrom(session.init_data)
else:
license_request.Type = wv_proto2.SignedLicenseRequestRaw.MessageType.Value('LICENSE_REQUEST')
license_request.Msg.ContentId.CencId.Pssh = session.init_data # bytes
license_request.Msg.ContentId.CencId.Pssh = session.init_data # bytes
if session.offline:
license_type = wv_proto2.LicenseType.Value('OFFLINE')
license_type = wv_proto2.LicenseType.Value('OFFLINE')
else:
license_type = wv_proto2.LicenseType.Value('DEFAULT')
license_type = wv_proto2.LicenseType.Value('DEFAULT')
license_request.Msg.ContentId.CencId.LicenseType = license_type
license_request.Msg.ContentId.CencId.RequestId = session_id
license_request.Msg.Type = wv_proto2.LicenseRequest.RequestType.Value('NEW')
license_request.Msg.RequestTime = int(time.time())
license_request.Msg.ProtocolVersion = wv_proto2.ProtocolVersion.Value('CURRENT')
if session.device_config.send_key_control_nonce:
license_request.Msg.KeyControlNonce = random.randrange(1, 2**31)
license_request.Msg.KeyControlNonce = random.randrange(1, 2 ** 31)
if session.privacy_mode:
if session.device_config.vmp:
@ -218,12 +214,40 @@ class Cdm:
else:
license_request.Msg.ClientId.CopyFrom(client_id)
kes="""-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAosIRKPWQ6+vptCMzSpZ4b4v5g8kp+qs2BgOepFOJG6Tuj1Ph
MzsK2ykmygErTW2X9PF610gjmCREslYzbcVdJ99+BAI1Yp18fFJCoOqYRUrs0mcb
1GaVuhU+hlml2AE8jOvHl/33fJir4Cws14/y680MGOJYPtHtYnvFrMUmMAReqlLs
HMvyMdGLR3oSOS35+xnh1Kuztrlw/KVlGSANFQ0bYMQEy6C4jC4aFu9IUMj+UPcG
pJ07t8cCPXh7eE1zb3SJuYvFqZKMT7OnethnaiDAE4xqQws5z6Wjrxmgop/Oz8hO
048G/jrNoDnfBK2nsWT+4EbuwFZTWsK28WyEdwIDAQABAoIBACkwyDr/ev/aIrlO
N0rnLe+9ExbBMHiaIAABpoKcCZUPdribV+EpTiQNFB4Hkbf0xoJdIuOdBDUa6K/h
lP5w9pSCwVeuX2hnxvuHrgkflg3jWnAdXDOzCq2fdsV1pr02Aub/PPJAegP0d3sy
ct7TNX1r1WXu0rqDUnqcLHj/JBz+drkyfcLOLbKffd98t1Sxsjy/aFMiUngHk/uj
imNamAMNhmob2xyah8pqg7Y7XFuZn3Wu+i+tL2HoAZRUaRWXiBPV1SST0F/4pQg5
we9xaMfuxuwBIdRPkiiyagK1IWqT2XsVG2byMEvcq3iIVyAS1dzb85tZbdv+ufR4
VoZ70lECgYEA1CvxfdmuqBG45GKQzin5+jnkGLnj6LjEH5EtUQPy2Is3N6WJIp6B
SHxgddoJZh3Pc9D62nKLTrAkkk1UpCrrFXpjy7VkIUBnEzVj0Nbh0xoV9brRPQOD
lqtrfj1NQNNY8ZgWpILnJ9n26Gqjr2nkUlAsu3bPaz/VzffA/waP4pUCgYEAxGEJ
MO99eIpkZdZU7PxjRs8rJmIzx77MekWpUJKtKzDA6BbwWI2oLuG9zbcANMKMdonD
j2ZXdVUQfqBvcwHuDmK+7FhKQ1Rw0jWWlrEADYQgK3MfqMPoOGv9Wn3hrBetSbWK
HTXOQQccDaEzSSCTOG3RPrMi2eIp7uFCENbqM9sCgYB4mdHW+1kv54L1LqGozmtt
NGLXOzK1IfE5EEh1+IydUeS9GLbumrJaBXi/BIS7Ks60wmEUsm9E9xKSpqop9stR
lhQLwrt7uyPb40kteDc8y2MYHmy5BbpSdnXPeADljDzOdujH8jB6koaqbZNFLie+
Mhx7InmcONjLDr0BOTWoUQKBgQCAzkzjBhK8P7m+eijWEG1lgnkBAiSIfYNNJ+f4
a1yeGapOEM2wp6mKppKCHehKstjC33Wf1zbCRPs+syimvLtSQD6OcxKyuu4NUwzk
5k/sjZ80IJzBa04jw+E3u52L7TPCRwrCQgp46Jrj7bnf2zf1KUK353OSih+LCcD1
nqGbRQKBgQDC5ns0X8TnJCgf1BD3cGvc3o9zo3gw/NuZ6cqm8q45u0kiw5pRs+7j
9CXENirhHL5JXighOFB78Q3WWMuppTDxj7S1rpYdgp6+ITGSOmY5Xs6uaimilt2H
JPmXCYQt2Qu51bJ+MqZRWYeyN01O6rdKX/zGD9UTN5D3Ty3KEzogkg==
-----END RSA PRIVATE KEY-----"""
if session.device_config.private_key_available:
key = RSA.importKey(open(session.device_config.device_private_key_filename).read())
session.device_key = key
key = RSA.importKey(kes)
session.device_key = key
else:
self.logger.error("need device private key, other methods unimplemented")
return 1
self.logger.error("need device private key, other methods unimplemented")
return 1
self.logger.debug("signing license request")
@ -318,7 +342,8 @@ class Cdm:
lic_hmac = HMAC.new(session.derived_keys['auth_1'], digestmod=SHA256)
lic_hmac.update(license.Msg.SerializeToString())
self.logger.debug("calculated sig: {} actual sig: {}".format(lic_hmac.hexdigest(), binascii.hexlify(license.Signature)))
self.logger.debug(
"calculated sig: {} actual sig: {}".format(lic_hmac.hexdigest(), binascii.hexlify(license.Signature)))
if lic_hmac.digest() != license.Signature:
self.logger.info("license signature doesn't match - writing bin so they can be debugged")

22
tx.py

@ -215,7 +215,7 @@ class Txckey:
h = (h << 5) - h + ord(c)
return str(h & 0xffffffff)
def ckey81(self, vid, tm, appVer='3.5.57', guid='52f0ea142b32b633', platform="10201",
def ckey81(self, vid, tm, appVer='3.5.57', guid='', platform="10201",
url="https://v.qq.com/x/cover/mzc00200b4jsdq6/l00469csvi7.html"):
url = url[0:48]
navigator = self.userAgent[0:48]
@ -395,16 +395,13 @@ class TX:
"defn": defn,
"ehost": url,
"refer": url,
"platform": "10201",
"guid": self.guid,
"cKey": ckey,
"logintoken": json.dumps(self.logintoken, separators=(',', ':')),
"tm": tm,
"charge": "0",
"otype": "ojson",
"defnpayver": "3",
"spau": "1",
"spaudio": "0",
"spwm": "1",
"sphls": "2",
"host": "v.qq.com",
@ -418,18 +415,21 @@ class TX:
"auth_from": "",
"auth_ext": "",
"fhdswitch": "0",
"dtype": "3",
"spsrt": "2",
"lang_code": "0",
"spvvpay": "1",
"spadseg": "3",
"spav1": "15",
"hevclv": "33",
"spsfrhdr": "0",
"spvideo": "0",
"spm3u8tag": "67",
"spmasterm3u8": "3",
"drm": "40"
"drm": "40",
"platform": "10201",
"dtype": 3,
"spav1": 15,
"hevclv": 28,
"spsfrhdr": 100,
"spvideo": 1044,
"spaudio": 70,
"defnpayver": 7
}
response = self.re.get("https://h5vv6.video.qq.com/getinfo", params=params)
data = response.json()
@ -581,6 +581,6 @@ class TX:
if __name__ == '__main__':
ck = ""
ck = ''
tx = TX(ck)
tx.run()

12
yk.py

@ -5,8 +5,8 @@ from urllib.parse import parse_qsl, urlsplit
import base64
from Crypto.Cipher import AES
from tabulate import tabulate
from pywidevineb.L3.cdm import deviceconfig
from pywidevineb.L3.decrypt.wvdecryptcustom import WvDecrypt
from pywidevine.L3.cdm import deviceconfig
from pywidevine.L3.decrypt.wvdecryptcustom import WvDecrypt
from tools import get_pssh, dealck
requests = requests.Session()
@ -142,10 +142,10 @@ class YouKu:
cmd = f"{common_args} --key {key} -M format=mp4"
else:
txt = f'''
#OUT,{savepath}
#DECMETHOD,ECB
#KEY,{key}
{title}_{resolution}_{size},{m3u8_url}
#OUT,{savepath}
#DECMETHOD,ECB
#KEY,{key}
{title}_{resolution}_{size},{m3u8_url}
'''
with open("{}.txt".format(title), "a", encoding="gbk") as f:
f.write(txt)

Loading…
Cancel
Save