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完成相应调整及修改