rsshub
Version:
Make RSS Great Again!
163 lines (160 loc) • 6.53 kB
JavaScript
import { t as config } from "./config-CCmw1BNE.mjs";
import { t as parseDate } from "./parse-date-Cr0wQjV_.mjs";
import { t as cache_default } from "./cache-BkqOokyU.mjs";
import { t as got_default } from "./got-BTowW50G.mjs";
import { webcrypto } from "node:crypto";
//#region lib/routes/douban/tv/coming.ts
const apiUrl = "https://frodo.douban.com/api/v2/tv/coming_soon";
const apiKey = "0dad551ec0f84ed02907ff5c42e8ec70";
const apiSecret = "bf7dddc7c9cfe6f7";
const apiClientUa = "api-client/1 com.douban.frodo/7.22.0.beta9(231) Android/23 product/Mate 40 vendor/HUAWEI model/Mate 40 brand/HUAWEI rom/android network/wifi platform/AndroidPad";
const signRequest = async (url, ts, method = "GET") => {
const urlPath = new URL(url).pathname;
const rawSign = `${method.toUpperCase()}&${encodeURIComponent(urlPath)}&${ts}`;
const keyData = new TextEncoder().encode(apiSecret);
const messageData = new TextEncoder().encode(rawSign);
const key = await webcrypto.subtle.importKey("raw", keyData, {
name: "HMAC",
hash: "SHA-1"
}, false, ["sign"]);
const signature = await webcrypto.subtle.sign("HMAC", key, messageData);
return Buffer.from(signature).toString("base64");
};
const getPubDateText = (pubdate) => pubdate?.[0];
const getPubDate = (pubdate) => {
const pubDateText = getPubDateText(pubdate);
if (!pubDateText) return;
const datePart = pubDateText.split("(", 1)[0];
return parseDate(datePart);
};
const getSortTimestamp = (pubdate) => {
const pubDateText = getPubDateText(pubdate);
if (!pubDateText) return Infinity;
const datePart = pubDateText.split("(", 1)[0].trim();
const match = /^(\d{4})(?:-(\d{1,2}))?(?:-(\d{1,2}))?/.exec(datePart);
if (!match) return Infinity;
const year = Number(match[1]);
const month = match[2] ? Number(match[2]) : 1;
const day = match[3] ? Number(match[3]) : 1;
const timestamp = Date.UTC(year, month - 1, day);
return Number.isNaN(timestamp) ? Infinity : timestamp;
};
const getWishCount = (wishCount) => {
if (typeof wishCount === "number") return wishCount;
if (typeof wishCount === "string") {
const parsed = Number(wishCount);
return Number.isNaN(parsed) ? 0 : parsed;
}
return 0;
};
const renderDescription = (subject) => {
const wishCount = getWishCount(subject.wish_count);
const wishCountText = wishCount > 0 ? `想看人数:${wishCount}` : "";
const introText = subject.intro ?? "";
if (wishCountText && introText) return `${wishCountText},${introText}`;
return wishCountText || introText;
};
const buildFetchError = (error) => {
const status = error?.response?.status;
if (status === 429) return /* @__PURE__ */ new Error("Douban 请求过于频繁(429)。请稍后重试,或降低请求频率。");
if (status === 403) return /* @__PURE__ */ new Error("Douban 拒绝访问(403),可能触发反爬策略。请稍后重试。");
return /* @__PURE__ */ new Error("Douban 数据请求失败,可能触发反爬或限频,请稍后重试。");
};
const route = {
path: "/tv/coming/:sortBy?/:count?",
categories: ["social-media"],
example: "/douban/tv/coming",
parameters: {
sortBy: "排序方式,可选,支持 `hot` 或 `time`,默认 `hot`",
count: "请求上游返回数量,可选,正整数,默认 `10`"
},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false
},
name: "即将播出的剧集",
maintainers: ["honue"],
handler,
description: `| 路径参数 | 含义 | 接受的值 | 默认值 |
| -------- | ---------------- | -------- | ------ |
| sortBy | 排序方式 | hot/time | hot |
| count | 请求上游返回数量 | 正整数 | 10 |
用例:\`/douban/tv/coming/hot/10\`
::: tip
服务端请求固定使用 \`sortby=hot\` 拉取数据,再按 \`sortBy\` 参数在本地重排;条目数量可通过 \`count\` 调整,仍可叠加 RSSHub 通用参数 \`limit\`。
:::`
};
async function handler(ctx) {
const sortByParam = ctx.req.param("sortBy");
const countParam = ctx.req.param("count");
const sortBy = sortByParam === "time" ? "time" : "hot";
const rawCount = Number(countParam || "");
const requestCount = Number.isNaN(rawCount) || rawCount <= 0 ? 10 : rawCount;
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replaceAll("-", "");
const searchParams = {
start: 0,
count: requestCount,
sortby: "hot",
os_rom: "android",
apiKey,
_ts: ts,
_sig: await signRequest(apiUrl, ts)
};
const cacheKey = `douban:tv:coming:${requestCount}`;
const data = await cache_default.tryGet(cacheKey, async () => {
try {
return (await got_default({
method: "get",
url: apiUrl,
searchParams,
headers: {
Accept: "application/json",
"User-Agent": apiClientUa
}
})).data;
} catch (error) {
throw buildFetchError(error);
}
}, config.cache.routeExpire, false);
if (!Array.isArray(data.subjects)) {
const details = data.msg || data.message || data.reason;
throw new Error(`Douban 返回数据结构异常,可能触发反爬或限频。${details ? `上游信息:${details}` : ""}`);
}
if (data.subjects.length === 0) throw new Error("Douban 返回空数据,可能触发反爬或限频。请稍后重试。");
const subscriptionCount = data.count ?? 0;
const total = data.total ?? 0;
const sortedSubjects = data.subjects.toSorted((a, b) => {
if (sortBy === "time") {
const timeDiff = getSortTimestamp(a.pubdate) - getSortTimestamp(b.pubdate);
if (timeDiff !== 0) return timeDiff;
return getWishCount(b.wish_count) - getWishCount(a.wish_count);
}
const wishDiff = getWishCount(b.wish_count) - getWishCount(a.wish_count);
if (wishDiff !== 0) return wishDiff;
return 0;
});
return {
title: "豆瓣剧集-即将播出",
link: "https://movie.douban.com/tv/",
description: `即将播出的剧集,请求参数: count=${subscriptionCount}, total=${total}, sortBy=${sortBy}, requestCount=${requestCount}`,
item: sortedSubjects.map((subject) => {
const link = subject.url || subject.sharing_url || `https://movie.douban.com/subject/${subject.id}/`;
const category = subject.card_subtitle ? [subject.card_subtitle] : subject.genres ?? [];
const pubDate = sortBy === "time" ? getPubDate(subject.pubdate) : void 0;
return {
title: subject.title,
category: category.length > 0 ? category : void 0,
pubDate,
description: renderDescription(subject),
link,
guid: link
};
})
};
}
//#endregion
export { route };