feat(手动下载): 保存目录选项接口与下载前指定作品
- GET /download-save-paths:按下载器列出默认保存目录与路径映射的下载器侧目录,并给出「自动」会用的下载器(站点指定 > 默认 > 第一个)
- POST /acquisition-plans 新增 identity(本地作品或 TMDB 外部身份):转调 /source-releases/{id}/identity 建 / 绑作品并加人工锁,再按该作品下载
- 测试 harness 支持装配 agent 模块;pg 测试覆盖保存目录选项与指定身份下载
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
fb6c76875c
commit
4bb19433ad
7 files changed
+260
-6
No files matched your search
@@ -5,6 +5,7 @@ import type { ModuleApp } from "../../types.ts";
|
||||
import { type AoDeps, enqueueTracked, timeSlot } from "../services/common.ts";
|
||||
import { downloadBoard } from "../services/download-board.ts";
|
||||
import { applySlotEvent, userPause, userResume } from "../services/downloads.ts";
|
||||
import { manualSavePaths } from "../services/manual-acquisition.ts";
|
||||
import { planWhys } from "../services/plan-why.ts";
|
||||
import { actorOf, conflict, notFound, toApi, toApiList } from "../util.ts";
|
||||
import { mount, paginate } from "./helpers.ts";
|
||||
@@ -38,6 +39,13 @@ export function registerDownloadRoutes(app: ModuleApp, d: AoDeps) {
|
||||
{ tag: TAG },
|
||||
);
|
||||
|
||||
mount(
|
||||
app,
|
||||
Api.acquisitionPlans.savePaths,
|
||||
async (_c, { query }) => manualSavePaths(db(), (query as { siteId?: string }).siteId),
|
||||
{ tag: TAG },
|
||||
);
|
||||
|
||||
mount(
|
||||
app,
|
||||
Api.acquisitionPlans.why,
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
import type { ManualAcquisitionRequest, ManualAcquisitionResult } from "@umt/contracts";
|
||||
import type { DownloadSavePaths, ManualAcquisitionRequest, ManualAcquisitionResult } from "@umt/contracts";
|
||||
import { sql } from "@umt/db";
|
||||
import { ApiException } from "../../../http/errors.ts";
|
||||
import type { ServerContext } from "../../types.ts";
|
||||
import { hashJson, jsonb, toApi } from "../util.ts";
|
||||
import { subscriptionSavePath } from "./acquisition.ts";
|
||||
import { enqueueTracked, loadInstances, pickInstance } from "./common.ts";
|
||||
import { type Exec, enqueueTracked, hostPathOf, loadInstances, pickInstance } from "./common.ts";
|
||||
|
||||
/**
|
||||
* 手动下载对话框的保存目录选项:每个可用下载器的默认保存目录(settings.savePath,没有时取第一条映射)
|
||||
* 加上各条路径映射的下载器侧目录;「自动」按 pickInstance 同一顺序解析(站点指定 > 默认下载器 > 第一个)。
|
||||
*/
|
||||
export async function manualSavePaths(exec: Exec, siteId?: string): Promise<DownloadSavePaths> {
|
||||
const insts = (await loadInstances(exec)).filter((t) => t.savePath);
|
||||
const auto = await pickInstance(exec, insts, { siteId });
|
||||
return {
|
||||
autoInstanceId: auto?.inst.id ?? null,
|
||||
instances: insts.map((t) => {
|
||||
const paths = [t.savePath!, ...t.mappings.map((m) => m.downloaderPath)];
|
||||
const seen = new Set<string>();
|
||||
const dirs = [];
|
||||
for (const p of paths) {
|
||||
const key = p.replace(/\/+$/, "") || "/";
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
dirs.push({ path: p, hostPath: hostPathOf(t, p), isDefault: dirs.length === 0 });
|
||||
}
|
||||
return { instanceId: t.inst.id, name: t.inst.name, isDefault: t.inst.is_default, dirs };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Manual choice is a one-off plan, independent of subscription filters and slots. */
|
||||
export async function planManualAcquisition(
|
||||
|
||||
@@ -10,6 +10,7 @@ import { type AgentRunRow, sql } from "@umt/db";
|
||||
import { hasScope } from "../../http/auth.ts";
|
||||
import { ApiException } from "../../http/errors.ts";
|
||||
import { planManualAcquisition } from "../acquisition-organize/services/manual-acquisition.ts";
|
||||
import { InternalDispatcher } from "../search/dispatch.ts";
|
||||
import type { ModuleApp, ServerContext } from "../types.ts";
|
||||
import { planBinding } from "./authorize.ts";
|
||||
import { cancelTask, retryTask, taskToApi } from "./capabilities.ts";
|
||||
@@ -115,6 +116,7 @@ export function registerAgentRoutes(
|
||||
limiter: InvokeLimiter = sharedInvokeLimiter(),
|
||||
) {
|
||||
const db = () => ctx.db;
|
||||
const dispatch = new InternalDispatcher(app);
|
||||
|
||||
// ── Agent 会话 ──
|
||||
route(app, Api.agentRuns.list, async ({ query }) => {
|
||||
@@ -433,7 +435,19 @@ export function registerAgentRoutes(
|
||||
});
|
||||
|
||||
// 手动资源选择不依赖订阅,提交后仍走共同下载及整理任务。
|
||||
route(app, Api.acquisitionPlans.create, async ({ body, actorId, c }) => {
|
||||
route(app, Api.acquisitionPlans.create, async ({ body: input, actorId, c }) => {
|
||||
let body = input;
|
||||
// 下载前指定了作品:走资源指定身份的同一端点(建 / 绑作品、人工锁、审计),再按该作品下载
|
||||
if (body.identity) {
|
||||
const { identity, ...rest } = body;
|
||||
const assigned = await dispatch.call<{ mediaEntityId: string }>(
|
||||
c,
|
||||
"POST",
|
||||
`/source-releases/${body.sourceReleaseId}/identity`,
|
||||
{ ...identity, reason: "手动下载时指定" },
|
||||
);
|
||||
body = { ...rest, entityId: assigned.mediaEntityId };
|
||||
}
|
||||
const rel = await db()
|
||||
.selectFrom("source_release")
|
||||
.select("media_entity_id")
|
||||
|
||||
Reference in new issue
Block a user