AI摘要:本文讨论了在部署M3U8视频离线下载项目时遇到的问题及其解决方案。主要问题包括容器无法启动和布局错乱。针对启动问题,提供了三种解决方法,其中手动复制配置文件为最佳选择。布局问题则通过修改HTML文件解决。此外,文章还提到上传到Alist和使用aria2下载的相关问题,并提供了相应的代码示例。最终,经过调整,项目基本可用。

前言

最近在某知名网站上看到自己喜欢的视频就想着下载下来,每次都用客户端、软件下载太麻烦,就想着能不能整个网页,直接下载好一键传到网盘,然后就找到了这个项目M3U8dudu,但实测用docker部署会有一些问题。

无法启动

碰到的第一个问题就是使用作者给的命令后容器无法启动,根本原因是挂载的/root/config目录下未生成config.ini配置文件。


手动复制一份config.ini文件到/root/config目录下,并将启动命令修改为python app.py --config /root/config/config.ini

修改启动命令是因为,两边文件无法自动同步更改,不修改的话容器仍然以容器内部config.ini为准,后续管理起来麻烦。


直接删掉/root/config:/app/config,取消挂载配置文件也可以启动。不过后续配置麻烦,且容器更新的话,配置文件会丢失。

修改镜像文件,删除项目内config文件夹及里面config.ini,新建一个generate_config.py来创建配置文件,实测是可以的。不过本人测试环境误删了,导致代码保存下来

布局问题

项目启动后,使用电脑端访问布局错乱,手机端和平板显示正常,这里提供我修改好的index.html和play.html。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>离线视频下载</title>
    <link rel="shortcut icon" href="https://cdn-icons-png.flaticon.com/512/3121/3121602.png">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <a class="navbar-brand" href=" ">Video Download</a >
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav">
                    <li class="nav-item active">
                        <a class="nav-link" href="/">视频下载 <span class="sr-only">(current)</span></a >
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/play">视频播放</a >
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="https://github.com/1314ysys/m3u8dudu">关于项目</a >
                    </li>
                </ul>
            </div>
        </nav>
    </header>

    <main class="container">
        <h2 class="text-center my-4">m3u8视频离线下载</h2>
        <form method="POST" action="/download" onsubmit="return onSubmit();">
            <div class="form-group">
                <label for="file_content">请输入要下载的m3u8地址链接:</label>
                <textarea class="form-control" id="file_content" name="file_content" rows="6" placeholder="每行一个链接,链接后空一格表示自定义标题"></textarea>
            </div>
            <div class="row justify-content-end">
                <div class="col-12 col-sm-4 col-md-3">
                    <button type="submit" class="btn btn-primary mb-2 btn-block">开始下载</button>
                </div>
            </div>
        </form>
        <hr>
        <h5>下载日志:</h5>
        <textarea id="log" class="form-control" style="height: 200px; overflow-y: scroll;" readonly></textarea>
        <div class="row justify-content-end mt-3">
            <div class="col-auto">
                <button class="btn btn-secondary btn-block" onclick="clearLog()">清空日志</button>
            </div>
        </div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>

    <script>
        function onSubmit() {
            const fileContent = document.getElementById("file_content").value.trim();
            if (!fileContent) {
                alert('没有输入链接');
                return false;
            }
            document.getElementById("file_content").value = "";

            alert('开始下载');
            fetch('/download', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: new URLSearchParams({ file_content: fileContent })
            })
            .then(response => response.text())
            .then(data => {
                const log = document.getElementById("log");
                log.value += data;
                localStorage.setItem('log', log.value);
            })
            .catch(error => console.error('下载失败:', error));

            return false;
        }

        let lastLineCount = 0;
        function pollLog() {
            fetch(`/log?last_line_count=${lastLineCount}`)
            .then(response => response.text())
            .then(data => {
                const logArea = document.getElementById('log');
                logArea.value = '';
                localStorage.removeItem('log');
                logArea.value += data;
                lastLineCount += data.trim().split('\n').length;
            })
            .catch(error => console.error('获取日志失败:', error));

            setTimeout(pollLog, 1000);
        }

        window.onload = pollLog;

        function clearLog() {
            if (confirm('确定要清空日志吗?')) {
                document.getElementById('log').value = '';
                localStorage.removeItem('log');

                fetch('/clear_log')
                .then(response => response.text())
                .then(data => console.log('日志已清空:', data))
                .catch(error => console.error('清空日志失败:', error));
            }
        }
    </script>    
