nilaoda / N_m3u8DL-CLI

[.NET] m3u8 downloader 开源的命令行m3u8/HLS/dash下载器,支持普通AES-128-CBC解密,多线程,自定义请求头等. 支持简体中文,繁体中文和英文. English Supported.
https://nilaoda.github.io/N_m3u8DL-CLI/
MIT License
13.84k stars 2.12k forks source link

关于DMM VR视频下载的讨论 #741

Open faafbb opened 1 year ago

faafbb commented 1 year ago

看到之前有issue #473 讨论过DMM视频的下载,但是关于VR视频如何下载并没有结论,今天抓包研究了一下,和普通的2D视频流程不太一样 环境:windows10,N_m3u8DL-CLI_v3.0.1

前置条件:

  1. 下载DMM的windows 端 VR 播放器
  2. 下载 fidder,完成相应配置(添加根域名证书、系统代理等)
  3. 下载 postman

流程:

  1. 打开 fidder
  2. 在DMM已购页点击流视频播放,浏览器将会跳转到形如 dmmvrplayerstreaming:digital?product_id={{product_id}}&quality=auto&shop=videoa&parent_product_id={{parent_product_id}}&past_st_flag=0&past_dl_flag=0&quality_groups=high&all_parts=1&part=1 的地址
  3. 该地址会唤起 DMM 的 VR 播放器
  4. 可在 fidder 中看到获取视频信息的请求
    
    POST https://www.dmm.co.jp/service/digitalapi/-/json/=/method=PcApp/ HTTP/1.1
    Host: www.dmm.co.jp
    User-Agent: UnityPlayer/2019.2.17f1 (UnityWebRequest/1.0, libcurl/7.52.0-DEV)
    Accept: */*
    Accept-Encoding: identity
    Authorization: Bearer {{ token }}
    Content-Type: application/octet-stream
    X-Unity-Version: 2019.2.17f1
    Content-Length: 513

message=Digital_Api_PlayableProvider.getContentInfo&params={"transfer_type":"stream","quality":"auto","device":"pc_vr","vr_appli_type":"pc","adult_flag":false,"product_id":"{{product_id}}","exploit_id":"{{exploit_id}}","part":"0","shop":"videoa","HTTP_USER_AGENT":"WINDOWSVR_DMMVRPLAY 2.0.5 Intel(R) UHD Graphics 630","is_past":false,"parent_product_id":"{{parent_product_id}}","quality_groups":["high"]}&appid=pc_movievrplayer&authkey=cb0809a54a507acdd880369ea0df939dae4596822fd46fe51616164a2409c030

5. 此时可以关闭VR播放器,打开postman,重放上面的请求
6. 得到返回值

{ "event": true, "data": { "cookie": [ { "name": "licenseUID", "value": "{{licenseUID}}", "expire": 0, "path": "/", "domain": "dmm.com" } ], "content_id": "{{content_id}}", "content_title": "{{content_title}}", "content_info": { "normal": { "redirect": "https://str.dmm.com:443/digital/st1:etmvTFqzN6%2BYjb9582jo7gR7KUPc1p2YtHpxgU6Q1d3J-Rxj0wdtXmqYjilTIDyOYPjysXwfsPOSrFACOubTfsCZ4CKH1THt3GOANei5IBE%3D/2xcmMgca4dTbhr9s3WvW4Zo/-/playlist.m3u8?ld=NE6DLADu3aH%2BKnQLk%2BBCzYGCZBUXD9BU3PQy7k1LGnwDzHUn4mkhD%2BT1%2Bv2bJUBXMdqSdaEO0gluniWZUWMLwN%2BFpWuYc4%2BLg6y4gWYgMaE%3D&luid=cojp", "recommended_viewing_type": "3d_horizontal_180", "height": 1920, "width": 3840 } } }, "exectime": 0.4461, "memory": "4,229,576" }

7. 然后将返回值的 `redirect` 拼接上 `&licenseUID={{licenseUID}}&smartphone_access=1`,即
`https://str.dmm.com:443/digital/st1:etmvTFqzN6%2BYjb9582jo7gR7KUPc1p2YtHpxgU6Q1d3J-Rxj0wdtXmqYjilTIDyOYPjysXwfsPOSrFACOubTfsCZ4CKH1THt3GOANei5IBE%3D/2xcmMgca4dTbhr9s3WvW4Zo/-/playlist.m3u8?ld=NE6DLADu3aH%2BKnQLk%2BBCzYGCZBUXD9BU3PQy7k1LGnwDzHUn4mkhD%2BT1%2Bv2bJUBXMdqSdaEO0gluniWZUWMLwN%2BFpWuYc4%2BLg6y4gWYgMaE%3D&luid=cojp&licenseUID={{licenseUID}}&smartphone_access=1`
填到下载器GUI的M3U8地址即可直接下载

在这里有个疑问,第4步的 request body 

message=Digital_Api_PlayableProvider.getContentInfo&params={"transfer_type":"stream","quality":"auto","device":"pc_vr","vr_appli_type":"pc","adult_flag":false,"product_id":"{{product_id}}","exploit_id":"{{exploit_id}}","part":"0","shop":"videoa","HTTP_USER_AGENT":"WINDOWSVR_DMMVRPLAY 2.0.5 Intel(R) UHD Graphics 630","is_past":false,"parent_product_id":"{{parent_product_id}}","quality_groups":["high"]}&appid=pc_movievrplayer&authkey=cb0809a54a507acdd880369ea0df939dae4596822fd46fe51616164a2409c030

其中的 authkey 是如何计算的,如果能解决这个问题应该就能批量爬取视频而不需要每次唤起VR播放器抓包了
没学过网络安全相关的知识,只能发现两个简单的特征:
1. request body 中随意添加其他参数(如 &x=1)并不影响鉴权
2. 参数的位置不影响鉴权

按长度猜测是 SHA256,但是不知道明文是按什么规则拼接的,希望有大神能够破解并告知
示例:

message=Digital_Api_PlayableProvider.getContentInfo&params={"transfer_type":"stream","quality":"auto","device":"pc_vr","vr_appli_type":"pc","adult_flag":false,"product_id":"h_1155crvr00100zero","exploit_id":"uid:Y2ETW22TVYvDF1ls","part":"0","shop":"videoa","HTTP_USER_AGENT":"WINDOWSVR_DMMVRPLAY 2.0.5 Intel(R) UHD Graphics 630","is_past":false,"parent_product_id":"h_1155crvr00100zerodl6","quality_groups":["high"]}&appid=pc_movievrplayer&authkey=cb0809a54a507acdd880369ea0df939dae4596822fd46fe51616164a2409c030

592767809 commented 1 year ago

我回复一下,猜中了一半,这是一个HmacSHA256

anoxuexi37 commented 1 year ago

POST https://www.dmm.co.jp/service/digitalapi/-/json/=/method=PcApp/ HTTP/1.1
Host: www.dmm.co.jp
User-Agent: UnityPlayer/2019.2.17f1 (UnityWebRequest/1.0, libcurl/7.52.0-DEV)
Accept: */*
Accept-Encoding: identity
Authorization: Bearer {token}
Content-Type: application/octet-stream
X-Unity-Version: 2019.2.17f1
Content-Length: 492

