feat: watcher icin anti-ban interval ve jitter politikasini ekle
This commit is contained in:
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -72,6 +72,9 @@ export const listScraperTrackers = async () => {
|
||||
label: item.label,
|
||||
cliSiteKey: item.key,
|
||||
supportsRemoveBookmark: true,
|
||||
minIntervalMinutes: 1,
|
||||
jitterSeconds: 0,
|
||||
recommendedIntervalLabel: "Tracker politikasina bakiniz",
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,6 +5,9 @@ export interface TrackerDefinition {
|
||||
label: string;
|
||||
cliSiteKey: string;
|
||||
supportsRemoveBookmark: boolean;
|
||||
minIntervalMinutes: number;
|
||||
jitterSeconds: number;
|
||||
recommendedIntervalLabel: string;
|
||||
}
|
||||
|
||||
export interface BookmarkRecord {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -85,6 +85,9 @@ export interface WatcherTracker {
|
||||
label: string;
|
||||
cliSiteKey: string;
|
||||
supportsRemoveBookmark: boolean;
|
||||
minIntervalMinutes: number;
|
||||
jitterSeconds: number;
|
||||
recommendedIntervalLabel: string;
|
||||
}
|
||||
|
||||
export interface Watcher {
|
||||
|
||||
Reference in New Issue
Block a user