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",
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",
},
];

View File

@@ -72,6 +72,9 @@ export const listScraperTrackers = async () => {
label: item.label,
cliSiteKey: item.key,
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 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);

View File

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

View File

@@ -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 = () => {
</Select>
</label>
</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">
<span className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">

View File

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