Parse Douban Info
สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greasyfork.org/scripts/438042/1006692/Douban%20Info%20Class.js
// ==UserScript== // @name Douban Info Class // @description parse douban info // @version 0.0.51 // @author Secant(TYT@NexusHD) // config cache encryption or compression gs.config.ttl = 43200; gs.config.encrypt = true; gs.config.encrypter = (data) => LZString.compress(data); gs.config.decrypter = (encryptedString) => LZString.decompress(encryptedString); class MInfo { 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]; }, }; } async getResponseText(url, options = { headers: {} }, cacheConfig = {}) { let responseText = null; responseText = gs.get(url, cacheConfig); if (responseText) { return (async () => responseText)(); } else { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: options.headers, timeout: options.timeout, onload: (resp) => { const { status, statusText, responseText } = resp; if (status === 200) { resolve(responseText); gs.set(url, responseText, cacheConfig); } else { console.warn(statusText); resolve(null); } }, ontimeout: (e) => { console.warn(e); resolve(null); }, onerror: (e) => { console.warn(e); resolve(null); }, }); }); } } flushCache(force = false) { gs.flush(force); } } class DoubanInfo extends MInfo { static origin = "https://movie.douban.com"; static timeout = 6000; static cacheConfig = { ttl: 43200, encrypt: true, encrypter: (data) => LZString.compress(data), decrypter: (encryptedString) => LZString.decompress(encryptedString), }; constructor(id) { super(); // define promised lazy getters 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 = null; if ( currentURL.origin === DoubanInfo.origin && currentURL.pathname === (await this.subjectPathname) ) { doc = document; } else { const url = new URL( await this.subjectPathname, DoubanInfo.origin ).toString(); const options = { headers: { referrer: DoubanInfo.origin, }, timeout: DoubanInfo.timeout, }; const cacheConfig = DoubanInfo.cacheConfig; const responseText = await this.getResponseText( url, options, cacheConfig ); if (responseText) { try { doc = new DOMParser().parseFromString( responseText, "text/html" ); } catch (e) { console.warn(e); } } else { console.warn("no response text"); } } return doc; }, "subjectDoc", false ), awardDoc: this.promisedGetterLazify( async () => { const currentURL = new URL(window.location.href); let doc = null; if ( currentURL.origin === DoubanInfo.origin && currentURL.pathname === (await this.awardPathname) ) { doc = document; } else { const url = new URL( await this.awardPathname, DoubanInfo.origin ).toString(); const options = { headers: { referrer: DoubanInfo.origin, }, timeout: DoubanInfo.timeout, }; const cacheConfig = DoubanInfo.cacheConfig; const responseText = await this.getResponseText( url, options, cacheConfig ); if (responseText) { try { doc = new DOMParser().parseFromString( responseText, "text/html" ); } catch (e) { console.warn(e); } } else { console.warn("no response text"); } } return doc; }, "awardDoc", false ), celebrityDoc: this.promisedGetterLazify( async () => { const currentURL = new URL(window.location.href); let doc = null; if ( currentURL.origin === DoubanInfo.origin && currentURL.pathname === (await this.celebrityPathname) ) { doc = document; } else { const url = new URL( await this.celebrityPathname, DoubanInfo.origin ).toString(); const options = { headers: { referrer: DoubanInfo.origin, }, timeout: DoubanInfo.timeout, }; const cacheConfig = DoubanInfo.cacheConfig; const responseText = await this.getResponseText( url, options, cacheConfig ); if (responseText) { try { doc = new DOMParser().parseFromString( responseText, "text/html" ); } catch (e) { console.warn(e); } } else { console.warn("no response text"); } } return doc; }, "celebrityDoc", false ), linkingData: this.promisedGetterLazify( async () => { const doc = await this.subjectDoc; const ld = dirtyJson.parse( htmlEntities.decode( 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(); const 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"), }); } } class IMDbInfo extends MInfo { static originalOrigin = "https://www.imdb.com"; static originalPluginOrigin = "http://p.media-imdb.com"; static proxyOrigin = "https://proxy.secant.workers.dev"; static isProxified = true; static origin = IMDbInfo.isProxified ? IMDbInfo.proxyOrigin : IMDbInfo.originalOrigin; static pluginOrigin = IMDbInfo.isProxified ? IMDbInfo.proxyOrigin : IMDbInfo.originalPluginOrigin; static timeout = 6000; static cacheConfig = { ttl: 43200, encrypt: true, encrypter: (data) => LZString.compress(data), decrypter: (encryptedString) => LZString.decompress(encryptedString), }; constructor(id) { super(); // define promised lazy getters Object.defineProperties(this, { id: this.promisedGetterLazify(async () => { return id; }, "id"), titlePathname: this.promisedGetterLazify( async () => { let titlePathname; if (IMDbInfo.isProxified) { titlePathname = `/worker/proxy/www.imdb.com/title/tt${await this .id}/`; } else { titlePathname = `/title/tt${await this.id}/`; } return titlePathname; }, "titlePathname", false ), releaseInfoPathname: this.promisedGetterLazify( async () => { let releaseInfoPathname; if (IMDbInfo.isProxified) { releaseInfoPathname = `/worker/proxy/www.imdb.com/title/tt${await this .id}/releaseinfo`; } else { releaseInfoPathname = `/title/tt${await this.id}/releaseinfo`; } return releaseInfoPathname; }, "releaseInfoPathname", false ), pluginPathname: this.promisedGetterLazify( async () => { let pluginPathname; if (IMDbInfo.isProxified) { pluginPathname = `/worker/proxy/p.media-imdb.com/static-content/documents/v1/title/tt${await this .id}/ratings%253Fjsonp=imdb.rating.run:imdb.api.title.ratings/data.json`; } else { pluginPathname = `/static-content/documents/v1/title/tt${await this .id}/ratings%3Fjsonp=imdb.rating.run:imdb.api.title.ratings/data.json`; } return pluginPathname; }, "pluginPathname", false ), titleDoc: this.promisedGetterLazify( async () => { const currentURL = new URL(window.location.href); let doc = null; if ( currentURL.origin === IMDbInfo.origin && currentURL.pathname === (await this.titlePathname) ) { doc = document; } else { const url = new URL( await this.titlePathname, IMDbInfo.origin ).toString(); const options = { headers: { referrer: IMDbInfo.origin, }, timeout: IMDbInfo.timeout, }; const cacheConfig = IMDbInfo.cacheConfig; const responseText = await this.getResponseText( url, options, cacheConfig ); if (responseText) { try { doc = new DOMParser().parseFromString( responseText, "text/html" ); } catch (e) { console.warn(e); } } else { console.warn("no response text"); } } return doc; }, "titleDoc", false ), releaseInfoDoc: this.promisedGetterLazify( async () => { const currentURL = new URL(window.location.href); let doc = null; if ( currentURL.origin === IMDbInfo.origin && currentURL.pathname === (await this.releaseInfoPathname) ) { doc = document; } else { const url = new URL( await this.releaseInfoPathname, IMDbInfo.origin ).toString(); const options = { headers: { referrer: IMDbInfo.origin, }, timeout: IMDbInfo.timeout, }; const cacheConfig = IMDbInfo.cacheConfig; const responseText = await this.getResponseText( url, options, cacheConfig ); if (responseText) { try { doc = new DOMParser().parseFromString( responseText, "text/html" ); } catch (e) { console.warn(e); } } else { console.warn("no response text"); } } return doc; }, "titleDoc", false ), pluginResponseText: this.promisedGetterLazify( async () => { let responseText = null; const url = new URL( await this.pluginPathname, IMDbInfo.pluginOrigin ).toString(); const options = { headers: { referrer: IMDbInfo.pluginOrigin, }, timeout: IMDbInfo.timeout, }; const cacheConfig = IMDbInfo.cacheConfig; const pluginResponseText = await this.getResponseText( url, options, cacheConfig ); if (!pluginResponseText) { console.warn("no response text"); } return pluginResponseText; }, "pluginResponseText", false ), linkingData: this.promisedGetterLazify( async () => { const doc = await this.titleDoc; const ld = dirtyJson.parse( htmlEntities.decode( doc?.querySelector("head>script[type='application/ld+json']") ?.textContent ) ) || null; return ld; }, "linkingData", false ), }); } } class MtimeInfo extends MInfo { constructor(id) { super(); // define promised lazy getters Object.defineProperties(this, { id: this.promisedGetterLazify(async () => { return id; }, "id"), }); } } 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() || null; if (ch === null) { en = en.match(splitEnEnRegExp)?.groups.en || en; } return { ch: ch ? ch.trim() : ch, en: en ? en.trim() : en, }; }