Fix vld bugs

This commit is contained in:
the1812 2019-06-30 16:16:32 +08:00
parent 9f9077d047
commit 7f93200f86
11 changed files with 802 additions and 986 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -154,7 +154,7 @@ namespace BilibiliEvolved.Build
} }
return source; 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 imported = match.Groups[1].Value.Replace(" as ", ":");
var source = convertToRuntimeSource(match.Groups[2].Value); var source = convertToRuntimeSource(match.Groups[2].Value);
@ -173,6 +173,7 @@ namespace BilibiliEvolved.Build
}; };
})();"; })();";
} }
// Console.WriteLine(input);
return new UglifyJs().Run(input); return new UglifyJs().Run(input);
} }
} }

Binary file not shown.

View File

@ -1,6 +1,6 @@
{ {
"name": "bilibili-evolved-video-link-downloader", "name": "bilibili-evolved-video-link-downloader",
"version": "1.8.1", "version": "1.8.2",
"description": "Bilibili Evolved 视频链接下载器", "description": "Bilibili Evolved 视频链接下载器",
"main": "video-link-downloader.js", "main": "video-link-downloader.js",
"bin": { "bin": {

View File

@ -64,8 +64,8 @@ class Downloader {
headers: { headers: {
Range: range, Range: range,
Origin: "https://www.bilibili.com", Origin: "https://www.bilibili.com",
Referer: "https://www.bilibili.com", Referer: this.inputData.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", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0",
}, },
}).on("complete", response => { }).on("complete", response => {
if (response.statusCode.toString()[0] === "2") { if (response.statusCode.toString()[0] === "2") {
@ -78,15 +78,15 @@ class Downloader {
this.progressMap.set(req, this.progressMap.get(req) + data.length); this.progressMap.set(req, this.progressMap.get(req) + data.length);
this.updateProgress(); this.updateProgress();
}).on("error", error => { }).on("error", error => {
// stream.close();
// fs.unlinkSync(partFilename); // fs.unlinkSync(partFilename);
// this.progressMap.delete(req); this.progressMap.delete(req);
// this.progressMap.set(makeRequest(), 0); this.progressMap.set(makeRequest(), 0);
// this.updateProgress(); this.updateProgress();
reject(`\n片段下载失败: ${error}`); reject(`\n片段下载失败: ${error}`);
}); });
console.log(`created stream ${partFilename}`); req.pipe(fs.createWriteStream(partFilename, {
req.pipe(fs.createWriteStream(partFilename)); autoClose: true
}));
return req; return req;
}; };
this.progressMap.set(makeRequest(), 0); this.progressMap.set(makeRequest(), 0);

View File

@ -6,21 +6,19 @@ import fs = require("fs");
import ProgressBar = require("progress"); import ProgressBar = require("progress");
import "colors"; import "colors";
interface Fragment interface Fragment {
{
length: number; length: number;
size: number; size: number;
url: string; url: string;
backupUrls: string[]; backupUrls: string[];
} }
interface InputData interface InputData {
{
fragments: Fragment[]; fragments: Fragment[];
title: string; title: string;
totalSize: number; totalSize: number;
referer: string;
} }
interface Settings interface Settings {
{
parts: number; parts: number;
info: string; info: string;
output: string; output: string;
@ -41,14 +39,12 @@ options.parts = Math.round(options.parts);
// options = Object.assign(jsonOptions, options); // options = Object.assign(jsonOptions, options);
// } // }
if (options.parts < 1) if (options.parts < 1) {
{
console.error("分段数不能小于1".red); console.error("分段数不能小于1".red);
process.exit(); process.exit();
} }
class Downloader class Downloader {
{
static workingDownloader: Downloader | null = null; static workingDownloader: Downloader | null = null;
private progressMap = new Map<request.Request | string, number>(); private progressMap = new Map<request.Request | string, number>();
private progressBar = new ProgressBar(":percent [:bar]", { private progressBar = new ProgressBar(":percent [:bar]", {
@ -61,24 +57,18 @@ class Downloader
constructor( constructor(
private inputData: InputData, private inputData: InputData,
) ) { }
{ } private getExtension(fragment: Fragment) {
private getExtension(fragment: Fragment)
{
this.extension = fragment.url.includes(".flv") ? ".flv" : ".mp4"; this.extension = fragment.url.includes(".flv") ? ".flv" : ".mp4";
} }
private updateProgress() private updateProgress() {
{
const progress = this.progressMap ? const progress = this.progressMap ?
[...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.inputData.totalSize : 0; [...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.inputData.totalSize : 0;
this.progressBar.update(progress); this.progressBar.update(progress);
} }
cancelDownload() cancelDownload() {
{ [...this.progressMap.keys()].forEach(it => {
[...this.progressMap.keys()].forEach(it => if (typeof it !== "string") {
{
if (typeof it !== "string")
{
it.abort(); it.abort();
} }
}); });
@ -88,58 +78,47 @@ class Downloader
parts.forEach(file => fs.unlinkSync(file)); parts.forEach(file => fs.unlinkSync(file));
console.log("已取消下载".blue); console.log("已取消下载".blue);
} }
private downloadFragmentPart(url: string, range: string, partFilename: string) private downloadFragmentPart(url: string, range: string, partFilename: string) {
{ return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => const makeRequest = () => {
{
const makeRequest = () =>
{
const req = request({ const req = request({
url: url, url: url,
method: "GET", method: "GET",
headers: { headers: {
Range: range, Range: range,
Origin: "https://www.bilibili.com", Origin: "https://www.bilibili.com",
Referer: "https://www.bilibili.com", Referer: this.inputData.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", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0",
}, },
// maxAttempts: 8, }).on("complete", response => {
// retryDelay: 1000, if (response.statusCode.toString()[0] === "2") {
}).on("complete", response =>
{
if (response.statusCode.toString()[0] === "2")
{
resolve(response); resolve(response);
} }
else else {
{
reject(`请求失败: ${response.statusCode}`); reject(`请求失败: ${response.statusCode}`);
} }
}).on("data", data => }).on("data", data => {
{
this.progressMap.set(req, this.progressMap.get(req)! + data.length); this.progressMap.set(req, this.progressMap.get(req)! + data.length);
this.updateProgress(); this.updateProgress();
}).on("error", error => }).on("error", error => {
{
// stream.close();
// fs.unlinkSync(partFilename); // fs.unlinkSync(partFilename);
// this.progressMap.delete(req); this.progressMap.delete(req);
// this.progressMap.set(makeRequest(), 0); this.progressMap.set(makeRequest(), 0);
// this.updateProgress(); this.updateProgress();
reject(`\n片段下载失败: ${error}`); reject(`\n片段下载失败: ${error}`);
}); });
req.pipe(fs.createWriteStream(partFilename)) req.pipe(fs.createWriteStream(partFilename, {
autoClose: true
}))
return req return req
}; };
this.progressMap.set(makeRequest(), 0); 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 partialLength = Math.round(fragment.size / options.parts);
const title = (index === -1 ? this.inputData.title : this.inputData.title + " - " + index.toString()); 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(); this.progressBar.terminate();
console.log(`跳过了已存在的文件 ${title + this.extension}`); console.log(`跳过了已存在的文件 ${title + this.extension}`);
return title; return title;
@ -147,8 +126,7 @@ class Downloader
let startByte = 0; let startByte = 0;
let part = 0; let part = 0;
const promises = []; const promises = [];
while (startByte < fragment.size) while (startByte < fragment.size) {
{
const partFilename = `${title}.part${part}`; const partFilename = `${title}.part${part}`;
// if (fs.existsSync(partFilename)) // if (fs.existsSync(partFilename))
// { // {
@ -167,62 +145,49 @@ class Downloader
await Promise.all(promises); await Promise.all(promises);
return title; return title;
} }
async download() async download() {
{
console.log(`正在下载: ${this.inputData.title}`.green); console.log(`正在下载: ${this.inputData.title}`.green);
Downloader.workingDownloader = this; Downloader.workingDownloader = this;
this.progressBar.render(); this.progressBar.render();
const [fragment] = this.inputData.fragments; const [fragment] = this.inputData.fragments;
this.getExtension(fragment); this.getExtension(fragment);
if (this.inputData.fragments.length === 1) if (this.inputData.fragments.length === 1) {
{
this.title = await this.downloadFragment(fragment); this.title = await this.downloadFragment(fragment);
} }
else else {
{
this.title = await Promise.all(this.inputData.fragments.map((f, i) => this.downloadFragment(f, i))); 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; const dest = title + this.extension;
if (!this.progressBar.complete) if (!this.progressBar.complete) {
{
this.progressBar.update(1); this.progressBar.update(1);
this.progressBar.terminate(); this.progressBar.terminate();
} }
if (fs.existsSync(dest)) if (fs.existsSync(dest)) {
{
return dest; return dest;
} }
if (index !== -1) if (index !== -1) {
{
console.log(`正在合并片段${index.toString()}...`.blue); console.log(`正在合并片段${index.toString()}...`.blue);
} }
else else {
{
console.log("正在合并文件...".blue); console.log("正在合并文件...".blue);
} }
if (options.parts === 1) if (options.parts === 1) {
{
fs.renameSync(title + ".part0", dest); fs.renameSync(title + ".part0", dest);
} }
else else {
{ if (fs.existsSync(dest)) {
if (fs.existsSync(dest))
{
fs.unlinkSync(dest); fs.unlinkSync(dest);
} }
const files = fs.readdirSync("."); const files = fs.readdirSync(".");
const parts = files.filter(it => it.includes(title + ".part")); const parts = files.filter(it => it.includes(title + ".part"));
const partRegex = /.*\.part([\d]+)/; const partRegex = /.*\.part([\d]+)/;
parts.sort((a, b) => parts.sort((a, b) => {
{
const partA = parseInt(a.replace(partRegex, "$1")); const partA = parseInt(a.replace(partRegex, "$1"));
const partB = parseInt(b.replace(partRegex, "$1")); const partB = parseInt(b.replace(partRegex, "$1"));
return partA - partB; return partA - partB;
}).forEach(file => }).forEach(file => {
{
const buffer = fs.readFileSync(file); const buffer = fs.readFileSync(file);
fs.appendFileSync(dest, buffer); fs.appendFileSync(dest, buffer);
}); });
@ -230,88 +195,68 @@ class Downloader
} }
return dest; return dest;
} }
async merge() async merge() {
{
let result: string | string[]; let result: string | string[];
if (typeof this.title === "string") if (typeof this.title === "string") {
{
result = await this.mergeFragment(this.title); result = await this.mergeFragment(this.title);
} }
else else {
{
result = await Promise.all(this.title.map((t, i) => this.mergeFragment(t, i))); result = await Promise.all(this.title.map((t, i) => this.mergeFragment(t, i)));
} }
console.log(`完成: `.green); console.log(`完成: `.green);
if (typeof result === "string") if (typeof result === "string") {
{
console.log(result); console.log(result);
} }
else else {
{
result.forEach(it => console.log(it)); result.forEach(it => console.log(it));
} }
} }
} }
(async () => (async () => {
{ try {
try
{
let jsonText = ''; let jsonText = '';
if (fs.existsSync(options.info)) if (fs.existsSync(options.info)) {
{
jsonText = fs.readFileSync(options.info).toString("utf-8"); jsonText = fs.readFileSync(options.info).toString("utf-8");
} }
else else {
{
jsonText = await clipboardy.read(); jsonText = await clipboardy.read();
} }
const inputData = JSON.parse(jsonText) as InputData | InputData[]; const inputData = JSON.parse(jsonText) as InputData | InputData[];
if (!(function (data: any): data is InputData | InputData[] if (!(function (data: any): data is InputData | InputData[] {
{ if (Array.isArray(data) && data.length > 0) {
if (Array.isArray(data) && data.length > 0)
{
return data[0].fragments !== undefined; return data[0].fragments !== undefined;
} }
else else {
{
return data.fragments !== undefined; return data.fragments !== undefined;
} }
})(inputData)) })(inputData)) {
{
throw new Error(); throw new Error();
} }
try try {
{
process.chdir(options.output); process.chdir(options.output);
process.on("SIGINT", () => process.on("SIGINT", () => {
{
Downloader.workingDownloader && Downloader.workingDownloader.cancelDownload(); Downloader.workingDownloader && Downloader.workingDownloader.cancelDownload();
process.exit(); process.exit();
}); });
if (Array.isArray(inputData)) if (Array.isArray(inputData)) {
{
const downloaders = inputData.map(data => new Downloader(data)); const downloaders = inputData.map(data => new Downloader(data));
for (const downloader of downloaders) for (const downloader of downloaders) {
{
await downloader.download(); await downloader.download();
await downloader.merge(); await downloader.merge();
} }
} }
else else {
{
const downloader = new Downloader(inputData); const downloader = new Downloader(inputData);
await downloader.download(); await downloader.download();
await downloader.merge(); await downloader.merge();
} }
} }
catch (error) catch (error) {
{
Downloader.workingDownloader && Downloader.workingDownloader.cancelDownload(); Downloader.workingDownloader && Downloader.workingDownloader.cancelDownload();
console.error(`\n错误: ${error}`.red); console.error(`\n错误: ${error}`.red);
} }
} }
catch (error) catch (error) {
{
console.log(`[无数据] 未在剪贴板检测到有效数据/没有指定输入文件/输入文件的数据无效.`.red); console.log(`[无数据] 未在剪贴板检测到有效数据/没有指定输入文件/输入文件的数据无效.`.red);
} }
})(); })();

View File

@ -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}}}})();

File diff suppressed because one or more lines are too long

View File

@ -1,148 +1,124 @@
class VideoEpisodeBatch class VideoEpisodeBatch {
{ static async test () {
static async test() if (!document.URL.includes('/www.bilibili.com/video/av')) {
{ return false
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) async collectData (quality) {
{ const api = `https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`
const api = `https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`; const json = await Ajax.getJson(api)
const json = await Ajax.getJson(api); if (json.code !== 0) {
if (json.code !== 0) Toast.error(`获取视频选集列表失败, message=${json.message}`, '批量下载')
{ return ''
Toast.error(`获取视频选集列表失败, message=${json.message}`, "批量下载");
return "";
} }
const pages = json.data.pages; const pages = json.data.pages
if (pages === undefined) if (pages === undefined) {
{ Toast.error(`获取视频选集列表失败, 没有找到选集信息.`, '批量下载')
Toast.error(`获取视频选集列表失败, 没有找到选集信息.`, "批量下载"); return ''
return "";
} }
const result = []; const result = []
for (const page of pages) 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 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 json = await Ajax.getJsonWithCredentials(url); const data = json.data || json.result || json
const data = json.data || json.result || json; if (data.quality !== quality) {
if (data.quality !== quality) Toast.error('获取下载链接失败, 请确认当前账号有下载权限后重试.', '批量下载')
{ return ''
Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.", "批量下载");
return "";
} }
const fragments = data.durl.map(it => const fragments = data.durl.map(it => {
{
return { return {
length: it.length, length: it.length,
size: it.size, size: it.size,
url: it.url, url: it.url
}; }
}); })
result.push({ result.push({
fragments, fragments,
title: `${page.page} - ${page.part}`, title: `${page.page} - ${page.part}`,
totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it), totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it),
cid: page.cid, cid: page.cid,
}); referer: document.URL.replace(window.location.search, '')
})
} }
return JSON.stringify(result); return JSON.stringify(result)
} }
} }
class BangumiBatch class BangumiBatch {
{ static async test () {
static async test() return document.URL.includes('/www.bilibili.com/bangumi')
{
return document.URL.includes("/www.bilibili.com/bangumi");
} }
async collectData(quality) async collectData (quality) {
{ const metaUrl = document.querySelector("meta[property='og:url']")
const metaUrl = document.querySelector("meta[property='og:url']"); if (metaUrl === null) {
if (metaUrl === null) Toast.error('获取番剧数据失败: 无法找到 Season ID', '批量下载')
{ return ''
Toast.error("获取番剧数据失败: 无法找到 Season ID", "批量下载");
return "";
} }
const seasonId = metaUrl.getAttribute("content").match(/play\/ss(\d+)/)[1]; const seasonId = metaUrl.getAttribute('content').match(/play\/ss(\d+)/)[1]
if (seasonId === undefined) if (seasonId === undefined) {
{ Toast.error('获取番剧数据失败: 无法解析 Season ID', '批量下载')
Toast.error("获取番剧数据失败: 无法解析 Season ID", "批量下载"); return ''
return "";
} }
const json = await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${seasonId}`); const json = await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${seasonId}`)
if (json.code !== 0) if (json.code !== 0) {
{ Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${json.message}`, '批量下载')
Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${json.message}`, "批量下载"); return ''
return "";
} }
const pages = json.result.main_section.episodes.map(it => const pages = json.result.main_section.episodes.map(it => {
{ return { aid: it.aid, cid: it.cid, number: it.title, title: it.long_title }
return { aid: it.aid, cid: it.cid, number: it.title, title: it.long_title }; })
}); const result = []
const result = []; for (const page of pages) {
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 url = `https://api.bilibili.com/pgc/player/web/playurl?avid=${page.aid}&cid=${page.cid}&qn=${quality}&otype=json`; const data = json.data || json.result || json
const json = await Ajax.getJsonWithCredentials(url); if (data.quality !== quality) {
const data = json.data || json.result || json; Toast.error('获取下载链接失败, 请确认当前账号有下载权限后重试.', '批量下载')
if (data.quality !== quality) return ''
{
Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.", "批量下载");
return "";
} }
const fragments = data.durl.map(it => const fragments = data.durl.map(it => {
{
return { return {
length: it.length, length: it.length,
size: it.size, size: it.size,
url: it.url, url: it.url
}; }
}); })
result.push({ result.push({
fragments, fragments,
title: `${page.number} - ${page.title}`, title: `${page.number} - ${page.title}`,
totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it), 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]; const extractors = [BangumiBatch, VideoEpisodeBatch]
let ExtractorClass = null; let ExtractorClass = null
export class BatchExtractor export class BatchExtractor {
{ static async test () {
static async test() for (const e of extractors) {
{ if (await e.test() === true) {
for (const e of extractors) ExtractorClass = e
{ return true
if (await e.test() === true)
{
ExtractorClass = e;
return true;
} }
} }
ExtractorClass = null; ExtractorClass = null
return false; return false
} }
async collectData(format, toast) async collectData (format, toast) {
{ if (ExtractorClass === null) {
if (ExtractorClass === null) logError('[批量下载] 未找到合适的解析模块.')
{ return null
logError("[批量下载] 未找到合适的解析模块.");
return null;
} }
const extractor = new ExtractorClass; const extractor = new ExtractorClass()
const result = await extractor.collectData(format.quality); const result = await extractor.collectData(format.quality)
toast.dismiss(); toast.dismiss()
return result; return result
} }
} }
export default { export default {
export: { export: {
BatchExtractor, BatchExtractor
}, }
}; }

View File

@ -1,577 +1,471 @@
import { getFriendlyTitle } from "../title"; import { getFriendlyTitle } from '../title'
const pageData = { const pageData = {
entity: null, entity: null,
aid: undefined, aid: undefined,
cid: undefined, cid: undefined
}; }
let formats = []; let formats = []
let selectedFormat = null; let selectedFormat = null
class Video class Video {
{ constructor () {
constructor() this.menuClasses = ['quality', 'action', 'progress']
{ this.currentMenuClass = 'quality'
this.menuClasses = ["quality", "action", "progress"];
this.currentMenuClass = "quality";
} }
get menuPanel() get menuPanel () {
{ return document.querySelector('.download-video-panel')
return document.querySelector(".download-video-panel");
} }
addMenuClass() addMenuClass () {
{ this.menuPanel.classList.remove(...this.menuClasses)
this.menuPanel.classList.remove(...this.menuClasses); this.menuPanel.classList.add(this.currentMenuClass)
this.menuPanel.classList.add(this.currentMenuClass); return this.currentMenuClass
return this.currentMenuClass;
} }
resetMenuClass() resetMenuClass () {
{ [this.currentMenuClass] = this.menuClasses
[this.currentMenuClass] = this.menuClasses; this.addMenuClass()
this.addMenuClass();
} }
nextMenuClass() nextMenuClass () {
{ const index = this.menuClasses.indexOf(this.currentMenuClass) + 1
const index = this.menuClasses.indexOf(this.currentMenuClass) + 1; const next = this.menuClasses[index >= this.menuClasses.length ? 0 : index]
const next = this.menuClasses[index >= this.menuClasses.length ? 0 : index]; this.currentMenuClass = next
this.currentMenuClass = next; this.addMenuClass()
this.addMenuClass(); return next
return next;
} }
addError() addError () {
{ this.menuPanel.classList.add('error')
this.menuPanel.classList.add("error");
} }
removeError() removeError () {
{ this.menuPanel.classList.remove('error')
this.menuPanel.classList.remove("error"); this.resetMenuClass()
this.resetMenuClass();
} }
async getUrl(quality) async getUrl (quality) {
{ if (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}&qn=${quality}&otype=json`; return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&otype=json`
}
else
{
return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&otype=json`;
} }
} }
} }
class Bangumi extends Video class Bangumi extends Video {
{ async getUrl (quality) {
async getUrl(quality) if (quality) {
{ return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json`
if (quality) } else {
{ return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=&otype=json`
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 class VideoFormat {
{ constructor (quality, internalName, displayName) {
constructor(quality, internalName, displayName) this.quality = quality
{ this.internalName = internalName
this.quality = quality; this.displayName = displayName
this.internalName = internalName;
this.displayName = displayName;
} }
async downloadInfo() async downloadInfo () {
{ const videoInfo = new VideoDownloader(this)
const videoInfo = new VideoDownloader(this); await videoInfo.fetchVideoInfo()
await videoInfo.fetchVideoInfo(); return videoInfo
return videoInfo;
} }
static get availableFormats() static get availableFormats () {
{ return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => pageData.entity.getUrl().then(url => {
{ const xhr = new XMLHttpRequest()
pageData.entity.getUrl().then(url => xhr.addEventListener('load', () => {
{ const json = JSON.parse(xhr.responseText)
const xhr = new XMLHttpRequest(); if (json.code !== 0) {
xhr.addEventListener("load", () => reject('获取清晰度信息失败.')
{ return
const json = JSON.parse(xhr.responseText);
if (json.code !== 0)
{
reject("获取清晰度信息失败.");
return;
} }
const data = json.data || json.result || json; const data = json.data || json.result || json
const qualities = data.accept_quality; const qualities = data.accept_quality
const internalNames = data.accept_format.split(","); const internalNames = data.accept_format.split(',')
const displayNames = data.accept_description; const displayNames = data.accept_description
const formats = []; const formats = []
while (qualities.length > 0) while (qualities.length > 0) {
{
const format = new VideoFormat( const format = new VideoFormat(
qualities.pop(), qualities.pop(),
internalNames.pop(), internalNames.pop(),
displayNames.pop() displayNames.pop()
); )
formats.push(format); formats.push(format)
} }
resolve(formats); resolve(formats)
}); })
xhr.addEventListener("error", () => reject(`获取清晰度信息失败.`)); xhr.addEventListener('error', () => reject(`获取清晰度信息失败.`))
xhr.withCredentials = true; xhr.withCredentials = true
xhr.open("GET", url); xhr.open('GET', url)
xhr.send(); xhr.send()
}); })
}); })
} }
} }
class VideoDownloaderFragment class VideoDownloaderFragment {
{ constructor (length, size, url, backupUrls) {
constructor(length, size, url, backupUrls) this.length = length
{ this.size = size
this.length = length; this.url = url
this.size = size; this.backupUrls = backupUrls
this.url = url;
this.backupUrls = backupUrls;
} }
} }
class VideoDownloader class VideoDownloader {
{ constructor (format, fragments) {
constructor(format, fragments) this.format = format
{ this.fragments = fragments || []
this.format = format; this.progress = null
this.fragments = fragments || []; // this.loaded = 0
this.progress = null; this.totalSize = null
// this.loaded = 0; this.workingXhr = null
this.totalSize = null; this.fragmentSplitFactor = 6 * 5
this.workingXhr = null;
this.fragmentSplitFactor = 6 * 5;
} }
fetchVideoInfo() fetchVideoInfo () {
{ return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => pageData.entity.getUrl(this.format.quality).then(url => {
{ const xhr = new XMLHttpRequest()
pageData.entity.getUrl(this.format.quality).then(url => xhr.addEventListener('load', () => {
{ const json = JSON.parse(xhr.responseText.replace(/http:/g, 'https:'))
const xhr = new XMLHttpRequest(); const data = json.data || json.result || json
xhr.addEventListener("load", () => if (data.quality !== this.format.quality) {
{ reject('获取下载链接失败, 请确认当前账号有下载权限后重试.')
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( this.fragments = urls.map(it => new VideoDownloaderFragment(
it.length, it.size, it.length, it.size,
it.url, it.url,
it.backup_url it.backup_url
)); ))
resolve(this.fragments); resolve(this.fragments)
}); })
xhr.withCredentials = true; xhr.withCredentials = true
xhr.open("GET", url); xhr.open('GET', url)
xhr.send(); xhr.send()
}); })
}); })
} }
updateProgress() updateProgress () {
{ const progress = this.progressMap
const progress = this.progressMap ? ? [...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.totalSize : 0
[...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.totalSize : 0; if (progress > 1 || progress < 0) {
if (progress > 1 || progress < 0) console.error(`[下载视频] 进度异常: ${progress}`, this.progressMap.values())
{
console.error(`[下载视频] 进度异常: ${progress}`, this.progressMap.values());
} }
this.progress && this.progress(progress); this.progress && this.progress(progress)
} }
cancelDownload() cancelDownload () {
{ if ('forEach' in this.workingXhr) {
if ("forEach" in this.workingXhr) this.workingXhr.forEach(it => it.abort())
{ } else {
this.workingXhr.forEach(it => it.abort()); logError('Cancel Download Failed: forEach in this.workingXhr not found.')
}
else
{
logError("Cancel Download Failed: forEach in this.workingXhr not found.");
} }
} }
downloadFragment(fragment) downloadFragment (fragment) {
{ const promises = []
const promises = []; this.workingXhr = []
this.workingXhr = []; this.progressMap = new Map()
this.progressMap = new Map(); this.updateProgress()
this.updateProgress(); const partialLength = Math.round(fragment.size / this.fragmentSplitFactor)
const partialLength = Math.round(fragment.size / this.fragmentSplitFactor); let startByte = 0
let startByte = 0; const getPartNumber = xhr => [...this.progressMap.keys()].indexOf(xhr) + 1
const getPartNumber = xhr => [...this.progressMap.keys()].indexOf(xhr) + 1; while (startByte < fragment.size) {
while (startByte < fragment.size) const endByte = Math.min(fragment.size - 1, Math.round(startByte + partialLength))
{ const range = `bytes=${startByte}-${endByte}`
const endByte = Math.min(fragment.size - 1, Math.round(startByte + partialLength)); const rangeLength = endByte - startByte + 1
const range = `bytes=${startByte}-${endByte}`; promises.push(new Promise((resolve, reject) => {
const rangeLength = endByte - startByte + 1; const xhr = new XMLHttpRequest()
promises.push(new Promise((resolve, reject) => xhr.open('GET', fragment.url)
{ xhr.responseType = 'arraybuffer'
const xhr = new XMLHttpRequest(); xhr.withCredentials = false
xhr.open("GET", fragment.url); xhr.addEventListener('progress', (e) => {
xhr.responseType = "arraybuffer"; console.log(`[下载视频] 视频片段${getPartNumber(xhr)}下载进度: ${e.loaded}/${rangeLength} bytes loaded, ${range}`)
xhr.withCredentials = false; this.progressMap.set(xhr, e.loaded)
xhr.addEventListener("progress", (e) => this.updateProgress()
{ })
console.log(`[下载视频] 视频片段${getPartNumber(xhr)}下载进度: ${e.loaded}/${rangeLength} bytes loaded, ${range}`); xhr.addEventListener('load', () => {
this.progressMap.set(xhr, e.loaded); if (('' + xhr.status)[0] === '2') {
this.updateProgress(); resolve(xhr.response)
}); } else {
xhr.addEventListener("load", () => reject(`请求失败.`)
{
if (("" + xhr.status)[0] === "2")
{
resolve(xhr.response);
} }
else })
{ xhr.addEventListener('abort', () => reject('下载已取消.'))
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)
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); copyUrl () {
const urls = this.fragments.map(it => it.url).reduce((acc, it) => acc + '\r\n' + it)
GM_setClipboard(urls, 'text')
} }
copyUrl() exportData (copy = false) {
{
const urls = this.fragments.map(it => it.url).reduce((acc, it) => acc + "\r\n" + it);
GM_setClipboard(urls, "text");
}
exportData(copy = false)
{
const data = JSON.stringify([{ const data = JSON.stringify([{
fragments: this.fragments, fragments: this.fragments,
title: getFriendlyTitle(true), title: getFriendlyTitle(true),
totalSize: this.fragments.map(it => it.size).reduce((acc, it) => acc + it), totalSize: this.fragments.map(it => it.size).reduce((acc, it) => acc + it),
}]); referer: document.URL.replace(window.location.search, '')
if (copy) }])
{ if (copy) {
GM_setClipboard(data, "text"); GM_setClipboard(data, 'text')
} } else {
else const a = document.createElement('a')
{ const blob = new Blob([data], { type: 'text/json' })
const a = document.createElement("a"); const url = URL.createObjectURL(blob)
const blob = new Blob([data], { type: "text/json" }); a.setAttribute('href', url)
const url = URL.createObjectURL(blob); a.setAttribute('download', `cid${unsafeWindow.cid}.json`)
a.setAttribute("href", url); document.body.appendChild(a)
a.setAttribute("download", `cid${unsafeWindow.cid}.json`); a.click()
document.body.appendChild(a); a.remove()
a.click(); URL.revokeObjectURL(url)
a.remove();
URL.revokeObjectURL(url);
} }
} }
extension(fragment) extension (fragment) {
{
return (fragment || this.fragments[0]).url return (fragment || this.fragments[0]).url
.indexOf(".flv") !== -1 .indexOf('.flv') !== -1
? ".flv" ? '.flv'
: ".mp4"; : '.mp4'
} }
makeBlob(data, fragment = null) makeBlob (data, fragment = null) {
{
return new Blob(Array.isArray(data) ? data : [data], { 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() cleanUpOldBlobUrl () {
{ const oldBlobUrl = document.querySelector('a#video-complete').getAttribute('href')
const oldBlobUrl = document.querySelector("a#video-complete").getAttribute("href"); if (oldBlobUrl && !document.querySelector(`.link[href="${oldBlobUrl}"]`)) {
if (oldBlobUrl && !document.querySelector(`.link[href="${oldBlobUrl}"]`)) URL.revokeObjectURL(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) downloadSingle (downloadedData) {
{ const [data] = downloadedData
const [data] = downloadedData; const blob = this.makeBlob(data)
const blob = this.makeBlob(data); const filename = getFriendlyTitle() + this.extension()
const filename = getFriendlyTitle() + this.extension(); return [blob, filename]
return [blob, filename];
} }
async downloadMultiple(downloadedData) async downloadMultiple (downloadedData) {
{ const zip = new JSZip()
const zip = new JSZip(); const title = getFriendlyTitle()
const title = getFriendlyTitle(); if (downloadedData.length > 1) {
if (downloadedData.length > 1) downloadedData.forEach((data, index) => {
{ const fragment = this.fragments[index]
downloadedData.forEach((data, index) => zip.file(`${title} - ${index + 1}${this.extension(fragment)}`, this.makeBlob(data, fragment))
{ })
const fragment = this.fragments[index]; } else {
zip.file(`${title} - ${index + 1}${this.extension(fragment)}`, this.makeBlob(data, fragment)); const [data] = downloadedData
}); zip.file(`${title}${this.extension()}`, this.makeBlob(data))
} }
else const blob = await zip.generateAsync({ type: 'blob' })
{ const filename = title + '.zip'
const [data] = downloadedData; return [blob, filename]
zip.file(`${title}${this.extension()}`, this.makeBlob(data));
} }
const blob = await zip.generateAsync({ type: "blob" }); async download () {
const filename = title + ".zip"; const downloadedData = []
return [blob, filename]; 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() if (downloadedData.length < 1) {
{ throw new Error('下载失败.')
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("下载失败.");
} }
let blob = null; let blob = null
let filename = null; let filename = null
if (downloadedData.length === 1) if (downloadedData.length === 1) {
{ [blob, filename] = this.downloadSingle(downloadedData)
[blob, filename] = this.downloadSingle(downloadedData); } else {
} [blob, filename] = await this.downloadMultiple(downloadedData)
else
{
[blob, filename] = await this.downloadMultiple(downloadedData);
} }
this.cleanUpOldBlobUrl(); this.cleanUpOldBlobUrl()
const blobUrl = URL.createObjectURL(blob); const blobUrl = URL.createObjectURL(blob)
this.progress && this.progress(0); this.progress && this.progress(0)
return { return {
url: blobUrl, url: blobUrl,
filename: filename filename: filename
}; }
} }
} }
async function checkBatch() async function checkBatch () {
{
const urls = [ const urls = [
"/www.bilibili.com/bangumi", '/www.bilibili.com/bangumi',
"/www.bilibili.com/video/av", '/www.bilibili.com/video/av'
]; ]
if (!urls.some(url => document.URL.includes(url))) if (!urls.some(url => document.URL.includes(url))) {
{ return
return;
} }
const { BatchExtractor } = await import("batchDownload"); const { BatchExtractor } = await import('batchDownload')
if (await BatchExtractor.test() !== true) if (await BatchExtractor.test() !== true) {
{ return
return;
} }
const extractor = new BatchExtractor(); const extractor = new BatchExtractor()
document.getElementById("download-video").classList.add("batch"); document.getElementById('download-video').classList.add('batch')
document.getElementById("video-action-batch-data").addEventListener("click", async () => document.getElementById('video-action-batch-data').addEventListener('click', async () => {
{ if (!selectedFormat) {
if (!selectedFormat) return
{
return;
} }
pageData.entity.resetMenuClass(); pageData.entity.resetMenuClass()
const toast = Toast.info("获取链接中...", "批量下载"); const toast = Toast.info('获取链接中...', '批量下载')
const data = await extractor.collectData(selectedFormat, toast); const data = await extractor.collectData(selectedFormat, toast)
if (!data) if (!data) {
{ return
return;
} }
GM_setClipboard(data, { type: "text/json" }); GM_setClipboard(data, { type: 'text/json' })
Toast.success("已复制批量数据到剪贴板.", "复制批量数据", 3000); Toast.success('已复制批量数据到剪贴板.', '复制批量数据', 3000)
}); })
document.getElementById("video-action-batch-download-data").addEventListener("click", async () => document.getElementById('video-action-batch-download-data').addEventListener('click', async () => {
{ if (!selectedFormat) {
if (!selectedFormat) return
{
return;
} }
pageData.entity.resetMenuClass(); pageData.entity.resetMenuClass()
const toast = Toast.info("获取链接中...", "批量下载"); const toast = Toast.info('获取链接中...', '批量下载')
const data = await extractor.collectData(selectedFormat, toast); const data = await extractor.collectData(selectedFormat, toast)
if (!data) if (!data) {
{ return
return;
} }
const a = document.createElement("a"); const a = document.createElement('a')
const blob = new Blob([data], { type: "text/json" }); const blob = new Blob([data], { type: 'text/json' })
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob)
a.setAttribute("href", url); a.setAttribute('href', url)
a.setAttribute("download", `export.json`); a.setAttribute('download', `export.json`)
document.body.appendChild(a); document.body.appendChild(a)
a.click(); a.click()
a.remove(); a.remove()
URL.revokeObjectURL(url); URL.revokeObjectURL(url)
}); })
} }
async function loadPageData() async function loadPageData () {
{ const aid = await SpinQuery.select(() => (unsafeWindow || window).aid)
const aid = await SpinQuery.select(() => (unsafeWindow || window).aid); const cid = await SpinQuery.select(() => (unsafeWindow || window).cid)
const cid = await SpinQuery.select(() => (unsafeWindow || window).cid); pageData.aid = aid
pageData.aid = aid; pageData.cid = cid
pageData.cid = cid; if (document.URL.indexOf('bangumi') !== -1) {
if (document.URL.indexOf("bangumi") !== -1) pageData.entity = new Bangumi()
{ } else {
pageData.entity = new Bangumi(); pageData.entity = new Video()
} }
else try {
{ formats = await VideoFormat.availableFormats
pageData.entity = new Video(); } catch (error) {
return false
} }
try return Boolean(aid && cid)
{
formats = await VideoFormat.availableFormats;
}
catch (error)
{
return false;
}
return Boolean(aid && cid);
} }
async function loadWidget() async function loadWidget () {
{ selectedFormat = formats[0]
selectedFormat = formats[0]; const loadQualities = async () => {
const loadQualities = async () => const canDownload = await loadPageData()
{ document.querySelector('#download-video').style.display = canDownload ? 'flex' : 'none'
const canDownload = await loadPageData(); if (canDownload === false) {
document.querySelector("#download-video").style.display = canDownload ? "flex" : "none"; return
if (canDownload === false)
{
return;
} }
// formats = await VideoFormat.availableFormats; // formats = await VideoFormat.availableFormats;
const list = document.querySelector("ol.video-quality"); const list = document.querySelector('ol.video-quality')
list.childNodes.forEach(list.removeChild); list.childNodes.forEach(list.removeChild)
formats.forEach(format => formats.forEach(format => {
{ const item = document.createElement('li')
const item = document.createElement("li"); item.innerHTML = format.displayName
item.innerHTML = format.displayName; item.addEventListener('click', () => {
item.addEventListener("click", () => selectedFormat = format
{ pageData.entity.nextMenuClass()
selectedFormat = format; })
pageData.entity.nextMenuClass(); list.insertAdjacentElement('afterbegin', item)
}); })
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;
} }
pageData.entity.nextMenuClass(); Observer.videoChange(loadQualities)
const info = await getVideoInfo(); const getVideoInfo = () => selectedFormat.downloadInfo().catch(error => {
info.progress = percent => pageData.entity.addError()
{ $('.video-error').text(error)
$(".download-progress-value").text(`${fixed(percent * 100)}`); })
$(".download-progress-foreground").css("transform", `scaleX(${percent})`); async function download () {
}; if (!selectedFormat) {
document.querySelector(".download-progress-cancel>span").onclick = () => info.cancelDownload(); 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() const result = await info.download()
.catch(error => .catch(error => {
{ pageData.entity.addError()
pageData.entity.addError(); $('.video-error').text(error)
$(".video-error").text(error); })
}); if (!result) { // canceled or other errors
if (!result) // canceled or other errors return
{
return;
} }
const completeLink = document.getElementById("video-complete"); const completeLink = document.getElementById('video-complete')
completeLink.setAttribute("href", result.url); completeLink.setAttribute('href', result.url)
completeLink.setAttribute("download", result.filename); completeLink.setAttribute('download', result.filename)
completeLink.click(); completeLink.click()
const message = `下载完成. <a class="link" href="${result.url}" download="${result.filename.replace(/"/g, "&quot;")}">再次保存</a>`; const message = `下载完成. <a class="link" href="${result.url}" download="${result.filename.replace(/"/g, '&quot;')}">再次保存</a>`
Toast.success(message, "下载视频"); Toast.success(message, '下载视频')
pageData.entity.resetMenuClass(); pageData.entity.resetMenuClass()
} }
async function copyLink() async function copyLink () {
{ if (!selectedFormat) {
if (!selectedFormat) return
{
return;
} }
const info = await getVideoInfo(); const info = await getVideoInfo()
info.copyUrl(); info.copyUrl()
Toast.success("已复制链接到剪贴板.", "复制链接", 3000); Toast.success('已复制链接到剪贴板.', '复制链接', 3000)
pageData.entity.resetMenuClass(); pageData.entity.resetMenuClass()
} }
document.querySelector("#video-action-download").addEventListener("click", download); document.querySelector('#video-action-download').addEventListener('click', download)
document.querySelector("#video-action-copy").addEventListener("click", copyLink); document.querySelector('#video-action-copy').addEventListener('click', copyLink)
document.querySelector("#video-action-copy-data").addEventListener("click", async () => document.querySelector('#video-action-copy-data').addEventListener('click', async () => {
{ if (!selectedFormat) {
if (!selectedFormat) return
{
return;
} }
const info = await getVideoInfo(); const info = await getVideoInfo()
info.exportData(true); info.exportData(true)
Toast.success("已复制数据到剪贴板.", "复制数据", 3000); Toast.success('已复制数据到剪贴板.', '复制数据', 3000)
pageData.entity.resetMenuClass(); pageData.entity.resetMenuClass()
}); })
document.querySelector("#video-action-download-data").addEventListener("click", async () => document.querySelector('#video-action-download-data').addEventListener('click', async () => {
{ if (!selectedFormat) {
if (!selectedFormat) return
{
return;
} }
const info = await getVideoInfo(); const info = await getVideoInfo()
info.exportData(false); info.exportData(false)
pageData.entity.resetMenuClass(); pageData.entity.resetMenuClass()
}); })
resources.applyStyle("downloadVideoStyle"); resources.applyStyle('downloadVideoStyle')
const downloadPanel = document.querySelector(".download-video-panel"); const downloadPanel = document.querySelector('.download-video-panel')
const togglePopup = () => $(".download-video-panel").toggleClass("opened"); const togglePopup = () => $('.download-video-panel').toggleClass('opened')
document.querySelector("#download-video").addEventListener("click", e => document.querySelector('#download-video').addEventListener('click', e => {
{ if (!downloadPanel.contains(e.target)) {
if (!downloadPanel.contains(e.target)) togglePopup()
{
togglePopup();
} }
}); })
document.querySelector(".video-error").addEventListener("click", () => document.querySelector('.video-error').addEventListener('click', () => {
{ document.querySelector('.video-error').innerHTML = ''
document.querySelector(".video-error").innerHTML = ""; pageData.entity.removeError()
pageData.entity.removeError(); })
}); await SpinQuery.select('.download-video-panel')
await SpinQuery.select(".download-video-panel"); pageData.entity.addMenuClass()
pageData.entity.addMenuClass(); checkBatch()
checkBatch();
} }
export default { export default {
widget: widget:
{ {
content: resources.data.downloadVideoHtml.text, content: resources.data.downloadVideoHtml.text,
condition: loadPageData, condition: loadPageData,
success: loadWidget, success: loadWidget
}, }
}; }