feat(loop): aynı torrent için birden fazla iş desteği ekle
Aynı torrent hash'i için oluşturulan yeni loop işleri, mevcut aktif işleri otomatik olarak durdurur. Stop-by-hash endpoint'i tüm ilgili işleri durduracak şekilde güncellendi. TorrentTable bileşeni çoklu işleri doğru şekilde işleyecek ve profil adını en güncel aktif işten alacak şekilde yeniden yazıldı. LoopJob arayüzüne createdAt ve updatedAt alanları eklendi.
This commit is contained in:
@@ -66,6 +66,16 @@ export const createLoopJob = async (
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
const db = await readDb();
|
const db = await readDb();
|
||||||
|
const active = db.loopJobs.filter((j) => j.torrentHash === job.torrentHash && j.status !== "COMPLETED" && j.status !== "STOPPED");
|
||||||
|
for (const existing of active) {
|
||||||
|
existing.status = "STOPPED";
|
||||||
|
existing.nextRunAt = undefined;
|
||||||
|
existing.currentRun = undefined;
|
||||||
|
existing.updatedAt = nowIso();
|
||||||
|
}
|
||||||
|
if (active.length) {
|
||||||
|
await writeDb(db);
|
||||||
|
}
|
||||||
db.loopJobs.push(job);
|
db.loopJobs.push(job);
|
||||||
await writeDb(db);
|
await writeDb(db);
|
||||||
await logJob(job.id, "INFO", `Loop job started for ${job.name}`, "JOB_STARTED");
|
await logJob(job.id, "INFO", `Loop job started for ${job.name}`, "JOB_STARTED");
|
||||||
|
|||||||
@@ -91,18 +91,24 @@ router.post("/stop-by-hash", async (req, res) => {
|
|||||||
return res.status(400).json({ error: "Missing hash" });
|
return res.status(400).json({ error: "Missing hash" });
|
||||||
}
|
}
|
||||||
const db = await readDb();
|
const db = await readDb();
|
||||||
const job = db.loopJobs.find((j) => j.torrentHash === hash);
|
const jobs = db.loopJobs.filter((j) => j.torrentHash === hash);
|
||||||
if (!job) {
|
if (!jobs.length) {
|
||||||
return res.status(404).json({ error: "Job not found" });
|
return res.status(404).json({ error: "Job not found" });
|
||||||
}
|
}
|
||||||
const stopped = await stopLoopJob(job.id);
|
const stopped = [] as any[];
|
||||||
|
for (const job of jobs) {
|
||||||
|
const result = await stopLoopJob(job.id);
|
||||||
|
if (result) {
|
||||||
|
stopped.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const qbit = getQbitClient();
|
const qbit = getQbitClient();
|
||||||
await qbit.deleteTorrent(hash, true);
|
await qbit.deleteTorrent(hash, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Best-effort delete
|
// Best-effort delete
|
||||||
}
|
}
|
||||||
res.json(stopped);
|
res.json({ ok: true, stopped });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/dry-run", async (req, res) => {
|
router.post("/dry-run", async (req, res) => {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const loopStartSchema = z.object({
|
|||||||
allowIp: allowIpSchema,
|
allowIp: allowIpSchema,
|
||||||
targetLoops: z.number().int().min(1).max(1000),
|
targetLoops: z.number().int().min(1).max(1000),
|
||||||
delayMs: z.number().int().min(0).max(86_400_000),
|
delayMs: z.number().int().min(0).max(86_400_000),
|
||||||
|
profileName: z.string().trim().min(1).max(64).optional(),
|
||||||
|
profileId: z.string().trim().min(1).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dryRunSchema = z.object({
|
export const dryRunSchema = z.object({
|
||||||
|
|||||||
@@ -112,6 +112,26 @@ export const LoopSetupCard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stopLoop = async () => {
|
||||||
|
if (!selectedHash) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.post("/api/loop/stop-by-hash", { hash: selectedHash });
|
||||||
|
pushAlert({
|
||||||
|
title: "Loop durduruldu",
|
||||||
|
description: "Seçili torrent için loop durduruldu.",
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
pushAlert({
|
||||||
|
title: "Durdurma başarısız",
|
||||||
|
description: "Loop durdurulamadı.",
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const applyProfile = async (profile: Profile) => {
|
const applyProfile = async (profile: Profile) => {
|
||||||
if (!selectedHash) {
|
if (!selectedHash) {
|
||||||
pushAlert({
|
pushAlert({
|
||||||
|
|||||||
@@ -75,23 +75,36 @@ export const TorrentTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getProfileName = (hash: string) => {
|
const getProfileName = (hash: string) => {
|
||||||
const job = jobs.find((j) => j.torrentHash === hash);
|
const hashKey = hash.toLowerCase();
|
||||||
if (!job) return null;
|
const related = jobs.filter((j) => j.torrentHash?.toLowerCase() === hashKey);
|
||||||
if (job.profileId) {
|
if (related.length === 0) return null;
|
||||||
const profileById = profiles.find((p) => p.id === job.profileId);
|
const active = related.find((j) => j.status === "RUNNING" || j.status === "WAITING_DELAY");
|
||||||
|
const latest = related.reduce((current, next) => {
|
||||||
|
const currentAt = Date.parse(current.updatedAt ?? current.createdAt ?? "");
|
||||||
|
const nextAt = Date.parse(next.updatedAt ?? next.createdAt ?? "");
|
||||||
|
if (Number.isFinite(nextAt) && Number.isFinite(currentAt)) {
|
||||||
|
return nextAt >= currentAt ? next : current;
|
||||||
|
}
|
||||||
|
if (Number.isFinite(nextAt)) return next;
|
||||||
|
return current;
|
||||||
|
}, related[0]);
|
||||||
|
const candidate = active ?? latest;
|
||||||
|
if (candidate.profileId) {
|
||||||
|
const profileById = profiles.find((p) => p.id === candidate.profileId);
|
||||||
if (profileById?.name) {
|
if (profileById?.name) {
|
||||||
return profileById.name;
|
return profileById.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (job.profileName) {
|
if (candidate.profileName) {
|
||||||
return job.profileName;
|
return candidate.profileName;
|
||||||
}
|
}
|
||||||
const profile = profiles.find((p) =>
|
const fallback = profiles.find(
|
||||||
p.allowIp === job.allowIp &&
|
(profile) =>
|
||||||
p.delayMs === job.delayMs &&
|
profile.allowIp === candidate.allowIp &&
|
||||||
p.targetLoops === job.targetLoops
|
profile.delayMs === candidate.delayMs &&
|
||||||
|
profile.targetLoops === candidate.targetLoops
|
||||||
);
|
);
|
||||||
return profile?.name ?? null;
|
return fallback?.name ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export interface LoopJob {
|
|||||||
nextRunAt?: string;
|
nextRunAt?: string;
|
||||||
lastError?: string;
|
lastError?: string;
|
||||||
totals?: { totalDownloadedBytes: number };
|
totals?: { totalDownloadedBytes: number };
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StatusSnapshot {
|
export interface StatusSnapshot {
|
||||||
|
|||||||
Reference in New Issue
Block a user