Greasy Fork

Douban Info Class

Parse Douban Info

目前为 2022-01-05 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.icu/scripts/438042/1005555/Douban%20Info%20Class.js

// ==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;
    })();
  }
}