Greasy Fork

Douban Info Class

Parse Douban Info

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

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

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