您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Parse Douban Info
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/438042/1006347/Douban%20Info%20Class.js
// ==UserScript== // @name Douban Info Class // @description parse douban info // @version 0.0.38 // @author Secant(TYT@NexusHD) class DoubanInfo { static origin = "https://movie.douban.com"; static timeout = 6000; constructor(id) { Object.defineProperties(this, { id: this.promisedGetterLazify(async () => { return id; }, "id"), subjectPathname: this.promisedGetterLazify( async () => { const subjectPathname = "/subject/" + (await this.id) + "/"; return subjectPathname; }, "subjectPathname", false ), awardPathname: this.promisedGetterLazify( async () => { const awardPathname = "/subject/" + (await this.id) + "/awards/"; return awardPathname; }, "awardPathname", false ), celebrityPathname: this.promisedGetterLazify( async () => { const celebrityPathname = "/subject/" + (await this.id) + "/celebrities"; return celebrityPathname; }, "celebrityPathname", false ), subjectDoc: this.promisedGetterLazify( async () => { const currentURL = new URL(window.location.href); let doc; if ( currentURL.origin === DoubanInfo.origin && currentURL.pathname === (await this.subjectPathname) ) { doc = document; } else { doc = await this.getDoc( new URL(await this.subjectPathname, DoubanInfo.origin).toString(), { headers: { referrer: DoubanInfo.origin, }, } ); } return doc; }, "subjectDoc", false ), awardDoc: this.promisedGetterLazify( async () => { const currentURL = new URL(window.location.href); let doc; if ( currentURL.origin === DoubanInfo.origin && currentURL.pathname === (await this.awardPathname) ) { doc = document; } else { doc = await this.getDoc( new URL(await this.awardPathname, DoubanInfo.origin).toString(), { headers: { referrer: DoubanInfo.origin, }, } ); } return doc; }, "awardDoc", false ), celebrityDoc: this.promisedGetterLazify( async () => { const currentURL = new URL(window.location.href); let doc; if ( currentURL.origin === DoubanInfo.origin && currentURL.pathname === (await this.celebrityPathname) ) { doc = document; } else { doc = await this.getDoc( new URL( await this.celebrityPathname, DoubanInfo.origin ).toString(), { headers: { referrer: DoubanInfo.origin, }, } ); } return doc; }, "celebrityDoc", false ), linkingData: this.promisedGetterLazify( async () => { const doc = await this.subjectDoc; const ld = dJSON.parse( heDecode( doc?.querySelector("head>script[type='application/ld+json']") ?.textContent ) ) || null; return ld; }, "linkingData", false ), type: this.promisedGetterLazify(async () => { const ld = await this.linkingData; const type = ld?.["@type"]?.toLowerCase() || null; return type; }, "type"), poster: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; const ld = await this.linkingData; const posterFromDoc = doc?.querySelector("body #mainpic img")?.src || null; const posterFromMeta = doc?.querySelector("head>meta[property='og:image']")?.content || null; const posterFromLD = ld?.image || null; const poster = (posterFromDoc || posterFromMeta || posterFromLD) ?.replace("s_ratio_poster", "l_ratio_poster") .replace(/img\d+\.doubanio\.com/, "img9.doubanio.com") .replace(/\.webp$/i, ".jpg") || null; return poster; }, "poster"), title: this.promisedGetterLazify( async () => { const doc = await this.subjectDoc; const ld = await this.linkingData; const titleFromDoc = doc?.querySelector("body #content h1>span[property]") ?.textContent || null; const titleFromMeta = doc?.querySelector("head>meta[property='og:title']")?.content || null; const titleFromLD = ld?.name || null; const title = titleFromDoc || titleFromMeta || titleFromLD; return title; }, "title", false ), year: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; const year = parseInt( doc ?.querySelector("body #content>h1>span.year") ?.textContent.slice(1, -1) || 0, 10 ) || null; return year; }, "year"), chineseTitle: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; const chineseTitle = doc?.title?.slice(0, -5); return chineseTitle; }, "chineseTitle"), originalTitle: this.promisedGetterLazify(async () => { let originalTitle; if (await this.isChinese) { originalTitle = await this.chineseTitle; } else { originalTitle = (await this.title) ?.replace(await this.chineseTitle, "") .trim(); } return originalTitle; }, "originalTitle"), aka: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; const priority = (t) => /\(港.?台\)/.test(t) ? 1 : /\((?:[港台]|香港|台湾)\)/.test(t) ? 2 : 3; let aka = [...(doc?.querySelectorAll("body #info span.pl") || [])] .find((n) => n.textContent.includes("又名")) ?.nextSibling?.textContent.split("/") .map((t) => t.trim()) .sort((t1, t2) => priority(t1) - priority(t2)) || []; if (aka.length === 0) { aka = null; } return aka; }, "aka"), isChinese: this.promisedGetterLazify( async () => { let isChinese = false; if ((await this.title) === (await this.chineseTitle)) { isChinese = true; } return isChinese; }, "isChinese", false ), region: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; let region = [...(doc?.querySelectorAll("body #info span.pl") || [])] .find((n) => n.textContent.includes("制片国家/地区")) ?.nextSibling?.textContent.split("/") .map((r) => r.trim()) || []; if (region.length === 0) { region = null; } return region; }, "region"), language: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; let language = [...(doc?.querySelectorAll("body #info span.pl") || [])] .find((n) => n.textContent.includes("语言")) ?.nextSibling?.textContent.split("/") .map((l) => l.trim()) || []; if (language.length === 0) { language = null; } return language; }, "language"), genre: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; const ld = await this.linkingData; let genreFromDoc = [ ...(doc?.querySelectorAll('body #info span[property="v:genre"]') || []), ].map((g) => g.textContent.trim()); if (genreFromDoc.length === 0) { genreFromDoc = null; } let genreFromLD = ld?.genre || []; if (genreFromLD.length === 0) { genreFromLD = null; } const genre = genreFromDoc || genreFromLD; return genre; }, "genre"), duration: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; const ld = await this.linkingData; const type = await this.type; let movieDurationFromDoc = null, episodeDurationFromDoc = null; if (type === "movie") { let durationString = ""; let node = doc?.querySelector('body span[property="v:runtime"]') || null; while (node && node.nodeName !== "BR") { durationString += node.textContent; node = node.nextSibling; } if (durationString !== "") { movieDurationFromDoc = durationString .split("/") .map((str) => { str = str.trim(); strOI = splitOI(str); const duration = parseInt(strOI.o || 0, 10) * 60 || null; const whereabouts = strOI.i || null; return { duration, whereabouts, }; }) .filter((d) => d.duration); if (movieDurationFromDoc.length === 0) { movieDurationFromDoc = null; } } } else if (type === "tvseries") { const episodeDurationSecondsFromDoc = parseInt( [...(doc?.querySelectorAll("body #info span.pl") || [])] .find((n) => n.textContent.includes("单集片长")) ?.nextSibling?.textContent.trim() || 0, 10 ) * 60 || null; if (episodeDurationSecondsFromDoc) { episodeDurationFromDoc = [ { duration: episodeDurationSecondsFromDoc, whereabouts: null, }, ]; } } let durationFromMeta = null; const durationSecondsFromMeta = parseInt( doc?.querySelector("head>meta[property='video:duration']") ?.content || 0, 10 ) || null; if (durationSecondsFromMeta) { durationFromMeta = [ { duration: durationSecondsFromMeta, whereabouts: null, }, ]; } let durationFromLD = null; const durationSecondsFromLD = parseInt( ld?.duration?.replace( /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/, (_, p1, p2, p3) => { return ( parseInt(p1 || 0, 10) * 3600 + parseInt(p2 || 0, 10) * 60 + parseInt(p3 || 0, 10) ).toString(); } ) || 0, 10 ) || null; if (durationSecondsFromLD) { durationFromLD = [ { duration: durationSecondsFromLD, whereabouts: null, }, ]; } const duration = movieDurationFromDoc || episodeDurationFromDoc || durationFromMeta || durationFromLD; return duration; }, "duration"), datePublished: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; const ld = await this.linkingData; let datePublishedFromDoc = [ ...(doc?.querySelectorAll( 'body #info span[property="v:initialReleaseDate"]' ) || []), ] .map((e) => { const str = e.textContent.trim(); const strOI = splitOI(str); if (!strOI.o) { return null; } else { return { date: new Date(strOI.o), whereabouts: strOI.i || null, }; } }) .filter((e) => !!e) .sort((d1, d2) => { d1.date - d2.date; }); if (datePublishedFromDoc.length === 0) { datePublishedFromDoc = null; } const datePublishedStringFromLD = ld?.datePublished || null; let datePublishedFromLD = null; if (datePublishedStringFromLD) { datePublishedFromLD = [ { date: new Date(datePublishedStringFromLD), whereabouts: null }, ]; } const datePublished = datePublishedFromDoc || datePublishedFromLD; return datePublished; }, "datePublished"), episodeCount: this.promisedGetterLazify(async () => { if ((await this.type) === "tvseries") { const doc = await this.subjectDoc; const episodeCount = parseInt( [...(doc?.querySelectorAll("body #info span.pl") || [])] .find((n) => n.textContent.includes("集数")) ?.nextSibling?.textContent.trim() || 0, 10 ) || null; return episodeCount; } else { return null; } }, "episodeCount"), tag: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; let tag = [ ...(doc?.querySelectorAll("body div.tags-body>a") || []), ].map((t) => t.textContent); if (tag.length === 0) { tag = null; } return tag; }, "tag"), rating: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; let ratingFromDoc = null; let countFromDoc = parseInt( doc?.querySelector('body #interest_sectl [property="v:votes"]') ?.textContent || 0, 10 ) || null; let valueFromDoc = parseFloat( doc?.querySelector('body #interest_sectl [property="v:average"]') ?.textContent || 0 ) || null; if (countFromDoc && valueFromDoc) { ratingFromDoc = { count: countFromDoc, value: valueFromDoc, max: 10, }; } const ld = await this.linkingData; let ratingFromLD = null; let countFromLD = parseInt(ld?.aggregateRating?.ratingCount || 0, 10) || null; let valueFromLD = parseFloat(ld?.aggregateRating?.ratingValue || 0) || null; if (countFromLD && valueFromLD) { ratingFromLD = { count: countFromLD, value: valueFromLD, max: 10, }; } const rating = ratingFromDoc || ratingFromLD; return rating; }, "rating"), description: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; const ld = await this.linkingData; const descriptionFromDoc = [ ...(doc?.querySelector( 'body #link-report>[property="v:summary"],body #link-report>span.all.hidden' )?.childNodes || []), ] .filter((e) => e.nodeType === 3) .map((e) => e.textContent.trim()) .join("\n") || null; const descriptionFromMeta = doc?.querySelector("head>meta[property='og:description']")?.content || null; const descriptionFromLD = ld?.description || null; const description = descriptionFromDoc || descriptionFromMeta || descriptionFromLD; return description; }, "description"), imdbId: this.promisedGetterLazify(async () => { const doc = await this.subjectDoc; let imdbId = null; if ( doc?.querySelector("body #season option:checked")?.textContent !== "1" || false ) { const doubanId = doc.querySelector("body #season option:first-of-type")?.value || null; if (doubanId) { const firstSeasonDoubanInfo = new DoubanInfo(doubanId); imdbId = await firstSeasonDoubanInfo.imdbId; } } else { imdbId = [...(doc?.querySelectorAll("body #info span.pl") || [])] .find((n) => n.textContent.includes("IMDb:")) ?.nextSibling?.textContent.match(/tt(\d+)/)?.[1] || null; } return imdbId; }, "imdbId"), awardData: this.promisedGetterLazify(async () => { const doc = await this.awardDoc; let awardData = [...(doc?.querySelectorAll("body div.awards") || [])] .map((awardNode) => { const event = awardNode?.querySelector(".hd>h2 a")?.textContent.trim() || null; const year = parseInt( awardNode ?.querySelector(".hd>h2 .year") ?.textContent.match(/\d+/)?.[0] || 0, 10 ) || null; let award = [...(awardNode?.querySelectorAll(".award") || [])] .map((a) => { const name = a.querySelector("li:first-of-type")?.textContent.trim() || null; let recipient = a .querySelector("li:nth-of-type(2)") ?.textContent.split("/") .map((p) => p.trim() || null) .filter((p) => !!p); if (recipient.length === 0) { recipient = null; } if (name) { return { name, recipient, }; } else { return null; } }) .filter((a) => !!a); if (award.length === 0) { award = null; } if (event) { return { event, year, award, }; } else { return null; } }) .filter((a) => !!a); if (awardData.length === 0) { awardData = null; } return awardData; }, "awardData"), celebrityData: this.promisedGetterLazify(async () => { const doc = await this.celebrityDoc; let celebrityData = [ ...(doc?.querySelectorAll("body #celebrities>div.list-wrapper") || []), ] .map((o) => { const occupation = o.querySelector("h2")?.textContent.trim() || null; let occupationCh = null; let occupationEn = null; if (occupation) { const occupationSplitted = splitChEn(occupation); occupationCh = occupationSplitted.ch; occupationEn = occupationSplitted.en; } const celebrities = [...(o.querySelectorAll("li.celebrity") || [])] .map((c) => { const name = c.querySelector(".info>.name")?.textContent.trim() || null; let nameCh = null; let nameEn = null; if (name) { const nameSplitted = splitChEn(name); nameCh = nameSplitted.ch; nameEn = nameSplitted.en; } const creditAndAttribute = c.querySelector(".info>.role")?.textContent.trim() || null; let credit = null; let attribute = null; let creditCh = null; let creditEn = null; if (creditAndAttribute) { const creditAndAttributeSplitted = splitOI(creditAndAttribute); credit = creditAndAttributeSplitted.o; attribute = creditAndAttributeSplitted.i; if (credit) { const creditSplitted = splitChEn(credit); creditCh = creditSplitted.ch; creditEn = creditSplitted.en; } } if (!credit && occupation) { credit = occupation; creditCh = occupationCh; creditEn = occupationEn; } if (!occupation && !name && !credit && !attribute) { return null; } else { return { occupation: { value: occupation, ch: occupationCh, en: occupationEn, }, name: { value: name, ch: nameCh, en: nameEn, }, credit: { value: credit, ch: creditCh, en: creditEn, }, attribute: { value: attribute, }, }; } }) .filter((c) => !!c); return celebrities; }) .flat(); if (celebrityData.length === 0) { celebrityData = null; } return celebrityData; }, "celebrityData"), }); } get info() { return (async () => { let info = {}; for (let key in this) { info[key] = await this[key]; } return info; })(); } promisedGetterLazify(fun, propertyName, isEnumarable = true) { return { configurable: true, enumerable: isEnumarable, get: function () { Object.defineProperty(this, propertyName, { writable: false, enumerable: isEnumarable, value: fun(), }); return this[propertyName]; }, }; } getDoc(url, { headers = {} } = {}) { let doc = null; const responseText = gs.get(url); if (responseText) { doc = (async () => new DOMParser().parseFromString(responseText, "text/html"))(); } else { doc = new Promise(async (resolve) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: headers, timout: DoubanInfo.timeout, onload: (resp) => { try { const { status, statusText, responseText } = resp; if (status === 200) { resolve( new DOMParser().parseFromString(responseText, "text/html") ); gs.set(url, responseText, { ttl: 30 }); } else { console.warn(statusText); resolve(null); } } catch (err) { console.warn(err); resolve(null); } }, ontimeout: (e) => { console.warn(e); resolve(null); }, onerror: (e) => { console.warn(e); resolve(null); }, }); }); } return doc; } } function splitOI(word) { word = word.trim(); const splitOIRegExp = /^(?<o>.*?)(?:\((?<i>[^\(]*?)\))?$/; let { o = null, i = null } = word.match(splitOIRegExp)?.groups || {}; return { o: o ? o.trim() : o, i: i ? i.trim() : i, }; } function splitChEn(word) { word = word.trim(); const splitChEnRegExp = /^(?<ch>.*\p{Script=Han}[^\s\p{Script=Han}]*)(?: +(?<en1>[^\p{Script=Han}]*))?$|^(?<en2>[^\p{Script=Han}]*)$/u; const splitEnEnRegExp = /^(?<en>[^\p{Script=Han}]*?) +\k<en>$/u; let { ch = null, en1 = null, en2 = null, } = word.match(splitChEnRegExp)?.groups || {}; let en = (en1 || en2).trim(); if (ch === null) { en = en.match(splitEnEnRegExp)?.groups.en || en; } return { ch: ch ? ch.trim() : ch, en: en ? en.trim() : en, }; }