GopeedLab / gopeed

A modern download manager that supports all platforms. Built with Golang and Flutter.
https://gopeed.com
GNU General Public License v3.0
17.1k stars 1.2k forks source link

使用Python服务接口下载短视频时遇到206响应问题 #721

Closed ShCode01 closed 2 months ago

ShCode01 commented 2 months ago

作者您好,我最近开发了一个扩展插件,目的是通过yt-dlp实现全网短视频下载。我将这个插件制作成了Python服务接口,在浏览器上测试时一切正常。然而,当我在Gopeed下载器中使用这个接口时,却出现了206 Partial Content的响应,导致Gopeed直接退出。能否请您帮助我优化这个Python接口,以确保它能在各种下载器中稳定运行? c253e855f0a0831f1d324f8e23fbe7c

import io
import shutil
import tempfile
from flask import Flask, Response, request, send_file, abort, jsonify,after_this_request
import yt_dlp

import os
import hashlib
import re

import urllib.parse
app = Flask(__name__)

def generate_md5_hash(text):
    """生成文本的 MD5 哈希值"""
    return hashlib.md5(text.encode('utf-8')).hexdigest()

def safe_filename(filename):
    """确保文件名不会包含非法字符"""
    return re.sub(r'[<>:"/\\|?*\x00-\x1F]', '_', filename)

@app.route('/download', methods=['GET'])
def download_video():
    url = request.args.get('url')
    if not url:
        abort(400, description="缺少视频链接参数")

    ydl_opts = {
        'format': 'bestvideo+bestaudio/best',  # 下载最佳质量的视频和音频
        'noplaylist': False,  # 允许下载播放列表
        'quiet': True,  # 抑制输出
        'no_warnings': True,  # 压制警告
    }

    try:
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            # 提取视频信息但不下载
            info_dict = ydl.extract_info(url, download=False)
            video_title = info_dict.get('title', 'unknown')

            # 返回视频标题和链接
            return jsonify({
                'title': video_title,
                'video_url': f'/video?url={url}'
            })

    except Exception as e:
        abort(500, description=f"提取视频信息时出错: {str(e)}")

@app.route('/video', methods=['GET'])
def return_video():
    url = request.args.get('url')
    if not url:
        abort(400, description="缺少视频链接参数")

    ydl_opts = {
        'format': 'bestvideo+bestaudio/best',  # 下载最佳质量的视频和音频
        'noplaylist': False,  # 允许下载播放列表
        'quiet': True,  # 抑制输出
        'no_warnings': True,  # 压制警告
        'outtmpl': 'downloads/%(title)s.%(ext)s',  # 设置下载文件的存储路径
        'merge_output_format': 'mp4',  # 将视频和音频合并成MP4
    }
    DOWNLOAD_DIRECTORY = "downloads"

    try:
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            info_dict = ydl.extract_info(url, download=True)
            video_title = info_dict.get('title', None)
            video_extension = info_dict.get('ext', 'mp4')
            video_filename = f"{video_title}.{video_extension}"
            file_path = os.path.join(DOWNLOAD_DIRECTORY, video_filename)
    except Exception as e:
        abort(500, description=f"下载视频时出错: {str(e)}")

    # Ensure file exists before trying to serve it
    if not os.path.exists(file_path):
        abort(404, description="文件未找到")

    # Open the file in binary mode and wrap it with io.BytesIO
    with open(file_path, 'rb') as file:
        file_bytes = io.BytesIO(file.read())

    # Ensure file is deleted after being sent
    @after_this_request
    def remove_file(response):
        try:
            os.remove(file_path)
        except Exception as error:
            app.logger.error(f"Error removing or closing downloaded file handle: {error}")
        return response

    # URL encode the file name to handle non-ASCII characters
    encoded_filename = urllib.parse.quote(video_filename)
    content_disposition = f"attachment; filename*=UTF-8''{encoded_filename}"

    # Use send_file to return the video file from the io.BytesIO object
    return send_file(
        file_bytes,
        mimetype='video/mp4',
        as_attachment=True,
        download_name=video_filename,

    )

@app.route('/', methods=['GET'])
def home():
    return '服务运行成功, 视频下载接口:/download, 视频返回接口:/video'

if __name__ == '__main__':
    if not os.path.exists('downloads'):
        os.makedirs('downloads')
    app.run(port=2024)
