// ==UserScript==
// @name Douban Info Class
// @description parse douban info
// @version 0.0.41
// @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 = null;
if (
currentURL.origin === DoubanInfo.origin &&
currentURL.pathname === (await this.subjectPathname)
) {
doc = document;
} else {
const responseText = await this.getResponseText(
new URL(await this.subjectPathname, DoubanInfo.origin).toString(),
{
headers: {
referrer: DoubanInfo.origin,
},
}
);
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 responseText = await this.getResponseText(
new URL(await this.awardPathname, DoubanInfo.origin).toString(),
{
headers: {
referrer: DoubanInfo.origin,
},
}
);
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 responseText = await this.getResponseText(
new URL(
await this.celebrityPathname,
DoubanInfo.origin
).toString(),
{
headers: {
referrer: DoubanInfo.origin,
},
}
);
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 =
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();
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"),
});
}
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, init = { headers: {} }, ttl = 43200) {
let responseText = null;
responseText = gs.get(url);
if (responseText) {
return (async () => responseText)();
} else {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: init.headers,
timout: DoubanInfo.timeout,
onload: (resp) => {
const { status, statusText, responseText } = resp;
if (status === 200) {
resolve(responseText);
gs.set(url, responseText, { ttl });
} else {
console.warn(statusText);
resolve(null);
}
},
ontimeout: (e) => {
console.warn(e);
resolve(null);
},
onerror: (e) => {
console.warn(e);
resolve(null);
},
});
});
}
}
}
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,
};
}