// ==UserScript==
// @name Douban Info Class
// @description parse douban info
// @version 0.0.15
// @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.defineProperty(this, "id", {
value: (async () => id)(),
writable: false,
});
}
get pathname() {
return (async () => {
const pathname = DoubanInfo.subjectPathName + (await this.id) + "/";
Object.defineProperty(this, "pathname", {
value: (async () => pathname)(),
writable: false,
});
return this.pathname;
})();
}
get subjectDoc() {
return (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);
},
});
});
}
Object.defineProperty(this, "subjectDoc", {
value: (async () => doc)(),
writable: false,
});
return this.subjectDoc;
})();
}
get linkingData() {
return (async () => {
const doc = await this.subjectDoc;
const ldJSON =
dJSON.parse(
heDecode(
doc?.querySelector("head>script[type='application/ld+json']")
?.textContent
)
) || null;
Object.defineProperty(this, "linkingData", {
value: (async () => ldJSON)(),
writable: false,
});
return this.linkingData;
})();
}
get type() {
return (async () => {
const ld = await this.linkingData;
const type = ld?.["@type"]?.toLowerCase() || null;
Object.defineProperty(this, "type", {
value: (async () => type)(),
writable: false,
});
return this.type;
})();
}
get poster() {
return (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;
Object.defineProperty(this, "poster", {
value: (async () => poster)(),
writable: false,
});
return this.poster;
})();
}
get title() {
return (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;
Object.defineProperty(this, "title", {
value: (async () => title)(),
writable: false,
});
return this.title;
})();
}
get year() {
return (async () => {
const doc = await this.subjectDoc;
const year =
parseInt(
doc
?.querySelector("body #content>h1>span.year")
?.textContent.slice(1, -1) || 0,
10
) || null;
Object.defineProperty(this, "year", {
value: (async () => year)(),
writable: false,
});
return this.year;
})();
}
get chineseTitle() {
return (async () => {
const doc = await this.subjectDoc;
const chineseTitle = doc?.title?.slice(0, -5);
Object.defineProperty(this, "chineseTitle", {
value: (async () => chineseTitle)(),
writable: false,
});
return this.chineseTitle;
})();
}
get originalTitle() {
return (async () => {
let originalTitle;
if (await this.isChinese) {
originalTitle = await this.chineseTitle;
} else {
originalTitle = (await this.title)
?.replace(await this.chineseTitle, "")
.trim();
}
Object.defineProperty(this, "originalTitle", {
value: (async () => originalTitle)(),
writable: false,
});
return this.originalTitle;
})();
}
get aka() {
return (async () => {
const doc = await this.subjectDoc;
const priority = (t) =>
/\(港.?台\)/.test(t) ? 1 : /\([港台]\)/.test(t) ? 2 : 3;
const aka =
doc
?.querySelector('body #info span.pl:contains("又名")')
?.nextSibling?.textContent.split("/")
.map((t) => t.trim())
.sort((t1, t2) => priority(t1) - priority(t2)) || [];
Object.defineProperty(this, "aka", {
value: (async () => aka)(),
writable: false,
});
return this.aka;
})();
}
get isChinese() {
return (async () => {
let isChinese = false;
if ((await this.title) === (await this.chineseTitle)) {
isChinese = true;
}
Object.defineProperty(this, "isChinese", {
value: (async () => isChinese)(),
writable: false,
});
return this.isChinese;
})();
}
get region() {
return (async () => {
const doc = await this.subjectDoc;
const region =
doc
?.querySelector('body #info span.pl:contains("制片国家/地区")')
?.nextSibling?.textContent.split("/")
.map((r) => r.trim()) || [];
Object.defineProperty(this, "region", {
value: (async () => region)(),
writable: false,
});
return this.region;
})();
}
get language() {
return (async () => {
const doc = await this.subjectDoc;
const language =
doc
?.querySelector('body #info span.pl:contains("语言")')
?.nextSibling?.textContent.split("/")
.map((l) => l.trim()) || [];
Object.defineProperty(this, "language", {
value: (async () => language)(),
writable: false,
});
return this.language;
})();
}
get duration() {
return (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 }]
: [];
Object.defineProperty(this, "duration", {
value: (async () => duration)(),
writable: false,
});
return this.duration;
})();
}
get genre() {
return (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;
Object.defineProperty(this, "genre", {
value: (async () => genre)(),
writable: false,
});
return this.genre;
})();
}
}