ShCode01 commented 2 months ago

插件代码

gopeed.events.onResolve(async function (ctx) {
  let u = ctx.req.url

  let baseUrl = 'http://127.0.0.1:2024'

  // fetch repo html
  const resp = await fetch(`${baseUrl}/download?url=${u}`, {
    headers: {
      'User-Agent': gopeed.settings.ua,
    },
  });
  const data = await resp.json();

    const url = data.video_url;
    const name = `${data.title}.mp4`;

    ctx.res = {
      name: "shcode",
      files: [
        {
          name,
          req: {
            name,
            url:`${baseUrl}${url}`,
          },
        },
      ],
    };
});
monkeyWie commented 2 months ago

@ShCode01 我看了下你的接口,在 /download 接口实际上已经拿到了真实的下载链接了,可以在这里做一个重定向喂给下载器,这样也不需要走一遍你自己的服务器中转

ShCode01 commented 2 months ago

感谢解答,我现在有个问题:在使用Gopeed下载时,如何设置文件名?目前,我的Python服务接口在Gopeed下载时出现了206 Partial Content响应,导致下载器退出。我需要一种方法,不仅能配合yt-dlp实现全网短视频下载,还能在Gopeed下载时为文件正确命名。您能否帮忙提供解决方案或优化现有的Python接口?

monkeyWie commented 2 months ago

206的时候Gopeed会闪退吗?现在这个接口有个问题就是需要避免重复访问,因为下载器因为多线程下载的情况可能会多次请求/video接口,然后我提供个解决思路:

  1. /download 接口获取到视频信息之后,生成一个对应的唯一token,用dict把视频信息和本地下载状态存好,然后返回,同时异步进行在服务器把文件下载到本地,在下载完成的时候把下载状态标识为完成
  2. /video?token=xxxx,直接通过token去下载视频,如果状态未完成就循环sleep,直到完成,之后调用send_file(filename, as_attachment=True)
ShCode01 commented 2 months ago

响应206 gopeed会闪退的, ys 太菜了,看来这个插件是做不了了

ShCode01 commented 2 months ago

python接口在本地运行,因为我没有服务器

monkeyWie commented 2 months ago

可以提供下gopeed日志目录里的crash.log吗,我看看咋肥事

ShCode01 commented 2 months ago

crash.log

monkeyWie commented 2 months ago

还有个简单的办法,在/video这个接口判断下,如果有Range请求头,就直接返回200,这样下载器就不会去做多线程下载了

ShCode01 commented 2 months ago

我试过python下载完视频后再返回视频内容的,但是都返回206响应了,206响应后gopeed就直接退了,后台也挂了,所以我开发扩展的时候很困难,要不我把我代码发给您吧

ShCode01 commented 2 months ago

https://www.123pan.com/s/xMALVv-4VcY,您可以复现一下

monkeyWie commented 2 months ago

看了下这个日志感觉这个闪退有点奇怪,看看设置界面的http连接数是多少

ShCode01 commented 2 months ago

206闪退的原因找到了,是因为我的连接数默认是0,所以会出现闪退的情况。后续能强制设置连接数默认值不能为0嘛

monkeyWie commented 2 months ago

206闪退的原因找到了,是因为我的连接数默认是0,所以会出现闪退的情况。后续能强制设置连接数默认值不能为0嘛

不闪退应该能下载了吧,但是你最好是设置成1个连接数下载,不然每个连接都会请求/video接口,然后还有你是咋设置成0的

monkeyWie commented 2 months ago

我也再加个兜底吧

ShCode01 commented 2 months ago

不闪退能下载了

ShCode01 commented 2 months ago

连接数

不知道啊,可能刚开始用的时候不小心设置为0了

ShCode01 commented 2 months ago

扩展开发好了,但是YouTube视频加密了,毁了。写扩展的js代码是不是必须加逗号作为代码结束符呀

monkeyWie commented 2 months ago

扩展开发好了,但是YouTube视频加密了,毁了。写扩展的js代码是不是必须加逗号作为代码结束符呀

不是必须的,不过脚手架内置好的eslint和prettier规则估计是要加的(但是也不影响编译),只不过vscode会警告

monkeyWie commented 2 months ago

fixed in v1.5.9