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;
};
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.

View File

@ -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": {

View File

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

View File

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

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
{
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
}
}

View File

@ -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
}
let formats = []
let selectedFormat = null
class Video {
constructor () {
this.menuClasses = ['quality', 'action', 'progress']
this.currentMenuClass = 'quality'
}
get menuPanel()
{
return document.querySelector(".download-video-panel");
get menuPanel () {
return document.querySelector('.download-video-panel')
}
addMenuClass()
{
this.menuPanel.classList.remove(...this.menuClasses);
this.menuPanel.classList.add(this.currentMenuClass);
return this.currentMenuClass;
addMenuClass () {
this.menuPanel.classList.remove(...this.menuClasses)
this.menuPanel.classList.add(this.currentMenuClass)
return this.currentMenuClass
}
resetMenuClass()
{
[this.currentMenuClass] = this.menuClasses;
this.addMenuClass();
resetMenuClass () {
[this.currentMenuClass] = this.menuClasses
this.addMenuClass()
}
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;
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
}
addError()
{
this.menuPanel.classList.add("error");
addError () {
this.menuPanel.classList.add('error')
}
removeError()
{
this.menuPanel.classList.remove("error");
this.resetMenuClass();
removeError () {
this.menuPanel.classList.remove('error')
this.resetMenuClass()
}
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`;
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;
}
catch (error)
{
return false;
}
return Boolean(aid && cid);
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, "&quot;")}">再次保存</a>`;
Toast.success(message, "下载视频");
pageData.entity.resetMenuClass();
const message = `下载完成. <a class="link" href="${result.url}" download="${result.filename.replace(/"/g, '&quot;')}">再次保存</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
}
}