feat: watcher icin anti-ban interval ve jitter politikasini ekle

This commit is contained in:
2026-03-13 12:39:44 +03:00
parent 4a11526445
commit af547ea384
6 changed files with 82 additions and 6 deletions

View File

@@ -6,12 +6,18 @@ export const trackerRegistry: TrackerDefinition[] = [
label: "HappyFappy", label: "HappyFappy",
cliSiteKey: "happyfappy", cliSiteKey: "happyfappy",
supportsRemoveBookmark: true, supportsRemoveBookmark: true,
minIntervalMinutes: 5,
jitterSeconds: 60,
recommendedIntervalLabel: "5-10 dakika",
}, },
{ {
key: "privatehd", key: "privatehd",
label: "PrivateHD", label: "PrivateHD",
cliSiteKey: "privatehd", cliSiteKey: "privatehd",
supportsRemoveBookmark: true, supportsRemoveBookmark: true,
minIntervalMinutes: 10,
jitterSeconds: 120,
recommendedIntervalLabel: "10-15 dakika",
}, },
]; ];

View File

@@ -72,6 +72,9 @@ export const listScraperTrackers = async () => {
label: item.label, label: item.label,
cliSiteKey: item.key, cliSiteKey: item.key,
supportsRemoveBookmark: true, supportsRemoveBookmark: true,
minIntervalMinutes: 1,
jitterSeconds: 0,
recommendedIntervalLabel: "Tracker politikasina bakiniz",
})); }));
}; };

View File

