Greasy Fork

Douban Info Class

Parse Douban Info

当前为 2022-01-05 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/438042/1005550/Douban%20Info%20Class.js

// ==UserScript==
// @name               Douban Info Class
// @description        parse douban info
// @version            0.0.18
// @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,
    });
    */
    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 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?.querySelectorAll("body #info span.pl") || [])]
          .find((n) => n.textContent.includes("又名"))
          ?.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?.querySelectorAll("body #info span.pl") || [])]
          .find((n) => n.textContent.includes("制片国家/地区"))
          ?.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?.querySelectorAll("body #info span.pl") || [])]
          .find((n) => n.textContent.includes("语言"))
          ?.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;
    })();
  }
  */
}