// ==UserScript==
// @name Douban Info Class
// @description parse douban info
// @version 0.0.20
// @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 subjectPathName = "/subject/";
static timeout = 6000;
constructor(id) {
Object.defineProperties(this, {
id: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "id", {
writable: false,
enumerable: true,
value: (async () => {
return id;
})(),
});
return this.id;
},
},
pathname: {
configurable: true,
get: function () {
Object.defineProperty(this, "pathname", {
writable: false,
value: (async () => {
const pathname =
DoubanInfo.subjectPathName + (await this.id) + "/";
return pathname;
})(),
});
return this.pathname;
},
},
subjectDoc: {
configurable: true,
get: function () {
Object.defineProperty(this, "subjectDoc", {
writable: false,
value: (async () => {
const currentURL = new URL(window.location.href);
let doc;
if (
currentURL.origin === DoubanInfo.origin &&
currentURL.pathname === (await this.pathname)
) {
doc = document;
} else {
doc = new Promise(async (resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: new URL(
await this.pathname,
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;
})(),
});
return this.subjectDoc;
},
},
linkingData: {
configurable: true,
get: function () {
Object.defineProperty(this, "linkingData", {
writable: false,
value: (async () => {
const doc = await this.subjectDoc;
const ld =
dJSON.parse(
heDecode(
doc?.querySelector(
"head>script[type='application/ld+json']"
)?.textContent
)
) || null;
return ld;
})(),
});
return this.linkingData;
},
},
type: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "type", {
writable: false,
enumerable: true,
value: (async () => {
const ld = await this.linkingData;
const type = ld?.["@type"]?.toLowerCase() || null;
return type;
})(),
});
return this.type;
},
},
poster: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "poster", {
writable: false,
enumerable: true,
value: (async () => {
const doc = await this.subjectDoc;
const ld = await this.linkingData;
const posterFromPage =
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 =
(posterFromPage || posterFromMeta || posterFromLD)
?.replace("s_ratio_poster", "l_ratio_poster")
.replace(/img\d+\.doubanio\.com/, "img9.doubanio.com")
.replace(/\.webp$/i, ".jpg") || null;
return poster;
})(),
});
return this.poster;
},
},
title: {
configurable: true,
get: function () {
Object.defineProperty(this, "title", {
writable: false,
value: (async () => {
const doc = await this.subjectDoc;
const ld = await this.linkingData;
const titleFromPage =
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 = titleFromPage || titleFromMeta || titleFromLD;
return title;
})(),
});
return this.title;
},
},
year: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "year", {
writable: false,
enumerable: true,
value: (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;
})(),
});
return this.year;
},
},
chineseTitle: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "chineseTitle", {
writable: false,
enumerable: true,
value: (async () => {
const doc = await this.subjectDoc;
const chineseTitle = doc?.title?.slice(0, -5);
return chineseTitle;
})(),
});
return this.chineseTitle;
},
},
originalTitle: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "originalTitle", {
writable: false,
enumerable: true,
value: (async () => {
let originalTitle;
if (await this.isChinese) {
originalTitle = await this.chineseTitle;
} else {
originalTitle = (await this.title)
?.replace(await this.chineseTitle, "")
.trim();
}
return originalTitle;
})(),
});
return this.originalTitle;
},
},
aka: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "aka", {
writable: false,
enumerable: true,
value: (async () => {
const doc = await this.subjectDoc;
const priority = (t) =>
/\(港.?台\)/.test(t) ? 1 : /\([港台]\)/.test(t) ? 2 : 3;
const 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)) || [];
return aka;
})(),
});
return this.aka;
},
},
isChinese: {
configurable: true,
get: function () {
Object.defineProperty(this, "isChinese", {
writable: false,
value: (async () => {
let isChinese = false;
if ((await this.title) === (await this.chineseTitle)) {
isChinese = true;
}
return isChinese;
})(),
});
return this.isChinese;
},
},
region: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "region", {
writable: false,
enumerable: true,
value: (async () => {
const doc = await this.subjectDoc;
const region =
[...(doc?.querySelectorAll("body #info span.pl") || [])]
.find((n) => n.textContent.includes("制片国家/地区"))
?.nextSibling?.textContent.split("/")
.map((r) => r.trim()) || [];
return region;
})(),
});
return this.region;
},
},
language: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "language", {
writable: false,
enumerable: true,
value: (async () => {
const doc = await this.subjectDoc;
const language =
[...(doc?.querySelectorAll("body #info span.pl") || [])]
.find((n) => n.textContent.includes("语言"))
?.nextSibling?.textContent.split("/")
.map((l) => l.trim()) || [];
return language;
})(),
});
return this.language;
},
},
duration: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "duration", {
writable: false,
enumerable: true,
value: (async () => {
const doc = await this.subjectDoc;
const ld = await this.linkingData;
const type = await this.type;
let movieDurationFromPage = null,
episodeDurationFromPage = 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 === "") {
movieDurationFromPage = null;
} else {
movieDurationFromPage = durationString
.split("/")
.map((str) => {
str = str.trim();
const duration = parseInt(str || 0, 10) * 60 || null;
const type = str.match(/(?<=\().+?(?=\)$)/)?.[0] || null;
return {
duration,
type,
};
})
.filter((d) => d.duration);
}
} else if (type === "tvseries") {
episodeDurationFromPage =
parseInt(
[...(doc?.querySelectorAll("body #info span.pl") || [])]
.find((n) => n.textContent.includes("单集片长"))
?.nextSibling?.textContent.trim() || 0,
10
) * 60 || null;
}
const durationFromMeta =
parseInt(
doc?.querySelector("head>meta[property='video:duration']")
?.content || 0,
10
) || null;
const durationFromLD =
ld?.duration?.replace(
/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/,
(_, p1, p2, p3) => {
return (
parseInt(p1, 10) * 3600 +
parseInt(p2, 10) * 60 +
parseInt(p3, 10)
).toString();
}
) || null;
const duration = movieDurationFromPage
? movieDurationFromPage
: episodeDurationFromPage
? [
{
duration: episodeDurationFromPage,
type: null,
},
]
: durationFromMeta
? [{ duration: durationFromMeta, type: null }]
: durationFromLD
? [{ duration: durationFromLD, type: null }]
: [];
return duration;
})(),
});
return this.duration;
},
},
genre: {
configurable: true,
enumerable: true,
get: function () {
Object.defineProperty(this, "genre", {
writable: false,
enumerable: true,
value: (async () => {
const doc = await this.subjectDoc;
const ld = await this.linkingData;
const genreFromPage = [
...(doc?.querySelectorAll(
'body #info span[property="v:genre"]'
) || []),
].map((g) => g.textContent.trim());
const genreFromLD = ld?.genre || [];
const genre = genreFromPage || genreFromLD;
return genre;
})(),
});
return this.genre;
},
},
});
}
get info() {
return (async () => {
let info = {};
for (let key in this) {
info[key] = await this[key];
}
return info;
})();
}
}