@@ -34,6 +34,12 @@ const watcherImageCache = new Map<
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const randomBetween = (min: number, max: number) => {
const lower = Math.ceil(min);
const upper = Math.floor(max);
return Math.floor(Math.random() * (upper - lower + 1)) + lower;
};
const toListItem = (watcher: Watcher): WatcherListItem => ({ const toListItem = (watcher: Watcher): WatcherListItem => ({
...watcher, ...watcher,
hasCookie: Boolean(watcher.cookieEncrypted), hasCookie: Boolean(watcher.cookieEncrypted),
@@ -169,8 +175,21 @@ const toCookieHeader = (cookieValue: string, targetUrl: string) => {
.join("; "); .join("; ");
}; };
const computeNextRunAt = (intervalMinutes: number) => { const computeNextRunAt = (tracker: { minIntervalMinutes: number; jitterSeconds: number }, intervalMinutes: number) => {
return new Date(Date.now() + intervalMinutes * 60_000).toISOString(); const baseMs = intervalMinutes * 60_000;
const minMs = tracker.minIntervalMinutes * 60_000;
const jitterMs = tracker.jitterSeconds > 0
? randomBetween(-tracker.jitterSeconds, tracker.jitterSeconds) * 1000
: 0;
return new Date(Date.now() + Math.max(minMs, baseMs + jitterMs)).toISOString();
};
const validateTrackerInterval = (tracker: { label: string; minIntervalMinutes: number }, intervalMinutes: number) => {
if (intervalMinutes < tracker.minIntervalMinutes) {
throw new Error(
`${tracker.label} icin minimum izleme araligi ${tracker.minIntervalMinutes} dakikadir.`
);
}
}; };
const deriveLiveStatus = ( const deriveLiveStatus = (
@@ -315,7 +334,18 @@ const persistWatcherProgress = async (payload: {
export const listTrackers = async () => { export const listTrackers = async () => {
try { try {
return await listScraperTrackers(); const remoteTrackers = await listScraperTrackers();
return remoteTrackers.map((tracker) => {
const local = getTrackerDefinition(tracker.key);
return local
? {
...tracker,
minIntervalMinutes: local.minIntervalMinutes,
jitterSeconds: local.jitterSeconds,
recommendedIntervalLabel: local.recommendedIntervalLabel,
}
: tracker;
});
} catch (error) { } catch (error) {
logger.warn({ error }, "Falling back to local watcher tracker registry"); logger.warn({ error }, "Falling back to local watcher tracker registry");
return trackerRegistry; return trackerRegistry;
@@ -358,6 +388,7 @@ export const createWatcher = async (payload: {
if (!payload.cookie.trim()) { if (!payload.cookie.trim()) {
throw new Error("Cookie is required"); throw new Error("Cookie is required");
} }
validateTrackerInterval(tracker, payload.intervalMinutes);
if (tracker.key === "privatehd" && !payload.wishlistUrl?.trim()) { if (tracker.key === "privatehd" && !payload.wishlistUrl?.trim()) {
throw new Error("PrivateHD icin wishlist URL zorunludur."); throw new Error("PrivateHD icin wishlist URL zorunludur.");
} }
@@ -372,7 +403,7 @@ export const createWatcher = async (payload: {
cookieHint: buildCookieHint(payload.cookie), cookieHint: buildCookieHint(payload.cookie),
intervalMinutes: payload.intervalMinutes, intervalMinutes: payload.intervalMinutes,
enabled: payload.enabled ?? true, enabled: payload.enabled ?? true,
nextRunAt: computeNextRunAt(payload.intervalMinutes), nextRunAt: computeNextRunAt(tracker, payload.intervalMinutes),
totalImported: 0, totalImported: 0,
totalSeen: 0, totalSeen: 0,
createdAt: nowIso(), createdAt: nowIso(),
@@ -396,13 +427,18 @@ export const updateWatcher = async (
} }
) => { ) => {
const updated = await persistWatcher(watcherId, (watcher) => { const updated = await persistWatcher(watcherId, (watcher) => {
const tracker = getTrackerDefinition(watcher.tracker);
if (!tracker) {
throw new Error("Unsupported tracker");
}
const next: Watcher = { const next: Watcher = {
...watcher, ...watcher,
updatedAt: nowIso(), updatedAt: nowIso(),
}; };
if (typeof payload.intervalMinutes === "number") { if (typeof payload.intervalMinutes === "number") {
validateTrackerInterval(tracker, payload.intervalMinutes);
next.intervalMinutes = payload.intervalMinutes; next.intervalMinutes = payload.intervalMinutes;
next.nextRunAt = computeNextRunAt(payload.intervalMinutes); next.nextRunAt = computeNextRunAt(tracker, payload.intervalMinutes);
} }
if (typeof payload.enabled === "boolean") { if (typeof payload.enabled === "boolean") {
next.enabled = payload.enabled; next.enabled = payload.enabled;
@@ -758,7 +794,7 @@ export const runWatcherById = async (watcherId: string) => {
...current, ...current,
lastRunAt: nowIso(), lastRunAt: nowIso(),
lastError: undefined, lastError: undefined,
nextRunAt: computeNextRunAt(current.intervalMinutes), nextRunAt: computeNextRunAt(tracker, current.intervalMinutes),
updatedAt: nowIso(), updatedAt: nowIso(),
})); }));
const cookie = decryptWatcherCookie(watcher.cookieEncrypted); const cookie = decryptWatcherCookie(watcher.cookieEncrypted);

View File

@@ -5,6 +5,9 @@ export interface TrackerDefinition {
label: string; label: string;
cliSiteKey: string; cliSiteKey: string;
supportsRemoveBookmark: boolean; supportsRemoveBookmark: boolean;
minIntervalMinutes: number;
jitterSeconds: number;
recommendedIntervalLabel: string;
} }
export interface BookmarkRecord { export interface BookmarkRecord {

View File

@@ -121,6 +121,10 @@ export const WatcherPage = () => {
() => watchers.find((watcher) => watcher.id === selectedWatcherId) ?? null, () => watchers.find((watcher) => watcher.id === selectedWatcherId) ?? null,
[selectedWatcherId, watchers] [selectedWatcherId, watchers]
); );
const selectedTrackerDefinition = useMemo(
() => watcherTrackers.find((entry) => entry.key === tracker) ?? null,
[tracker, watcherTrackers]
);
const load = async () => { const load = async () => {
const [trackersRes, categoriesRes, watchersRes, summaryRes, itemsRes] = const [trackersRes, categoriesRes, watchersRes, summaryRes, itemsRes] =
@@ -229,6 +233,17 @@ export const WatcherPage = () => {
}); });
return; return;
} }
if (
selectedTrackerDefinition &&
intervalMinutes < selectedTrackerDefinition.minIntervalMinutes
) {
pushAlert({
title: "Izleme araligi cok dusuk",
description: `${selectedTrackerDefinition.label} icin minimum izleme araligi ${selectedTrackerDefinition.minIntervalMinutes} dakikadir.`,
variant: "warn",
});
return;
}
setSaving(true); setSaving(true);
try { try {
if (selectedWatcher) { if (selectedWatcher) {
@@ -629,6 +644,16 @@ export const WatcherPage = () => {
</Select> </Select>
</label> </label>
</div> </div>
{selectedTrackerDefinition ? (
<div className="rounded-xl border border-amber-200 bg-amber-50/80 px-4 py-3 text-xs text-amber-900">
<div className="font-semibold">
{selectedTrackerDefinition.label} icin minimum izleme araligi: {selectedTrackerDefinition.minIntervalMinutes} dakika
</div>
<div className="mt-1 text-amber-800/90">
Onerilen kullanim: {selectedTrackerDefinition.recommendedIntervalLabel}. Scheduler sabit bot paterni olusmaması icin calisma zamanina otomatik jitter ekler.
</div>
</div>
) : null}
<label className="block space-y-2"> <label className="block space-y-2">
<span className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500"> <span className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">

View File

@@ -85,6 +85,9 @@ export interface WatcherTracker {
label: string; label: string;
cliSiteKey: string; cliSiteKey: string;
supportsRemoveBookmark: boolean; supportsRemoveBookmark: boolean;
minIntervalMinutes: number;
jitterSeconds: number;
recommendedIntervalLabel: string;
} }
export interface Watcher { export interface Watcher {