// ==UserScript==
// @name Douban Info Class
// @description parse douban info
// @version 0.0.27
// @author Secant(TYT@NexusHD)
// @icon https://movie.douban.com/favicon.ico
// @contributionURL https://i.loli.net/2020/02/28/JPGgHc3UMwXedhv.jpg
// @contributionAmount 10
// @namespace https://greasyfork.org/users/152136
// @grant GM_xmlhttpRequest
// @connect movie.douban.com
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
),
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 = new Promise(async (resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: new URL(
await this.subjectPathname,
DoubanInfo.origin
).toString(),
headers: {
referrer: DoubanInfo.origin,
},
timout: DoubanInfo.timeout,
onload: (resp) => {
try {
resolve(
new DOMParser().parseFromString(
resp.responseText,
"text/html"
)
);
} catch (err) {
console.warn(err);
resolve(null);
}
},
ontimeout: (e) => {
console.warn(e);
resolve(null);
},
onerror: (e) => {
console.warn(e);
resolve(null);
},
});
});
}
return doc;
},
"subjectDoc",
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 duration = parseInt(str || 0, 10) * 60 || null;
const whereabouts = str.match(/(?<=\().+?(?=\)$)/)?.[0] || 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) => ({
date: new Date(e.textContent.trim()),
whereabouts: e.textContent.match(/(?<=\().+?(?=\)$)/)?.[0] || null,
}))
.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 ratingCountFromDoc =
parseInt(
doc?.querySelector('body #interest_sectl [property="v:votes"]')
?.textContent || 0,
10
) || null;
let ratingValueFromDoc =
parseFloat(
doc?.querySelector('body #interest_sectl [property="v:average"]')
?.textContent || 0
) || null;
if (ratingCountFromDoc && ratingValueFromDoc) {
ratingFromDoc = {
ratingCount: ratingCountFromDoc,
ratingValue: ratingValueFromDoc,
bestRating: 10,
};
}
const ld = await this.linkingData;
let ratingFromLD = null;
let ratingCountFromLD =
parseInt(ld?.aggregateRating?.ratingCount || 0, 10) || null;
let ratingValueFromLD =
parseFloat(ld?.aggregateRating?.ratingValue || 0) || null;
if (ratingCountFromLD && ratingValueFromLD) {
ratingFromLD = {
ratingCount: ratingCountFromLD,
ratingValue: ratingValueFromLD,
bestRating: 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"),
});
}
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];
},
};
}
}