feat(web): simplify subscription and manual download flows and sync UHD styles
This commit is contained in:
1 parent
27c0f27434
commit
c3c4456913
33 files changed
+1083
-633
No files matched your search
@@ -1,5 +1,4 @@
|
||||
import type {
|
||||
AcquisitionSlot,
|
||||
AutoFillMode,
|
||||
CalendarEntryState,
|
||||
CoverageCell,
|
||||
@@ -7,13 +6,10 @@ import type {
|
||||
Subscription,
|
||||
SubscriptionCalendarEntry,
|
||||
SubscriptionCoverage,
|
||||
SubscriptionEffect,
|
||||
SubscriptionGroup,
|
||||
SubscriptionMediaKind,
|
||||
SubscriptionOverviewItem,
|
||||
SubscriptionResolution,
|
||||
SubscriptionSlotSummary,
|
||||
SubscriptionSourcePreset,
|
||||
} from "@umt/contracts";
|
||||
import type { StatusTone } from "~/components/console/status-badge.tsx";
|
||||
|
||||
@@ -75,7 +71,7 @@ export function foldNames(names: string[], max = 2, emptyLabel = "全部站点")
|
||||
return names.length > max ? `${head} +${names.length - max}` : head;
|
||||
}
|
||||
|
||||
export const AUTOFILL_LABEL: Record<AutoFillMode, string> = { inherit: "跟随规则组", on: "开", off: "关" };
|
||||
export const AUTOFILL_LABEL: Record<AutoFillMode, string> = { inherit: "跟随主版本规则", on: "开", off: "关" };
|
||||
export const AUTOFILL_SOURCE: Record<"subscription" | "rule_group" | "default", string> = {
|
||||
subscription: "订阅设置",
|
||||
rule_group: "规则组",
|
||||
@@ -155,27 +151,6 @@ export function seasonTitle(s: SubscriptionCoverage["seasons"][number], kind: Su
|
||||
return s.seasonNumber === 0 ? "特别篇" : `第 ${s.seasonNumber} 季`;
|
||||
}
|
||||
|
||||
// ── 收录版本 ──
|
||||
|
||||
export type UpgradeStrategy = "off" | "per_unit" | "full_pack" | "movie";
|
||||
export const UPGRADE_LABEL: Record<UpgradeStrategy, string> = {
|
||||
off: "不洗版",
|
||||
per_unit: "按集洗版",
|
||||
full_pack: "全集洗版",
|
||||
movie: "洗版",
|
||||
};
|
||||
|
||||
/** 订阅收录版本当前的升级策略:任一开启取其 mode */
|
||||
export function upgradeOf(
|
||||
slots: Pick<AcquisitionSlot, "upgradeThreshold">[],
|
||||
kind: SubscriptionMediaKind,
|
||||
): UpgradeStrategy {
|
||||
const on = slots.find((s) => (s.upgradeThreshold as { enabled?: unknown }).enabled === true);
|
||||
if (!on) return "off";
|
||||
if (kind === "movie") return "movie";
|
||||
return (on.upgradeThreshold as { mode?: unknown }).mode === "full_pack" ? "full_pack" : "per_unit";
|
||||
}
|
||||
|
||||
/** 把 from 移到 to(拖动排序) */
|
||||
export function moveItem<T>(arr: readonly T[], from: number, to: number): T[] {
|
||||
const out = arr.slice();
|
||||
@@ -187,30 +162,6 @@ export function moveItem<T>(arr: readonly T[], from: number, to: number): T[] {
|
||||
|
||||
// ── 表单 ──
|
||||
|
||||
export const RESOLUTION_LABEL: Record<SubscriptionResolution, string> = {
|
||||
"4k": "4K",
|
||||
"1080p": "1080p",
|
||||
"720p": "720p",
|
||||
};
|
||||
export const SOURCE_PRESET_LABEL: Record<SubscriptionSourcePreset, string> = {
|
||||
bluray_disc: "蓝光原盘",
|
||||
remux: "Remux",
|
||||
bluray: "BluRay",
|
||||
uhd: "UHD",
|
||||
web: "WEB-DL",
|
||||
hdtv: "HDTV",
|
||||
h265: "H.265",
|
||||
h264: "H.264",
|
||||
};
|
||||
export const EFFECT_LABEL: Record<SubscriptionEffect, string> = {
|
||||
dv: "杜比视界",
|
||||
hdr: "HDR",
|
||||
sdr: "SDR",
|
||||
atmos: "杜比全景声",
|
||||
};
|
||||
const oneOf = <K extends string>(v: unknown, labels: Record<K, string>): K | "" =>
|
||||
typeof v === "string" && v in labels ? (v as K) : "";
|
||||
|
||||
export type SubscriptionDraft = {
|
||||
mediaKind: SubscriptionMediaKind;
|
||||
seasonUnitId: string;
|
||||
@@ -226,15 +177,9 @@ export type SubscriptionDraft = {
|
||||
libraryRootId: string;
|
||||
downloadSavePath: string;
|
||||
policyRevisionId: string;
|
||||
upgrade: UpgradeStrategy;
|
||||
ruleGroupIds: string[];
|
||||
include: string;
|
||||
exclude: string;
|
||||
keywords: string;
|
||||
/** MP 分辨率 / 资源质量 / 特效预设(写到该订阅自己的收录版本) */
|
||||
resolution: SubscriptionResolution | "";
|
||||
source: SubscriptionSourcePreset | "";
|
||||
effect: SubscriptionEffect | "";
|
||||
searchIntervalHours: string;
|
||||
stopAfter: string;
|
||||
autoFill: AutoFillMode;
|
||||
@@ -267,14 +212,9 @@ export function emptyDraft(kind: SubscriptionMediaKind = "series"): Subscription
|
||||
libraryRootId: "",
|
||||
downloadSavePath: "",
|
||||
policyRevisionId: "",
|
||||
upgrade: "off",
|
||||
ruleGroupIds: [],
|
||||
include: "",
|
||||
exclude: "",
|
||||
keywords: "",
|
||||
resolution: "",
|
||||
source: "",
|
||||
effect: "",
|
||||
searchIntervalHours: "",
|
||||
stopAfter: "",
|
||||
autoFill: "inherit",
|
||||
@@ -284,6 +224,7 @@ export function emptyDraft(kind: SubscriptionMediaKind = "series"): Subscription
|
||||
}
|
||||
|
||||
const GB = 1024 ** 3;
|
||||
const gbStr = (v: unknown) => (typeof v === "number" ? String(Math.round((v / GB) * 10) / 10) : "");
|
||||
|
||||
/** datetime-local 值 ↔ ISO */
|
||||
export function toLocalInput(iso: string): string {
|
||||
@@ -293,11 +234,10 @@ export function toLocalInput(iso: string): string {
|
||||
return `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}T${p(t.getHours())}:${p(t.getMinutes())}`;
|
||||
}
|
||||
|
||||
export function draftFromSubscription(sub: Subscription, upgrade: UpgradeStrategy): SubscriptionDraft {
|
||||
export function draftFromSubscription(sub: Subscription): SubscriptionDraft {
|
||||
const p = sub.params as Record<string, unknown>;
|
||||
const range = (p.episodeRange ?? {}) as { start?: unknown; end?: unknown };
|
||||
const budget = (p.fillBudget ?? {}) as { unitsPerDay?: unknown; bytesPerDay?: unknown };
|
||||
const bytes = typeof budget.bytesPerDay === "number" ? budget.bytesPerDay : null;
|
||||
return {
|
||||
...emptyDraft(sub.mediaKind),
|
||||
mediaKind: sub.mediaKind,
|
||||
@@ -312,19 +252,14 @@ export function draftFromSubscription(sub: Subscription, upgrade: UpgradeStrateg
|
||||
libraryRootId: str(p.libraryRootId),
|
||||
downloadSavePath: str(p.downloadSavePath),
|
||||
policyRevisionId: sub.policyRevisionId ?? "",
|
||||
upgrade,
|
||||
ruleGroupIds: strArr(p.ruleGroupIds),
|
||||
include: strArr(p.include).join("\n"),
|
||||
exclude: strArr(p.exclude).join("\n"),
|
||||
keywords: strArr(p.keywords).join("\n"),
|
||||
resolution: oneOf(p.resolution, RESOLUTION_LABEL),
|
||||
source: oneOf(p.source, SOURCE_PRESET_LABEL),
|
||||
effect: oneOf(p.effect, EFFECT_LABEL),
|
||||
searchIntervalHours: numStr(p.searchIntervalHours),
|
||||
stopAfter: typeof p.stopAfter === "string" ? toLocalInput(p.stopAfter) : "",
|
||||
autoFill: p.autoFill === "on" || p.autoFill === "off" ? p.autoFill : "inherit",
|
||||
unitsPerDay: numStr(budget.unitsPerDay),
|
||||
gbPerDay: bytes !== null ? String(Math.round((bytes / GB) * 10) / 10) : "",
|
||||
gbPerDay: gbStr(budget.bytesPerDay),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -334,32 +269,27 @@ const isInt = (s: string) => /^\d+$/.test(s.trim());
|
||||
|
||||
/**
|
||||
* 草稿 → params(PATCH 整体替换,所以以现有 params 为底保留未知键)。
|
||||
* upgradeChanged:只有升级策略改了才带 best_version(否则每次保存都会 bump 收录版本 revision)。
|
||||
* 收录规则只在版本编辑器维护;旧导入的质量预设与规则组原样保留,不从普通表单重建。
|
||||
*/
|
||||
export function draftToParams(
|
||||
d: SubscriptionDraft,
|
||||
base: JsonObject = {},
|
||||
opts: { upgradeChanged: boolean; totalChanged: boolean },
|
||||
opts: { totalChanged: boolean; autoFillChanged: boolean; clearQualityPreset?: boolean },
|
||||
): { params: JsonObject; errors: DraftErrors } {
|
||||
const errors: DraftErrors = {};
|
||||
const out: Record<string, unknown> = { ...base };
|
||||
if (opts.clearQualityPreset) for (const k of ["resolution", "source", "effect"]) delete out[k];
|
||||
const known = [
|
||||
"siteIds",
|
||||
"episodeRange",
|
||||
"searchIntervalHours",
|
||||
"stopAfter",
|
||||
"downloaderInstanceId",
|
||||
"ruleGroupIds",
|
||||
"keywords",
|
||||
"include",
|
||||
"exclude",
|
||||
"libraryRootId",
|
||||
"downloadSavePath",
|
||||
"autoFill",
|
||||
"fillBudget",
|
||||
"resolution",
|
||||
"source",
|
||||
"effect",
|
||||
];
|
||||
for (const k of known) delete out[k];
|
||||
const set = (k: string, v: unknown) => {
|
||||
@@ -370,15 +300,11 @@ export function draftToParams(
|
||||
set("downloaderInstanceId", d.downloaderInstanceId);
|
||||
set("libraryRootId", d.libraryRootId);
|
||||
set("downloadSavePath", d.downloadSavePath.trim());
|
||||
set("ruleGroupIds", d.ruleGroupIds);
|
||||
set("include", lines(d.include));
|
||||
set("exclude", lines(d.exclude));
|
||||
const kw = lines(d.keywords);
|
||||
if (kw.length > 10) errors.keywords = "最多 10 个";
|
||||
set("keywords", kw);
|
||||
set("resolution", d.resolution);
|
||||
set("source", d.source);
|
||||
set("effect", d.effect);
|
||||
if (d.mediaKind !== "movie") {
|
||||
const s = d.episodeStart.trim();
|
||||
const e = d.episodeEnd.trim();
|
||||
@@ -407,21 +333,24 @@ export function draftToParams(
|
||||
if (Number.isNaN(t.getTime())) errors.stopAfter = "时间无效";
|
||||
else out.stopAfter = t.toISOString();
|
||||
}
|
||||
if (d.autoFill !== "inherit") out.autoFill = d.autoFill;
|
||||
const budget: Record<string, number> = {};
|
||||
if (d.unitsPerDay.trim()) {
|
||||
if (!isInt(d.unitsPerDay)) errors.unitsPerDay = "填非负整数";
|
||||
else budget.unitsPerDay = Number(d.unitsPerDay);
|
||||
}
|
||||
if (d.gbPerDay.trim()) {
|
||||
const g = Number(d.gbPerDay);
|
||||
if (!Number.isFinite(g) || g < 0) errors.gbPerDay = "填非负数";
|
||||
else budget.bytesPerDay = Math.round(g * GB);
|
||||
}
|
||||
if (Object.keys(budget).length) out.fillBudget = budget;
|
||||
if (opts.upgradeChanged) {
|
||||
out.best_version = d.upgrade !== "off";
|
||||
if (d.mediaKind !== "movie") out.best_version_full = d.upgrade === "full_pack";
|
||||
if (opts.autoFillChanged) {
|
||||
delete out.autoFill;
|
||||
if (d.autoFill !== "inherit") out.autoFill = d.autoFill;
|
||||
const original = (base.fillBudget ?? {}) as Record<string, unknown>;
|
||||
const budget = { ...original };
|
||||
if (d.unitsPerDay.trim() && !isInt(d.unitsPerDay)) errors.unitsPerDay = "填非负整数";
|
||||
if (d.unitsPerDay !== numStr(original.unitsPerDay)) {
|
||||
delete budget.unitsPerDay;
|
||||
if (d.unitsPerDay.trim() && !errors.unitsPerDay) budget.unitsPerDay = Number(d.unitsPerDay);
|
||||
}
|
||||
if (d.gbPerDay.trim() && (!Number.isFinite(Number(d.gbPerDay)) || Number(d.gbPerDay) < 0))
|
||||
errors.gbPerDay = "填非负数";
|
||||
if (d.gbPerDay !== gbStr(original.bytesPerDay)) {
|
||||
delete budget.bytesPerDay;
|
||||
if (d.gbPerDay.trim() && !errors.gbPerDay) budget.bytesPerDay = Math.round(Number(d.gbPerDay) * GB);
|
||||
}
|
||||
delete out.fillBudget;
|
||||
if (Object.keys(budget).length) out.fillBudget = budget;
|
||||
}
|
||||
return { params: out as JsonObject, errors };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user