cross-seed 7.0.0-1 → 7.0.0-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Result.d.ts +27 -0
- package/dist/Result.js +64 -0
- package/dist/Result.js.map +1 -0
- package/dist/action.d.ts +34 -0
- package/dist/action.js +694 -0
- package/dist/action.js.map +1 -0
- package/dist/arr.d.ts +31 -0
- package/dist/arr.js +267 -0
- package/dist/arr.js.map +1 -0
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +28 -0
- package/dist/auth.js.map +1 -0
- package/dist/clients/Deluge.d.ts +153 -0
- package/dist/clients/Deluge.js +698 -0
- package/dist/clients/Deluge.js.map +1 -0
- package/dist/clients/QBittorrent.d.ts +218 -0
- package/dist/clients/QBittorrent.js +785 -0
- package/dist/clients/QBittorrent.js.map +1 -0
- package/dist/clients/RTorrent.d.ts +43 -0
- package/dist/clients/RTorrent.js +657 -0
- package/dist/clients/RTorrent.js.map +1 -0
- package/dist/clients/TorrentClient.d.ts +108 -0
- package/dist/clients/TorrentClient.js +341 -0
- package/dist/clients/TorrentClient.js.map +1 -0
- package/dist/clients/Transmission.d.ts +43 -0
- package/dist/clients/Transmission.js +404 -0
- package/dist/clients/Transmission.js.map +1 -0
- package/dist/cmd.d.ts +2 -0
- package/dist/cmd.js +128 -0
- package/dist/cmd.js.map +1 -0
- package/dist/configSchema.d.ts +1 -0
- package/dist/configSchema.js +2 -0
- package/dist/configSchema.js.map +1 -0
- package/dist/configuration.d.ts +63 -0
- package/dist/configuration.js +321 -0
- package/dist/configuration.js.map +1 -0
- package/dist/constants.d.ts +108 -0
- package/dist/constants.js +251 -0
- package/dist/constants.js.map +1 -0
- package/dist/dataFiles.d.ts +8 -0
- package/dist/dataFiles.js +223 -0
- package/dist/dataFiles.js.map +1 -0
- package/dist/db.d.ts +3 -0
- package/dist/db.js +216 -0
- package/dist/db.js.map +1 -0
- package/dist/dbConfig.d.ts +4 -0
- package/dist/dbConfig.js +67 -0
- package/dist/dbConfig.js.map +1 -0
- package/dist/decide.d.ts +25 -0
- package/dist/decide.js +553 -0
- package/dist/decide.js.map +1 -0
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +24 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -0
- package/dist/indexers.d.ts +105 -0
- package/dist/indexers.js +248 -0
- package/dist/indexers.js.map +1 -0
- package/dist/inject.d.ts +2 -0
- package/dist/inject.js +594 -0
- package/dist/inject.js.map +1 -0
- package/dist/jobs.d.ts +29 -0
- package/dist/jobs.js +151 -0
- package/dist/jobs.js.map +1 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.js +157 -0
- package/dist/logger.js.map +1 -0
- package/dist/migrations/00-initialSchema.d.ts +9 -0
- package/dist/migrations/00-initialSchema.js +30 -0
- package/dist/migrations/00-initialSchema.js.map +1 -0
- package/dist/migrations/01-jobs.d.ts +9 -0
- package/dist/migrations/01-jobs.js +12 -0
- package/dist/migrations/01-jobs.js.map +1 -0
- package/dist/migrations/02-timestamps.d.ts +9 -0
- package/dist/migrations/02-timestamps.js +21 -0
- package/dist/migrations/02-timestamps.js.map +1 -0
- package/dist/migrations/03-rateLimits.d.ts +9 -0
- package/dist/migrations/03-rateLimits.js +14 -0
- package/dist/migrations/03-rateLimits.js.map +1 -0
- package/dist/migrations/04-auth.d.ts +9 -0
- package/dist/migrations/04-auth.js +13 -0
- package/dist/migrations/04-auth.js.map +1 -0
- package/dist/migrations/05-caps.d.ts +9 -0
- package/dist/migrations/05-caps.js +16 -0
- package/dist/migrations/05-caps.js.map +1 -0
- package/dist/migrations/06-uniqueDecisions.d.ts +9 -0
- package/dist/migrations/06-uniqueDecisions.js +29 -0
- package/dist/migrations/06-uniqueDecisions.js.map +1 -0
- package/dist/migrations/07-limits.d.ts +9 -0
- package/dist/migrations/07-limits.js +12 -0
- package/dist/migrations/07-limits.js.map +1 -0
- package/dist/migrations/08-rss.d.ts +9 -0
- package/dist/migrations/08-rss.js +15 -0
- package/dist/migrations/08-rss.js.map +1 -0
- package/dist/migrations/09-clientAndDataSearchees.d.ts +9 -0
- package/dist/migrations/09-clientAndDataSearchees.js +34 -0
- package/dist/migrations/09-clientAndDataSearchees.js.map +1 -0
- package/dist/migrations/10-indexerNameAudioBookCaps.d.ts +9 -0
- package/dist/migrations/10-indexerNameAudioBookCaps.js +18 -0
- package/dist/migrations/10-indexerNameAudioBookCaps.js.map +1 -0
- package/dist/migrations/11-trackers.d.ts +9 -0
- package/dist/migrations/11-trackers.js +38 -0
- package/dist/migrations/11-trackers.js.map +1 -0
- package/dist/migrations/12-user-auth.d.ts +9 -0
- package/dist/migrations/12-user-auth.js +22 -0
- package/dist/migrations/12-user-auth.js.map +1 -0
- package/dist/migrations/13-settings.d.ts +9 -0
- package/dist/migrations/13-settings.js +23 -0
- package/dist/migrations/13-settings.js.map +1 -0
- package/dist/migrations/14-indexer-enabled-flag.d.ts +9 -0
- package/dist/migrations/14-indexer-enabled-flag.js +12 -0
- package/dist/migrations/14-indexer-enabled-flag.js.map +1 -0
- package/dist/migrations/15-remove-url-unique-constraint.d.ts +9 -0
- package/dist/migrations/15-remove-url-unique-constraint.js +14 -0
- package/dist/migrations/15-remove-url-unique-constraint.js.map +1 -0
- package/dist/migrations/16-prune-inactive-indexers.d.ts +9 -0
- package/dist/migrations/16-prune-inactive-indexers.js +17 -0
- package/dist/migrations/16-prune-inactive-indexers.js.map +1 -0
- package/dist/migrations/migrations.d.ts +13 -0
- package/dist/migrations/migrations.js +41 -0
- package/dist/migrations/migrations.js.map +1 -0
- package/dist/parseTorrent.d.ts +53 -0
- package/dist/parseTorrent.js +128 -0
- package/dist/parseTorrent.js.map +1 -0
- package/dist/pipeline.d.ts +41 -0
- package/dist/pipeline.js +574 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/preFilter.d.ts +25 -0
- package/dist/preFilter.js +250 -0
- package/dist/preFilter.js.map +1 -0
- package/dist/problems/linking.d.ts +2 -0
- package/dist/problems/linking.js +80 -0
- package/dist/problems/linking.js.map +1 -0
- package/dist/problems/path.d.ts +22 -0
- package/dist/problems/path.js +96 -0
- package/dist/problems/path.js.map +1 -0
- package/dist/problems.d.ts +13 -0
- package/dist/problems.js +48 -0
- package/dist/problems.js.map +1 -0
- package/dist/pushNotifier.d.ts +19 -0
- package/dist/pushNotifier.js +137 -0
- package/dist/pushNotifier.js.map +1 -0
- package/dist/routes/baseApi.d.ts +2 -0
- package/dist/routes/baseApi.js +354 -0
- package/dist/routes/baseApi.js.map +1 -0
- package/dist/routes/indexerApi.d.ts +6 -0
- package/dist/routes/indexerApi.js +165 -0
- package/dist/routes/indexerApi.js.map +1 -0
- package/dist/routes/staticFrontendPlugin.d.ts +4 -0
- package/dist/routes/staticFrontendPlugin.js +61 -0
- package/dist/routes/staticFrontendPlugin.js.map +1 -0
- package/dist/runtimeConfig.d.ts +6 -0
- package/dist/runtimeConfig.js +27 -0
- package/dist/runtimeConfig.js.map +1 -0
- package/dist/searchee.d.ts +108 -0
- package/dist/searchee.js +689 -0
- package/dist/searchee.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +65 -0
- package/dist/server.js.map +1 -0
- package/dist/services/indexerService.d.ts +96 -0
- package/dist/services/indexerService.js +287 -0
- package/dist/services/indexerService.js.map +1 -0
- package/dist/sessionCookies.d.ts +5 -0
- package/dist/sessionCookies.js +27 -0
- package/dist/sessionCookies.js.map +1 -0
- package/dist/startup.d.ts +25 -0
- package/dist/startup.js +157 -0
- package/dist/startup.js.map +1 -0
- package/dist/torrent.d.ts +69 -0
- package/dist/torrent.js +641 -0
- package/dist/torrent.js.map +1 -0
- package/dist/torznab.d.ts +60 -0
- package/dist/torznab.js +711 -0
- package/dist/torznab.js.map +1 -0
- package/dist/trpc/fastifyAdapter.d.ts +2 -0
- package/dist/trpc/fastifyAdapter.js +9 -0
- package/dist/trpc/fastifyAdapter.js.map +1 -0
- package/dist/trpc/index.d.ts +49 -0
- package/dist/trpc/index.js +53 -0
- package/dist/trpc/index.js.map +1 -0
- package/dist/trpc/routers/auth.d.ts +43 -0
- package/dist/trpc/routers/auth.js +116 -0
- package/dist/trpc/routers/auth.js.map +1 -0
- package/dist/trpc/routers/clients.d.ts +21 -0
- package/dist/trpc/routers/clients.js +65 -0
- package/dist/trpc/routers/clients.js.map +1 -0
- package/dist/trpc/routers/health.d.ts +14 -0
- package/dist/trpc/routers/health.js +20 -0
- package/dist/trpc/routers/health.js.map +1 -0
- package/dist/trpc/routers/index.d.ts +391 -0
- package/dist/trpc/routers/index.js +23 -0
- package/dist/trpc/routers/index.js.map +1 -0
- package/dist/trpc/routers/indexers.d.ts +75 -0
- package/dist/trpc/routers/indexers.js +79 -0
- package/dist/trpc/routers/indexers.js.map +1 -0
- package/dist/trpc/routers/jobs.d.ts +33 -0
- package/dist/trpc/routers/jobs.js +84 -0
- package/dist/trpc/routers/jobs.js.map +1 -0
- package/dist/trpc/routers/logs.d.ts +27 -0
- package/dist/trpc/routers/logs.js +91 -0
- package/dist/trpc/routers/logs.js.map +1 -0
- package/dist/trpc/routers/searchees.d.ts +51 -0
- package/dist/trpc/routers/searchees.js +156 -0
- package/dist/trpc/routers/searchees.js.map +1 -0
- package/dist/trpc/routers/settings.d.ts +83 -0
- package/dist/trpc/routers/settings.js +92 -0
- package/dist/trpc/routers/settings.js.map +1 -0
- package/dist/trpc/routers/stats.d.ts +42 -0
- package/dist/trpc/routers/stats.js +102 -0
- package/dist/trpc/routers/stats.js.map +1 -0
- package/dist/userAuth.d.ts +21 -0
- package/dist/userAuth.js +86 -0
- package/dist/userAuth.js.map +1 -0
- package/dist/utils/authUtils.d.ts +10 -0
- package/dist/utils/authUtils.js +24 -0
- package/dist/utils/authUtils.js.map +1 -0
- package/dist/utils/logWatcher.d.ts +28 -0
- package/dist/utils/logWatcher.js +218 -0
- package/dist/utils/logWatcher.js.map +1 -0
- package/dist/utils/object.d.ts +1 -0
- package/dist/utils/object.js +4 -0
- package/dist/utils/object.js.map +1 -0
- package/dist/utils.d.ts +175 -0
- package/dist/utils.js +660 -0
- package/dist/utils.js.map +1 -0
- package/package.json +2 -2
package/dist/torznab.js
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ms from "ms";
|
|
3
|
+
import { inspect } from "util";
|
|
4
|
+
import xml2js from "xml2js";
|
|
5
|
+
import { arrIdsEqual, formatFoundIds, getRelevantArrIds, scanAllArrsForMedia, } from "./arr.js";
|
|
6
|
+
import { CALIBRE_INDEXNUM_REGEX, EP_REGEX, MediaType, SEASON_REGEX, UNKNOWN_TRACKER, USER_AGENT, } from "./constants.js";
|
|
7
|
+
import { db } from "./db.js";
|
|
8
|
+
import { ALL_CAPS, getAllIndexers, getEnabledIndexers, IndexerStatus, updateIndexerCapsById, updateIndexerStatus, } from "./indexers.js";
|
|
9
|
+
import { Label, logger } from "./logger.js";
|
|
10
|
+
import { getRuntimeConfig } from "./runtimeConfig.js";
|
|
11
|
+
import { getMediaType, getSearcheeNewestFileAge, } from "./searchee.js";
|
|
12
|
+
import { cleanTitle, comparing, extractInt, formatAsList, getAnimeQueries, getLogString, getVideoQueries, humanReadableDate, isTruthy, nMsAgo, reformatTitleForSearching, stripExtension, wait, } from "./utils.js";
|
|
13
|
+
function parseTorznabResults(xml, indexerId) {
|
|
14
|
+
const items = xml?.rss?.channel?.[0]?.item;
|
|
15
|
+
if (!items || !Array.isArray(items)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return items.map((item) => ({
|
|
19
|
+
guid: item.guid[0],
|
|
20
|
+
name: item.title[0],
|
|
21
|
+
tracker: (item?.prowlarrindexer?.[0]?._ ??
|
|
22
|
+
item?.jackettindexer?.[0]?._ ??
|
|
23
|
+
item?.indexer?.[0]?._ ??
|
|
24
|
+
UNKNOWN_TRACKER).trim(),
|
|
25
|
+
link: item.link[0],
|
|
26
|
+
size: Number(item.size[0]),
|
|
27
|
+
pubDate: new Date(item.pubDate[0]).getTime(),
|
|
28
|
+
indexerId,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
function parseTorznabCaps(xml) {
|
|
32
|
+
const limits = xml?.caps?.limits?.map((limit) => ({
|
|
33
|
+
default: parseInt(limit.$.default),
|
|
34
|
+
max: parseInt(limit.$.max),
|
|
35
|
+
}))[0] ?? { default: 100, max: 100 };
|
|
36
|
+
const searchingSection = xml?.caps?.searching?.[0];
|
|
37
|
+
const isAvailable = (searchTechnique) => searchTechnique?.[0]?.$?.available === "yes";
|
|
38
|
+
function getSupportedIds(searchTechnique) {
|
|
39
|
+
const supportedParamsStr = searchTechnique?.[0]?.$?.supportedParams;
|
|
40
|
+
const supportedIds = supportedParamsStr
|
|
41
|
+
?.split(",")
|
|
42
|
+
?.filter((token) => token.includes("id")) ?? [];
|
|
43
|
+
return {
|
|
44
|
+
tvdbId: supportedIds.includes("tvdbid"),
|
|
45
|
+
tmdbId: supportedIds.includes("tmdbid"),
|
|
46
|
+
imdbId: supportedIds.includes("imdbid"),
|
|
47
|
+
tvMazeId: supportedIds.includes("tvmazeid"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const categoryCaps = xml?.caps?.categories?.[0]?.category;
|
|
51
|
+
function getCatCaps(item) {
|
|
52
|
+
const categories = (item ?? []).map((cat) => ({
|
|
53
|
+
id: parseInt(cat.$.id),
|
|
54
|
+
name: cat.$.name.toLowerCase(),
|
|
55
|
+
}));
|
|
56
|
+
const caps = {
|
|
57
|
+
movie: false,
|
|
58
|
+
tv: false,
|
|
59
|
+
anime: false,
|
|
60
|
+
xxx: false,
|
|
61
|
+
audio: false,
|
|
62
|
+
book: false,
|
|
63
|
+
additional: false,
|
|
64
|
+
};
|
|
65
|
+
const keys = Object.keys(caps);
|
|
66
|
+
keys.splice(keys.indexOf("additional"), 1);
|
|
67
|
+
for (const { id, name } of categories) {
|
|
68
|
+
let isAdditional = true;
|
|
69
|
+
for (const cap of keys) {
|
|
70
|
+
if (name.includes(cap)) {
|
|
71
|
+
caps[cap] = true;
|
|
72
|
+
isAdditional = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (isAdditional && id < 100000 && (id < 8000 || id > 8999)) {
|
|
76
|
+
caps.additional = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return caps;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
search: Boolean(isAvailable(searchingSection?.search)),
|
|
83
|
+
tvSearch: Boolean(isAvailable(searchingSection?.["tv-search"])),
|
|
84
|
+
movieSearch: Boolean(isAvailable(searchingSection?.["movie-search"])),
|
|
85
|
+
musicSearch: Boolean(isAvailable(searchingSection?.["music-search"])),
|
|
86
|
+
audioSearch: Boolean(isAvailable(searchingSection?.["audio-search"])),
|
|
87
|
+
bookSearch: Boolean(isAvailable(searchingSection?.["book-search"])),
|
|
88
|
+
movieIdSearch: getSupportedIds(searchingSection?.["movie-search"]),
|
|
89
|
+
tvIdSearch: getSupportedIds(searchingSection?.["tv-search"]),
|
|
90
|
+
categories: getCatCaps(categoryCaps),
|
|
91
|
+
limits,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function createTorznabSearchQueries(searchee, mediaType, caps, parsedMedia) {
|
|
95
|
+
const stem = stripExtension(searchee.title);
|
|
96
|
+
const relevantIds = parsedMedia
|
|
97
|
+
? getRelevantArrIds(caps, parsedMedia)
|
|
98
|
+
: {};
|
|
99
|
+
const useIds = Object.values(relevantIds).some(isTruthy);
|
|
100
|
+
if (mediaType === MediaType.EPISODE && caps.tvSearch) {
|
|
101
|
+
const match = stem.match(EP_REGEX);
|
|
102
|
+
const groups = match.groups;
|
|
103
|
+
let query;
|
|
104
|
+
if (!useIds) {
|
|
105
|
+
query = reformatTitleForSearching(stem);
|
|
106
|
+
if (!query.length)
|
|
107
|
+
query = stem;
|
|
108
|
+
}
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
t: "tvsearch",
|
|
112
|
+
q: query,
|
|
113
|
+
season: groups.season ? extractInt(groups.season) : groups.year,
|
|
114
|
+
ep: groups.episode
|
|
115
|
+
? extractInt(groups.episode)
|
|
116
|
+
: `${groups.month}/${groups.day}`,
|
|
117
|
+
...relevantIds,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
else if (mediaType === MediaType.SEASON && caps.tvSearch) {
|
|
122
|
+
const match = stem.match(SEASON_REGEX);
|
|
123
|
+
const groups = match.groups;
|
|
124
|
+
let query;
|
|
125
|
+
if (!useIds) {
|
|
126
|
+
query = reformatTitleForSearching(stem);
|
|
127
|
+
if (!query.length)
|
|
128
|
+
query = stem;
|
|
129
|
+
}
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
t: "tvsearch",
|
|
133
|
+
q: query,
|
|
134
|
+
season: extractInt(groups.season),
|
|
135
|
+
...relevantIds,
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
else if (mediaType === MediaType.MOVIE && caps.movieSearch) {
|
|
140
|
+
let query;
|
|
141
|
+
if (!useIds) {
|
|
142
|
+
query = reformatTitleForSearching(stem);
|
|
143
|
+
if (!query.length)
|
|
144
|
+
query = stem;
|
|
145
|
+
}
|
|
146
|
+
return [
|
|
147
|
+
{
|
|
148
|
+
t: "movie",
|
|
149
|
+
q: query,
|
|
150
|
+
...relevantIds,
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
if (useIds && caps.tvSearch && parsedMedia?.series) {
|
|
155
|
+
const eps = parsedMedia.episodes;
|
|
156
|
+
const season = eps.length > 0 ? eps[0].seasonNumber : undefined;
|
|
157
|
+
const ep = eps.length === 1 ? eps[0].episodeNumber : undefined;
|
|
158
|
+
return [
|
|
159
|
+
{ t: "tvsearch", q: undefined, season, ep, ...relevantIds },
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
else if (useIds && caps.movieSearch && parsedMedia?.movie) {
|
|
163
|
+
return [{ t: "movie", q: undefined, ...relevantIds }];
|
|
164
|
+
}
|
|
165
|
+
else if (mediaType === MediaType.ANIME) {
|
|
166
|
+
return getAnimeQueries(stem).map((animeQuery) => ({
|
|
167
|
+
t: "search",
|
|
168
|
+
q: animeQuery,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
else if (mediaType === MediaType.VIDEO) {
|
|
172
|
+
return getVideoQueries(stem).map((videoQuery) => ({
|
|
173
|
+
t: "search",
|
|
174
|
+
q: videoQuery,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
else if (mediaType === MediaType.BOOK && searchee.path) {
|
|
178
|
+
const query = cleanTitle(stem.replace(CALIBRE_INDEXNUM_REGEX, ""));
|
|
179
|
+
return [
|
|
180
|
+
{
|
|
181
|
+
t: "search",
|
|
182
|
+
q: query.length ? query : stem,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
const query = cleanTitle(stem);
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
t: "search",
|
|
190
|
+
q: query.length ? query : stem,
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Used to calculate what searchees should share the same cached search.
|
|
196
|
+
* @param searchee - The searchee object to generate the search string for.
|
|
197
|
+
* @returns The search string that would be used to query indexers.
|
|
198
|
+
*/
|
|
199
|
+
export async function getSearchString(searchee) {
|
|
200
|
+
const mediaType = getMediaType(searchee);
|
|
201
|
+
const params = (await createTorznabSearchQueries(searchee, mediaType, ALL_CAPS))[0];
|
|
202
|
+
const season = params.season !== undefined ? `.S${params.season}` : "";
|
|
203
|
+
const ep = params.ep !== undefined ? `.E${params.ep}` : "";
|
|
204
|
+
return `${params.q}${season}${ep}`.toLowerCase();
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Useful for calculating the number of unique queries with just a searchee name.
|
|
208
|
+
* @param name - The name of the searchee.
|
|
209
|
+
* @return The search string that would be used to query indexers.
|
|
210
|
+
*/
|
|
211
|
+
export async function estimateSearchString(name) {
|
|
212
|
+
const searchee = {
|
|
213
|
+
name,
|
|
214
|
+
title: name,
|
|
215
|
+
length: 1,
|
|
216
|
+
files: [{ name: "a.mkv", path: "a.mkv", length: 1 }],
|
|
217
|
+
};
|
|
218
|
+
return getSearchString(searchee);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Only for testing purposes. (createTorznabSearchQueries now accepts searchee
|
|
222
|
+
* instead of stem (title))
|
|
223
|
+
*
|
|
224
|
+
* Logs the queries that would be sent to indexers for id and non-id searches.
|
|
225
|
+
* Ensure that item exists in your arr for the id search example.
|
|
226
|
+
* Ensure mediaType is what cross-seed would actually parse the item as.
|
|
227
|
+
*/
|
|
228
|
+
export async function logQueries(searcheeTitle, mediaType) {
|
|
229
|
+
const stem = stripExtension(searcheeTitle);
|
|
230
|
+
logger.info(
|
|
231
|
+
// @ts-expect-error needs conversion to use searchee instead of stem
|
|
232
|
+
`RAW: ${inspect(await createTorznabSearchQueries(stem, mediaType, ALL_CAPS))}`);
|
|
233
|
+
const res = await scanAllArrsForMedia(searcheeTitle, mediaType);
|
|
234
|
+
const parsedMedia = res.orElse(undefined);
|
|
235
|
+
logger.info(
|
|
236
|
+
// @ts-expect-error needs conversion to use searchee instead of stem
|
|
237
|
+
`ID: ${inspect(await createTorznabSearchQueries(stem, mediaType, ALL_CAPS, parsedMedia))}`);
|
|
238
|
+
}
|
|
239
|
+
export function indexerDoesSupportMediaType(mediaType, i) {
|
|
240
|
+
switch (mediaType) {
|
|
241
|
+
case MediaType.EPISODE:
|
|
242
|
+
case MediaType.SEASON:
|
|
243
|
+
return i.tvSearchCap || i.categories.xxx;
|
|
244
|
+
case MediaType.MOVIE:
|
|
245
|
+
return i.movieSearchCap || i.categories.xxx;
|
|
246
|
+
case MediaType.ANIME:
|
|
247
|
+
case MediaType.VIDEO:
|
|
248
|
+
return (i.movieSearchCap ||
|
|
249
|
+
i.tvSearchCap ||
|
|
250
|
+
i.categories.movie ||
|
|
251
|
+
i.categories.tv ||
|
|
252
|
+
i.categories.anime ||
|
|
253
|
+
i.categories.xxx);
|
|
254
|
+
case MediaType.AUDIO:
|
|
255
|
+
return i.audioSearchCap || i.musicSearchCap || i.categories.audio;
|
|
256
|
+
case MediaType.BOOK:
|
|
257
|
+
return i.bookSearchCap || i.categories.book;
|
|
258
|
+
case MediaType.OTHER:
|
|
259
|
+
return i.categories.additional;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export async function* rssPager(indexer, timeSinceLastRun) {
|
|
263
|
+
const limit = indexer.limits.max;
|
|
264
|
+
const lastSeenGuid = (await db("rss")
|
|
265
|
+
.where({ indexer_id: indexer.id })
|
|
266
|
+
.select("last_seen_guid")
|
|
267
|
+
.first())?.last_seen_guid;
|
|
268
|
+
let newLastSeenGuid = lastSeenGuid;
|
|
269
|
+
let pageBackUntil = 0;
|
|
270
|
+
const maxPage = 10;
|
|
271
|
+
const maxCandidates = 10000;
|
|
272
|
+
let totalCandidates = 0;
|
|
273
|
+
let i = -1;
|
|
274
|
+
while (++i < maxPage) {
|
|
275
|
+
let currentPageCandidates;
|
|
276
|
+
try {
|
|
277
|
+
currentPageCandidates = (await makeRequest({
|
|
278
|
+
indexerId: indexer.id,
|
|
279
|
+
baseUrl: indexer.url,
|
|
280
|
+
apikey: indexer.apikey,
|
|
281
|
+
query: { t: "search", q: "", limit, offset: i * limit },
|
|
282
|
+
name: indexer.name,
|
|
283
|
+
}, Label.RSS)).sort(comparing((candidate) => -candidate.pubDate));
|
|
284
|
+
if (!currentPageCandidates.length) {
|
|
285
|
+
(i === 0 ? logger.error : logger.verbose)({
|
|
286
|
+
label: Label.RSS,
|
|
287
|
+
message: `Paging ${indexer.name ?? indexer.url} stopped at page ${i + 1}: no results returned`,
|
|
288
|
+
});
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
if (i === 0) {
|
|
292
|
+
newLastSeenGuid = currentPageCandidates[0].guid;
|
|
293
|
+
pageBackUntil =
|
|
294
|
+
currentPageCandidates[0].pubDate - timeSinceLastRun;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
logger.error({
|
|
299
|
+
label: Label.RSS,
|
|
300
|
+
message: `Paging ${indexer.name ?? indexer.url} stopped at page ${i + 1}: ${e.message}`,
|
|
301
|
+
});
|
|
302
|
+
logger.debug(e);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
let newCandidates = [];
|
|
306
|
+
let found = false;
|
|
307
|
+
for (const candidate of currentPageCandidates) {
|
|
308
|
+
if (candidate.guid === lastSeenGuid) {
|
|
309
|
+
found = true;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
newCandidates.push(candidate);
|
|
313
|
+
}
|
|
314
|
+
if (!found) {
|
|
315
|
+
newCandidates = newCandidates.filter((candidate) => candidate.pubDate >= pageBackUntil);
|
|
316
|
+
}
|
|
317
|
+
if (!newCandidates.length) {
|
|
318
|
+
logger.verbose({
|
|
319
|
+
label: Label.RSS,
|
|
320
|
+
message: `Paging ${indexer.name ?? indexer.url} stopped at page ${i + 1}: no new candidates`,
|
|
321
|
+
});
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
logger.verbose({
|
|
325
|
+
label: Label.RSS,
|
|
326
|
+
message: `${newCandidates.length} new candidates on ${indexer.name ?? indexer.url} page ${i + 1}`,
|
|
327
|
+
});
|
|
328
|
+
totalCandidates += newCandidates.length;
|
|
329
|
+
yield* newCandidates;
|
|
330
|
+
if (newCandidates.length !== currentPageCandidates.length) {
|
|
331
|
+
logger.verbose({
|
|
332
|
+
label: Label.RSS,
|
|
333
|
+
message: `Paging ${indexer.name ?? indexer.url} stopped at page ${i + 1}: reached last seen guid or pageBackUntil ${humanReadableDate(pageBackUntil)}`,
|
|
334
|
+
});
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
if (totalCandidates >= maxCandidates) {
|
|
338
|
+
logger.verbose({
|
|
339
|
+
label: Label.RSS,
|
|
340
|
+
message: `Paging ${indexer.name ?? indexer.url} stopped at page ${i + 1}: reached max candidates ${maxCandidates}`,
|
|
341
|
+
});
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
await db("rss")
|
|
346
|
+
.insert({ indexer_id: indexer.id, last_seen_guid: newLastSeenGuid })
|
|
347
|
+
.onConflict("indexer_id")
|
|
348
|
+
.merge(["last_seen_guid"]);
|
|
349
|
+
if (i >= maxPage) {
|
|
350
|
+
logger.verbose({
|
|
351
|
+
label: Label.RSS,
|
|
352
|
+
message: `Paging ${indexer.name ?? indexer.url} stopped: reached ${maxPage} pages`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
export async function queryRssFeeds(lastRun, indexers) {
|
|
357
|
+
const timeSinceLastRun = Date.now() - lastRun;
|
|
358
|
+
return indexers.map((indexer) => rssPager(indexer, timeSinceLastRun));
|
|
359
|
+
}
|
|
360
|
+
export async function searchTorznab(searchee, indexerSearchCount, cachedSearch, progress, options) {
|
|
361
|
+
const mediaType = getMediaType(searchee);
|
|
362
|
+
const { indexersToSearch, parsedMedia } = await getAndLogIndexers(searchee, indexerSearchCount, cachedSearch, mediaType, progress, options);
|
|
363
|
+
const indexerCandidates = await makeRequests(indexersToSearch, searchee.label, async (indexer) => {
|
|
364
|
+
const caps = {
|
|
365
|
+
search: indexer.searchCap,
|
|
366
|
+
tvSearch: indexer.tvSearchCap,
|
|
367
|
+
movieSearch: indexer.movieSearchCap,
|
|
368
|
+
musicSearch: indexer.musicSearchCap,
|
|
369
|
+
audioSearch: indexer.audioSearchCap,
|
|
370
|
+
bookSearch: indexer.bookSearchCap,
|
|
371
|
+
tvIdSearch: indexer.tvIdCaps,
|
|
372
|
+
movieIdSearch: indexer.movieIdCaps,
|
|
373
|
+
categories: indexer.categories,
|
|
374
|
+
limits: indexer.limits,
|
|
375
|
+
};
|
|
376
|
+
return createTorznabSearchQueries(searchee, mediaType, caps, parsedMedia);
|
|
377
|
+
});
|
|
378
|
+
return [...cachedSearch.indexerCandidates, ...indexerCandidates];
|
|
379
|
+
}
|
|
380
|
+
export function assembleUrl(baseUrl, apikey, params) {
|
|
381
|
+
const url = new URL(baseUrl);
|
|
382
|
+
const searchParams = new URLSearchParams();
|
|
383
|
+
searchParams.set("apikey", apikey);
|
|
384
|
+
for (const [key, value] of Object.entries(params)) {
|
|
385
|
+
if (value != null)
|
|
386
|
+
searchParams.set(key, value);
|
|
387
|
+
}
|
|
388
|
+
url.search = searchParams.toString();
|
|
389
|
+
return url.toString();
|
|
390
|
+
}
|
|
391
|
+
async function fetchCaps(indexer) {
|
|
392
|
+
let response;
|
|
393
|
+
try {
|
|
394
|
+
response = await fetch(assembleUrl(indexer.url, indexer.apikey, { t: "caps" }), {
|
|
395
|
+
headers: { "User-Agent": USER_AGENT },
|
|
396
|
+
signal: AbortSignal.timeout(ms("1 minute")),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
catch (e) {
|
|
400
|
+
const error = new Error(`${indexer.name ?? indexer.url} failed to respond, check verbose logs: ${e.message}`);
|
|
401
|
+
logger.error(error.message);
|
|
402
|
+
logger.debug(e);
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
const responseText = await response.text();
|
|
406
|
+
if (!response.ok) {
|
|
407
|
+
let error;
|
|
408
|
+
if (response.status === 429) {
|
|
409
|
+
error = new Error(`${indexer.name ?? indexer.url} was rate limited when fetching caps${indexer.retryAfter && indexer.retryAfter > Date.now() ? `, snoozing until ${humanReadableDate(indexer.retryAfter)}` : ""}`);
|
|
410
|
+
logger.warn({
|
|
411
|
+
label: Label.TORZNAB,
|
|
412
|
+
message: error.message,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
else if (response.status === 401) {
|
|
416
|
+
error = new Error(`${indexer.name ?? indexer.url} returned 401 Unauthorized when fetching caps, check your apikey (all torznab entries use the Prowlarr/Jackett apikey)`);
|
|
417
|
+
logger.error(error.message);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
error = new Error(`${indexer.name ?? indexer.url} responded with code ${response.status} when fetching caps, check verbose logs`);
|
|
421
|
+
logger.error(error.message);
|
|
422
|
+
}
|
|
423
|
+
logger.debug(`Response body first 1000 characters: ${responseText.substring(0, 1000)}`);
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
const parsedXml = await xml2js.parseStringPromise(responseText);
|
|
428
|
+
return parseTorznabCaps(parsedXml);
|
|
429
|
+
}
|
|
430
|
+
catch (_) {
|
|
431
|
+
const error = new Error(`${indexer.name ?? indexer.url} responded with invalid XML when fetching caps, check verbose logs`);
|
|
432
|
+
logger.error(error.message);
|
|
433
|
+
logger.debug(`Response body first 1000 characters: ${responseText.substring(0, 1000)}`);
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function collateOutcomes(correlators, outcomes) {
|
|
438
|
+
return outcomes.reduce(({ rejected, fulfilled }, cur, idx) => {
|
|
439
|
+
if (cur.status === "rejected") {
|
|
440
|
+
rejected.push([correlators[idx], cur.reason]);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
fulfilled.push([correlators[idx], cur.value]);
|
|
444
|
+
}
|
|
445
|
+
return { rejected, fulfilled };
|
|
446
|
+
}, { rejected: [], fulfilled: [] });
|
|
447
|
+
}
|
|
448
|
+
export async function updateCaps() {
|
|
449
|
+
const indexers = await getAllIndexers();
|
|
450
|
+
const outcomes = await Promise.allSettled(indexers.map((indexer) => fetchCaps(indexer)));
|
|
451
|
+
const { fulfilled } = collateOutcomes(indexers.map((i) => i.id), outcomes);
|
|
452
|
+
for (const [indexerId, caps] of fulfilled) {
|
|
453
|
+
await updateIndexerCapsById(indexerId, caps);
|
|
454
|
+
}
|
|
455
|
+
for (const indexer of indexers) {
|
|
456
|
+
if (!indexer.categories) {
|
|
457
|
+
logger.error({
|
|
458
|
+
label: Label.TORZNAB,
|
|
459
|
+
message: `Indexer ${indexer.name ?? indexer.url} failed to fetch caps`,
|
|
460
|
+
});
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const supported = [];
|
|
464
|
+
const unsupported = [];
|
|
465
|
+
for (const mediaType of Object.keys(MediaType)) {
|
|
466
|
+
if (indexerDoesSupportMediaType(MediaType[mediaType], indexer)) {
|
|
467
|
+
supported.push(mediaType);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
unsupported.push(mediaType);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
logger.verbose({
|
|
474
|
+
label: Label.TORZNAB,
|
|
475
|
+
message: `${indexer.name ?? indexer.url} MediaTypes: Supported [${supported.join(", ")}] | Unsupported [${unsupported.join(", ")}]`,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Snooze indexers based on the response headers and status code.
|
|
481
|
+
* specifically for a search, probably not applicable to a caps fetch.
|
|
482
|
+
* @returns the retry time in ms
|
|
483
|
+
*/
|
|
484
|
+
async function onResponseNotOk(response, indexerId, indexerName) {
|
|
485
|
+
const retryAfterSeconds = Number(response.headers.get("Retry-After"));
|
|
486
|
+
const retryAfter = !Number.isNaN(retryAfterSeconds)
|
|
487
|
+
? Date.now() + ms(`${retryAfterSeconds} seconds`)
|
|
488
|
+
: response.status === 429
|
|
489
|
+
? Date.now() + ms("1 hour")
|
|
490
|
+
: Date.now() + ms("10 minutes");
|
|
491
|
+
await updateIndexerStatus(response.status === 429
|
|
492
|
+
? IndexerStatus.RATE_LIMITED
|
|
493
|
+
: IndexerStatus.UNKNOWN_ERROR, retryAfter, [indexerId], new Set([indexerName]));
|
|
494
|
+
return retryAfter;
|
|
495
|
+
}
|
|
496
|
+
async function makeRequest(request, searcheeLabel) {
|
|
497
|
+
const { searchTimeout } = getRuntimeConfig();
|
|
498
|
+
const url = assembleUrl(request.baseUrl, request.apikey, request.query);
|
|
499
|
+
const abortSignal = typeof searchTimeout === "number"
|
|
500
|
+
? AbortSignal.timeout(searchTimeout)
|
|
501
|
+
: undefined;
|
|
502
|
+
logger.verbose({
|
|
503
|
+
label: searcheeLabel,
|
|
504
|
+
message: `Querying ${request.name ?? request.indexerId} at ${request.baseUrl} with ${inspect(request.query)}`,
|
|
505
|
+
});
|
|
506
|
+
const response = await fetch(url, {
|
|
507
|
+
headers: { "User-Agent": USER_AGENT },
|
|
508
|
+
signal: abortSignal,
|
|
509
|
+
});
|
|
510
|
+
if (!response.ok) {
|
|
511
|
+
const retryAfter = await onResponseNotOk(response, request.indexerId, request.name ?? request.baseUrl);
|
|
512
|
+
throw new Error(`request failed with code ${response.status}${response.status === 429 ? " due to rate limiting" : ""}, snoozing until ${humanReadableDate(retryAfter)}`);
|
|
513
|
+
}
|
|
514
|
+
const xml = await response.text();
|
|
515
|
+
const torznabResults = await xml2js.parseStringPromise(xml);
|
|
516
|
+
const candidates = parseTorznabResults(torznabResults, request.indexerId);
|
|
517
|
+
if (candidates.length && candidates[0].tracker !== UNKNOWN_TRACKER) {
|
|
518
|
+
await db("indexer")
|
|
519
|
+
.where({ id: request.indexerId })
|
|
520
|
+
.update({ name: candidates[0].tracker });
|
|
521
|
+
}
|
|
522
|
+
return candidates;
|
|
523
|
+
}
|
|
524
|
+
async function makeRequests(indexers, searcheeLabel, getQueriesForIndexer) {
|
|
525
|
+
const requests = [];
|
|
526
|
+
for (const indexer of indexers) {
|
|
527
|
+
const queries = await getQueriesForIndexer(indexer);
|
|
528
|
+
requests.push(...queries.map((query) => ({
|
|
529
|
+
indexerId: indexer.id,
|
|
530
|
+
baseUrl: indexer.url,
|
|
531
|
+
apikey: indexer.apikey,
|
|
532
|
+
query,
|
|
533
|
+
name: indexer.name,
|
|
534
|
+
})));
|
|
535
|
+
}
|
|
536
|
+
const outcomes = await Promise.allSettled(requests.map((request) => makeRequest(request, searcheeLabel)));
|
|
537
|
+
const { rejected, fulfilled } = collateOutcomes(requests.map((request) => request.indexerId), outcomes);
|
|
538
|
+
for (const [indexerId, reason] of rejected) {
|
|
539
|
+
const indexer = indexers.find((i) => i.id === indexerId);
|
|
540
|
+
logger.warn({
|
|
541
|
+
label: searcheeLabel,
|
|
542
|
+
message: `Failed to reach ${indexer.name ?? indexer.url}: ${reason instanceof Error ? reason.message : reason}`,
|
|
543
|
+
});
|
|
544
|
+
logger.debug(reason);
|
|
545
|
+
}
|
|
546
|
+
return fulfilled.map(([indexerId, results]) => ({
|
|
547
|
+
indexerId,
|
|
548
|
+
candidates: results,
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
async function getAndLogIndexers(searchee, indexerSearchCount, cachedSearch, mediaType, progress, options) {
|
|
552
|
+
const { delay, excludeRecentSearch, excludeOlder, seasonFromEpisodes, searchLimit, } = getRuntimeConfig(options?.configOverride);
|
|
553
|
+
const searcheeLog = getLogString(searchee, chalk.bold.white);
|
|
554
|
+
const mediaTypeLog = chalk.white(mediaType.toUpperCase());
|
|
555
|
+
const allIndexers = await getAllIndexers();
|
|
556
|
+
const enabledIndexers = await getEnabledIndexers();
|
|
557
|
+
// search history for name across all indexers
|
|
558
|
+
const timestampDataSql = await db("searchee")
|
|
559
|
+
.join("timestamp", "searchee.id", "timestamp.searchee_id")
|
|
560
|
+
.join("indexer", "timestamp.indexer_id", "indexer.id")
|
|
561
|
+
.whereIn("indexer.id", allIndexers.map((i) => i.id))
|
|
562
|
+
.andWhere("searchee.name", searchee.title)
|
|
563
|
+
.select({
|
|
564
|
+
indexerId: "indexer.id",
|
|
565
|
+
firstSearched: "timestamp.first_searched",
|
|
566
|
+
lastSearched: "timestamp.last_searched",
|
|
567
|
+
});
|
|
568
|
+
const skipBefore = excludeOlder
|
|
569
|
+
? nMsAgo(excludeOlder)
|
|
570
|
+
: Number.NEGATIVE_INFINITY;
|
|
571
|
+
const skipAfter = excludeRecentSearch
|
|
572
|
+
? nMsAgo(excludeRecentSearch)
|
|
573
|
+
: Number.POSITIVE_INFINITY;
|
|
574
|
+
const isEnsemble = seasonFromEpisodes && !searchee.infoHash && !searchee.path;
|
|
575
|
+
const newestFileAge = isEnsemble
|
|
576
|
+
? await getSearcheeNewestFileAge(searchee)
|
|
577
|
+
: Number.POSITIVE_INFINITY;
|
|
578
|
+
const disabledIndexers = [];
|
|
579
|
+
const timeFilteredIndexers = allIndexers.filter((indexer) => {
|
|
580
|
+
if (indexer.searchCap === false || !indexer.categories)
|
|
581
|
+
return false;
|
|
582
|
+
const entry = timestampDataSql.find((entry) => entry.indexerId === indexer.id);
|
|
583
|
+
if (!entry) {
|
|
584
|
+
if (!enabledIndexers.some((i) => i.id === indexer.id)) {
|
|
585
|
+
if (indexerDoesSupportMediaType(mediaType, indexer)) {
|
|
586
|
+
disabledIndexers.push(indexer);
|
|
587
|
+
}
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
if (isEnsemble &&
|
|
593
|
+
entry.lastSearched &&
|
|
594
|
+
entry.lastSearched < newestFileAge) {
|
|
595
|
+
if (!enabledIndexers.some((i) => i.id === indexer.id)) {
|
|
596
|
+
if (indexerDoesSupportMediaType(mediaType, indexer)) {
|
|
597
|
+
disabledIndexers.push(indexer);
|
|
598
|
+
}
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
if (entry.firstSearched && entry.firstSearched < skipBefore) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
if (entry.lastSearched && entry.lastSearched > skipAfter) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
if (!enabledIndexers.some((i) => i.id === indexer.id)) {
|
|
610
|
+
if (indexerDoesSupportMediaType(mediaType, indexer)) {
|
|
611
|
+
disabledIndexers.push(indexer);
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
return true;
|
|
616
|
+
});
|
|
617
|
+
const indexersToUse = timeFilteredIndexers.filter((indexer) => indexerDoesSupportMediaType(mediaType, indexer));
|
|
618
|
+
const disabledMsg = disabledIndexers.length
|
|
619
|
+
? `Skipping searching for ${searcheeLog} on temporarily disabled indexers [${disabledIndexers.map((i) => i.name ?? i.url).join(", ")}]`
|
|
620
|
+
: null;
|
|
621
|
+
// Invalidate cache if searchStr or ids is different
|
|
622
|
+
let shouldScanArr = true;
|
|
623
|
+
let parsedMedia;
|
|
624
|
+
const searchStr = await getSearchString(searchee);
|
|
625
|
+
if (cachedSearch.q === searchStr) {
|
|
626
|
+
shouldScanArr = false;
|
|
627
|
+
const res = await scanAllArrsForMedia(searchee.title, mediaType);
|
|
628
|
+
parsedMedia = res.orElse(undefined);
|
|
629
|
+
const ids = parsedMedia?.movie ?? parsedMedia?.series;
|
|
630
|
+
if (!arrIdsEqual(ids, cachedSearch.ids)) {
|
|
631
|
+
cachedSearch.indexerCandidates.length = 0;
|
|
632
|
+
cachedSearch.ids = ids;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
cachedSearch.q = searchStr;
|
|
637
|
+
cachedSearch.indexerCandidates.length = 0;
|
|
638
|
+
cachedSearch.ids = undefined; // Don't prematurely get ids if skipping
|
|
639
|
+
}
|
|
640
|
+
const searchLimitedIndexers = [];
|
|
641
|
+
const indexersToSearch = indexersToUse.filter((indexer) => {
|
|
642
|
+
if (cachedSearch.indexerCandidates.some((candidates) => candidates.indexerId === indexer.id)) {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
if (!searchLimit)
|
|
646
|
+
return true;
|
|
647
|
+
if (!indexerSearchCount.has(indexer.id)) {
|
|
648
|
+
indexerSearchCount.set(indexer.id, 0);
|
|
649
|
+
}
|
|
650
|
+
if (indexerSearchCount.get(indexer.id) >= searchLimit) {
|
|
651
|
+
searchLimitedIndexers.push(indexer);
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
indexerSearchCount.set(indexer.id, indexerSearchCount.get(indexer.id) + 1);
|
|
655
|
+
return true;
|
|
656
|
+
});
|
|
657
|
+
const searchLimitedMsg = searchLimitedIndexers.length
|
|
658
|
+
? `Skipping searching for ${searcheeLog} due to search limit on [${searchLimitedIndexers.map((i) => i.name ?? i.url).join(", ")}]`
|
|
659
|
+
: null;
|
|
660
|
+
if (!indexersToSearch.length && !cachedSearch.indexerCandidates.length) {
|
|
661
|
+
cachedSearch.q = null; // Won't scan arrs for multiple skips in a row
|
|
662
|
+
const filteringCauses = [
|
|
663
|
+
enabledIndexers.length > timeFilteredIndexers.length &&
|
|
664
|
+
"timestamps",
|
|
665
|
+
timeFilteredIndexers.length > indexersToUse.length && "category",
|
|
666
|
+
disabledIndexers.length && "temporarily disabled indexers",
|
|
667
|
+
searchLimitedIndexers.length && "searchLimit",
|
|
668
|
+
].filter(isTruthy);
|
|
669
|
+
const reasonStr = filteringCauses.length
|
|
670
|
+
? ` (filtered by ${formatAsList(filteringCauses, { sort: true })})`
|
|
671
|
+
: "";
|
|
672
|
+
logger.info({
|
|
673
|
+
label: searchee.label,
|
|
674
|
+
message: `${progress}Skipped searching on indexers for ${searcheeLog}${reasonStr} | MediaType: ${mediaTypeLog} | IDs: N/A`,
|
|
675
|
+
});
|
|
676
|
+
if (searchLimitedMsg) {
|
|
677
|
+
logger.verbose({
|
|
678
|
+
label: searchee.label,
|
|
679
|
+
message: searchLimitedMsg,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
if (disabledMsg) {
|
|
683
|
+
logger.verbose({ label: searchee.label, message: disabledMsg });
|
|
684
|
+
}
|
|
685
|
+
return { indexersToSearch };
|
|
686
|
+
}
|
|
687
|
+
if (shouldScanArr) {
|
|
688
|
+
const res = await scanAllArrsForMedia(searchee.title, mediaType);
|
|
689
|
+
parsedMedia = res.orElse(undefined);
|
|
690
|
+
cachedSearch.ids = parsedMedia?.movie ?? parsedMedia?.series;
|
|
691
|
+
}
|
|
692
|
+
const idsStr = cachedSearch.ids ? formatFoundIds(cachedSearch.ids) : "NONE";
|
|
693
|
+
if (indexersToSearch.length) {
|
|
694
|
+
const waitUntil = cachedSearch.lastSearch + ms(`${delay} seconds`);
|
|
695
|
+
if (Date.now() < waitUntil)
|
|
696
|
+
await wait(waitUntil - Date.now());
|
|
697
|
+
cachedSearch.lastSearch = Date.now();
|
|
698
|
+
}
|
|
699
|
+
logger.info({
|
|
700
|
+
label: searchee.label,
|
|
701
|
+
message: `${progress}Searching for ${searcheeLog} | MediaType: ${mediaTypeLog} | IDs: ${idsStr}`,
|
|
702
|
+
});
|
|
703
|
+
if (searchLimitedMsg) {
|
|
704
|
+
logger.verbose({ label: searchee.label, message: searchLimitedMsg });
|
|
705
|
+
}
|
|
706
|
+
if (disabledMsg) {
|
|
707
|
+
logger.verbose({ label: searchee.label, message: disabledMsg });
|
|
708
|
+
}
|
|
709
|
+
return { indexersToSearch, parsedMedia };
|
|
710
|
+
}
|
|
711
|
+
//# sourceMappingURL=torznab.js.map
|