mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Fix vld bugs
This commit is contained in:
parent
9f9077d047
commit
7f93200f86
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -154,7 +154,7 @@ namespace BilibiliEvolved.Build
|
||||
}
|
||||
return source;
|
||||
};
|
||||
input = RegexReplacer.Replace(input, @"import (.*) from ([^;]*)", match =>
|
||||
input = RegexReplacer.Replace(input, @"import (.*) from ([^\r\n;]*)", match =>
|
||||
{
|
||||
var imported = match.Groups[1].Value.Replace(" as ", ":");
|
||||
var source = convertToRuntimeSource(match.Groups[2].Value);
|
||||
@ -173,6 +173,7 @@ namespace BilibiliEvolved.Build
|
||||
};
|
||||
})();";
|
||||
}
|
||||
// Console.WriteLine(input);
|
||||
return new UglifyJs().Run(input);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bilibili-evolved-video-link-downloader",
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.2",
|
||||
"description": "Bilibili Evolved 视频链接下载器",
|
||||
"main": "video-link-downloader.js",
|
||||
"bin": {
|
||||
|
||||
@ -64,8 +64,8 @@ class Downloader {
|
||||
headers: {
|
||||
Range: range,
|
||||
Origin: "https://www.bilibili.com",
|
||||
Referer: "https://www.bilibili.com",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36",
|
||||
Referer: this.inputData.referer || "https://www.bilibili.com",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0",
|
||||
},
|
||||
}).on("complete", response => {
|
||||
if (response.statusCode.toString()[0] === "2") {
|
||||
@ -78,15 +78,15 @@ class Downloader {
|
||||
this.progressMap.set(req, this.progressMap.get(req) + data.length);
|
||||
this.updateProgress();
|
||||
}).on("error", error => {
|
||||
// stream.close();
|
||||
// fs.unlinkSync(partFilename);
|
||||
// this.progressMap.delete(req);
|
||||
// this.progressMap.set(makeRequest(), 0);
|
||||
// this.updateProgress();
|
||||
this.progressMap.delete(req);
|
||||
this.progressMap.set(makeRequest(), 0);
|
||||
this.updateProgress();
|
||||
reject(`\n片段下载失败: ${error}`);
|
||||
});
|
||||
console.log(`created stream ${partFilename}`);
|
||||
req.pipe(fs.createWriteStream(partFilename));
|
||||
req.pipe(fs.createWriteStream(partFilename, {
|
||||
autoClose: true
|
||||
}));
|
||||
return req;
|
||||
};
|
||||
this.progressMap.set(makeRequest(), 0);
|
||||
|
||||
@ -6,21 +6,19 @@ import fs = require("fs");
|
||||
import ProgressBar = require("progress");
|
||||
import "colors";
|
||||
|
||||
interface Fragment
|
||||
{
|
||||
interface Fragment {
|
||||
length: number;
|
||||
size: number;
|
||||
url: string;
|
||||
backupUrls: string[];
|
||||
}
|
||||
interface InputData
|
||||
{
|
||||
interface InputData {
|
||||
fragments: Fragment[];
|
||||
title: string;
|
||||
totalSize: number;
|
||||
referer: string;
|
||||
}
|
||||
interface Settings
|
||||
{
|
||||
interface Settings {
|
||||
parts: number;
|
||||
info: string;
|
||||
output: string;
|
||||
@ -41,14 +39,12 @@ options.parts = Math.round(options.parts);
|
||||
// options = Object.assign(jsonOptions, options);
|
||||
// }
|
||||
|
||||
if (options.parts < 1)
|
||||
{
|
||||
if (options.parts < 1) {
|
||||
console.error("分段数不能小于1".red);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
class Downloader
|
||||
{
|
||||
class Downloader {
|
||||
static workingDownloader: Downloader | null = null;
|
||||
private progressMap = new Map<request.Request | string, number>();
|
||||
private progressBar = new ProgressBar(":percent [:bar]", {
|
||||
@ -61,24 +57,18 @@ class Downloader
|
||||
|
||||
constructor(
|
||||
private inputData: InputData,
|
||||
)
|
||||
{ }
|
||||
private getExtension(fragment: Fragment)
|
||||
{
|
||||
) { }
|
||||
private getExtension(fragment: Fragment) {
|
||||
this.extension = fragment.url.includes(".flv") ? ".flv" : ".mp4";
|
||||
}
|
||||
private updateProgress()
|
||||
{
|
||||
private updateProgress() {
|
||||
const progress = this.progressMap ?
|
||||
[...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.inputData.totalSize : 0;
|
||||
this.progressBar.update(progress);
|
||||
}
|
||||
cancelDownload()
|
||||
{
|
||||
[...this.progressMap.keys()].forEach(it =>
|
||||
{
|
||||
if (typeof it !== "string")
|
||||
{
|
||||
cancelDownload() {
|
||||
[...this.progressMap.keys()].forEach(it => {
|
||||
if (typeof it !== "string") {
|
||||
it.abort();
|
||||
}
|
||||
});
|
||||
@ -88,58 +78,47 @@ class Downloader
|
||||
parts.forEach(file => fs.unlinkSync(file));
|
||||
console.log("已取消下载".blue);
|
||||
}
|
||||
private downloadFragmentPart(url: string, range: string, partFilename: string)
|
||||
{
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
const makeRequest = () =>
|
||||
{
|
||||
private downloadFragmentPart(url: string, range: string, partFilename: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const makeRequest = () => {
|
||||
const req = request({
|
||||
url: url,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Range: range,
|
||||
Origin: "https://www.bilibili.com",
|
||||
Referer: "https://www.bilibili.com",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36",
|
||||
Referer: this.inputData.referer || "https://www.bilibili.com",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0",
|
||||
},
|
||||
// maxAttempts: 8,
|
||||
// retryDelay: 1000,
|
||||
}).on("complete", response =>
|
||||
{
|
||||
if (response.statusCode.toString()[0] === "2")
|
||||
{
|
||||
}).on("complete", response => {
|
||||
if (response.statusCode.toString()[0] === "2") {
|
||||
resolve(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
reject(`请求失败: ${response.statusCode}`);
|
||||
}
|
||||
}).on("data", data =>
|
||||
{
|
||||
}).on("data", data => {
|
||||
this.progressMap.set(req, this.progressMap.get(req)! + data.length);
|
||||
this.updateProgress();
|
||||
}).on("error", error =>
|
||||
{
|
||||
// stream.close();
|
||||
}).on("error", error => {
|
||||
// fs.unlinkSync(partFilename);
|
||||
// this.progressMap.delete(req);
|
||||
// this.progressMap.set(makeRequest(), 0);
|
||||
// this.updateProgress();
|
||||
this.progressMap.delete(req);
|
||||
this.progressMap.set(makeRequest(), 0);
|
||||
this.updateProgress();
|
||||
reject(`\n片段下载失败: ${error}`);
|
||||
});
|
||||
req.pipe(fs.createWriteStream(partFilename))
|
||||
req.pipe(fs.createWriteStream(partFilename, {
|
||||
autoClose: true
|
||||
}))
|
||||
return req
|
||||
};
|
||||
this.progressMap.set(makeRequest(), 0);
|
||||
});
|
||||
}
|
||||
private async downloadFragment(fragment: Fragment, index: number = -1)
|
||||
{
|
||||
private async downloadFragment(fragment: Fragment, index: number = -1) {
|
||||
const partialLength = Math.round(fragment.size / options.parts);
|
||||
const title = (index === -1 ? this.inputData.title : this.inputData.title + " - " + index.toString());
|
||||
if (fs.existsSync(title + this.extension))
|
||||
{
|
||||
if (fs.existsSync(title + this.extension)) {
|
||||
this.progressBar.terminate();
|
||||
console.log(`跳过了已存在的文件 ${title + this.extension}`);
|
||||
return title;
|
||||
@ -147,8 +126,7 @@ class Downloader
|
||||
let startByte = 0;
|
||||
let part = 0;
|
||||
const promises = [];
|
||||
while (startByte < fragment.size)
|
||||
{
|
||||
while (startByte < fragment.size) {
|
||||
const partFilename = `${title}.part${part}`;
|
||||
// if (fs.existsSync(partFilename))
|
||||
// {
|
||||
@ -167,62 +145,49 @@ class Downloader
|
||||
await Promise.all(promises);
|
||||
return title;
|
||||
}
|
||||
async download()
|
||||
{
|
||||
async download() {
|
||||
console.log(`正在下载: ${this.inputData.title}`.green);
|
||||
Downloader.workingDownloader = this;
|
||||
this.progressBar.render();
|
||||
const [fragment] = this.inputData.fragments;
|
||||
this.getExtension(fragment);
|
||||
if (this.inputData.fragments.length === 1)
|
||||
{
|
||||
if (this.inputData.fragments.length === 1) {
|
||||
this.title = await this.downloadFragment(fragment);
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
this.title = await Promise.all(this.inputData.fragments.map((f, i) => this.downloadFragment(f, i)));
|
||||
}
|
||||
}
|
||||
private async mergeFragment(title: string, index = -1)
|
||||
{
|
||||
private async mergeFragment(title: string, index = -1) {
|
||||
const dest = title + this.extension;
|
||||
if (!this.progressBar.complete)
|
||||
{
|
||||
if (!this.progressBar.complete) {
|
||||
this.progressBar.update(1);
|
||||
this.progressBar.terminate();
|
||||
}
|
||||
if (fs.existsSync(dest))
|
||||
{
|
||||
if (fs.existsSync(dest)) {
|
||||
return dest;
|
||||
}
|
||||
if (index !== -1)
|
||||
{
|
||||
if (index !== -1) {
|
||||
console.log(`正在合并片段${index.toString()}...`.blue);
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
console.log("正在合并文件...".blue);
|
||||
}
|
||||
if (options.parts === 1)
|
||||
{
|
||||
if (options.parts === 1) {
|
||||
fs.renameSync(title + ".part0", dest);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fs.existsSync(dest))
|
||||
{
|
||||
else {
|
||||
if (fs.existsSync(dest)) {
|
||||
fs.unlinkSync(dest);
|
||||
}
|
||||
const files = fs.readdirSync(".");
|
||||
const parts = files.filter(it => it.includes(title + ".part"));
|
||||
const partRegex = /.*\.part([\d]+)/;
|
||||
parts.sort((a, b) =>
|
||||
{
|
||||
parts.sort((a, b) => {
|
||||
const partA = parseInt(a.replace(partRegex, "$1"));
|
||||
const partB = parseInt(b.replace(partRegex, "$1"));
|
||||
return partA - partB;
|
||||
}).forEach(file =>
|
||||
{
|
||||
}).forEach(file => {
|
||||
const buffer = fs.readFileSync(file);
|
||||
fs.appendFileSync(dest, buffer);
|
||||
});
|
||||
@ -230,88 +195,68 @@ class Downloader
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
async merge()
|
||||
{
|
||||
async merge() {
|
||||
let result: string | string[];
|
||||
if (typeof this.title === "string")
|
||||
{
|
||||
if (typeof this.title === "string") {
|
||||
result = await this.mergeFragment(this.title);
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
result = await Promise.all(this.title.map((t, i) => this.mergeFragment(t, i)));
|
||||
}
|
||||
console.log(`完成: `.green);
|
||||
if (typeof result === "string")
|
||||
{
|
||||
if (typeof result === "string") {
|
||||
console.log(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
result.forEach(it => console.log(it));
|
||||
}
|
||||
}
|
||||
}
|
||||
(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
(async () => {
|
||||
try {
|
||||
let jsonText = '';
|
||||
if (fs.existsSync(options.info))
|
||||
{
|
||||
if (fs.existsSync(options.info)) {
|
||||
jsonText = fs.readFileSync(options.info).toString("utf-8");
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
jsonText = await clipboardy.read();
|
||||
}
|
||||
const inputData = JSON.parse(jsonText) as InputData | InputData[];
|
||||
if (!(function (data: any): data is InputData | InputData[]
|
||||
{
|
||||
if (Array.isArray(data) && data.length > 0)
|
||||
{
|
||||
if (!(function (data: any): data is InputData | InputData[] {
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
return data[0].fragments !== undefined;
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
return data.fragments !== undefined;
|
||||
}
|
||||
})(inputData))
|
||||
{
|
||||
})(inputData)) {
|
||||
throw new Error();
|
||||
}
|
||||
try
|
||||
{
|
||||
try {
|
||||
process.chdir(options.output);
|
||||
process.on("SIGINT", () =>
|
||||
{
|
||||
process.on("SIGINT", () => {
|
||||
Downloader.workingDownloader && Downloader.workingDownloader.cancelDownload();
|
||||
process.exit();
|
||||
});
|
||||
if (Array.isArray(inputData))
|
||||
{
|
||||
if (Array.isArray(inputData)) {
|
||||
const downloaders = inputData.map(data => new Downloader(data));
|
||||
for (const downloader of downloaders)
|
||||
{
|
||||
for (const downloader of downloaders) {
|
||||
await downloader.download();
|
||||
await downloader.merge();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
const downloader = new Downloader(inputData);
|
||||
await downloader.download();
|
||||
await downloader.merge();
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
catch (error) {
|
||||
Downloader.workingDownloader && Downloader.workingDownloader.cancelDownload();
|
||||
console.error(`\n错误: ${error}`.red);
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
catch (error) {
|
||||
console.log(`[无数据] 未在剪贴板检测到有效数据/没有指定输入文件/输入文件的数据无效.`.red);
|
||||
}
|
||||
})();
|
||||
2
min/batch-download.min.js
vendored
2
min/batch-download.min.js
vendored
@ -1 +1 @@
|
||||
(()=>{return(t,e)=>{class i{static async test(){if(!document.URL.includes("/www.bilibili.com/video/av")){return false}return await SpinQuery.select("#multi_page")!==null}async collectData(t){const e=`https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`;const i=await Ajax.getJson(e);if(i.code!==0){Toast.error(`获取视频选集列表失败, message=${i.message}`,"批量下载");return""}const s=i.data.pages;if(s===undefined){Toast.error(`获取视频选集列表失败, 没有找到选集信息.`,"批量下载");return""}const a=[];for(const e of s){const i=`https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${e.cid}&qn=${t}&otype=json`;const s=await Ajax.getJsonWithCredentials(i);const r=s.data||s.result||s;if(r.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=r.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});a.push({fragments:n,title:`${e.page} - ${e.part}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(a)}}class s{static async test(){return document.URL.includes("/www.bilibili.com/bangumi")}async collectData(t){const e=document.querySelector("meta[property='og:url']");if(e===null){Toast.error("获取番剧数据失败: 无法找到 Season ID","批量下载");return""}const i=e.getAttribute("content").match(/play\/ss(\d+)/)[1];if(i===undefined){Toast.error("获取番剧数据失败: 无法解析 Season ID","批量下载");return""}const s=await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${i}`);if(s.code!==0){Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${s.message}`,"批量下载");return""}const a=s.result.main_section.episodes.map(t=>{return{aid:t.aid,cid:t.cid,number:t.title,title:t.long_title}});const r=[];for(const e of a){const i=`https://api.bilibili.com/pgc/player/web/playurl?avid=${e.aid}&cid=${e.cid}&qn=${t}&otype=json`;const s=await Ajax.getJsonWithCredentials(i);const a=s.data||s.result||s;if(a.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=a.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});r.push({fragments:n,title:`${e.number} - ${e.title}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(r)}}const a=[s,i];let r=null;class n{static async test(){for(const t of a){if(await t.test()===true){r=t;return true}}r=null;return false}async collectData(t,e){if(r===null){logError("[批量下载] 未找到合适的解析模块.");return null}const i=new r;const s=await i.collectData(t.quality);e.dismiss();return s}}return{export:{BatchExtractor:n}}}})();
|
||||
(()=>{return(t,e)=>{class i{static async test(){if(!document.URL.includes("/www.bilibili.com/video/av")){return false}return await SpinQuery.select("#multi_page")!==null}async collectData(t){const e=`https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`;const i=await Ajax.getJson(e);if(i.code!==0){Toast.error(`获取视频选集列表失败, message=${i.message}`,"批量下载");return""}const r=i.data.pages;if(r===undefined){Toast.error(`获取视频选集列表失败, 没有找到选集信息.`,"批量下载");return""}const a=[];for(const e of r){const i=`https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${e.cid}&qn=${t}&otype=json`;const r=await Ajax.getJsonWithCredentials(i);const s=r.data||r.result||r;if(s.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=s.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});a.push({fragments:n,title:`${e.page} - ${e.part}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid,referer:document.URL.replace(window.location.search,"")})}return JSON.stringify(a)}}class r{static async test(){return document.URL.includes("/www.bilibili.com/bangumi")}async collectData(t){const e=document.querySelector("meta[property='og:url']");if(e===null){Toast.error("获取番剧数据失败: 无法找到 Season ID","批量下载");return""}const i=e.getAttribute("content").match(/play\/ss(\d+)/)[1];if(i===undefined){Toast.error("获取番剧数据失败: 无法解析 Season ID","批量下载");return""}const r=await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${i}`);if(r.code!==0){Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${r.message}`,"批量下载");return""}const a=r.result.main_section.episodes.map(t=>{return{aid:t.aid,cid:t.cid,number:t.title,title:t.long_title}});const s=[];for(const e of a){const i=`https://api.bilibili.com/pgc/player/web/playurl?avid=${e.aid}&cid=${e.cid}&qn=${t}&otype=json`;const r=await Ajax.getJsonWithCredentials(i);const a=r.data||r.result||r;if(a.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=a.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});s.push({fragments:n,title:`${e.number} - ${e.title}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(s)}}const a=[r,i];let s=null;class n{static async test(){for(const t of a){if(await t.test()===true){s=t;return true}}s=null;return false}async collectData(t,e){if(s===null){logError("[批量下载] 未找到合适的解析模块.");return null}const i=new s;const r=await i.collectData(t.quality);e.dismiss();return r}}return{export:{BatchExtractor:n}}}})();
|
||||
2
min/download-video.min.js
vendored
2
min/download-video.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,148 +1,124 @@
|
||||
class VideoEpisodeBatch
|
||||
{
|
||||
static async test()
|
||||
{
|
||||
if (!document.URL.includes("/www.bilibili.com/video/av"))
|
||||
{
|
||||
return false;
|
||||
class VideoEpisodeBatch {
|
||||
static async test () {
|
||||
if (!document.URL.includes('/www.bilibili.com/video/av')) {
|
||||
return false
|
||||
}
|
||||
return await SpinQuery.select("#multi_page") !== null;
|
||||
return await SpinQuery.select('#multi_page') !== null
|
||||
}
|
||||
async collectData(quality)
|
||||
{
|
||||
const api = `https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`;
|
||||
const json = await Ajax.getJson(api);
|
||||
if (json.code !== 0)
|
||||
{
|
||||
Toast.error(`获取视频选集列表失败, message=${json.message}`, "批量下载");
|
||||
return "";
|
||||
async collectData (quality) {
|
||||
const api = `https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`
|
||||
const json = await Ajax.getJson(api)
|
||||
if (json.code !== 0) {
|
||||
Toast.error(`获取视频选集列表失败, message=${json.message}`, '批量下载')
|
||||
return ''
|
||||
}
|
||||
const pages = json.data.pages;
|
||||
if (pages === undefined)
|
||||
{
|
||||
Toast.error(`获取视频选集列表失败, 没有找到选集信息.`, "批量下载");
|
||||
return "";
|
||||
const pages = json.data.pages
|
||||
if (pages === undefined) {
|
||||
Toast.error(`获取视频选集列表失败, 没有找到选集信息.`, '批量下载')
|
||||
return ''
|
||||
}
|
||||
const result = [];
|
||||
for (const page of pages)
|
||||
{
|
||||
const url = `https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${page.cid}&qn=${quality}&otype=json`;
|
||||
const json = await Ajax.getJsonWithCredentials(url);
|
||||
const data = json.data || json.result || json;
|
||||
if (data.quality !== quality)
|
||||
{
|
||||
Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.", "批量下载");
|
||||
return "";
|
||||
const result = []
|
||||
for (const page of pages) {
|
||||
const url = `https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${page.cid}&qn=${quality}&otype=json`
|
||||
const json = await Ajax.getJsonWithCredentials(url)
|
||||
const data = json.data || json.result || json
|
||||
if (data.quality !== quality) {
|
||||
Toast.error('获取下载链接失败, 请确认当前账号有下载权限后重试.', '批量下载')
|
||||
return ''
|
||||
}
|
||||
const fragments = data.durl.map(it =>
|
||||
{
|
||||
const fragments = data.durl.map(it => {
|
||||
return {
|
||||
length: it.length,
|
||||
size: it.size,
|
||||
url: it.url,
|
||||
};
|
||||
});
|
||||
url: it.url
|
||||
}
|
||||
})
|
||||
result.push({
|
||||
fragments,
|
||||
title: `${page.page} - ${page.part}`,
|
||||
totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it),
|
||||
cid: page.cid,
|
||||
});
|
||||
referer: document.URL.replace(window.location.search, '')
|
||||
})
|
||||
}
|
||||
return JSON.stringify(result);
|
||||
return JSON.stringify(result)
|
||||
}
|
||||
}
|
||||
class BangumiBatch
|
||||
{
|
||||
static async test()
|
||||
{
|
||||
return document.URL.includes("/www.bilibili.com/bangumi");
|
||||
class BangumiBatch {
|
||||
static async test () {
|
||||
return document.URL.includes('/www.bilibili.com/bangumi')
|
||||
}
|
||||
async collectData(quality)
|
||||
{
|
||||
const metaUrl = document.querySelector("meta[property='og:url']");
|
||||
if (metaUrl === null)
|
||||
{
|
||||
Toast.error("获取番剧数据失败: 无法找到 Season ID", "批量下载");
|
||||
return "";
|
||||
async collectData (quality) {
|
||||
const metaUrl = document.querySelector("meta[property='og:url']")
|
||||
if (metaUrl === null) {
|
||||
Toast.error('获取番剧数据失败: 无法找到 Season ID', '批量下载')
|
||||
return ''
|
||||
}
|
||||
const seasonId = metaUrl.getAttribute("content").match(/play\/ss(\d+)/)[1];
|
||||
if (seasonId === undefined)
|
||||
{
|
||||
Toast.error("获取番剧数据失败: 无法解析 Season ID", "批量下载");
|
||||
return "";
|
||||
const seasonId = metaUrl.getAttribute('content').match(/play\/ss(\d+)/)[1]
|
||||
if (seasonId === undefined) {
|
||||
Toast.error('获取番剧数据失败: 无法解析 Season ID', '批量下载')
|
||||
return ''
|
||||
}
|
||||
const json = await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${seasonId}`);
|
||||
if (json.code !== 0)
|
||||
{
|
||||
Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${json.message}`, "批量下载");
|
||||
return "";
|
||||
const json = await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${seasonId}`)
|
||||
if (json.code !== 0) {
|
||||
Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${json.message}`, '批量下载')
|
||||
return ''
|
||||
}
|
||||
const pages = json.result.main_section.episodes.map(it =>
|
||||
{
|
||||
return { aid: it.aid, cid: it.cid, number: it.title, title: it.long_title };
|
||||
});
|
||||
const result = [];
|
||||
for (const page of pages)
|
||||
{
|
||||
const url = `https://api.bilibili.com/pgc/player/web/playurl?avid=${page.aid}&cid=${page.cid}&qn=${quality}&otype=json`;
|
||||
const json = await Ajax.getJsonWithCredentials(url);
|
||||
const data = json.data || json.result || json;
|
||||
if (data.quality !== quality)
|
||||
{
|
||||
Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.", "批量下载");
|
||||
return "";
|
||||
const pages = json.result.main_section.episodes.map(it => {
|
||||
return { aid: it.aid, cid: it.cid, number: it.title, title: it.long_title }
|
||||
})
|
||||
const result = []
|
||||
for (const page of pages) {
|
||||
const url = `https://api.bilibili.com/pgc/player/web/playurl?avid=${page.aid}&cid=${page.cid}&qn=${quality}&otype=json`
|
||||
const json = await Ajax.getJsonWithCredentials(url)
|
||||
const data = json.data || json.result || json
|
||||
if (data.quality !== quality) {
|
||||
Toast.error('获取下载链接失败, 请确认当前账号有下载权限后重试.', '批量下载')
|
||||
return ''
|
||||
}
|
||||
const fragments = data.durl.map(it =>
|
||||
{
|
||||
const fragments = data.durl.map(it => {
|
||||
return {
|
||||
length: it.length,
|
||||
size: it.size,
|
||||
url: it.url,
|
||||
};
|
||||
});
|
||||
url: it.url
|
||||
}
|
||||
})
|
||||
result.push({
|
||||
fragments,
|
||||
title: `${page.number} - ${page.title}`,
|
||||
totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it),
|
||||
cid: page.cid,
|
||||
});
|
||||
cid: page.cid
|
||||
})
|
||||
}
|
||||
return JSON.stringify(result);
|
||||
return JSON.stringify(result)
|
||||
}
|
||||
}
|
||||
const extractors = [BangumiBatch, VideoEpisodeBatch];
|
||||
let ExtractorClass = null;
|
||||
export class BatchExtractor
|
||||
{
|
||||
static async test()
|
||||
{
|
||||
for (const e of extractors)
|
||||
{
|
||||
if (await e.test() === true)
|
||||
{
|
||||
ExtractorClass = e;
|
||||
return true;
|
||||
const extractors = [BangumiBatch, VideoEpisodeBatch]
|
||||
let ExtractorClass = null
|
||||
export class BatchExtractor {
|
||||
static async test () {
|
||||
for (const e of extractors) {
|
||||
if (await e.test() === true) {
|
||||
ExtractorClass = e
|
||||
return true
|
||||
}
|
||||
}
|
||||
ExtractorClass = null;
|
||||
return false;
|
||||
ExtractorClass = null
|
||||
return false
|
||||
}
|
||||
async collectData(format, toast)
|
||||
{
|
||||
if (ExtractorClass === null)
|
||||
{
|
||||
logError("[批量下载] 未找到合适的解析模块.");
|
||||
return null;
|
||||
async collectData (format, toast) {
|
||||
if (ExtractorClass === null) {
|
||||
logError('[批量下载] 未找到合适的解析模块.')
|
||||
return null
|
||||
}
|
||||
const extractor = new ExtractorClass;
|
||||
const result = await extractor.collectData(format.quality);
|
||||
toast.dismiss();
|
||||
return result;
|
||||
const extractor = new ExtractorClass()
|
||||
const result = await extractor.collectData(format.quality)
|
||||
toast.dismiss()
|
||||
return result
|
||||
}
|
||||
}
|
||||
export default {
|
||||
export: {
|
||||
BatchExtractor,
|
||||
},
|
||||
};
|
||||
BatchExtractor
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,577 +1,471 @@
|
||||
import { getFriendlyTitle } from "../title";
|
||||
import { getFriendlyTitle } from '../title'
|
||||
|
||||
const pageData = {
|
||||
entity: null,
|
||||
aid: undefined,
|
||||
cid: undefined,
|
||||
};
|
||||
let formats = [];
|
||||
let selectedFormat = null;
|
||||
class Video
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this.menuClasses = ["quality", "action", "progress"];
|
||||
this.currentMenuClass = "quality";
|
||||
cid: undefined
|
||||
}
|
||||
get menuPanel()
|
||||
{
|
||||
return document.querySelector(".download-video-panel");
|
||||
let formats = []
|
||||
let selectedFormat = null
|
||||
class Video {
|
||||
constructor () {
|
||||
this.menuClasses = ['quality', 'action', 'progress']
|
||||
this.currentMenuClass = 'quality'
|
||||
}
|
||||
addMenuClass()
|
||||
{
|
||||
this.menuPanel.classList.remove(...this.menuClasses);
|
||||
this.menuPanel.classList.add(this.currentMenuClass);
|
||||
return this.currentMenuClass;
|
||||
get menuPanel () {
|
||||
return document.querySelector('.download-video-panel')
|
||||
}
|
||||
resetMenuClass()
|
||||
{
|
||||
[this.currentMenuClass] = this.menuClasses;
|
||||
this.addMenuClass();
|
||||
addMenuClass () {
|
||||
this.menuPanel.classList.remove(...this.menuClasses)
|
||||
this.menuPanel.classList.add(this.currentMenuClass)
|
||||
return this.currentMenuClass
|
||||
}
|
||||
nextMenuClass()
|
||||
{
|
||||
const index = this.menuClasses.indexOf(this.currentMenuClass) + 1;
|
||||
const next = this.menuClasses[index >= this.menuClasses.length ? 0 : index];
|
||||
this.currentMenuClass = next;
|
||||
this.addMenuClass();
|
||||
return next;
|
||||
resetMenuClass () {
|
||||
[this.currentMenuClass] = this.menuClasses
|
||||
this.addMenuClass()
|
||||
}
|
||||
addError()
|
||||
{
|
||||
this.menuPanel.classList.add("error");
|
||||
nextMenuClass () {
|
||||
const index = this.menuClasses.indexOf(this.currentMenuClass) + 1
|
||||
const next = this.menuClasses[index >= this.menuClasses.length ? 0 : index]
|
||||
this.currentMenuClass = next
|
||||
this.addMenuClass()
|
||||
return next
|
||||
}
|
||||
removeError()
|
||||
{
|
||||
this.menuPanel.classList.remove("error");
|
||||
this.resetMenuClass();
|
||||
addError () {
|
||||
this.menuPanel.classList.add('error')
|
||||
}
|
||||
async getUrl(quality)
|
||||
{
|
||||
if (quality)
|
||||
{
|
||||
return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json`;
|
||||
removeError () {
|
||||
this.menuPanel.classList.remove('error')
|
||||
this.resetMenuClass()
|
||||
}
|
||||
else
|
||||
{
|
||||
return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&otype=json`;
|
||||
async getUrl (quality) {
|
||||
if (quality) {
|
||||
return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json`
|
||||
} else {
|
||||
return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&otype=json`
|
||||
}
|
||||
}
|
||||
}
|
||||
class Bangumi extends Video
|
||||
{
|
||||
async getUrl(quality)
|
||||
{
|
||||
if (quality)
|
||||
{
|
||||
return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json`;
|
||||
}
|
||||
else
|
||||
{
|
||||
return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=&otype=json`;
|
||||
class Bangumi extends Video {
|
||||
async getUrl (quality) {
|
||||
if (quality) {
|
||||
return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json`
|
||||
} else {
|
||||
return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=&otype=json`
|
||||
}
|
||||
}
|
||||
}
|
||||
class VideoFormat
|
||||
{
|
||||
constructor(quality, internalName, displayName)
|
||||
{
|
||||
this.quality = quality;
|
||||
this.internalName = internalName;
|
||||
this.displayName = displayName;
|
||||
class VideoFormat {
|
||||
constructor (quality, internalName, displayName) {
|
||||
this.quality = quality
|
||||
this.internalName = internalName
|
||||
this.displayName = displayName
|
||||
}
|
||||
async downloadInfo()
|
||||
{
|
||||
const videoInfo = new VideoDownloader(this);
|
||||
await videoInfo.fetchVideoInfo();
|
||||
return videoInfo;
|
||||
async downloadInfo () {
|
||||
const videoInfo = new VideoDownloader(this)
|
||||
await videoInfo.fetchVideoInfo()
|
||||
return videoInfo
|
||||
}
|
||||
static get availableFormats()
|
||||
{
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
pageData.entity.getUrl().then(url =>
|
||||
{
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener("load", () =>
|
||||
{
|
||||
const json = JSON.parse(xhr.responseText);
|
||||
if (json.code !== 0)
|
||||
{
|
||||
reject("获取清晰度信息失败.");
|
||||
return;
|
||||
static get availableFormats () {
|
||||
return new Promise((resolve, reject) => {
|
||||
pageData.entity.getUrl().then(url => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.addEventListener('load', () => {
|
||||
const json = JSON.parse(xhr.responseText)
|
||||
if (json.code !== 0) {
|
||||
reject('获取清晰度信息失败.')
|
||||
return
|
||||
}
|
||||
const data = json.data || json.result || json;
|
||||
const qualities = data.accept_quality;
|
||||
const internalNames = data.accept_format.split(",");
|
||||
const displayNames = data.accept_description;
|
||||
const formats = [];
|
||||
while (qualities.length > 0)
|
||||
{
|
||||
const data = json.data || json.result || json
|
||||
const qualities = data.accept_quality
|
||||
const internalNames = data.accept_format.split(',')
|
||||
const displayNames = data.accept_description
|
||||
const formats = []
|
||||
while (qualities.length > 0) {
|
||||
const format = new VideoFormat(
|
||||
qualities.pop(),
|
||||
internalNames.pop(),
|
||||
displayNames.pop()
|
||||
);
|
||||
formats.push(format);
|
||||
)
|
||||
formats.push(format)
|
||||
}
|
||||
resolve(formats);
|
||||
});
|
||||
xhr.addEventListener("error", () => reject(`获取清晰度信息失败.`));
|
||||
xhr.withCredentials = true;
|
||||
xhr.open("GET", url);
|
||||
xhr.send();
|
||||
});
|
||||
});
|
||||
resolve(formats)
|
||||
})
|
||||
xhr.addEventListener('error', () => reject(`获取清晰度信息失败.`))
|
||||
xhr.withCredentials = true
|
||||
xhr.open('GET', url)
|
||||
xhr.send()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
class VideoDownloaderFragment
|
||||
{
|
||||
constructor(length, size, url, backupUrls)
|
||||
{
|
||||
this.length = length;
|
||||
this.size = size;
|
||||
this.url = url;
|
||||
this.backupUrls = backupUrls;
|
||||
class VideoDownloaderFragment {
|
||||
constructor (length, size, url, backupUrls) {
|
||||
this.length = length
|
||||
this.size = size
|
||||
this.url = url
|
||||
this.backupUrls = backupUrls
|
||||
}
|
||||
}
|
||||
class VideoDownloader
|
||||
{
|
||||
constructor(format, fragments)
|
||||
{
|
||||
this.format = format;
|
||||
this.fragments = fragments || [];
|
||||
this.progress = null;
|
||||
// this.loaded = 0;
|
||||
this.totalSize = null;
|
||||
this.workingXhr = null;
|
||||
this.fragmentSplitFactor = 6 * 5;
|
||||
class VideoDownloader {
|
||||
constructor (format, fragments) {
|
||||
this.format = format
|
||||
this.fragments = fragments || []
|
||||
this.progress = null
|
||||
// this.loaded = 0
|
||||
this.totalSize = null
|
||||
this.workingXhr = null
|
||||
this.fragmentSplitFactor = 6 * 5
|
||||
}
|
||||
fetchVideoInfo()
|
||||
{
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
pageData.entity.getUrl(this.format.quality).then(url =>
|
||||
{
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener("load", () =>
|
||||
{
|
||||
const json = JSON.parse(xhr.responseText.replace(/http:/g, "https:"));
|
||||
const data = json.data || json.result || json;
|
||||
if (data.quality !== this.format.quality)
|
||||
{
|
||||
reject("获取下载链接失败, 请确认当前账号有下载权限后重试.");
|
||||
fetchVideoInfo () {
|
||||
return new Promise((resolve, reject) => {
|
||||
pageData.entity.getUrl(this.format.quality).then(url => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.addEventListener('load', () => {
|
||||
const json = JSON.parse(xhr.responseText.replace(/http:/g, 'https:'))
|
||||
const data = json.data || json.result || json
|
||||
if (data.quality !== this.format.quality) {
|
||||
reject('获取下载链接失败, 请确认当前账号有下载权限后重试.')
|
||||
}
|
||||
const urls = data.durl;
|
||||
const urls = data.durl
|
||||
this.fragments = urls.map(it => new VideoDownloaderFragment(
|
||||
it.length, it.size,
|
||||
it.url,
|
||||
it.backup_url
|
||||
));
|
||||
resolve(this.fragments);
|
||||
});
|
||||
xhr.withCredentials = true;
|
||||
xhr.open("GET", url);
|
||||
xhr.send();
|
||||
});
|
||||
});
|
||||
))
|
||||
resolve(this.fragments)
|
||||
})
|
||||
xhr.withCredentials = true
|
||||
xhr.open('GET', url)
|
||||
xhr.send()
|
||||
})
|
||||
})
|
||||
}
|
||||
updateProgress()
|
||||
{
|
||||
const progress = this.progressMap ?
|
||||
[...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.totalSize : 0;
|
||||
if (progress > 1 || progress < 0)
|
||||
{
|
||||
console.error(`[下载视频] 进度异常: ${progress}`, this.progressMap.values());
|
||||
updateProgress () {
|
||||
const progress = this.progressMap
|
||||
? [...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.totalSize : 0
|
||||
if (progress > 1 || progress < 0) {
|
||||
console.error(`[下载视频] 进度异常: ${progress}`, this.progressMap.values())
|
||||
}
|
||||
this.progress && this.progress(progress);
|
||||
this.progress && this.progress(progress)
|
||||
}
|
||||
cancelDownload()
|
||||
{
|
||||
if ("forEach" in this.workingXhr)
|
||||
{
|
||||
this.workingXhr.forEach(it => it.abort());
|
||||
}
|
||||
else
|
||||
{
|
||||
logError("Cancel Download Failed: forEach in this.workingXhr not found.");
|
||||
cancelDownload () {
|
||||
if ('forEach' in this.workingXhr) {
|
||||
this.workingXhr.forEach(it => it.abort())
|
||||
} else {
|
||||
logError('Cancel Download Failed: forEach in this.workingXhr not found.')
|
||||
}
|
||||
}
|
||||
downloadFragment(fragment)
|
||||
{
|
||||
const promises = [];
|
||||
this.workingXhr = [];
|
||||
this.progressMap = new Map();
|
||||
this.updateProgress();
|
||||
const partialLength = Math.round(fragment.size / this.fragmentSplitFactor);
|
||||
let startByte = 0;
|
||||
const getPartNumber = xhr => [...this.progressMap.keys()].indexOf(xhr) + 1;
|
||||
while (startByte < fragment.size)
|
||||
{
|
||||
const endByte = Math.min(fragment.size - 1, Math.round(startByte + partialLength));
|
||||
const range = `bytes=${startByte}-${endByte}`;
|
||||
const rangeLength = endByte - startByte + 1;
|
||||
promises.push(new Promise((resolve, reject) =>
|
||||
{
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", fragment.url);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.withCredentials = false;
|
||||
xhr.addEventListener("progress", (e) =>
|
||||
{
|
||||
console.log(`[下载视频] 视频片段${getPartNumber(xhr)}下载进度: ${e.loaded}/${rangeLength} bytes loaded, ${range}`);
|
||||
this.progressMap.set(xhr, e.loaded);
|
||||
this.updateProgress();
|
||||
});
|
||||
xhr.addEventListener("load", () =>
|
||||
{
|
||||
if (("" + xhr.status)[0] === "2")
|
||||
{
|
||||
resolve(xhr.response);
|
||||
downloadFragment (fragment) {
|
||||
const promises = []
|
||||
this.workingXhr = []
|
||||
this.progressMap = new Map()
|
||||
this.updateProgress()
|
||||
const partialLength = Math.round(fragment.size / this.fragmentSplitFactor)
|
||||
let startByte = 0
|
||||
const getPartNumber = xhr => [...this.progressMap.keys()].indexOf(xhr) + 1
|
||||
while (startByte < fragment.size) {
|
||||
const endByte = Math.min(fragment.size - 1, Math.round(startByte + partialLength))
|
||||
const range = `bytes=${startByte}-${endByte}`
|
||||
const rangeLength = endByte - startByte + 1
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', fragment.url)
|
||||
xhr.responseType = 'arraybuffer'
|
||||
xhr.withCredentials = false
|
||||
xhr.addEventListener('progress', (e) => {
|
||||
console.log(`[下载视频] 视频片段${getPartNumber(xhr)}下载进度: ${e.loaded}/${rangeLength} bytes loaded, ${range}`)
|
||||
this.progressMap.set(xhr, e.loaded)
|
||||
this.updateProgress()
|
||||
})
|
||||
xhr.addEventListener('load', () => {
|
||||
if (('' + xhr.status)[0] === '2') {
|
||||
resolve(xhr.response)
|
||||
} else {
|
||||
reject(`请求失败.`)
|
||||
}
|
||||
else
|
||||
{
|
||||
reject(`请求失败.`);
|
||||
})
|
||||
xhr.addEventListener('abort', () => reject('下载已取消.'))
|
||||
xhr.addEventListener('error', () => {
|
||||
console.error(`[下载视频] 视频片段${getPartNumber(xhr)}下载失败: ${range}`)
|
||||
this.progressMap.set(xhr, 0)
|
||||
this.updateProgress()
|
||||
xhr.open('GET', fragment.url)
|
||||
xhr.setRequestHeader('Range', range)
|
||||
xhr.send()
|
||||
})
|
||||
xhr.setRequestHeader('Range', range)
|
||||
this.progressMap.set(xhr, 0)
|
||||
xhr.send()
|
||||
this.workingXhr.push(xhr)
|
||||
}))
|
||||
startByte = Math.round(startByte + partialLength) + 1
|
||||
}
|
||||
});
|
||||
xhr.addEventListener("abort", () => reject("下载已取消."));
|
||||
xhr.addEventListener("error", () =>
|
||||
{
|
||||
console.error(`[下载视频] 视频片段${getPartNumber(xhr)}下载失败: ${range}`);
|
||||
this.progressMap.set(xhr, 0);
|
||||
this.updateProgress();
|
||||
xhr.open("GET", fragment.url);
|
||||
xhr.setRequestHeader("Range", range);
|
||||
xhr.send();
|
||||
});
|
||||
xhr.setRequestHeader("Range", range);
|
||||
this.progressMap.set(xhr, 0);
|
||||
xhr.send();
|
||||
this.workingXhr.push(xhr);
|
||||
}));
|
||||
startByte = Math.round(startByte + partialLength) + 1;
|
||||
return Promise.all(promises)
|
||||
}
|
||||
return Promise.all(promises);
|
||||
copyUrl () {
|
||||
const urls = this.fragments.map(it => it.url).reduce((acc, it) => acc + '\r\n' + it)
|
||||
GM_setClipboard(urls, 'text')
|
||||
}
|
||||
copyUrl()
|
||||
{
|
||||
const urls = this.fragments.map(it => it.url).reduce((acc, it) => acc + "\r\n" + it);
|
||||
GM_setClipboard(urls, "text");
|
||||
}
|
||||
exportData(copy = false)
|
||||
{
|
||||
exportData (copy = false) {
|
||||
const data = JSON.stringify([{
|
||||
fragments: this.fragments,
|
||||
title: getFriendlyTitle(true),
|
||||
totalSize: this.fragments.map(it => it.size).reduce((acc, it) => acc + it),
|
||||
}]);
|
||||
if (copy)
|
||||
{
|
||||
GM_setClipboard(data, "text");
|
||||
}
|
||||
else
|
||||
{
|
||||
const a = document.createElement("a");
|
||||
const blob = new Blob([data], { type: "text/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
a.setAttribute("href", url);
|
||||
a.setAttribute("download", `cid${unsafeWindow.cid}.json`);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
referer: document.URL.replace(window.location.search, '')
|
||||
}])
|
||||
if (copy) {
|
||||
GM_setClipboard(data, 'text')
|
||||
} else {
|
||||
const a = document.createElement('a')
|
||||
const blob = new Blob([data], { type: 'text/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute('download', `cid${unsafeWindow.cid}.json`)
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
}
|
||||
extension(fragment)
|
||||
{
|
||||
extension (fragment) {
|
||||
return (fragment || this.fragments[0]).url
|
||||
.indexOf(".flv") !== -1
|
||||
? ".flv"
|
||||
: ".mp4";
|
||||
.indexOf('.flv') !== -1
|
||||
? '.flv'
|
||||
: '.mp4'
|
||||
}
|
||||
makeBlob(data, fragment = null)
|
||||
{
|
||||
makeBlob (data, fragment = null) {
|
||||
return new Blob(Array.isArray(data) ? data : [data], {
|
||||
type: this.extension(fragment) === ".flv" ? "video/x-flv" : "video/mp4"
|
||||
});
|
||||
type: this.extension(fragment) === '.flv' ? 'video/x-flv' : 'video/mp4'
|
||||
})
|
||||
}
|
||||
cleanUpOldBlobUrl()
|
||||
{
|
||||
const oldBlobUrl = document.querySelector("a#video-complete").getAttribute("href");
|
||||
if (oldBlobUrl && !document.querySelector(`.link[href="${oldBlobUrl}"]`))
|
||||
{
|
||||
URL.revokeObjectURL(oldBlobUrl);
|
||||
cleanUpOldBlobUrl () {
|
||||
const oldBlobUrl = document.querySelector('a#video-complete').getAttribute('href')
|
||||
if (oldBlobUrl && !document.querySelector(`.link[href="${oldBlobUrl}"]`)) {
|
||||
URL.revokeObjectURL(oldBlobUrl)
|
||||
}
|
||||
[...document.querySelectorAll(".toast-card-header")].filter(it => it.innerText.includes("下载视频")).forEach(it => it.querySelector(".toast-card-dismiss").click());
|
||||
[...document.querySelectorAll('.toast-card-header')].filter(it => it.innerText.includes('下载视频')).forEach(it => it.querySelector('.toast-card-dismiss').click())
|
||||
}
|
||||
downloadSingle(downloadedData)
|
||||
{
|
||||
const [data] = downloadedData;
|
||||
const blob = this.makeBlob(data);
|
||||
const filename = getFriendlyTitle() + this.extension();
|
||||
return [blob, filename];
|
||||
downloadSingle (downloadedData) {
|
||||
const [data] = downloadedData
|
||||
const blob = this.makeBlob(data)
|
||||
const filename = getFriendlyTitle() + this.extension()
|
||||
return [blob, filename]
|
||||
}
|
||||
async downloadMultiple(downloadedData)
|
||||
{
|
||||
const zip = new JSZip();
|
||||
const title = getFriendlyTitle();
|
||||
if (downloadedData.length > 1)
|
||||
{
|
||||
downloadedData.forEach((data, index) =>
|
||||
{
|
||||
const fragment = this.fragments[index];
|
||||
zip.file(`${title} - ${index + 1}${this.extension(fragment)}`, this.makeBlob(data, fragment));
|
||||
});
|
||||
async downloadMultiple (downloadedData) {
|
||||
const zip = new JSZip()
|
||||
const title = getFriendlyTitle()
|
||||
if (downloadedData.length > 1) {
|
||||
downloadedData.forEach((data, index) => {
|
||||
const fragment = this.fragments[index]
|
||||
zip.file(`${title} - ${index + 1}${this.extension(fragment)}`, this.makeBlob(data, fragment))
|
||||
})
|
||||
} else {
|
||||
const [data] = downloadedData
|
||||
zip.file(`${title}${this.extension()}`, this.makeBlob(data))
|
||||
}
|
||||
else
|
||||
{
|
||||
const [data] = downloadedData;
|
||||
zip.file(`${title}${this.extension()}`, this.makeBlob(data));
|
||||
const blob = await zip.generateAsync({ type: 'blob' })
|
||||
const filename = title + '.zip'
|
||||
return [blob, filename]
|
||||
}
|
||||
const blob = await zip.generateAsync({ type: "blob" });
|
||||
const filename = title + ".zip";
|
||||
return [blob, filename];
|
||||
async download () {
|
||||
const downloadedData = []
|
||||
this.totalSize = this.fragments.map(it => it.size).reduce((acc, it) => acc + it)
|
||||
for (const fragment of this.fragments) {
|
||||
const data = await this.downloadFragment(fragment)
|
||||
downloadedData.push(data)
|
||||
}
|
||||
async download()
|
||||
{
|
||||
const downloadedData = [];
|
||||
this.totalSize = this.fragments.map(it => it.size).reduce((acc, it) => acc + it);
|
||||
for (const fragment of this.fragments)
|
||||
{
|
||||
const data = await this.downloadFragment(fragment);
|
||||
downloadedData.push(data);
|
||||
}
|
||||
if (downloadedData.length < 1)
|
||||
{
|
||||
throw new Error("下载失败.");
|
||||
if (downloadedData.length < 1) {
|
||||
throw new Error('下载失败.')
|
||||
}
|
||||
|
||||
let blob = null;
|
||||
let filename = null;
|
||||
if (downloadedData.length === 1)
|
||||
{
|
||||
[blob, filename] = this.downloadSingle(downloadedData);
|
||||
}
|
||||
else
|
||||
{
|
||||
[blob, filename] = await this.downloadMultiple(downloadedData);
|
||||
let blob = null
|
||||
let filename = null
|
||||
if (downloadedData.length === 1) {
|
||||
[blob, filename] = this.downloadSingle(downloadedData)
|
||||
} else {
|
||||
[blob, filename] = await this.downloadMultiple(downloadedData)
|
||||
}
|
||||
|
||||
this.cleanUpOldBlobUrl();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
this.progress && this.progress(0);
|
||||
this.cleanUpOldBlobUrl()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
this.progress && this.progress(0)
|
||||
return {
|
||||
url: blobUrl,
|
||||
filename: filename
|
||||
};
|
||||
}
|
||||
}
|
||||
async function checkBatch()
|
||||
{
|
||||
}
|
||||
async function checkBatch () {
|
||||
const urls = [
|
||||
"/www.bilibili.com/bangumi",
|
||||
"/www.bilibili.com/video/av",
|
||||
];
|
||||
if (!urls.some(url => document.URL.includes(url)))
|
||||
{
|
||||
return;
|
||||
'/www.bilibili.com/bangumi',
|
||||
'/www.bilibili.com/video/av'
|
||||
]
|
||||
if (!urls.some(url => document.URL.includes(url))) {
|
||||
return
|
||||
}
|
||||
const { BatchExtractor } = await import("batchDownload");
|
||||
if (await BatchExtractor.test() !== true)
|
||||
{
|
||||
return;
|
||||
const { BatchExtractor } = await import('batchDownload')
|
||||
if (await BatchExtractor.test() !== true) {
|
||||
return
|
||||
}
|
||||
const extractor = new BatchExtractor();
|
||||
document.getElementById("download-video").classList.add("batch");
|
||||
document.getElementById("video-action-batch-data").addEventListener("click", async () =>
|
||||
{
|
||||
if (!selectedFormat)
|
||||
{
|
||||
return;
|
||||
const extractor = new BatchExtractor()
|
||||
document.getElementById('download-video').classList.add('batch')
|
||||
document.getElementById('video-action-batch-data').addEventListener('click', async () => {
|
||||
if (!selectedFormat) {
|
||||
return
|
||||
}
|
||||
pageData.entity.resetMenuClass();
|
||||
const toast = Toast.info("获取链接中...", "批量下载");
|
||||
const data = await extractor.collectData(selectedFormat, toast);
|
||||
if (!data)
|
||||
{
|
||||
return;
|
||||
pageData.entity.resetMenuClass()
|
||||
const toast = Toast.info('获取链接中...', '批量下载')
|
||||
const data = await extractor.collectData(selectedFormat, toast)
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
GM_setClipboard(data, { type: "text/json" });
|
||||
Toast.success("已复制批量数据到剪贴板.", "复制批量数据", 3000);
|
||||
});
|
||||
document.getElementById("video-action-batch-download-data").addEventListener("click", async () =>
|
||||
{
|
||||
if (!selectedFormat)
|
||||
{
|
||||
return;
|
||||
GM_setClipboard(data, { type: 'text/json' })
|
||||
Toast.success('已复制批量数据到剪贴板.', '复制批量数据', 3000)
|
||||
})
|
||||
document.getElementById('video-action-batch-download-data').addEventListener('click', async () => {
|
||||
if (!selectedFormat) {
|
||||
return
|
||||
}
|
||||
pageData.entity.resetMenuClass();
|
||||
const toast = Toast.info("获取链接中...", "批量下载");
|
||||
const data = await extractor.collectData(selectedFormat, toast);
|
||||
if (!data)
|
||||
{
|
||||
return;
|
||||
pageData.entity.resetMenuClass()
|
||||
const toast = Toast.info('获取链接中...', '批量下载')
|
||||
const data = await extractor.collectData(selectedFormat, toast)
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
const a = document.createElement("a");
|
||||
const blob = new Blob([data], { type: "text/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
a.setAttribute("href", url);
|
||||
a.setAttribute("download", `export.json`);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
const a = document.createElement('a')
|
||||
const blob = new Blob([data], { type: 'text/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute('download', `export.json`)
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
})
|
||||
}
|
||||
async function loadPageData()
|
||||
{
|
||||
const aid = await SpinQuery.select(() => (unsafeWindow || window).aid);
|
||||
const cid = await SpinQuery.select(() => (unsafeWindow || window).cid);
|
||||
pageData.aid = aid;
|
||||
pageData.cid = cid;
|
||||
if (document.URL.indexOf("bangumi") !== -1)
|
||||
{
|
||||
pageData.entity = new Bangumi();
|
||||
async function loadPageData () {
|
||||
const aid = await SpinQuery.select(() => (unsafeWindow || window).aid)
|
||||
const cid = await SpinQuery.select(() => (unsafeWindow || window).cid)
|
||||
pageData.aid = aid
|
||||
pageData.cid = cid
|
||||
if (document.URL.indexOf('bangumi') !== -1) {
|
||||
pageData.entity = new Bangumi()
|
||||
} else {
|
||||
pageData.entity = new Video()
|
||||
}
|
||||
else
|
||||
{
|
||||
pageData.entity = new Video();
|
||||
try {
|
||||
formats = await VideoFormat.availableFormats
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
try
|
||||
{
|
||||
formats = await VideoFormat.availableFormats;
|
||||
return Boolean(aid && cid)
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return Boolean(aid && cid);
|
||||
}
|
||||
async function loadWidget()
|
||||
{
|
||||
selectedFormat = formats[0];
|
||||
const loadQualities = async () =>
|
||||
{
|
||||
const canDownload = await loadPageData();
|
||||
document.querySelector("#download-video").style.display = canDownload ? "flex" : "none";
|
||||
if (canDownload === false)
|
||||
{
|
||||
return;
|
||||
async function loadWidget () {
|
||||
selectedFormat = formats[0]
|
||||
const loadQualities = async () => {
|
||||
const canDownload = await loadPageData()
|
||||
document.querySelector('#download-video').style.display = canDownload ? 'flex' : 'none'
|
||||
if (canDownload === false) {
|
||||
return
|
||||
}
|
||||
// formats = await VideoFormat.availableFormats;
|
||||
|
||||
const list = document.querySelector("ol.video-quality");
|
||||
list.childNodes.forEach(list.removeChild);
|
||||
formats.forEach(format =>
|
||||
{
|
||||
const item = document.createElement("li");
|
||||
item.innerHTML = format.displayName;
|
||||
item.addEventListener("click", () =>
|
||||
{
|
||||
selectedFormat = format;
|
||||
pageData.entity.nextMenuClass();
|
||||
});
|
||||
list.insertAdjacentElement("afterbegin", item);
|
||||
});
|
||||
};
|
||||
Observer.videoChange(loadQualities);
|
||||
const getVideoInfo = () => selectedFormat.downloadInfo().catch(error =>
|
||||
{
|
||||
pageData.entity.addError();
|
||||
$(".video-error").text(error);
|
||||
});
|
||||
async function download()
|
||||
{
|
||||
if (!selectedFormat)
|
||||
{
|
||||
return;
|
||||
const list = document.querySelector('ol.video-quality')
|
||||
list.childNodes.forEach(list.removeChild)
|
||||
formats.forEach(format => {
|
||||
const item = document.createElement('li')
|
||||
item.innerHTML = format.displayName
|
||||
item.addEventListener('click', () => {
|
||||
selectedFormat = format
|
||||
pageData.entity.nextMenuClass()
|
||||
})
|
||||
list.insertAdjacentElement('afterbegin', item)
|
||||
})
|
||||
}
|
||||
pageData.entity.nextMenuClass();
|
||||
const info = await getVideoInfo();
|
||||
info.progress = percent =>
|
||||
{
|
||||
$(".download-progress-value").text(`${fixed(percent * 100)}`);
|
||||
$(".download-progress-foreground").css("transform", `scaleX(${percent})`);
|
||||
};
|
||||
document.querySelector(".download-progress-cancel>span").onclick = () => info.cancelDownload();
|
||||
Observer.videoChange(loadQualities)
|
||||
const getVideoInfo = () => selectedFormat.downloadInfo().catch(error => {
|
||||
pageData.entity.addError()
|
||||
$('.video-error').text(error)
|
||||
})
|
||||
async function download () {
|
||||
if (!selectedFormat) {
|
||||
return
|
||||
}
|
||||
pageData.entity.nextMenuClass()
|
||||
const info = await getVideoInfo()
|
||||
info.progress = percent => {
|
||||
$('.download-progress-value').text(`${fixed(percent * 100)}`)
|
||||
$('.download-progress-foreground').css('transform', `scaleX(${percent})`)
|
||||
}
|
||||
document.querySelector('.download-progress-cancel>span').onclick = () => info.cancelDownload()
|
||||
const result = await info.download()
|
||||
.catch(error =>
|
||||
{
|
||||
pageData.entity.addError();
|
||||
$(".video-error").text(error);
|
||||
});
|
||||
if (!result) // canceled or other errors
|
||||
{
|
||||
return;
|
||||
.catch(error => {
|
||||
pageData.entity.addError()
|
||||
$('.video-error').text(error)
|
||||
})
|
||||
if (!result) { // canceled or other errors
|
||||
return
|
||||
}
|
||||
const completeLink = document.getElementById("video-complete");
|
||||
completeLink.setAttribute("href", result.url);
|
||||
completeLink.setAttribute("download", result.filename);
|
||||
completeLink.click();
|
||||
const completeLink = document.getElementById('video-complete')
|
||||
completeLink.setAttribute('href', result.url)
|
||||
completeLink.setAttribute('download', result.filename)
|
||||
completeLink.click()
|
||||
|
||||
const message = `下载完成. <a class="link" href="${result.url}" download="${result.filename.replace(/"/g, """)}">再次保存</a>`;
|
||||
Toast.success(message, "下载视频");
|
||||
pageData.entity.resetMenuClass();
|
||||
const message = `下载完成. <a class="link" href="${result.url}" download="${result.filename.replace(/"/g, '"')}">再次保存</a>`
|
||||
Toast.success(message, '下载视频')
|
||||
pageData.entity.resetMenuClass()
|
||||
}
|
||||
async function copyLink()
|
||||
{
|
||||
if (!selectedFormat)
|
||||
{
|
||||
return;
|
||||
async function copyLink () {
|
||||
if (!selectedFormat) {
|
||||
return
|
||||
}
|
||||
const info = await getVideoInfo();
|
||||
info.copyUrl();
|
||||
Toast.success("已复制链接到剪贴板.", "复制链接", 3000);
|
||||
pageData.entity.resetMenuClass();
|
||||
const info = await getVideoInfo()
|
||||
info.copyUrl()
|
||||
Toast.success('已复制链接到剪贴板.', '复制链接', 3000)
|
||||
pageData.entity.resetMenuClass()
|
||||
}
|
||||
document.querySelector("#video-action-download").addEventListener("click", download);
|
||||
document.querySelector("#video-action-copy").addEventListener("click", copyLink);
|
||||
document.querySelector("#video-action-copy-data").addEventListener("click", async () =>
|
||||
{
|
||||
if (!selectedFormat)
|
||||
{
|
||||
return;
|
||||
document.querySelector('#video-action-download').addEventListener('click', download)
|
||||
document.querySelector('#video-action-copy').addEventListener('click', copyLink)
|
||||
document.querySelector('#video-action-copy-data').addEventListener('click', async () => {
|
||||
if (!selectedFormat) {
|
||||
return
|
||||
}
|
||||
const info = await getVideoInfo();
|
||||
info.exportData(true);
|
||||
Toast.success("已复制数据到剪贴板.", "复制数据", 3000);
|
||||
pageData.entity.resetMenuClass();
|
||||
});
|
||||
document.querySelector("#video-action-download-data").addEventListener("click", async () =>
|
||||
{
|
||||
if (!selectedFormat)
|
||||
{
|
||||
return;
|
||||
const info = await getVideoInfo()
|
||||
info.exportData(true)
|
||||
Toast.success('已复制数据到剪贴板.', '复制数据', 3000)
|
||||
pageData.entity.resetMenuClass()
|
||||
})
|
||||
document.querySelector('#video-action-download-data').addEventListener('click', async () => {
|
||||
if (!selectedFormat) {
|
||||
return
|
||||
}
|
||||
const info = await getVideoInfo();
|
||||
info.exportData(false);
|
||||
pageData.entity.resetMenuClass();
|
||||
});
|
||||
resources.applyStyle("downloadVideoStyle");
|
||||
const downloadPanel = document.querySelector(".download-video-panel");
|
||||
const togglePopup = () => $(".download-video-panel").toggleClass("opened");
|
||||
document.querySelector("#download-video").addEventListener("click", e =>
|
||||
{
|
||||
if (!downloadPanel.contains(e.target))
|
||||
{
|
||||
togglePopup();
|
||||
const info = await getVideoInfo()
|
||||
info.exportData(false)
|
||||
pageData.entity.resetMenuClass()
|
||||
})
|
||||
resources.applyStyle('downloadVideoStyle')
|
||||
const downloadPanel = document.querySelector('.download-video-panel')
|
||||
const togglePopup = () => $('.download-video-panel').toggleClass('opened')
|
||||
document.querySelector('#download-video').addEventListener('click', e => {
|
||||
if (!downloadPanel.contains(e.target)) {
|
||||
togglePopup()
|
||||
}
|
||||
});
|
||||
document.querySelector(".video-error").addEventListener("click", () =>
|
||||
{
|
||||
document.querySelector(".video-error").innerHTML = "";
|
||||
pageData.entity.removeError();
|
||||
});
|
||||
await SpinQuery.select(".download-video-panel");
|
||||
pageData.entity.addMenuClass();
|
||||
checkBatch();
|
||||
})
|
||||
document.querySelector('.video-error').addEventListener('click', () => {
|
||||
document.querySelector('.video-error').innerHTML = ''
|
||||
pageData.entity.removeError()
|
||||
})
|
||||
await SpinQuery.select('.download-video-panel')
|
||||
pageData.entity.addMenuClass()
|
||||
checkBatch()
|
||||
}
|
||||
export default {
|
||||
widget:
|
||||
{
|
||||
content: resources.data.downloadVideoHtml.text,
|
||||
condition: loadPageData,
|
||||
success: loadWidget,
|
||||
},
|
||||
};
|
||||
success: loadWidget
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user