fix(sites): integrate shared download access and register synced adapters
This commit is contained in:
1 parent
6dab134617
commit
36f3ece5b5
20 files changed
+175
-38
No files matched your search
@@ -9,12 +9,10 @@ import {
|
||||
fetchEpisodeGroup,
|
||||
fetchRichDetail,
|
||||
fetchRichSeason,
|
||||
getSiteAdapter,
|
||||
getSiteDefinition,
|
||||
groupSeasonAsRichSeason,
|
||||
type ProbeResult,
|
||||
qbAuthConfig,
|
||||
type SecretResolver,
|
||||
SiteError,
|
||||
type TmdbRichDetail,
|
||||
type TmdbRichSeasonDetail,
|
||||
@@ -200,20 +198,8 @@ export function createPorts(
|
||||
const siteMeta = (raw._site ?? {}) as Record<string, unknown>;
|
||||
const defId = [str(siteMeta.adapterSiteId), site.key].find((k) => k && getSiteDefinition(k));
|
||||
if (!defId) throw new SiteError(site.id, "unsupported", `站点 ${site.key} 没有可用的适配器定义`);
|
||||
const accounts = await ctx.db
|
||||
.selectFrom("site_account")
|
||||
.select(["auth_kind", "secret_ref"])
|
||||
.where("site_id", "=", site.id)
|
||||
.where("enabled", "=", true)
|
||||
.execute();
|
||||
const secrets: SecretResolver = {
|
||||
async resolve({ kind }) {
|
||||
const want = kind === "apiKey" ? "api_key" : kind;
|
||||
const acc = accounts.find((a) => a.auth_kind === want);
|
||||
return (await resolveSecret(acc?.secret_ref ?? null)) ?? null;
|
||||
},
|
||||
};
|
||||
const adapter = getSiteAdapter(defId, { secrets, baseUrl: site.base_url });
|
||||
// 与搜索/详情共用站点出口、账号会话、UA 和跨进程限流;API 基址仍取站点配置。
|
||||
const adapter = rt().siteAdapter({ ...site, key: defId });
|
||||
const t = await adapter.downloadTarget(release.site_release_id);
|
||||
if (t.kind === "torrent")
|
||||
return { kind: "file", bytes: t.bytes, ...(t.fileName ? { filename: t.fileName } : {}) };
|
||||
|
||||
@@ -199,6 +199,13 @@ export class SourcesRuntime {
|
||||
const pick = typeof ua === "function" ? ua() : ua !== undefined ? Promise.resolve(ua) : this.defaultUserAgent();
|
||||
headers.set("user-agent", await pick);
|
||||
}
|
||||
const credentials = init?.credentials ?? (input instanceof Request ? input.credentials : undefined);
|
||||
if (credentials === "omit") {
|
||||
// 签名下载不复用账号 Cookie 或浏览器 profile,包括 cf_clearance。
|
||||
headers.delete("cookie");
|
||||
headers.delete("authorization");
|
||||
return base(input, { ...init, headers, credentials });
|
||||
}
|
||||
if (!siteId) return base(input, { ...init, headers });
|
||||
const url = input instanceof Request ? input.url : String(input);
|
||||
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
||||
|
||||
@@ -413,10 +413,11 @@ export async function refreshSiteAccount(
|
||||
const profile = (def.umt as { profile?: Record<string, string> }).profile;
|
||||
const vipLevels = (def.umt as { vipLevels?: string[] }).vipLevels;
|
||||
// 馒头 API 基址与适配器一致:站点配置 > 定义里的 apiBaseUrl;不再由站点地址推 api.<根域>(IP / 本地地址会拼成非法 URL)
|
||||
const siteConfig = (site.config ?? {}) as { apiBaseUrl?: string; userdataPath?: string };
|
||||
const apiUrl =
|
||||
def.umt.kind === "mteam"
|
||||
? (site.config as { apiBaseUrl?: string })?.apiBaseUrl || mteamApiBase(def as MteamDefinition)
|
||||
: null;
|
||||
? siteConfig.apiBaseUrl || mteamApiBase(def as MteamDefinition)
|
||||
: siteConfig.apiBaseUrl || ("api_url" in def ? def.api_url : null);
|
||||
|
||||
const fetchUserData = async (
|
||||
cred: { cookie?: string | null; apikey?: string | null },
|
||||
@@ -425,6 +426,7 @@ export async function refreshSiteAccount(
|
||||
const info = await refreshSiteUserData({
|
||||
name: site.name,
|
||||
url: site.base_url,
|
||||
...(siteConfig.userdataPath ? { userdataPath: siteConfig.userdataPath } : {}),
|
||||
schema: (def as { schema?: string }).schema ?? null,
|
||||
public: true,
|
||||
cookie: cred.cookie ?? null,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { SourcesRuntime } from "../src/modules/sources-identity/runtime.ts";
|
||||
|
||||
it("签名下载的 credentials omit 不复用账号或浏览器Cookie", async () => {
|
||||
let sent = new Headers();
|
||||
const rt = new SourcesRuntime({} as never, {
|
||||
fetch: async (_input, init) => {
|
||||
sent = new Headers(init?.headers);
|
||||
return new Response("subtitle bytes");
|
||||
},
|
||||
});
|
||||
const browser = vi.spyOn(rt.cloudflare, "handle");
|
||||
const fetch = rt.siteFetch({ id: "original-site", proxy: false }, undefined, "Site User Agent");
|
||||
const headers = { cookie: "account-cookie", authorization: "account-token" };
|
||||
const url = "https://signed-cdn.example/file?credential=ephemeral";
|
||||
const response = await fetch(url, { headers, credentials: "omit" });
|
||||
expect(await response.text()).toBe("subtitle bytes");
|
||||
expect(sent.has("cookie")).toBe(false);
|
||||
expect(sent.has("authorization")).toBe(false);
|
||||
expect(sent.get("user-agent")).toBe("Site User Agent");
|
||||
expect(browser).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TaskRegistry } from "@umt/tasks";
|
||||
import { createMockMteamApi, type MockMteamApi } from "@umt/testkit";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createPorts } from "../../src/modules/acquisition-organize/ports.ts";
|
||||
import { snapshotSite } from "../../src/modules/sources-identity/service/snapshot.ts";
|
||||
import { createHarness, type Harness, pgAvailable } from "./harness.ts";
|
||||
|
||||
@@ -50,6 +51,27 @@ describe.skipIf(!up)("M-Team 快照与任务注册", () => {
|
||||
expect(JSON.stringify(rows)).not.toContain(api.apiKey);
|
||||
});
|
||||
|
||||
it("下载种子沿用配置的 API 基址,并与站点访问共用 PG 限流桶", async () => {
|
||||
const site = await h.db
|
||||
.updateTable("site")
|
||||
.set({ rate_limit: JSON.stringify({ minIntervalMs: 10, burst: 1 }) })
|
||||
.where("id", "=", siteId)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
const release = await h.db
|
||||
.selectFrom("source_release")
|
||||
.selectAll()
|
||||
.where("site_id", "=", siteId)
|
||||
.executeTakeFirstOrThrow();
|
||||
const before = api.issuedTokens;
|
||||
const source = await createPorts(h.ctx).torrentSource({ release, site });
|
||||
expect(source.kind).toBe("file");
|
||||
expect(api.issuedTokens).toBe(before + 1);
|
||||
expect(
|
||||
await h.db.selectFrom("rate_limit_bucket").select("key").where("key", "=", `site:${siteId}`).executeTakeFirst(),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("key 失效:账号标失效 + 待处理,快照结果 failed", async () => {
|
||||
api.setFailMode("banned_key");
|
||||
const r = await snapshotSite(h.rt, { siteId, kind: "manual" });
|
||||
|
||||
@@ -23,6 +23,7 @@ const UNIT = [
|
||||
"test/mapping-compat.test.ts",
|
||||
"test/modules.test.ts",
|
||||
"test/sse.test.ts",
|
||||
"test/site-fetch-credentials.test.ts",
|
||||
"test/static.test.ts",
|
||||
"test/system-resources.test.ts",
|
||||
];
|
||||
|
||||
@@ -49,7 +49,7 @@ export const SUBTITLE_LANGUAGE_LABEL: Record<SubtitleLanguage, string> = {
|
||||
ko: "韩文",
|
||||
unknown: "未知",
|
||||
};
|
||||
const SOURCE_LABEL = { upload: "上传", subhd: "SubHD", zimuku: "字幕库" } as const;
|
||||
const SOURCE_LABEL = { upload: "上传", subhd: "SubHD", zimuku: "字幕库", mteam: "馒头" } as const;
|
||||
const SKIP = "__skip__";
|
||||
const ACCEPT = ".srt,.ass,.ssa,.vtt,.sub,.sbv,.zip";
|
||||
|
||||
|
||||
Reference in new issue
Block a user