message=Digital_Api_PlayableProvider.getContentInfo¶ms={"transfer_type":"stream","quality":"auto","device":"pc_vr","vr_appli_type":"pc","adult_flag":false,"product_id":"sivr00135","exploit_id":"uid:{uid}","part":"1","shop":"videoa","HTTP_USER_AGENT":"WINDOWSVR_DMMVRPLAY 2.0.5  Intel(R) HD Graphics 4000","is_past":true,"parent_product_id":"sivr00135dl6","quality_groups":["high"]}&appid=pc_movievrplayer&authkey={authkey}

スクリーンショット 2022-11-16 230925 感谢兄弟分享方法 我试了你的工程但不好搞了 token也正确…为什么呢

faafbb commented 1 year ago

我回复一下,猜中了一半,这是一个HmacSHA256

HmacSHA256加密是可以配置密钥的,应该只能通过反编译播放器来获取这个密钥了(触及知识盲区了)

faafbb commented 1 year ago

POST https://www.dmm.co.jp/service/digitalapi/-/json/=/method=PcApp/ HTTP/1.1
Host: www.dmm.co.jp
User-Agent: UnityPlayer/2019.2.17f1 (UnityWebRequest/1.0, libcurl/7.52.0-DEV)
Accept: */*
Accept-Encoding: identity
Authorization: Bearer {token}
Content-Type: application/octet-stream
X-Unity-Version: 2019.2.17f1
Content-Length: 492

message=Digital_Api_PlayableProvider.getContentInfo¶ms={"transfer_type":"stream","quality":"auto","device":"pc_vr","vr_appli_type":"pc","adult_flag":false,"product_id":"sivr00135","exploit_id":"uid:{uid}","part":"1","shop":"videoa","HTTP_USER_AGENT":"WINDOWSVR_DMMVRPLAY 2.0.5  Intel(R) HD Graphics 4000","is_past":true,"parent_product_id":"sivr00135dl6","quality_groups":["high"]}&appid=pc_movievrplayer&authkey={authkey}

スクリーンショット 2022-11-16 230925 感谢兄弟分享方法 我试了你的工程但不好搞了 token也正确…为什么呢


POST https://www.dmm.co.jp/service/digitalapi/-/json/=/method=PcApp/ HTTP/1.1
Host: www.dmm.co.jp
User-Agent: UnityPlayer/2019.2.17f1 (UnityWebRequest/1.0, libcurl/7.52.0-DEV)
Accept: */*
Accept-Encoding: identity
Authorization: Bearer {token}
Content-Type: application/octet-stream
X-Unity-Version: 2019.2.17f1
Content-Length: 492

message=Digital_Api_PlayableProvider.getContentInfo¶ms={"transfer_type":"stream","quality":"auto","device":"pc_vr","vr_appli_type":"pc","adult_flag":false,"product_id":"sivr00135","exploit_id":"uid:{uid}","part":"1","shop":"videoa","HTTP_USER_AGENT":"WINDOWSVR_DMMVRPLAY 2.0.5  Intel(R) HD Graphics 4000","is_past":true,"parent_product_id":"sivr00135dl6","quality_groups":["high"]}&appid=pc_movievrplayer&authkey={authkey}

fidder里的是http请求报文,从 Host 到 Content-Length 是请求头,最后一行才是请求体,其中请求头应该放在 headers里,请求体放在bodyimage image

anoxuexi37 commented 1 year ago

谢谢回复 成功我得到返回值,并开始下载,但它总是在中间卡了。我试了很多次,都是就卡了,似乎在一半之后就卡不前,例如50%、70%等等。 你都下载了吗?有什么需要的选项吗?

环境:windows10,N_m3u8DL-CLI_v3.0.2 tomaru

ramondsq commented 1 year ago

谢谢回复 成功我得到返回值,并开始下载,但它总是在中间卡了。我试了很多次,都是就卡了,似乎在一半之后就卡不前,例如50%、70%等等。 你都下载了吗?有什么需要的选项吗?

环境:windows10,N_m3u8DL-CLI_v3.0.2 tomaru

视频开始播放之前把播放器关了就行了,如果已经开始播放了你再去发请求,那就会出现这种情况。

anoxuexi37 commented 1 year ago

谢谢回复 成功我得到返回值,并开始下载,但它总是在中间卡了。我试了很多次,都是就卡了,似乎在一半之后就卡不前,例如50%、70%等等。 你都下载了吗?有什么需要的选项吗? 环境:windows10,N_m3u8DL-CLI_v3.0.2 tomaru

