Greasy Fork

Douban Info Class

Parse Douban Info

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

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

// ==UserScript==
// @name               Douban Info Class
// @description        parse douban info
// @version            0.0.24
// @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 timeout = 6000;

  constructor(id) {
    Object.defineProperties(this, {
      id: this.promisedGetterLazify(async () => {
        return id;
      }, "id"),
      subjectPathname: this.promisedGetterLazify(
        async () => {
          const subjectPathname = "/subject/" + (await this.id) + "/";
          return subjectPathname;
        },
        "subjectPathname",
        false
      ),
      subjectDoc: this.promisedGetterLazify(
        async () => {
          const currentURL = new URL(window.location.href);
          let doc;
          if (
            currentURL.origin === DoubanInfo.origin &&
            currentURL.pathname === (await this.subjectPathname)
          ) {
            doc = document;
          } else {
            doc = new Promise(async (resolve) => {
              GM_xmlhttpRequest({
                method: "GET",
                url: new URL(
                  await this.subjectPathname,
                  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;
        },
        "subjectDoc",
        false
      ),
      linkingData: this.promisedGetterLazify(
        async () => {
          const doc = await this.subjectDoc;
          const ld =
            dJSON.parse(
              heDecode(
                doc?.querySelector("head>script[type='application/ld+json']")
                  ?.textContent
              )
            ) || null;
          return ld;
        },
        "linkingData",
        false
      ),
      type: this.promisedGetterLazify(async () => {
        const ld = await this.linkingData;
        const type = ld?.["@type"]?.toLowerCase() || null;
        return type;
      }, "type"),
      poster: this.promisedGetterLazify(async () => {
        const doc = await this.subjectDoc;
        const ld = await this.linkingData;
        const posterFromDoc =
          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 =
          (posterFromDoc || posterFromMeta || posterFromLD)
            ?.replace("s_ratio_poster", "l_ratio_poster")
            .replace(/img\d+\.doubanio\.com/, "img9.doubanio.com")
            .replace(/\.webp$/i, ".jpg") || null;
        return poster;
      }, "poster"),
      title: this.promisedGetterLazify(
        async () => {
          const doc = await this.subjectDoc;
          const ld = await this.linkingData;
          const titleFromDoc =
            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 = titleFromDoc || titleFromMeta || titleFromLD;
          return title;
        },
        "title",
        false
      ),
      year: this.promisedGetterLazify(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;
      }, "year"),
      chineseTitle: this.promisedGetterLazify(async () => {
        const doc = await this.subjectDoc;
        const chineseTitle = doc?.title?.slice(0, -5);
        return chineseTitle;
      }, "chineseTitle"),
      originalTitle: this.promisedGetterLazify(async () => {
        let originalTitle;
        if (await this.isChinese) {
          originalTitle = await this.chineseTitle;
        } else {
          originalTitle = (await this.title)
            ?.replace(await this.chineseTitle, "")
            .trim();
        }
        return originalTitle;
      }, "originalTitle"),
      aka: this.promisedGetterLazify(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;
      }, "aka"),
      isChinese: this.promisedGetterLazify(
        async () => {
          let isChinese = false;
          if ((await this.title) === (await this.chineseTitle)) {
            isChinese = true;
          }
          return isChinese;
        },
        "isChinese",
        false
      ),
      region: this.promisedGetterLazify(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;
      }, "region"),
      language: this.promisedGetterLazify(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;
      }, "language"),
      genre: this.promisedGetterLazify(async () => {
        const doc = await this.subjectDoc;
        const ld = await this.linkingData;
        const genreFromDoc = [
          ...(doc?.querySelectorAll('body #info span[property="v:genre"]') ||
            []),
        ].map((g) => g.textContent.trim());
        const genreFromLD = ld?.genre || [];
        const genre = genreFromDoc || genreFromLD;
        return genre;
      }, "genre"),
      duration: this.promisedGetterLazify(async () => {
        const doc = await this.subjectDoc;
        const ld = await this.linkingData;
        const type = await this.type;
        let movieDurationFromDoc = [],
          episodeDurationFromDoc = [];
        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 !== "") {
            movieDurationFromDoc = durationString
              .split("/")
              .map((str) => {
                str = str.trim();
                const duration = parseInt(str || 0, 10) * 60 || null;
                const whereabouts = str.match(/(?<=\().+?(?=\)$)/)?.[0] || null;
                return {
                  duration,
                  whereabouts,
                };
              })
              .filter((d) => d.duration);
          }
        } else if (type === "tvseries") {
          const episodeDurationSecondsFromDoc =
            parseInt(
              [...(doc?.querySelectorAll("body #info span.pl") || [])]
                .find((n) => n.textContent.includes("单集片长"))
                ?.nextSibling?.textContent.trim() || 0,
              10
            ) * 60 || null;
          if (episodeDurationSecondsFromDoc) {
            episodeDurationFromDoc = [
              {
                duration: episodeDurationSecondsFromDoc,
                whereabouts: null,
              },
            ];
          }
        }
        let durationFromMeta = [];
        const durationSecondsFromMeta =
          parseInt(
            doc?.querySelector("head>meta[property='video:duration']")
              ?.content || 0,
            10
          ) || null;
        if (durationSecondsFromMeta) {
          durationFromMeta = [
            {
              duration: durationSecondsFromMeta,
              whereabouts: null,
            },
          ];
        }
        let durationFromLD = [];
        const durationSecondsFromLD =
          parseInt(
            ld?.duration?.replace(
              /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/,
              (_, p1, p2, p3) => {
                return (
                  parseInt(p1 || 0, 10) * 3600 +
                  parseInt(p2 || 0, 10) * 60 +
                  parseInt(p3 || 0, 10)
                ).toString();
              }
            ) || 0,
            10
          ) || null;
        if (durationSecondsFromLD) {
          durationFromLD = [
            {
              duration: durationSecondsFromLD,
              whereabouts: null,
            },
          ];
        }
        const duration =
          movieDurationFromDoc ||
          episodeDurationFromDoc ||
          durationFromMeta ||
          durationFromLD;
        return duration;
      }, "duration"),
      datePublished: this.promisedGetterLazify(async () => {
        const doc = await this.subjectDoc;
        const ld = await this.linkingData;
        const datePublishedFromDoc = [
          ...(doc?.querySelectorAll(
            'body #info span[property="v:initialReleaseDate"]'
          ) || []),
        ]
          .map((e) => ({
            date: new Date(e.textContent.trim()),
            whereabouts: e.textContent.match(/(?<=\().+?(?=\)$)/)?.[0] || null,
          }))
          .sort((d1, d2) => {
            d1.date - d2.date;
          });
        const datePublishedStringFromLD = ld?.datePublished || null;
        let datePublishedFromLD = [];
        if (datePublishedStringFromLD) {
          datePublishedFromLD = [
            { date: new Date(datePublishedStringFromLD), whereabouts: null },
          ];
        }
        const datePublished = datePublishedFromDoc || datePublishedFromLD;
        return datePublished;
      }, "datePublished"),
      episodeCount: this.promisedGetterLazify(async () => {
        if ((await this.type) === "tvseries") {
          const doc = await this.subjectDoc;
          const episodeCount =
            parseInt(
              [...(doc?.querySelectorAll("body #info span.pl") || [])]
                .find((n) => n.textContent.includes("集数"))
                ?.nextSibling?.textContent.trim() || 0,
              10
            ) || null;
          return episodeCount;
        } else {
          return null;
        }
      }, "episodeCount"),
      tag: this.promisedGetterLazify(async () => {
        const doc = await this.subjectDoc;
        const tag = [
          ...(doc?.querySelectorAll("body div.tags-body>a") || []),
        ].map((t) => t.textContent);
        return tag;
      }, "tag"),
      rating: this.promisedGetterLazify(async () => {
        const doc = await this.subjectDoc;
        let ratingFromDoc = null;
        let ratingCountFromDoc =
          parseInt(
            doc?.querySelector('body #interest_sectl [property="v:votes"]')
              ?.textContent || 0,
            10
          ) || null;
        let ratingValueFromDoc =
          parseFloat(
            doc?.querySelector('body #interest_sectl [property="v:average"]')
              ?.textContent || 0
          ) || null;
        if (ratingCountFromDoc && ratingValueFromDoc) {
          ratingFromDoc = {
            ratingCount: ratingCountFromDoc,
            ratingValue: ratingValueFromDoc,
            bestRating: 10,
          };
        }
        const ld = await this.linkingData;
        let ratingFromLD = null;
        let ratingCountFromLD =
          parseInt(ld?.aggregateRating?.ratingCount || 0, 10) || null;
        let ratingValueFromLD =
          parseFloat(ld?.aggregateRating?.ratingValue || 0) || null;
        if (ratingCountFromLD && ratingValueFromLD) {
          ratingFromLD = {
            ratingCount: ratingCountFromLD,
            ratingValue: ratingValueFromLD,
            bestRating: 10,
          };
        }
        const rating = ratingFromDoc || ratingFromLD;
        return rating;
      }, "rating"),
      description: this.promisedGetterLazify(async () => {
        const doc = await this.subjectDoc;
        const ld = await this.linkingData;
        const descriptionFromDoc =
          [
            ...(doc?.querySelector(
              'body #link-report>[property="v:summary"],body #link-report>span.all.hidden'
            )?.childNodes || []),
          ]
            .filter((e) => e.nodeType === 3)
            .map((e) => e.textContent.trim())
            .join("\n") || null;
        const descriptionFromMeta =
          doc?.querySelector("head>meta[property='og:description']")?.content ||
          null;
        const descriptionFromLD = ld?.description || null;
        const description =
          descriptionFromDoc || descriptionFromMeta || descriptionFromLD;
        return description;
      }, "description"),
      imdbId: this.promisedGetterLazify(async () => {
        const doc = await this.subjectDoc;
        let imdbId = null;
        if (
          doc?.querySelector("body #season option:checked")?.textContent !==
            "1" ||
          false
        ) {
          const doubanId =
            doc.querySelector("body #season option:first-of-type")?.value ||
            null;
          if (doubanId) {
            const firstSeasonDoubanInfo = new DoubanInfo(doubanId);
            imdbId = await firstSeasonDoubanInfo.imdbId;
          }
        } else {
          imdbId =
            [...(doc?.querySelectorAll("body #info span.pl") || [])]
              .find((n) => n.textContent.includes("IMDb:"))
              ?.nextSibling?.textContent.match(/tt(\d+)/)?.[1] || null;
        }
        return imdbId;
      }),
    });
  }

  get info() {
    return (async () => {
      let info = {};
      for (let key in this) {
        info[key] = await this[key];
      }
      return info;
    })();
  }

  promisedGetterLazify(fun, propertyName, isEnumarable = true) {
    return {
      configurable: true,
      enumerable: isEnumarable,
      get: function () {
        Object.defineProperty(this, propertyName, {
          writable: false,
          enumerable: isEnumarable,
          value: fun(),
        });
        return this[propertyName];
      },
    };
  }
}