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",
|
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",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user