视频开始播放之前把播放器关了就行了,如果已经开始播放了你再去发请求,那就会出现这种情况。

谢谢回复 嗯,我使用N_m3u8DL-CLI视频开始下载之前 先把DMM播放器立即就关了(视频开始播放之前) 然后开始下载视频 但是还是卡了 好奇怪 dmmvr播放器v2.0.5 难道这是个原因吗? スクリーンショット 2022-11-22 183220

ramondsq commented 1 year ago

谢谢回复 成功我得到返回值,并开始下载,但它总是在中间卡了。我试了很多次,都是就卡了,似乎在一半之后就卡不前,例如50%、70%等等。 你都下载了吗?有什么需要的选项吗?

环境:windows10,N_m3u8DL-CLI_v3.0.2 tomaru

我也是相同版本的播放器。抓包信息里面出现第二个 /service 的时候就可以把播放器关了。 image

anoxuexi37 commented 1 year ago

谢谢回复 成功我得到返回值,并开始下载,但它总是在中间卡了。我试了很多次,都是就卡了,似乎在一半之后就卡不前,例如50%、70%等等。 你都下载了吗?有什么需要的选项吗? 环境:windows10,N_m3u8DL-CLI_v3.0.2 tomaru

我也是相同版本的播放器。抓包信息里面出现第二个 /service 的时候就可以把播放器关了。 image

那没有关系vr播放器版本 嗯嗯我也一样的你的工程 可是每次卡了 我好次考虑原因是什么…想不到… 卡了原因是变了m3u8的地址 所以就卡了 看抓包信息是有的404error 可是正在下载的时候我什么都不做…

スクリーンショット 2022-11-22 193137

Kiva007 commented 11 months ago

屏幕截图 2023-07-27 161708 屏幕截图 2023-07-27 165642

请问这个方法还有效吗,我用fiddler获取了请求,但是将请求按照上述例子在postman里填写后发送,返回值为403,接着我发现fiddler在获取请求后也接收了回复,于是我将redirect的值按例子的方法拼接上licenseUID的值填写到了M3U8地址栏,以及post里authkey的值填到了自定义KEY里,运行后大概下载了5M左右内容就报错闪退了

Kiva007 commented 11 months ago

屏幕截图 2023-07-27 161708 屏幕截图 2023-07-27 165642

请问这个方法还有效吗,我用fiddler获取了请求,但是将请求按照上述例子在postman里填写后发送,返回值为403,接着我发现fiddler在获取请求后也接收了回复,于是我将redirect的值按例子的方法拼接上licenseUID的值填写到了M3U8地址栏,以及post里authkey的值填到了自定义KEY里,运行后大概下载了5M左右内容就报错闪退了

刚才继续分析了一下fiddler抓取的请求,发现自己犯了一个小白错误,我把笔者例子里的花括号也放进地址里了,然后我发现了更简洁的做法。

首先还是准备好系统代理,最新版的dmmVR,fiddler,DL。

浏览器打开流媒体跳转,依次会有以下请求,发送包选择查看RAW,接收包选择查看JSON,图1是调起播放器,图2是请求认证,图3就是我们需要的,直接把GET的值填到M3U8栏了就可以下载了。

实际上GET的值就是返回值里redirect和UID的拼接,我也是在这发现多加了花括号,postman没接触过看不太懂,这里也不需要

屏幕截图 2023-07-27 174429 屏幕截图 2023-07-27 174204 屏幕截图 2023-07-27 174233

Kiva007 commented 11 months ago

新的发现,之前postman使用失败的原因是默认请求头为GET,实际应该改为POST,成功收到回复。两种方法收到的链接唯一区别在于443端口的显示,但是它是HTTPS的默认端口,所以就是一样的。 当我再次尝试下载时出现了大量的504错误,通过查看fiddler发现问题在于Connection: CLOSE,但是这个请求是基于HTTP/1.1的,默认应该会保持活跃。经过多次尝试之后发现原因在于我们只填写了url,请求头的参数是不完整的,正好下载器还提供了请求头的填写框,填上再发送,果然正常下载了。

