// ==UserScript==
// @name Douban Info Class
// @description parse douban info
// @version 0.0.14
// @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 posterFromMeta =
doc?.querySelector("head>meta[property='og:image']")?.content || null;
const posterFromLD = ld?.image || null;
const posterFromPage =
doc?.querySelector("body #mainpic img")?.src || null;
const poster =
(posterFromMeta || posterFromLD || posterFromPage)
?.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 || null;
Object.defineProperty(this, "title", {
value: (async () => title)(),
writable: false,
});
return this.title;
})();
}
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 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 duration() {
return (async () => {
const doc = await this.subjectDoc;
const ld = await this.linkingData;
const durationFromMeta =
parseInt(
doc?.querySelector("head>meta[property='video:duration']")?.content,
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 type = await this.type;
let movieDurationFromPage = null,
episodeDurationFromPage = null;
if (type === "movie") {
let durationString = "";
let node = doc.querySelector('body span[property="v:runtime"]');
while (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 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 ld = await this.linkingData;
const doc = await this.subjectDoc;
const genreFromLD = ld?.genre || [];
const genreFromPage = [
...(doc?.querySelectorAll('body #info span[property="v:genre"]') || []),
].map((g) => g.textContent.trim());
const genre = genreFromLD || genreFromPage || [];
Object.defineProperty(this, "genre", {
value: (async () => genre)(),
writable: false,
});
return this.genre;
})();
}
}