diff --git a/apps/server/src/watcher/watcher.registry.ts b/apps/server/src/watcher/watcher.registry.ts index 1b6b82f..f50759a 100644 --- a/apps/server/src/watcher/watcher.registry.ts +++ b/apps/server/src/watcher/watcher.registry.ts @@ -6,12 +6,18 @@ export const trackerRegistry: TrackerDefinition[] = [ label: "HappyFappy", cliSiteKey: "happyfappy", supportsRemoveBookmark: true, + minIntervalMinutes: 5, + jitterSeconds: 60, + recommendedIntervalLabel: "5-10 dakika", }, { key: "privatehd", label: "PrivateHD", cliSiteKey: "privatehd", supportsRemoveBookmark: true, + minIntervalMinutes: 10, + jitterSeconds: 120, + recommendedIntervalLabel: "10-15 dakika", }, ]; diff --git a/apps/server/src/watcher/watcher.scraper.ts b/apps/server/src/watcher/watcher.scraper.ts index d08bc1b..6cfbfe3 100644 --- a/apps/server/src/watcher/watcher.scraper.ts +++ b/apps/server/src/watcher/watcher.scraper.ts @@ -72,6 +72,9 @@ export const listScraperTrackers = async () => { label: item.label, cliSiteKey: item.key, supportsRemoveBookmark: true, + minIntervalMinutes: 1, + jitterSeconds: 0, + recommendedIntervalLabel: "Tracker politikasina bakiniz", })); }; diff --git a/apps/server/src/watcher/watcher.service.ts b/apps/server/src/watcher/watcher.service.ts index e848f81..90a2314 100644 --- a/apps/server/src/watcher/watcher.service.ts +++ b/apps/server/src/watcher/watcher.service.ts @@ -34,6 +34,12 @@ const watcherImageCache = new Map< 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 => ({ ...watcher, hasCookie: Boolean(watcher.cookieEncrypted), @@ -169,8 +175,21 @@ const toCookieHeader = (cookieValue: string, targetUrl: string) => { .join("; "); }; -const computeNextRunAt = (intervalMinutes: number) => { - return new Date(Date.now() + intervalMinutes * 60_000).toISOString(); +const computeNextRunAt = (tracker: { minIntervalMinutes: number; jitterSeconds: number }, intervalMinutes: number) => { + 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 = ( @@ -315,7 +334,18 @@ const persistWatcherProgress = async (payload: { export const listTrackers = async () => { 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) { logger.warn({ error }, "Falling back to local watcher tracker registry"); return trackerRegistry; @@ -358,6 +388,7 @@ export const createWatcher = async (payload: { if (!payload.cookie.trim()) { throw new Error("Cookie is required"); } + validateTrackerInterval(tracker, payload.intervalMinutes); if (tracker.key === "privatehd" && !payload.wishlistUrl?.trim()) { throw new Error("PrivateHD icin wishlist URL zorunludur."); } @@ -372,7 +403,7 @@ export const createWatcher = async (payload: { cookieHint: buildCookieHint(payload.cookie), intervalMinutes: payload.intervalMinutes, enabled: payload.enabled ?? true, - nextRunAt: computeNextRunAt(payload.intervalMinutes), + nextRunAt: computeNextRunAt(tracker, payload.intervalMinutes), totalImported: 0, totalSeen: 0, createdAt: nowIso(), @@ -396,13 +427,18 @@ export const updateWatcher = async ( } ) => { const updated = await persistWatcher(watcherId, (watcher) => { + const tracker = getTrackerDefinition(watcher.tracker); + if (!tracker) { + throw new Error("Unsupported tracker"); + } const next: Watcher = { ...watcher, updatedAt: nowIso(), }; if (typeof payload.intervalMinutes === "number") { + validateTrackerInterval(tracker, payload.intervalMinutes); next.intervalMinutes = payload.intervalMinutes; - next.nextRunAt = computeNextRunAt(payload.intervalMinutes); + next.nextRunAt = computeNextRunAt(tracker, payload.intervalMinutes); } if (typeof payload.enabled === "boolean") { next.enabled = payload.enabled; @@ -758,7 +794,7 @@ export const runWatcherById = async (watcherId: string) => { ...current, lastRunAt: nowIso(), lastError: undefined, - nextRunAt: computeNextRunAt(current.intervalMinutes), + nextRunAt: computeNextRunAt(tracker, current.intervalMinutes), updatedAt: nowIso(), })); const cookie = decryptWatcherCookie(watcher.cookieEncrypted); diff --git a/apps/server/src/watcher/watcher.types.ts b/apps/server/src/watcher/watcher.types.ts index bd2658c..a0128fa 100644 --- a/apps/server/src/watcher/watcher.types.ts +++ b/apps/server/src/watcher/watcher.types.ts @@ -5,6 +5,9 @@ export interface TrackerDefinition { label: string; cliSiteKey: string; supportsRemoveBookmark: boolean; + minIntervalMinutes: number; + jitterSeconds: number; + recommendedIntervalLabel: string; } export interface BookmarkRecord { diff --git a/apps/web/src/pages/WatcherPage.tsx b/apps/web/src/pages/WatcherPage.tsx index 0afde6f..ccc72fd 100644 --- a/apps/web/src/pages/WatcherPage.tsx +++ b/apps/web/src/pages/WatcherPage.tsx @@ -121,6 +121,10 @@ export const WatcherPage = () => { () => watchers.find((watcher) => watcher.id === selectedWatcherId) ?? null, [selectedWatcherId, watchers] ); + const selectedTrackerDefinition = useMemo( + () => watcherTrackers.find((entry) => entry.key === tracker) ?? null, + [tracker, watcherTrackers] + ); const load = async () => { const [trackersRes, categoriesRes, watchersRes, summaryRes, itemsRes] = @@ -229,6 +233,17 @@ export const WatcherPage = () => { }); 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); try { if (selectedWatcher) { @@ -629,6 +644,16 @@ export const WatcherPage = () => { + {selectedTrackerDefinition ? ( +
+
+ {selectedTrackerDefinition.label} icin minimum izleme araligi: {selectedTrackerDefinition.minIntervalMinutes} dakika +
+
+ Onerilen kullanim: {selectedTrackerDefinition.recommendedIntervalLabel}. Scheduler sabit bot paterni olusmaması icin calisma zamanina otomatik jitter ekler. +
+
+ ) : null}