暂行结论 使用service之后的首个digital链接 将url填入M3U8地址,即{https://至access=1} header填入请求头,即{GET至HTTP1.1}

diesun commented 11 months ago

你好,我根据你的方法进行下载 QQ图片20230812151552 QQ图片20230812151618 QQ图片20230812151719 但是和你一样下载不了 QQ图片20230812152049 但是如果挂了梯子,就会得到这个结果,一直卡在0bytes/s 到底哪里出问题了呢,纯小白第一次用fidder

Kiva007 commented 10 months ago

你好,我根据你的方法进行下载 QQ图片20230812151552 QQ图片20230812151618 QQ图片20230812151719 但是和你一样下载不了 QQ图片20230812152049 但是如果挂了梯子,就会得到这个结果,一直卡在0bytes/s 到底哪里出问题了呢,纯小白第一次用fidder

import os
import tkinter as tk

CONFIG_PATH = "save_config.txt"
formatted_request = ""  # Declare it as a global variable

def format_request(request, save_name, workdir):
    request_line, headers = request.split('\n', 1)
    method, url, _ = request_line.split(' ', 2)
    headers = headers.split('\n')
    headers = '|'.join(h for h in headers if h)

    result = f"\"{url}\" --saveName \"{save_name}\""

    # If the headers checkbox is checked, add headers to the result
    if use_headers.get():
        result += f" --headers \"{method} {url} HTTP/1.1|{headers}\""

    if workdir:
        result += f" --workDir \"{workdir}\""

    return result

def generate_request():
    """Generates and returns the formatted request."""
    global formatted_request  # Use the global variable
    raw_request = request_entry.get('1.0', tk.END)
    save_name = save_name_entry.get()
    workdir = workdir_entry.get() if workdir_entry.get() != '当前目录' else ''
    formatted_request = format_request(raw_request, save_name, workdir)

    if enable_del_after_done.get():
        formatted_request += " --enableDelAfterDone"
    if disable_date_info.get():
        formatted_request += " --disableDateInfo"

    return formatted_request

def submit():
    request = generate_request()
    os.system(f"start cmd /k N_m3u8DL-CLI_v3.0.2.exe {request}")  # Use os.system instead

    with open(CONFIG_PATH, 'w') as f:
        f.write(f"{save_name_entry.get()}\n{workdir_entry.get()}\n")

def run_program(path):
    os.startfile(path)

def paste(event):
    content = root.clipboard_get()
    event.widget.insert('insert', content)

def clear():
    request_entry.delete('1.0', tk.END)

def clear_entry(event):
    event.widget.delete(0, tk.END)

def workdir_focus_in(event):
    workdir_label.pack_forget()

def workdir_focus_out(event):
    if not workdir_entry.get():
        workdir_label.pack(fill='both', expand=True)

def load_config():
    if os.path.exists(CONFIG_PATH):
        with open(CONFIG_PATH, 'r') as f:
            lines = f.readlines()
            if len(lines) > 0:
                save_name_entry.insert(0, lines[0].strip())
            if len(lines) > 1:
                workdir_entry.insert(0, lines[1].strip())
                if lines[1].strip():
                    workdir_label.pack_forget()

root = tk.Tk()
root.title("Request Formatter")
root.resizable(False, False)
root.attributes('-topmost', True)

window_width = 380
window_height = 420
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
position_top = int(screen_height / 2 - window_height / 2)
position_right = int(screen_width / 2 - window_width / 2)
root.geometry(f"{window_width}x{window_height}+{position_right}+{position_top}")

request_label = tk.Label(root, text="HTTP GET request:")
request_label.grid(row=0, column=0, columnspan=2)

request_entry = tk.Text(root, height=19, width=50)
request_entry.grid(row=1, column=0, columnspan=2, padx=10, pady=(0, 5))
request_entry.bind("<Button-3>", paste)

button_frame = tk.Frame(root)
button_frame.grid(row=2, column=0, columnspan=2)

submit_button = tk.Button(button_frame, text="Submit", command=submit)
submit_button.pack(side=tk.LEFT)

clear_button = tk.Button(button_frame, text="Clear", command=clear)
clear_button.pack(side=tk.LEFT)

result_button = tk.Button(button_frame, text="Result", command=lambda: (root.clipboard_clear(), root.clipboard_append(generate_request())))
result_button.pack(side=tk.LEFT)

programs = ['DMMVR', 'Fiddler']

for i, program in enumerate(programs):
    button = tk.Button(button_frame, text=program, command=lambda p=program: run_program(p))
    button.pack(side=tk.LEFT)

save_name_label = tk.Label(root, text="Save name:")
save_name_label.grid(row=3, column=0)

save_name_entry = tk.Entry(root)
save_name_entry.grid(row=4, column=0)
save_name_entry.bind("<Button-3>", paste)

workdir_label = tk.Label(root, text="WorkDir:")
workdir_label.grid(row=3, column=1)

workdir_entry = tk.Entry(root)
workdir_entry.grid(row=4, column=1)
workdir_entry.bind("<Button-3>", paste)
workdir_entry.bind("<FocusIn>", workdir_focus_in)
workdir_entry.bind("<FocusOut>", workdir_focus_out)

workdir_label = tk.Label(workdir_entry, text='当前目录', foreground='grey')
workdir_label.pack(fill='both', expand=True)

disable_date_info = tk.BooleanVar()
disable_date_info.set(True)
disable_date_info_checkbutton = tk.Checkbutton(root, text="disableDateInfo", variable=disable_date_info)
disable_date_info_checkbutton.grid(row=5, column=0, sticky=tk.W, padx=(33, 0))

enable_del_after_done = tk.BooleanVar()
enable_del_after_done.set(True)
enable_del_after_done_checkbutton = tk.Checkbutton(root, text="enableDelAfterDone", variable=enable_del_after_done)
enable_del_after_done_checkbutton.grid(row=5, column=1)

use_headers = tk.BooleanVar()
use_headers.set(False)  # Default is not checked
headers_checkbutton = tk.Checkbutton(root, text="headers", variable=use_headers)
headers_checkbutton.grid(row=6, column=0, sticky=tk.W, padx=(33, 0))

load_config()

root.mainloop()

我没看出来问题,但是我尝试后发现方法没有失效,另外我现在是把整个伪装头加入参数的。你可以试试这个代码,我利用ChatGPT 3.5编写的GUI。我的python环境为3.11。把它保存在下载器的根目录,另外创建DMMVR和FD的快捷方式,也放到下载器根目录,命名为DMMVR, Fiddler,然后就可以运行脚本了。(第三方编译器可能会出错,最好在原生环境下运行,我debug 搞了半天,和GPT斗智斗勇,发现是解释器有毛病...)

运行后有两个输入框,上为输入链接,下为输入文件名(右键可以自动粘贴)

逻辑和之前一样,把digital的那一段复制下来,但是现在我们复制全部,即从GET到Alive。 复制粘贴后点击Submit,不出意外的话会唤起下载器进行下载。关于参数的三个选项参见下载器的解释。点击result可以查看生成的链接。FD显示链接失效后(404)关闭窗口,点击Clear,再重复上述步骤即可。链接状态码200却报红的话建议更换节点试试。 其中DMMVR, Fiddler两个按钮点击即启动对应程序,这在更新链接时比较方便。

另外并非只有日本本土IP可以下载,获取下载链接启动下载器解析链接后,部分非jp节点也有速度。具体机制尚未探明,可以确定的是jp节点更加稳定。

Logosng commented 6 months ago

1 2 在 fidder 获取视频信息的请求postman,我失敗了(Could not send request ) 是不是需要Authorization: Bearer {{ token }}??

AmemiaErika commented 3 months ago

你好,我根据你的方法进行下载 QQ图片20230812151552 QQ图片20230812151618 QQ图片20230812151719 但是和你一样下载不了 QQ图片20230812152049 但是如果挂了梯子,就会得到这个结果,一直卡在0bytes/s 到底哪里出问题了呢,纯小白第一次用fidder

import os
import tkinter as tk

CONFIG_PATH = "save_config.txt"
formatted_request = ""  # Declare it as a global variable

def format_request(request, save_name, workdir):
    request_line, headers = request.split('\n', 1)
    method, url, _ = request_line.split(' ', 2)
    headers = headers.split('\n')
    headers = '|'.join(h for h in headers if h)

    result = f"\"{url}\" --saveName \"{save_name}\""

    # If the headers checkbox is checked, add headers to the result
    if use_headers.get():
        result += f" --headers \"{method} {url} HTTP/1.1|{headers}\""

    if workdir:
        result += f" --workDir \"{workdir}\""

    return result

def generate_request():
    """Generates and returns the formatted request."""
    global formatted_request  # Use the global variable
    raw_request = request_entry.get('1.0', tk.END)
    save_name = save_name_entry.get()
    workdir = workdir_entry.get() if workdir_entry.get() != '当前目录' else ''
    formatted_request = format_request(raw_request, save_name, workdir)

    if enable_del_after_done.get():
        formatted_request += " --enableDelAfterDone"
    if disable_date_info.get():
        formatted_request += " --disableDateInfo"

    return formatted_request

def submit():
    request = generate_request()
    os.system(f"start cmd /k N_m3u8DL-CLI_v3.0.2.exe {request}")  # Use os.system instead

    with open(CONFIG_PATH, 'w') as f:
        f.write(f"{save_name_entry.get()}\n{workdir_entry.get()}\n")

def run_program(path):
    os.startfile(path)

def paste(event):
    content = root.clipboard_get()
    event.widget.insert('insert', content)

def clear():
    request_entry.delete('1.0', tk.END)

def clear_entry(event):
    event.widget.delete(0, tk.END)

def workdir_focus_in(event):
    workdir_label.pack_forget()

def workdir_focus_out(event):
    if not workdir_entry.get():
        workdir_label.pack(fill='both', expand=True)

def load_config():
    if os.path.exists(CONFIG_PATH):
        with open(CONFIG_PATH, 'r') as f:
            lines = f.readlines()
            if len(lines) > 0:
                save_name_entry.insert(0, lines[0].strip())
            if len(lines) > 1:
                workdir_entry.insert(0, lines[1].strip())
                if lines[1].strip():
                    workdir_label.pack_forget()

root = tk.Tk()
root.title("Request Formatter")
root.resizable(False, False)
root.attributes('-topmost', True)

window_width = 380
window_height = 420
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
position_top = int(screen_height / 2 - window_height / 2)
position_right = int(screen_width / 2 - window_width / 2)
root.geometry(f"{window_width}x{window_height}+{position_right}+{position_top}")

request_label = tk.Label(root, text="HTTP GET request:")
request_label.grid(row=0, column=0, columnspan=2)

request_entry = tk.Text(root, height=19, width=50)
request_entry.grid(row=1, column=0, columnspan=2, padx=10, pady=(0, 5))
request_entry.bind("<Button-3>", paste)

button_frame = tk.Frame(root)
button_frame.grid(row=2, column=0, columnspan=2)

submit_button = tk.Button(button_frame, text="Submit", command=submit)
submit_button.pack(side=tk.LEFT)

clear_button = tk.Button(button_frame, text="Clear", command=clear)
clear_button.pack(side=tk.LEFT)

result_button = tk.Button(button_frame, text="Result", command=lambda: (root.clipboard_clear(), root.clipboard_append(generate_request())))
result_button.pack(side=tk.LEFT)

programs = ['DMMVR', 'Fiddler']

for i, program in enumerate(programs):
    button = tk.Button(button_frame, text=program, command=lambda p=program: run_program(p))
    button.pack(side=tk.LEFT)

save_name_label = tk.Label(root, text="Save name:")
save_name_label.grid(row=3, column=0)

save_name_entry = tk.Entry(root)
save_name_entry.grid(row=4, column=0)
save_name_entry.bind("<Button-3>", paste)

workdir_label = tk.Label(root, text="WorkDir:")
workdir_label.grid(row=3, column=1)

workdir_entry = tk.Entry(root)
workdir_entry.grid(row=4, column=1)
workdir_entry.bind("<Button-3>", paste)
workdir_entry.bind("<FocusIn>", workdir_focus_in)
workdir_entry.bind("<FocusOut>", workdir_focus_out)

workdir_label = tk.Label(workdir_entry, text='当前目录', foreground='grey')
workdir_label.pack(fill='both', expand=True)

disable_date_info = tk.BooleanVar()
disable_date_info.set(True)
disable_date_info_checkbutton = tk.Checkbutton(root, text="disableDateInfo", variable=disable_date_info)
disable_date_info_checkbutton.grid(row=5, column=0, sticky=tk.W, padx=(33, 0))

enable_del_after_done = tk.BooleanVar()
enable_del_after_done.set(True)
enable_del_after_done_checkbutton = tk.Checkbutton(root, text="enableDelAfterDone", variable=enable_del_after_done)
enable_del_after_done_checkbutton.grid(row=5, column=1)

use_headers = tk.BooleanVar()
use_headers.set(False)  # Default is not checked
headers_checkbutton = tk.Checkbutton(root, text="headers", variable=use_headers)
headers_checkbutton.grid(row=6, column=0, sticky=tk.W, padx=(33, 0))

load_config()

root.mainloop()

我没看出来问题,但是我尝试后发现方法没有失效,另外我现在是把整个伪装头加入参数的。你可以试试这个代码,我利用ChatGPT 3.5编写的GUI。我的python环境为3.11。把它保存在下载器的根目录,另外创建DMMVR和FD的快捷方式,也放到下载器根目录,命名为DMMVR, Fiddler,然后就可以运行脚本了。(第三方编译器可能会出错,最好在原生环境下运行,我debug 搞了半天,和GPT斗智斗勇,发现是解释器有毛病...)

运行后有两个输入框,上为输入链接,下为输入文件名(右键可以自动粘贴)

逻辑和之前一样,把digital的那一段复制下来,但是现在我们复制全部,即从GET到Alive。 复制粘贴后点击Submit,不出意外的话会唤起下载器进行下载。关于参数的三个选项参见下载器的解释。点击result可以查看生成的链接。FD显示链接失效后(404)关闭窗口,点击Clear,再重复上述步骤即可。链接状态码200却报红的话建议更换节点试试。 其中DMMVR, Fiddler两个按钮点击即启动对应程序,这在更新链接时比较方便。

另外并非只有日本本土IP可以下载,获取下载链接启动下载器解析链接后,部分非jp节点也有速度。具体机制尚未探明,可以确定的是jp节点更加稳定。

感谢脚本分享,我从dmm打开流媒体传输几乎都是404,试过换节点,但是我按照你提供的方法成功了一次,那次恰好可以正常从链接播放视频,这种情况是网络的问题吗? 3R(OR_ZVV(BSQ~ RDP`2Q2V

另外我无意间发现Fiddler抓取播放器播放下载到本地的wsdcf文件时,idm会提示能下载MP4格式的视频 PZZ 7G(VHTG4{`GN1YZF1XJ

但是会显示http服务器无响应 S9PC~TP$FJ4_}M0 }5))_`G

由于我对专业知识一窍不通,想知道这是否是一种有效的视频下载办法

c2879351010 commented 3 months ago

感谢,今天测试下来下载没问题

samuraiEX commented 1 month ago

建议想抓包的,直接用上面提供的py脚本,只需用Fiddler,按上面说的无脑复制对应信息进去,即可正常下载,2024年6月亲测,还能正常下载

592767809 commented 1 month ago

建议想抓包的,直接用上面提供的py脚本,只需用Fiddler,按上面说的无脑复制对应信息进去,即可正常下载,2024年6月亲测,还能正常下载

那如果想发包呢?

diesun commented 1 week ago

建议想抓包的,直接用上面提供的py脚本,只需用Fiddler,按上面说的无脑复制信息进去,即可正常下载,2024年6月亲测,还能正常下载

2024/06/28测试已经不行了,在线播放都放不出来,日本、美国的梯子都不行,我问了问dmm的客服,他说用了vpn好像不能在线播放了,fidder监测只出现一个service和digital,即使把digital里的连接复制dl下载也没有反应

diesun commented 1 week ago

你好,我根据你的方法进行下载但是和你一样下载不了但是如果挂了梯子,就会得到这个结果,一直卡在0bytes/s 到底哪里出问题了呢,纯小白第一次用fidderQQ图片20230812151552 QQ图片20230812151618 QQ图片20230812151719QQ图片20230812152049

import os
import tkinter as tk

CONFIG_PATH = "save_config.txt"
formatted_request = ""  # Declare it as a global variable

def format_request(request, save_name, workdir):
    request_line, headers = request.split('\n', 1)
    method, url, _ = request_line.split(' ', 2)
    headers = headers.split('\n')
    headers = '|'.join(h for h in headers if h)

    result = f"\"{url}\" --saveName \"{save_name}\""

    # If the headers checkbox is checked, add headers to the result
    if use_headers.get():
        result += f" --headers \"{method} {url} HTTP/1.1|{headers}\""

    if workdir:
        result += f" --workDir \"{workdir}\""

    return result

def generate_request():
    """Generates and returns the formatted request."""
    global formatted_request  # Use the global variable
    raw_request = request_entry.get('1.0', tk.END)
    save_name = save_name_entry.get()
    workdir = workdir_entry.get() if workdir_entry.get() != '当前目录' else ''
    formatted_request = format_request(raw_request, save_name, workdir)

    if enable_del_after_done.get():
        formatted_request += " --enableDelAfterDone"
    if disable_date_info.get():
        formatted_request += " --disableDateInfo"

    return formatted_request

def submit():
    request = generate_request()
    os.system(f"start cmd /k N_m3u8DL-CLI_v3.0.2.exe {request}")  # Use os.system instead

    with open(CONFIG_PATH, 'w') as f:
        f.write(f"{save_name_entry.get()}\n{workdir_entry.get()}\n")

def run_program(path):
    os.startfile(path)

def paste(event):
    content = root.clipboard_get()
    event.widget.insert('insert', content)

def clear():
    request_entry.delete('1.0', tk.END)

def clear_entry(event):
    event.widget.delete(0, tk.END)

def workdir_focus_in(event):
    workdir_label.pack_forget()

def workdir_focus_out(event):
    if not workdir_entry.get():
        workdir_label.pack(fill='both', expand=True)

def load_config():
    if os.path.exists(CONFIG_PATH):
        with open(CONFIG_PATH, 'r') as f:
            lines = f.readlines()
            if len(lines) > 0:
                save_name_entry.insert(0, lines[0].strip())
            if len(lines) > 1:
                workdir_entry.insert(0, lines[1].strip())
                if lines[1].strip():
                    workdir_label.pack_forget()

root = tk.Tk()
root.title("Request Formatter")
root.resizable(False, False)
root.attributes('-topmost', True)

window_width = 380
window_height = 420
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
position_top = int(screen_height / 2 - window_height / 2)
position_right = int(screen_width / 2 - window_width / 2)
root.geometry(f"{window_width}x{window_height}+{position_right}+{position_top}")

request_label = tk.Label(root, text="HTTP GET request:")
request_label.grid(row=0, column=0, columnspan=2)

request_entry = tk.Text(root, height=19, width=50)
request_entry.grid(row=1, column=0, columnspan=2, padx=10, pady=(0, 5))
request_entry.bind("<Button-3>", paste)

button_frame = tk.Frame(root)
button_frame.grid(row=2, column=0, columnspan=2)

submit_button = tk.Button(button_frame, text="Submit", command=submit)
submit_button.pack(side=tk.LEFT)

clear_button = tk.Button(button_frame, text="Clear", command=clear)
clear_button.pack(side=tk.LEFT)

result_button = tk.Button(button_frame, text="Result", command=lambda: (root.clipboard_clear(), root.clipboard_append(generate_request())))
result_button.pack(side=tk.LEFT)

programs = ['DMMVR', 'Fiddler']

for i, program in enumerate(programs):
    button = tk.Button(button_frame, text=program, command=lambda p=program: run_program(p))
    button.pack(side=tk.LEFT)

save_name_label = tk.Label(root, text="Save name:")
save_name_label.grid(row=3, column=0)

save_name_entry = tk.Entry(root)
save_name_entry.grid(row=4, column=0)
save_name_entry.bind("<Button-3>", paste)

workdir_label = tk.Label(root, text="WorkDir:")
workdir_label.grid(row=3, column=1)

workdir_entry = tk.Entry(root)
workdir_entry.grid(row=4, column=1)
workdir_entry.bind("<Button-3>", paste)
workdir_entry.bind("<FocusIn>", workdir_focus_in)
workdir_entry.bind("<FocusOut>", workdir_focus_out)

workdir_label = tk.Label(workdir_entry, text='当前目录', foreground='grey')
workdir_label.pack(fill='both', expand=True)

disable_date_info = tk.BooleanVar()
disable_date_info.set(True)
disable_date_info_checkbutton = tk.Checkbutton(root, text="disableDateInfo", variable=disable_date_info)
disable_date_info_checkbutton.grid(row=5, column=0, sticky=tk.W, padx=(33, 0))

enable_del_after_done = tk.BooleanVar()
enable_del_after_done.set(True)
enable_del_after_done_checkbutton = tk.Checkbutton(root, text="enableDelAfterDone", variable=enable_del_after_done)
enable_del_after_done_checkbutton.grid(row=5, column=1)

use_headers = tk.BooleanVar()
use_headers.set(False)  # Default is not checked
headers_checkbutton = tk.Checkbutton(root, text="headers", variable=use_headers)
headers_checkbutton.grid(row=6, column=0, sticky=tk.W, padx=(33, 0))

load_config()

root.mainloop()

我没有看到有任何疑问,但是我尝试后发现方法没有失效,另外我现在是把整个伪装头加入参数的。你可以试试这个代码,我利用ChatGPT 3.5编写的GUI。我的python环境为3.11。它将保存在下载器的根目录,另外创建DMMVR和FD的快捷方式,也放到下载器根目录,命名为DMMVR、Fiddler,然后就可以运行脚本了。(第三方编译器可能会出错,最好在原生环境下运行,我debug搞了半天,和GPT斗智斗勇,发现是解释器有毛病...)

运行后 这两个输入框,上为输入链接,下为输入文件名(右键可以自动粘贴)

逻辑和之前一样,把digital的那一段复制下来,但是现在我们复制全部,即从GET到Alive。 复制粘贴后点击Submit,不出意外的话会唤起下载器进行下载。关于参数的三个选项参见下载器的解释。点击结果可以查看生成的链接。FD显示链接失效后(404)关闭,点击Clear,再重复上述步骤即可。链接状态码200却报红的话建议更换节点试试。 其中DMMVR, Fiddler两个按钮点击即启动对应程序,这在更新链接时比较方便。

另外并非本土IP可以下载,获取下载链接启动器解析链接后,部分非jp节点也有所变化。具体机制尚不明确,可以确定的是jp节点更加稳定。

抱歉大佬,再次打扰,现在似乎无法用以前的方法下载了,最近已经没办法进行在线播放,我更换了多个jp的节点甚至其他地区的节点都没办法,我咨询了dmm的客服,他说使用vpn可能无法在线播放,使用fidder只能获得一个service和digital QQ图片20240628205848 然后就不动了,不会继续获取第二个digital,dmmvr播放器也根本没有播放,即使我强行使用digital里的链接复制到dl下载也只会一直显示开始解析,再没有后续反应

diesun commented 1 week ago

你好,我根据你的方法进行下载 QQ图片20230812151552 QQ图片20230812151618 QQ图片20230812151719 但是和你一样下载不了 QQ图片20230812152049 但是如果挂了梯子,就会得到这个结果,一直卡在0bytes/s 到底哪里出问题了呢,纯小白第一次用fidder

import os
import tkinter as tk

CONFIG_PATH = "save_config.txt"
formatted_request = ""  # Declare it as a global variable

def format_request(request, save_name, workdir):
    request_line, headers = request.split('\n', 1)
    method, url, _ = request_line.split(' ', 2)
    headers = headers.split('\n')
    headers = '|'.join(h for h in headers if h)

    result = f"\"{url}\" --saveName \"{save_name}\""

    # If the headers checkbox is checked, add headers to the result
    if use_headers.get():
        result += f" --headers \"{method} {url} HTTP/1.1|{headers}\""

    if workdir:
        result += f" --workDir \"{workdir}\""

    return result

def generate_request():
    """Generates and returns the formatted request."""
    global formatted_request  # Use the global variable
    raw_request = request_entry.get('1.0', tk.END)
    save_name = save_name_entry.get()
    workdir = workdir_entry.get() if workdir_entry.get() != '当前目录' else ''
    formatted_request = format_request(raw_request, save_name, workdir)

    if enable_del_after_done.get():
        formatted_request += " --enableDelAfterDone"
    if disable_date_info.get():
        formatted_request += " --disableDateInfo"

    return formatted_request

def submit():
    request = generate_request()
    os.system(f"start cmd /k N_m3u8DL-CLI_v3.0.2.exe {request}")  # Use os.system instead

    with open(CONFIG_PATH, 'w') as f:
        f.write(f"{save_name_entry.get()}\n{workdir_entry.get()}\n")

def run_program(path):
    os.startfile(path)

def paste(event):
    content = root.clipboard_get()
    event.widget.insert('insert', content)

def clear():
    request_entry.delete('1.0', tk.END)

def clear_entry(event):
    event.widget.delete(0, tk.END)

def workdir_focus_in(event):
    workdir_label.pack_forget()

def workdir_focus_out(event):
    if not workdir_entry.get():
        workdir_label.pack(fill='both', expand=True)

def load_config():
    if os.path.exists(CONFIG_PATH):
        with open(CONFIG_PATH, 'r') as f:
            lines = f.readlines()
            if len(lines) > 0:
                save_name_entry.insert(0, lines[0].strip())
            if len(lines) > 1:
                workdir_entry.insert(0, lines[1].strip())
                if lines[1].strip():
                    workdir_label.pack_forget()

root = tk.Tk()
root.title("Request Formatter")
root.resizable(False, False)
root.attributes('-topmost', True)

window_width = 380
window_height = 420
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
position_top = int(screen_height / 2 - window_height / 2)
position_right = int(screen_width / 2 - window_width / 2)
root.geometry(f"{window_width}x{window_height}+{position_right}+{position_top}")

request_label = tk.Label(root, text="HTTP GET request:")
request_label.grid(row=0, column=0, columnspan=2)

request_entry = tk.Text(root, height=19, width=50)
request_entry.grid(row=1, column=0, columnspan=2, padx=10, pady=(0, 5))
request_entry.bind("<Button-3>", paste)

button_frame = tk.Frame(root)
button_frame.grid(row=2, column=0, columnspan=2)

submit_button = tk.Button(button_frame, text="Submit", command=submit)
submit_button.pack(side=tk.LEFT)

clear_button = tk.Button(button_frame, text="Clear", command=clear)
clear_button.pack(side=tk.LEFT)

result_button = tk.Button(button_frame, text="Result", command=lambda: (root.clipboard_clear(), root.clipboard_append(generate_request())))
result_button.pack(side=tk.LEFT)

programs = ['DMMVR', 'Fiddler']

for i, program in enumerate(programs):
    button = tk.Button(button_frame, text=program, command=lambda p=program: run_program(p))
    button.pack(side=tk.LEFT)

save_name_label = tk.Label(root, text="Save name:")
save_name_label.grid(row=3, column=0)

save_name_entry = tk.Entry(root)
save_name_entry.grid(row=4, column=0)
save_name_entry.bind("<Button-3>", paste)

workdir_label = tk.Label(root, text="WorkDir:")
workdir_label.grid(row=3, column=1)

workdir_entry = tk.Entry(root)
workdir_entry.grid(row=4, column=1)
workdir_entry.bind("<Button-3>", paste)
workdir_entry.bind("<FocusIn>", workdir_focus_in)
workdir_entry.bind("<FocusOut>", workdir_focus_out)

workdir_label = tk.Label(workdir_entry, text='当前目录', foreground='grey')
workdir_label.pack(fill='both', expand=True)

disable_date_info = tk.BooleanVar()
disable_date_info.set(True)
disable_date_info_checkbutton = tk.Checkbutton(root, text="disableDateInfo", variable=disable_date_info)
disable_date_info_checkbutton.grid(row=5, column=0, sticky=tk.W, padx=(33, 0))

enable_del_after_done = tk.BooleanVar()
enable_del_after_done.set(True)
enable_del_after_done_checkbutton = tk.Checkbutton(root, text="enableDelAfterDone", variable=enable_del_after_done)
enable_del_after_done_checkbutton.grid(row=5, column=1)

use_headers = tk.BooleanVar()
use_headers.set(False)  # Default is not checked
headers_checkbutton = tk.Checkbutton(root, text="headers", variable=use_headers)
headers_checkbutton.grid(row=6, column=0, sticky=tk.W, padx=(33, 0))

load_config()

root.mainloop()

我没看出来问题,但是我尝试后发现方法没有失效,另外我现在是把整个伪装头加入参数的。你可以试试这个代码,我利用ChatGPT 3.5编写的GUI。我的python环境为3.11。把它保存在下载器的根目录,另外创建DMMVR和FD的快捷方式,也放到下载器根目录,命名为DMMVR, Fiddler,然后就可以运行脚本了。(第三方编译器可能会出错,最好在原生环境下运行,我debug 搞了半天,和GPT斗智斗勇,发现是解释器有毛病...)

运行后有两个输入框,上为输入链接,下为输入文件名(右键可以自动粘贴)

逻辑和之前一样,把digital的那一段复制下来,但是现在我们复制全部,即从GET到Alive。 复制粘贴后点击Submit,不出意外的话会唤起下载器进行下载。关于参数的三个选项参见下载器的解释。点击result可以查看生成的链接。FD显示链接失效后(404)关闭窗口,点击Clear,再重复上述步骤即可。链接状态码200却报红的话建议更换节点试试。 其中DMMVR, Fiddler两个按钮点击即启动对应程序,这在更新链接时比较方便。

另外并非只有日本本土IP可以下载,获取下载链接启动下载器解析链接后,部分非jp节点也有速度。具体机制尚未探明,可以确定的是jp节点更加稳定。

兄弟们,好消息,我找到一个新的下载方法,https://tieba.baidu.com/p/9068142085,要不怎么说贴吧人才多,这个软件github也能找到,跟咱们原来的方法不同,它不需要抓取链接,而是需要下载的wsdcf的加密视频文件,这个软件也要梯子,也得调用dmmvr播放器,不知道是直接破解,还是在播放本地文件时与服务器通讯时获取了什么链接,但是我机场的流量监测没有变化,什么原理呢。。。