</body>
</html>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>m3u8在线视频播放器</title>
    <link rel="shortcut icon" type="image/x-icon" href="https://cdn-icons-png.flaticon.com/512/2696/2696537.png">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
    <style>
        .video-box {
            width: 100%;
            max-width: 90vw;
            max-height: 80vh;
            margin-top: 2vh;
        }
        .container {
            width: 100%;
            max-width: 90vw;
        }
    </style>
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <a class="navbar-brand" href="#">Video Player</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav">
                    <li class="nav-item">
                        <a class="nav-link" href="/">视频下载</a>
                    </li>
                    <li class="nav-item active">
                        <a class="nav-link" href="/play">视频播放<span class="sr-only">(current)</span></a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="https://github.com/1314ysys/m3u8dudu">关于项目</a>
                    </li>
                </ul>
            </div>
        </nav>
    </header>

    <main class="container">
        <h2 class="text-center my-4">m3u8在线播放器</h2>
        <div class="mt-5">
            <form id="video-form">
                <div class="d-flex flex-row">
                    <div class="flex-grow-1">
                        <input type="text" class="form-control rounded-0" id="url" placeholder="输入m3u8视频链接" required>
                    </div>
                    <div class="align-self-start ml-0">
                        <button type="submit" class="btn btn-primary rounded-0">播放</button>
                    </div>
                </div>          
            </form>
        </div>
    
        <div class="video-box">
            <video id="my-video" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto" width="100%" height="100%"></video>
        </div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
    
    <script>
        const video = document.getElementById('my-video');
        const videoSrc = document.getElementById('url');
        const form = document.getElementById('video-form');
        const hls = new Hls();

        form.addEventListener('submit', function(event) {
            event.preventDefault();
            const url = videoSrc.value.trim();

            if (Hls.isSupported()) {
                hls.loadSource(url);
                hls.attachMedia(video);
                hls.on(Hls.Events.MANIFEST_PARSED, function() {
                    video.play();
                });
                hls.on(Hls.Events.ERROR, function(event, data) {
                    if (data.fatal) {
                        switch (data.fatal) {
                            case Hls.ErrorTypes.NETWORK_ERROR:
                                alert('网络错误,无法加载视频.');
                                break;
                            case Hls.ErrorTypes.MEDIA_ERROR:
                                alert('媒体错误,无法播放视频.');
                                break;
                            case Hls.ErrorTypes.OTHER_ERROR:
                                alert('其他错误.');
                                break;
                        }
                    }
                });
            } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
                video.src = url;
                video.addEventListener('loadedmetadata', function() {
                    video.play();
                });
            } else {
                alert('此浏览器不支持播放该视频格式。');
            }
        });
    </script>
</body>
</html>


原视频下载界面
原视频下载界面
原视频播放界面
原视频播放界面
修改后视频下载
修改后视频下载
修改后视频播放
修改后视频播放

上传问题及aria2下载问题


使用原本上传代码,测试一直无法成功上传到alist上,不过脚本还可以完善,比如增加上传进度检测等,但实在是懒得整了,主打一个功能正常,能用就行。

import requests
from urllib.parse import quote
import os
import json

# 读取配置文件
import configparser
config = configparser.ConfigParser()
config.read("./config/config.ini", encoding="utf-8")

base_url = config.get("upload_settings", "base_url", fallback="")
remote_path = config.get("upload_settings", "remote_path", fallback="")
log_path_file = config.get("download_settings", "log_path_file", fallback="./log.txt")

def login():
    """
    登录 Alist 并获取 token
    """
   url = base_url + '/api/auth/login'
    payload = {'Username': config.get("upload_settings", "UserName", fallback=""),
               'Password': config.get("upload_settings", "PassWord", fallback="")}
    try:
        response = requests.post(url, data=payload)
        response_data = response.json()
        if response_data.get('code') in ['200', 200]:
            return response_data.get('data').get('token')
        else:
            print(f"登录失败!错误信息:{response_data}")
            return None
    except Exception as e:
        print(f"登录过程中发生错误!错误信息:{e}")
        return None

