这篇是在上一篇的基础上写的,这篇负责抖音作者详情页的视频转声音提取,这篇需要用到后端。
本地启动后端后,在控制台输入对应代码,即可实现hover在封面上,按d一键下载音频
- 控制台代码
// 获取作者的视频列表
var liElements = document.querySelectorAll('ul[data-e2e="scroll-list"] li');
// 添加鼠标悬停事件监听器
liElements.forEach(function(li) {
li.addEventListener('mouseenter', function() {
// 添加键盘按下事件监听器
document.addEventListener('keydown', keydownHandler);
});
li.addEventListener('mouseleave', function() {
// 移除键盘按下事件监听器
document.removeEventListener('keydown', keydownHandler);
});
});
// 处理键盘按下事件
function keydownHandler(event) {
// 判断按下的键是否为 'D' 键,keyCode为 '68'
if (event.keyCode === 68) {
// 获取下载链接
var sourceTag = document.querySelector('.basePlayerContainer xg-video-container > video > source:nth-child(1)');
const alt = document.querySelector('.basePlayerContainer').previousElementSibling.querySelector('img').getAttribute('alt');
// 找到第一个冒号并截取之后的部分
let contentAfterColon = alt.includes(':') ?
alt.split(':').slice(1).join(':'):
alt;
// 去除所有的.和空格
let resultString = contentAfterColon.replace(/[.\s]/g, '');
// 如果最终结果为空,替换为'空'
const name = resultString === '' ? '空' : resultString;
// 提取src属性值
var url = sourceTag.getAttribute('src');
// 执行下载操作,这里使用一个假设的下载函数
downloadFile(url, name);
}
}
function downloadFile(url, name) {
// 发送POST请求到后台接口
fetch('http://localhost:3000/convert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ videoUrl: url }),
})
.then((response) => response.blob())
.then((blob) => {
// 创建一个临时的<a>元素用于下载
const a = document.createElement('a');
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = name + '.mp3';
// 触发点击事件以启动下载
a.click();
// 释放URL对象
window.URL.revokeObjectURL(url);
})
.catch((error) => {
console.error('Error:', error);
alert('下载失败,原因:' + error)
});
}
- 后端 node 代码
const express = require('express');
const axios = require('axios');
const ffmpeg = require('fluent-ffmpeg');
const fs = require('fs');
const path = require('path');
const cors = require('cors');
const app = express();
app.use(express.json());
// 允许所有域的请求
app.use(cors());
app.post('/convert', async (req, res) => {
try {
const { videoUrl } = req.body;
if (!videoUrl) {
return res.status(400).json({ error: 'Missing videoUrl parameter' });
}
const videoFileName = 'inputVideo.mp4';
const audioFileName = 'outputAudio.mp3';
// Download the video file
const response = await axios.get(videoUrl, { responseType: 'arraybuffer' });
fs.writeFileSync(videoFileName, Buffer.from(response.data));
// Convert video to audio using ffmpeg
await new Promise((resolve, reject) => {
ffmpeg()
.input(videoFileName)
.audioCodec('libmp3lame')
.toFormat('mp3')
.on('end', () => resolve())
.on('error', (err) => reject(err))
.save(audioFileName);
});
// Send the converted audio file to the client
res.download(audioFileName, (err) => {
if (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
}
// Clean up temporary files
fs.unlinkSync(videoFileName);
fs.unlinkSync(audioFileName);
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
如果不想用控制台,也可以用暴力猴,暴力猴脚本如下:
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 2024-01-12
// @description try to take over the world!
// @author You
// @match https://www.douyin.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=douyin.com
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Your code here...
// 目标节点
var targetNode = document.querySelector('ul[data-e2e="scroll-list"]');
// 初始时的li数量
var initialCount = targetNode.children.length;
// 配置观察器的设置
var config = { childList: true };
// 观察器回调
var callback = function(mutationsList, observer) {
// 当前的li数量
var currentCount = targetNode.children.length;
// 检查li数量是否增加
if (currentCount > initialCount) {
// 执行你的函数
addWatch();
console.log('添加监听器')
// 更新初始时的li数量
initialCount = currentCount;
}
};
// 创建一个观察器实例并传入回调函数
var observer = new MutationObserver(callback);
// 使用配置和目标节点开始观察
observer.observe(targetNode, config);
// 给所有 li 添加监听器
function addWatch() {
// 获取作者的视频列表
var liElements = document.querySelectorAll('ul[data-e2e="scroll-list"] li');
// 添加鼠标悬停事件监听器
liElements.forEach(function(li) {
li.addEventListener('mouseenter', function() {
// 添加键盘按下事件监听器
document.addEventListener('keydown', keydownHandler);
});
li.addEventListener('mouseleave', function() {
// 移除键盘按下事件监听器
document.removeEventListener('keydown', keydownHandler);
});
});
}
// 处理键盘按下事件
function keydownHandler(event) {
// 判断按下的键是否为 'D' 键,keyCode为 '68'
if (event.keyCode === 68) {
// 获取下载链接
var sourceTag = document.querySelector('.basePlayerContainer xg-video-container > video > source:nth-child(1)');
const alt = document.querySelector('.basePlayerContainer').previousElementSibling.querySelector('img').getAttribute('alt');
// 找到第一个冒号并截取之后的部分
let contentAfterColon = alt.includes(':') ?
alt.split(':').slice(1).join(':'):
alt;
// 去除所有的.和空格
let resultString = contentAfterColon.replace(/[.\s]/g, '');
// 如果最终结果为空,替换为'空'
const name = resultString === '' ? '空' : resultString;
// 提取src属性值
var url = sourceTag.getAttribute('src');
// 执行下载操作,这里使用一个假设的下载函数
downloadFile(url, name);
}
}
function downloadFile(url, name) {
// 发送POST请求到后台接口
fetch('http://localhost:3000/convert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ videoUrl: url }),
})
.then((response) => response.blob())
.then((blob) => {
// 创建一个临时的<a>元素用于下载
const a = document.createElement('a');
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = name + '.mp3';
// 触发点击事件以启动下载
a.click();
// 释放URL对象
window.URL.revokeObjectURL(url);
})
.catch((error) => {
console.error('Error:', error);
alert('下载失败,原因:' + error)
});
}
})();