Compare commits
2 Commits
dc5f7fa5c8
...
dcd66fdd11
| Author | SHA1 | Date | |
|---|---|---|---|
| dcd66fdd11 | |||
| ce1693cf4e |
@@ -51,7 +51,7 @@ Ardından http://localhost:3001
|
|||||||
- `QBIT_BASE_URL`, `QBIT_USERNAME`, `QBIT_PASSWORD`
|
- `QBIT_BASE_URL`, `QBIT_USERNAME`, `QBIT_PASSWORD`
|
||||||
- `APP_USERNAME`, `APP_PASSWORD`, `JWT_SECRET`
|
- `APP_USERNAME`, `APP_PASSWORD`, `JWT_SECRET`
|
||||||
- `POLL_INTERVAL_MS`, `ENFORCE_INTERVAL_MS`, `DEFAULT_DELAY_MS`, `MAX_LOOP_LIMIT`
|
- `POLL_INTERVAL_MS`, `ENFORCE_INTERVAL_MS`, `DEFAULT_DELAY_MS`, `MAX_LOOP_LIMIT`
|
||||||
- `WEB_ALLOWED_HOSTS` (ör: `localhost,qbuffer.bee`)
|
- `WEB_ALLOWED_HOSTS` (ör: `localhost,qbuffer.bee,qbuffer.panda`)
|
||||||
|
|
||||||
## Klasör Yapısı
|
## Klasör Yapısı
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const router = Router();
|
|||||||
const ruleSchema = z.object({
|
const ruleSchema = z.object({
|
||||||
tags: z.array(z.string().min(1)).min(1),
|
tags: z.array(z.string().min(1)).min(1),
|
||||||
seedLimitSeconds: z.number().int().min(60).max(60 * 60 * 24 * 365),
|
seedLimitSeconds: z.number().int().min(60).max(60 * 60 * 24 * 365),
|
||||||
|
deleteFiles: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/rules", async (_req, res) => {
|
router.get("/rules", async (_req, res) => {
|
||||||
@@ -27,6 +28,7 @@ router.post("/rules", async (req, res) => {
|
|||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
tags: parsed.data.tags,
|
tags: parsed.data.tags,
|
||||||
seedLimitSeconds: parsed.data.seedLimitSeconds,
|
seedLimitSeconds: parsed.data.seedLimitSeconds,
|
||||||
|
deleteFiles: parsed.data.deleteFiles ?? true,
|
||||||
createdAt: nowIso(),
|
createdAt: nowIso(),
|
||||||
};
|
};
|
||||||
db.timerRules = [...(db.timerRules ?? []), rule];
|
db.timerRules = [...(db.timerRules ?? []), rule];
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface TimerRuleInput {
|
export interface TimerRuleInput {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
seedLimitSeconds: number;
|
seedLimitSeconds: number;
|
||||||
|
deleteFiles?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const startTimerWorker = (qbit: QbitClient, intervalMs: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await qbit.deleteTorrent(torrent.hash, true);
|
await qbit.deleteTorrent(torrent.hash, matchingRule.deleteFiles ?? true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export interface TimerRule {
|
|||||||
id: string;
|
id: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
seedLimitSeconds: number;
|
seedLimitSeconds: number;
|
||||||
|
deleteFiles: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ const formatCountdown = (seconds: number) => {
|
|||||||
return `${days}g ${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
return `${days}g ${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatAddedOn = (addedOn?: number) => {
|
||||||
|
if (!Number.isFinite(addedOn)) return "Bilinmiyor";
|
||||||
|
return new Date(addedOn * 1000).toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
const trackerLabel = (tracker?: string) => {
|
const trackerLabel = (tracker?: string) => {
|
||||||
if (!tracker) return "Bilinmiyor";
|
if (!tracker) return "Bilinmiyor";
|
||||||
try {
|
try {
|
||||||
@@ -91,6 +96,7 @@ export const TimerPage = () => {
|
|||||||
const [seedUnit, setSeedUnit] = useState<(typeof unitOptions)[number]["value"]>(
|
const [seedUnit, setSeedUnit] = useState<(typeof unitOptions)[number]["value"]>(
|
||||||
"weeks"
|
"weeks"
|
||||||
);
|
);
|
||||||
|
const [deleteFiles, setDeleteFiles] = useState(true);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const pushAlert = useUiStore((s) => s.pushAlert);
|
const pushAlert = useUiStore((s) => s.pushAlert);
|
||||||
const [nowTick, setNowTick] = useState(() => Date.now());
|
const [nowTick, setNowTick] = useState(() => Date.now());
|
||||||
@@ -225,9 +231,11 @@ export const TimerPage = () => {
|
|||||||
const response = await api.post("/api/timer/rules", {
|
const response = await api.post("/api/timer/rules", {
|
||||||
tags: selectedTags,
|
tags: selectedTags,
|
||||||
seedLimitSeconds,
|
seedLimitSeconds,
|
||||||
|
deleteFiles,
|
||||||
});
|
});
|
||||||
setTimerRules([response.data, ...timerRules]);
|
setTimerRules([response.data, ...timerRules]);
|
||||||
setSelectedTags([]);
|
setSelectedTags([]);
|
||||||
|
setDeleteFiles(true);
|
||||||
pushAlert({
|
pushAlert({
|
||||||
title: "Kural kaydedildi",
|
title: "Kural kaydedildi",
|
||||||
description: "Timer kuralı aktif edildi.",
|
description: "Timer kuralı aktif edildi.",
|
||||||
@@ -322,6 +330,9 @@ export const TimerPage = () => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(", ") || "-"}
|
.join(", ") || "-"}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="truncate">
|
||||||
|
Added: {formatAddedOn(torrent.added_on)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -439,6 +450,20 @@ export const TimerPage = () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<label className="flex items-start gap-2 text-sm text-slate-700">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={deleteFiles}
|
||||||
|
onChange={(event) => setDeleteFiles(event.target.checked)}
|
||||||
|
className="mt-1 h-4 w-4 rounded border-slate-300 text-slate-900"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Dosyayı Disk'ten Kaldır
|
||||||
|
<span className="block text-xs text-slate-500">
|
||||||
|
İşaretli değilse torrent sadece qBittorrent'tan kaldırılır, dosya disk üzerinde kalır.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<Button onClick={handleSaveRule} disabled={busy}>
|
<Button onClick={handleSaveRule} disabled={busy}>
|
||||||
{busy ? "Kaydediliyor..." : "Kuralı Kaydet"}
|
{busy ? "Kaydediliyor..." : "Kuralı Kaydet"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -464,6 +489,10 @@ export const TimerPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-500">
|
<div className="text-xs text-slate-500">
|
||||||
Seed limiti: {formatDuration(rule.seedLimitSeconds)}
|
Seed limiti: {formatDuration(rule.seedLimitSeconds)}
|
||||||
|
{" • "}
|
||||||
|
{rule.deleteFiles ?? true
|
||||||
|
? "Silme: Disk + qBittorrent"
|
||||||
|
: "Silme: Sadece qBittorrent"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export interface TimerRule {
|
|||||||
id: string;
|
id: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
seedLimitSeconds: number;
|
seedLimitSeconds: number;
|
||||||
|
deleteFiles?: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user