def upload_alist(local_file_path, title):
    """
    上传文件到 Alist
    """
    token = login()
    if not token:
        print("获取 Alist token 失败,无法上传文件!")
        return False

    try:
        # 构造请求
        url = base_url + "/api/fs/put"
        file_size = os.path.getsize(local_file_path)
        ori_url = remote_path + title + ".mp4"
        encode_url = quote(ori_url, 'utf-8')

        headers = {
            "Authorization": token,
            "Content-Length": str(file_size),
            "Content-Type": "video/mp4",
            "File-Path": encode_url,
            'As-Task': 'true',
        }
    # 上传文件
        with open(local_file_path, "rb") as f:
            response = requests.put(url, data=f, headers=headers)

        # 读取响应内容(只读取一次)
        response_data = response.json()

        # 检查响应状态
        if response_data.get("code") in ["200", 200]:
            with open(log_path_file, "a", encoding="utf-8") as log_file:
                log_file.write(f"文件 {title}.mp4 上传成功!\n")
            return True
        else:
            with open(log_path_file, "a", encoding="utf-8") as log_file:
                log_file.write(f"上传失败!错误信息:{response_data}\n")
            return False

    except Exception as e:
        with open(log_path_file, "a", encoding="utf-8") as log_file:
            log_file.write(f"上传过程中发生错误!错误信息:{e}\n")
        return False


然后本人aria2-pro同样是docker部署,alist可以调用,但是这个脚本一直调用不了,所有稍微修改了一下,直接调用Alist添加 离线下载任务。

import http.client
import json
import configparser
import datetime
import time
import upload  # 引用 upload.py 中的 login 函数
from typing import Optional, List

# 读取配置文件
config = configparser.ConfigParser()
config.read("./config/config.ini", encoding="utf-8")

# 配置项
try:
    base_url = config.get("upload_settings", "base_url")
except:
    base_url = "http://127.0.0.1:5244"
try:
    download_path = config.get("aria2_settings", "download_path")
except:
    download_path = "./"
try:
    log_path_file = config.get("download_settings", "log_path_file")
except:
    log_path_file = "./log.txt"


def update_last_line(filename, new_line):
    """
    更新指定文件的最后一行
    """
    try:
        with open(filename, 'r', encoding='utf-8') as file:
            lines = file.readlines()
        if len(lines) > 0:  # 如果读到内容,更新最后一行
            lines[-1] = new_line + '\n'
            with open(filename, 'w', encoding='utf-8') as file:
                file.writelines(lines)
        else:  # 如果没有读到内容,直接写入
            with open(filename, 'w', encoding='utf-8') as file:
               file.writelines(new_line)
    except FileNotFoundError:
        with open(filename, 'w', encoding='utf-8') as file:
            file.writelines(f"{filename} not found.")

def adduri(uri):
    """
    提交离线下载任务到 Alist
    """
    # 获取文件名
    title = uri.split('/')[-1]

    # 获取 Alist 的 token
    token = upload.login()
    if not token:
        with open(log_path_file, "a", encoding="utf-8") as log_file:
            log_file.write("获取 Alist token 失败,无法提交离线下载任务!\n")
        return

    # 构造请求
    conn = http.client.HTTPConnection(base_url.split('//')[1])  # 去掉协议部分
    payload = json.dumps({
        "path": download_path,
        "urls": [uri],
        "tool": "aria2",
        "delete_policy": "delete_on_upload_succeed"
    })
    headers = {
        'Authorization': token,
        'Content-Type': 'application/json'
    }

    try:
        # 提交任务
        conn.request("POST", "/api/fs/add_offline_download", payload, headers)
        res = conn.getresponse()
        data = res.read().decode("utf-8")
        res_data = json.loads(data)

        if res_data.get("code") in [200, "200"]:
            with open(log_path_file, "a", encoding="utf-8") as log_file:
                log_file.write(f"任务 {title} 已成功提交到 Alist 离线下载队列\n")
                print(f"任务 {title} 已成功提交到 Alist 离线下载队列")
        else:
            with open(log_path_file, "a", encoding="utf-8") as log_file:
                log_file.write(f"提交任务失败!错误信息:{res_data.get('message')}\n")
                print(f"提交任务失败!错误信息:{res_data.get('message')}")

    except Exception as e:
        with open(log_path_file, "a", encoding="utf-8") as log_file:
            log_file.write(f"提交任务时发生错误!错误信息:{e}\n")
        print(f"提交任务时发生错误!错误信息:{e}")

    finally:
        conn.close()

后记

这样修改完基本属于可以使用了,本人基本也不太会代码修改,这里面的代码基本依靠deepseek完成相应调整及修改

END
本文作者:
文章标题:部署M3U8视频离线下载遇到的问题及解决
本文地址:https://233.517128.xyz/archives/36.html
版权说明:若无注明,本文皆学习笔记原创,转载请保留文章出处。
最后修改:2025 年 02 月 22 日
如果觉得我的文章对你有用,请随意赞赏