背景
有时候我们看油管视频的时候,想下载一些学习视频的时候,往往是得把视频的url复制好了,粘贴到一个些下载的网站(savefrom.net)下载,但是这些网站会有一定的限制,如视频格式和清晰度。我们可以尝试搭建一个通过谷歌插件去下载。
前提
你能科学上网或者有一台国外的云服务器。简单暴力的科学上网是,购买台国外的window系统服务器,远程登录,打开浏览器就可以访问油管了。
思路
如果有台国外的服务器,可以通过在服务器那边部署一个web服务,把要下载的地址给通过web服务给到服务器那里,然后通过yt-dlp命令下载油管视频。还有一种方式通过代理方式,yt-dlp是支持代理的。首先编写谷歌插件,谷歌插件那边要实现的就是根据地址栏是油管,发现有视频,就给视频的标题下添加下载的界面。视频的下载分辨率和下载的操作都是访问web后端,右侧栏就是显示下载进度和下载完成。
谷歌插件
content.js打开和刷新页面加载时就加载,可以修改网页样式service-worker.js主要监听浏览器的事件sidepanel.html右侧栏界面
教程可以看官网有中文文档。github上有很多的demo案例,我这里使用的是右侧栏(cookbook.sidepanel-global)做模板,可以方便的看到下载任务。
manifest.json代码
{
"manifest_version": 3,
"name": "油管视频下载",
"version": "1.0",
"description": "油管视频下载插件",
"permissions": [
"activeTab",
"scripting",
"sidePanel",
"tabs"
],
"background": {
"service_worker": "service-worker.js"
},
"action": {
"default_title": "点击打开右侧栏"
},
"side_panel": {
"default_path": "sidepanel.html"
},
"content_scripts": [
{
"css": [
"./content/styles.css"
],
"js": [
"./content/content.js"
],
"matches": [
"https://www.youtube.com/*"
]
}
],
"icons": {
"16": "images/icon-16.png",
"48": "images/icon-48.png",
"128": "images/icon-128.png"
}
}
谷歌浏览器插件加载地址
后端
先在服务端安装yt-dlp
apt install yt-dlp
选择使用nodejs,web服务框架使用express,通过命令去调用yt-dlp下载,解析命令返回的结构,后端部分代码如下:
var express = require('express');
var router = express.Router();
var config = require('../config');
const fs = require('fs')
const { spawn } = require('child_process');
var task = [];
function sendWs(req, data) {
req.wss.clients.forEach((client) => {
if (client.readyState === 1) {
client.send(JSON.stringify(data));
}
});
}
router.get('/download', function (req, res) {
const queryObject = req.query;
var taskIndex = 0;
const command = 'yt-dlp';
var args = ['--no-playlist', '-F', queryObject.url];
if (config.socks) {
args = [
'--proxy', config.socks,
'--no-playlist',
'-F', queryObject.url
];
}
const ytDlpProcess = spawn(command, args);
let outputBuffer = '';
let read = false
const info = [];
let uid = '';
const regex = /for\s(.*?):/
ytDlpProcess.stdout.on('data', (data) => {
console.log(`stdout: ${data.toString()}`);
outputBuffer += data.toString();
const lines = outputBuffer.split(/(\n|\r)/);
outputBuffer = lines.pop();
lines.forEach(line => {
if (read) {
const l = line.split('\|')[0];
const d = l.split(/\s+/);
if (d.length > 2) {
if (d[1] == 'mp4') {
var id = d[0];
var ext = d[1];
var resolution = d[2];
var ch = d[4];
if (resolution == 'audio') {
if (!info.some(obj => obj.resolution == resolution)) {
info.push({ id, ext, resolution,uid })
}
} else {
if (!info.some(obj => obj.resolution == resolution && obj.ch == ch)) {
info.push({ id, ext, resolution,uid, ch })
}
}
}
}
}
if (line.indexOf('-----------------------------') != -1) {
read = true
}else if(line.indexOf('[info] Available formats for')!=-1){
const match = line.match(regex);
if (match) {
uid = match[1]
}
}
});
});
ytDlpProcess.stderr.on('data', (data) => {
console.error(`stderr: ${data.toString()}`);
});
ytDlpProcess.on('close', (code) => {
info.sort((a, b) => {
var x = a.ch? 1:0;
var y = b.ch? 1:0;
return y - x;
});
res.json({ code: 0, msg: 'success', data: info });
});
ytDlpProcess.on('error', (error) => {
console.error(`Failed to start yt-dlp process: ${error.message}`);
});
//下载封面
const regexes = [
/.*\?v=([^&]+)/,
/\/shorts\/(.*)$/,
];
var videoId;
for(var i = 0;i<regexes.length;i++){
var match = queryObject.url.match(regexes[i])
if(match){
videoId = match[1];
break;
}
}
if(videoId){
const imgCommand = 'yt-dlp';
var imgArgs = ['--no-playlist','--write-thumbnail','--skip-download','-P', config.downloadPath,'-o',videoId ,queryObject.url];
if (config.socks) {
imgArgs = [
'--proxy', config.socks,
'--no-playlist',
'--write-thumbnail',
'--skip-download',
'-P', config.downloadPath,
'-o',videoId,
queryObject.url
];
}
const imgProcess = spawn(imgCommand, imgArgs);
imgProcess.stdout.on('data', (data) => {
console.log(`imgProcess: ${data.toString()}`);
});
imgProcess.stderr.on('data', (data) => {
console.error(`imgProcess: ${data.toString()}`);
});
imgProcess.on('close', (code) => {
console.log(`imgProcess: ${code.toString()}`);
});
imgProcess.on('error', (error) => {
console.error(`imgProcess Failed to start yt-dlp process: ${error.message}`);
});
}
});
module.exports = router;
为了保证页面进度的实时更新,使用websocket推送信息
const WebSocket = require('ws');
const createWebSocketServer = (httpServer) => {
const wss = new WebSocket.Server({ server: httpServer });
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (message) => {
console.log(`Received: ${message}`);
// Echo the received message back to the client
ws.send(`Echo: ${message}`);
});
ws.on('close', () => {
console.log('Client disconnected');
});
});
return wss;
};
module.exports = createWebSocketServer;
图标
图标可以通过阿里的icon,找一个图标
可以去通过IconKitchen调整一下图标
通过python去生成一下对应尺寸16,48,128
from PIL import Image
if __name__ == '__main__':
# 原始图片路径
input_image_path = 'icon-512.png'
# 读取原始图片
original_image = Image.open(input_image_path)
# 定义需要生成的尺寸列表
sizes = [
(16, 16),
(48, 48),
(128, 128)
]
# 遍历尺寸列表,为每个尺寸创建并保存图片
for size in sizes:
# 调整图片大小
resized_image = original_image.resize(size, Image.LANCZOS) # 使用 LANCZOS 替换 ANTIALIAS
# 保存图片,文件名包含尺寸信息
output_image_path = f'icon-{size[0]}.png'
resized_image.save(output_image_path)
print(f"Saved {output_image_path}")
遇到问题
1.express使用默认端口,启动Websocket有问题,更换端口解决,好奇怪更换端口了默认3000端口还是可以访问的。
2.使用内联事件会违反了内容安全策略(CSP),就是html标签上不能使用onclick等事件监听,通js去监听事件解决。