cross-seed 6.0.0-3 → 6.0.0-30

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.
Files changed (67) hide show
  1. package/dist/Result.js +17 -11
  2. package/dist/Result.js.map +1 -1
  3. package/dist/action.js +158 -47
  4. package/dist/action.js.map +1 -1
  5. package/dist/arr.js +200 -0
  6. package/dist/arr.js.map +1 -0
  7. package/dist/clients/Deluge.js +129 -71
  8. package/dist/clients/Deluge.js.map +1 -1
  9. package/dist/clients/QBittorrent.js +74 -58
  10. package/dist/clients/QBittorrent.js.map +1 -1
  11. package/dist/clients/RTorrent.js +12 -10
  12. package/dist/clients/RTorrent.js.map +1 -1
  13. package/dist/clients/TorrentClient.js.map +1 -1
  14. package/dist/clients/Transmission.js +10 -10
  15. package/dist/clients/Transmission.js.map +1 -1
  16. package/dist/cmd.js +16 -16
  17. package/dist/cmd.js.map +1 -1
  18. package/dist/config.template.cjs +83 -46
  19. package/dist/config.template.cjs.map +1 -1
  20. package/dist/configSchema.js +97 -13
  21. package/dist/configSchema.js.map +1 -1
  22. package/dist/configuration.js +3 -0
  23. package/dist/configuration.js.map +1 -1
  24. package/dist/constants.js +100 -6
  25. package/dist/constants.js.map +1 -1
  26. package/dist/dataFiles.js +4 -5
  27. package/dist/dataFiles.js.map +1 -1
  28. package/dist/decide.js +263 -158
  29. package/dist/decide.js.map +1 -1
  30. package/dist/diff.js +12 -2
  31. package/dist/diff.js.map +1 -1
  32. package/dist/errors.js +5 -2
  33. package/dist/errors.js.map +1 -1
  34. package/dist/indexers.js +31 -4
  35. package/dist/indexers.js.map +1 -1
  36. package/dist/jobs.js +2 -0
  37. package/dist/jobs.js.map +1 -1
  38. package/dist/logger.js +19 -5
  39. package/dist/logger.js.map +1 -1
  40. package/dist/migrations/05-caps.js +16 -0
  41. package/dist/migrations/05-caps.js.map +1 -0
  42. package/dist/migrations/06-uniqueDecisions.js +29 -0
  43. package/dist/migrations/06-uniqueDecisions.js.map +1 -0
  44. package/dist/migrations/migrations.js +11 -1
  45. package/dist/migrations/migrations.js.map +1 -1
  46. package/dist/parseTorrent.js +5 -0
  47. package/dist/parseTorrent.js.map +1 -1
  48. package/dist/pipeline.js +181 -85
  49. package/dist/pipeline.js.map +1 -1
  50. package/dist/preFilter.js +130 -52
  51. package/dist/preFilter.js.map +1 -1
  52. package/dist/pushNotifier.js +4 -2
  53. package/dist/pushNotifier.js.map +1 -1
  54. package/dist/runtimeConfig.js.map +1 -1
  55. package/dist/searchee.js +203 -13
  56. package/dist/searchee.js.map +1 -1
  57. package/dist/server.js +22 -15
  58. package/dist/server.js.map +1 -1
  59. package/dist/startup.js +2 -0
  60. package/dist/startup.js.map +1 -1
  61. package/dist/torrent.js +84 -39
  62. package/dist/torrent.js.map +1 -1
  63. package/dist/torznab.js +285 -95
  64. package/dist/torznab.js.map +1 -1
  65. package/dist/utils.js +155 -31
  66. package/dist/utils.js.map +1 -1
  67. package/package.json +3 -1
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,cAAc,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAElD,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC,IAAI,CAAC;AAChD,MAAM,CAAC,MAAM,eAAe,GAAG,cAAc,CAAC,OAAO,CAAC;AACtD,MAAM,CAAC,MAAM,UAAU,GAAG,aAAa,eAAe,EAAE,CAAC;AACzD,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC;AACxC,MAAM,CAAC,MAAM,uBAAuB,GAAG,aAAa,CAAC;AAErD,MAAM,CAAC,MAAM,QAAQ,GACpB,mNAAmN,CAAC;AACrN,MAAM,CAAC,MAAM,YAAY,GACxB,4FAA4F,CAAC;AAC9F,MAAM,CAAC,MAAM,WAAW,GACvB,uEAAuE,CAAC;AACzE,MAAM,CAAC,MAAM,WAAW,GACvB,sTAAsT,CAAC;AACxT,MAAM,CAAC,MAAM,mBAAmB,GAC/B,6GAA6G,CAAC;AAE/G,MAAM,CAAC,MAAM,mBAAmB,GAC/B,mEAAmE,CAAC;AAErE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;AAEhE,MAAM,CAAC,MAAM,qBAAqB,GACjC,+CAA+C,CAAC;AAEjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAC;AAEpD,MAAM,CAAN,IAAY,MAGX;AAHD,WAAY,MAAM;IACjB,uBAAa,CAAA;IACb,2BAAiB,CAAA;AAClB,CAAC,EAHW,MAAM,KAAN,MAAM,QAGjB;AAED,MAAM,CAAN,IAAY,eAKX;AALD,WAAY,eAAe;IAC1B,uCAAoB,CAAA;IACpB,sCAAmB,CAAA;IACnB,oDAAiC,CAAA;IACjC,gEAA6C,CAAA;AAC9C,CAAC,EALW,eAAe,KAAf,eAAe,QAK1B;AAED,MAAM,CAAN,IAAY,UAEX;AAFD,WAAY,UAAU;IACrB,6BAAe,CAAA;AAChB,CAAC,EAFW,UAAU,KAAV,UAAU,QAErB;AAID,MAAM,CAAN,IAAY,QAaX;AAbD,WAAY,QAAQ;IACnB,2BAAe,CAAA;IACf,+CAAmC,CAAA;IACnC,2CAA+B,CAAA;IAC/B,2CAA+B,CAAA;IAC/B,iDAAqC,CAAA;IACrC,+CAAmC,CAAA;IACnC,yCAA6B,CAAA;IAC7B,iEAAqD,CAAA;IACrD,qDAAyC,CAAA;IACzC,6DAAiD,CAAA;IACjD,+CAAmC,CAAA;IACnC,6DAAiD,CAAA;AAClD,CAAC,EAbW,QAAQ,KAAR,QAAQ,QAanB;AAED,MAAM,CAAN,IAAY,SAIX;AAJD,WAAY,SAAS;IACpB,0BAAa,CAAA;IACb,4BAAe,CAAA;IACf,gCAAmB,CAAA;AACpB,CAAC,EAJW,SAAS,KAAT,SAAS,QAIpB;AAED,MAAM,CAAN,IAAY,QAGX;AAHD,WAAY,QAAQ;IACnB,+BAAmB,CAAA;IACnB,iCAAqB,CAAA;AACtB,CAAC,EAHW,QAAQ,KAAR,QAAQ,QAGnB;AAED,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACzC,QAAQ;IACR,OAAO;IACP,MAAM;IACN,OAAO;IACP,aAAa;IACb,UAAU;CACV,CAAC"}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"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"}
package/dist/dataFiles.js CHANGED
@@ -1,15 +1,14 @@
1
1
  import { readdirSync, statSync } from "fs";
2
2
  import { basename, extname, join } from "path";
3
- import { IGNORED_FOLDERS_SUBSTRINGS, IGNORED_FOLDERS_REGEX, VIDEO_EXTENSIONS, } from "./constants.js";
3
+ import { IGNORED_FOLDERS_SUBSTRINGS, VIDEO_EXTENSIONS } from "./constants.js";
4
4
  import { getRuntimeConfig } from "./runtimeConfig.js";
5
5
  function shouldIgnorePathHeuristically(root, isDir) {
6
- const folderBaseName = basename(root);
6
+ const searchBasename = basename(root);
7
7
  if (isDir) {
8
- return (IGNORED_FOLDERS_SUBSTRINGS.includes(folderBaseName.toLowerCase()) ||
9
- IGNORED_FOLDERS_REGEX.test(folderBaseName));
8
+ return IGNORED_FOLDERS_SUBSTRINGS.includes(searchBasename.toLowerCase());
10
9
  }
11
10
  else {
12
- return !VIDEO_EXTENSIONS.includes(extname(folderBaseName));
11
+ return !VIDEO_EXTENSIONS.includes(extname(searchBasename));
13
12
  }
14
13
  }
15
14
  export function findPotentialNestedRoots(root, depth, isDirHint) {
@@ -1 +1 @@
1
- {"version":3,"file":"dataFiles.js","sourceRoot":"","sources":["../src/dataFiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC/C,OAAO,EACN,0BAA0B,EAC1B,qBAAqB,EACrB,gBAAgB,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,SAAS,6BAA6B,CAAC,IAAY,EAAE,KAAc;IAClE,MAAM,cAAc,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,KAAK,EAAE,CAAC;QACX,OAAO,CACN,0BAA0B,CAAC,QAAQ,CAAC,cAAc,CAAC,WAAW,EAAE,CAAC;YACjE,qBAAqB,CAAC,IAAI,CAAC,cAAc,CAAC,CAC1C,CAAC;IACH,CAAC;SAAM,CAAC;QACP,OAAO,CAAC,gBAAgB,CAAC,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC;IAC5D,CAAC;AACF,CAAC;AACD,MAAM,UAAU,wBAAwB,CACvC,IAAY,EACZ,KAAa,EACb,SAAmB;IAEnB,MAAM,KAAK,GACV,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IACpE,IAAI,KAAK,IAAI,CAAC,IAAI,6BAA6B,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,CAAC;IACX,CAAC;IACD,wCAAwC;SACnC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC;QAC7B,MAAM,cAAc,GAAG,WAAW,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAClE,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CACxD,wBAAwB,CACvB,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,EACvB,KAAK,GAAG,CAAC,EACT,MAAM,CAAC,WAAW,EAAE,CACpB,CACD,CAAC;QACF,OAAO,CAAC,IAAI,EAAE,GAAG,cAAc,CAAC,CAAC;IAClC,CAAC;SAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,CAAC;IACf,CAAC;AACF,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC3C,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAAC;IACtD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CACnC,WAAW,CAAC,OAAO,CAAC;SAClB,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;SACtC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,wBAAwB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CACjE,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"dataFiles.js","sourceRoot":"","sources":["../src/dataFiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC/C,OAAO,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,SAAS,6BAA6B,CAAC,IAAY,EAAE,KAAc;IAClE,MAAM,cAAc,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,KAAK,EAAE,CAAC;QACX,OAAO,0BAA0B,CAAC,QAAQ,CACzC,cAAc,CAAC,WAAW,EAAE,CAC5B,CAAC;IACH,CAAC;SAAM,CAAC;QACP,OAAO,CAAC,gBAAgB,CAAC,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC;IAC5D,CAAC;AACF,CAAC;AACD,MAAM,UAAU,wBAAwB,CACvC,IAAY,EACZ,KAAa,EACb,SAAmB;IAEnB,MAAM,KAAK,GACV,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IACpE,IAAI,KAAK,IAAI,CAAC,IAAI,6BAA6B,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,CAAC;IACX,CAAC;IACD,wCAAwC;SACnC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC;QAC7B,MAAM,cAAc,GAAG,WAAW,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAClE,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CACxD,wBAAwB,CACvB,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,EACvB,KAAK,GAAG,CAAC,EACT,MAAM,CAAC,WAAW,EAAE,CACpB,CACD,CAAC;QACF,OAAO,CAAC,IAAI,EAAE,GAAG,cAAc,CAAC,CAAC;IAClC,CAAC;SAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,CAAC;IACf,CAAC;AACF,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC3C,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAAC;IACtD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CACnC,WAAW,CAAC,OAAO,CAAC;SAClB,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;SACtC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,wBAAwB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CACjE,CAAC;AACH,CAAC"}
package/dist/decide.js CHANGED
@@ -1,20 +1,17 @@
1
- import { existsSync, writeFileSync } from "fs";
1
+ import { existsSync, statSync, utimesSync, writeFileSync } from "fs";
2
2
  import path from "path";
3
3
  import { appDir } from "./configuration.js";
4
- import { Decision, MatchMode, RELEASE_GROUP_REGEX, REPACK_PROPER_REGEX, TORRENT_CACHE_FOLDER, } from "./constants.js";
4
+ import { Decision, isAnyMatchedDecision, isStaticDecision, MatchMode, RELEASE_GROUP_REGEX, REPACK_PROPER_REGEX, RES_STRICT_REGEX, parseSource, TORRENT_CACHE_FOLDER, ANIME_GROUP_REGEX, SEASON_REGEX, } from "./constants.js";
5
5
  import { db } from "./db.js";
6
6
  import { Label, logger } from "./logger.js";
7
- import { findBlockedStringInReleaseMaybe } from "./preFilter.js";
7
+ import { Metafile } from "./parseTorrent.js";
8
+ import { findBlockedStringInReleaseMaybe, isSingleEpisode, } from "./preFilter.js";
8
9
  import { getRuntimeConfig } from "./runtimeConfig.js";
9
- import { parseTorrentFromFilename, parseTorrentFromURL, SnatchError, } from "./torrent.js";
10
- import { humanReadableSize } from "./utils.js";
11
- const createReasonLogger = (Title, tracker, name) => (decision, cached, searchee, candidate) => {
12
- function logReason(reason) {
13
- logger.verbose({
14
- label: Label.DECIDE,
15
- message: `${name} - no match for ${tracker} torrent ${Title} - ${reason}`,
16
- });
17
- }
10
+ import { parseTorrentFromFilename, snatch, SnatchError } from "./torrent.js";
11
+ import { extractInt, getFuzzySizeFactor, getLogString, getMediaType, getMinSizeRatio, sanitizeInfoHash, stripExtension, wait, } from "./utils.js";
12
+ import ms from "ms";
13
+ function logDecision(searchee, candidate, decision, metafile, tracker) {
14
+ const { matchMode } = getRuntimeConfig();
18
15
  let reason;
19
16
  switch (decision) {
20
17
  case Decision.MATCH_PARTIAL:
@@ -23,8 +20,17 @@ const createReasonLogger = (Title, tracker, name) => (decision, cached, searchee
23
20
  return;
24
21
  case Decision.MATCH:
25
22
  return;
23
+ case Decision.FUZZY_SIZE_MISMATCH:
24
+ reason = `the total sizes are outside of the fuzzySizeThreshold range: ${Math.abs((candidate.size - searchee.length) / searchee.length).toFixed(3)} > ${getFuzzySizeFactor()}`;
25
+ break;
26
26
  case Decision.SIZE_MISMATCH:
27
- reason = `its size does not match - (${humanReadableSize(searchee.length)} -> ${humanReadableSize(candidate.size)})`;
27
+ reason = `some files are missing or have different sizes${compareFileTreesPartial(metafile, searchee) ? ` (will match in partial match mode)` : ""}`;
28
+ break;
29
+ case Decision.PARTIAL_SIZE_MISMATCH:
30
+ reason = `too many files are missing or have different sizes: torrent progress would be ${(getPartialSizeRatio(metafile, searchee) * 100).toFixed(3)}%`;
31
+ break;
32
+ case Decision.RESOLUTION_MISMATCH:
33
+ reason = `its resolution does not match: ${searchee.title.match(RES_STRICT_REGEX)?.groups?.res} -> ${candidate.name.match(RES_STRICT_REGEX)?.groups?.res}`;
28
34
  break;
29
35
  case Decision.NO_DOWNLOAD_LINK:
30
36
  reason = "it doesn't have a download link";
@@ -35,34 +41,41 @@ const createReasonLogger = (Title, tracker, name) => (decision, cached, searchee
35
41
  case Decision.DOWNLOAD_FAILED:
36
42
  reason = "the torrent file failed to download";
37
43
  break;
44
+ case Decision.MAGNET_LINK:
45
+ reason = "the torrent is a magnet link";
46
+ break;
38
47
  case Decision.INFO_HASH_ALREADY_EXISTS:
39
48
  reason = "the info hash matches a torrent you already have";
40
49
  break;
41
50
  case Decision.FILE_TREE_MISMATCH:
42
- reason = "it has a different file tree";
51
+ reason = `it has a different file tree${matchMode === MatchMode.SAFE ? " (will match in risky or partial match mode)" : ""}`;
43
52
  break;
44
53
  case Decision.RELEASE_GROUP_MISMATCH:
45
- reason = `it has a different release group - (${searchee.name
46
- .match(RELEASE_GROUP_REGEX)?.[0]
47
- ?.trim()} -> ${candidate.name
48
- .match(RELEASE_GROUP_REGEX)?.[0]
49
- ?.trim()})`;
54
+ reason = `it has a different release group: ${stripExtension(searchee.title)
55
+ .match(RELEASE_GROUP_REGEX)
56
+ ?.groups?.group?.trim()} -> ${stripExtension(candidate.name)
57
+ .match(RELEASE_GROUP_REGEX)
58
+ ?.groups?.group?.trim()}`;
50
59
  break;
51
60
  case Decision.PROPER_REPACK_MISMATCH:
52
- reason = `one is a different subsequent release - (${searchee.name.match(REPACK_PROPER_REGEX)?.groups?.type ??
53
- "INITIAL"} -> ${candidate.name.match(REPACK_PROPER_REGEX)?.groups?.type ?? "INITIAL"})`;
61
+ reason = `one is a different subsequent release: ${searchee.title.match(REPACK_PROPER_REGEX)?.groups?.type ??
62
+ "INITIAL"} -> ${candidate.name.match(REPACK_PROPER_REGEX)?.groups?.type ?? "INITIAL"}`;
63
+ break;
64
+ case Decision.SOURCE_MISMATCH:
65
+ reason = `it has a different source: ${parseSource(searchee.title)} -> ${parseSource(candidate.name)}`;
54
66
  break;
55
67
  case Decision.BLOCKED_RELEASE:
56
- reason = `it matches the blocklist - ("${findBlockedStringInReleaseMaybe(searchee, getRuntimeConfig().blockList)}")`;
68
+ reason = `it matches the blocklist: "${findBlockedStringInReleaseMaybe(searchee, getRuntimeConfig().blockList)}"`;
57
69
  break;
58
70
  default:
59
71
  reason = decision;
60
72
  break;
61
73
  }
62
- if (!cached) {
63
- logReason(reason);
64
- }
65
- };
74
+ logger.verbose({
75
+ label: Label.DECIDE,
76
+ message: `${getLogString(searchee)} - no match for ${tracker} torrent ${candidate.name}${metafile ? ` [${sanitizeInfoHash(metafile.infoHash)}]` : ""} - ${reason}`,
77
+ });
78
+ }
66
79
  export function compareFileTrees(candidate, searchee) {
67
80
  const cmp = (elOfA, elOfB) => {
68
81
  const lengthsAreEqual = elOfB.length === elOfA.length;
@@ -72,13 +85,18 @@ export function compareFileTrees(candidate, searchee) {
72
85
  return candidate.files.every((elOfA) => searchee.files.some((elOfB) => cmp(elOfA, elOfB)));
73
86
  }
74
87
  export function compareFileTreesIgnoringNames(candidate, searchee) {
75
- const cmp = (candidate, searchee) => {
76
- return searchee.length === candidate.length;
77
- };
78
- return candidate.files.every((elOfA) => searchee.files.some((elOfB) => cmp(elOfA, elOfB)));
88
+ for (const candidateFile of candidate.files) {
89
+ let matchedSearcheeFiles = searchee.files.filter((searcheeFile) => searcheeFile.length === candidateFile.length);
90
+ if (matchedSearcheeFiles.length > 1) {
91
+ matchedSearcheeFiles = matchedSearcheeFiles.filter((searcheeFile) => searcheeFile.name === candidateFile.name);
92
+ }
93
+ if (matchedSearcheeFiles.length === 0) {
94
+ return false;
95
+ }
96
+ }
97
+ return true;
79
98
  }
80
- export function compareFileTreesPartialIgnoringNames(candidate, searchee) {
81
- const { fuzzySizeThreshold } = getRuntimeConfig();
99
+ export function getPartialSizeRatio(candidate, searchee) {
82
100
  let matchedSizes = 0;
83
101
  for (const candidateFile of candidate.files) {
84
102
  const searcheeHasFileSize = searchee.files.some((searcheeFile) => searcheeFile.length === candidateFile.length);
@@ -86,10 +104,9 @@ export function compareFileTreesPartialIgnoringNames(candidate, searchee) {
86
104
  matchedSizes += candidateFile.length;
87
105
  }
88
106
  }
89
- return matchedSizes / candidate.length >= 1 - fuzzySizeThreshold;
107
+ return matchedSizes / candidate.length;
90
108
  }
91
109
  export function compareFileTreesPartial(candidate, searchee) {
92
- const { fuzzySizeThreshold } = getRuntimeConfig();
93
110
  let matchedSizes = 0;
94
111
  for (const candidateFile of candidate.files) {
95
112
  let matchedSearcheeFiles = searchee.files.filter((searcheeFile) => searcheeFile.length === candidateFile.length);
@@ -102,193 +119,281 @@ export function compareFileTreesPartial(candidate, searchee) {
102
119
  }
103
120
  const totalPieces = Math.ceil(candidate.length / candidate.pieceLength);
104
121
  const availablePieces = Math.floor(matchedSizes / candidate.pieceLength);
105
- return availablePieces / totalPieces >= 1 - fuzzySizeThreshold;
122
+ return availablePieces / totalPieces >= getMinSizeRatio();
106
123
  }
107
- function sizeDoesMatch(resultSize, searchee) {
108
- const { fuzzySizeThreshold } = getRuntimeConfig();
124
+ function fuzzySizeDoesMatch(resultSize, searchee) {
125
+ const fuzzySizeFactor = getFuzzySizeFactor();
109
126
  const { length } = searchee;
110
- const lowerBound = length - fuzzySizeThreshold * length;
111
- const upperBound = length + fuzzySizeThreshold * length;
127
+ const lowerBound = length - fuzzySizeFactor * length;
128
+ const upperBound = length + fuzzySizeFactor * length;
112
129
  return resultSize >= lowerBound && resultSize <= upperBound;
113
130
  }
114
- function releaseVersionDoesMatch(searcheeName, candidateName, matchMode) {
115
- const searcheeVersionType = searcheeName.match(REPACK_PROPER_REGEX);
116
- const candidateVersionType = candidateName.match(REPACK_PROPER_REGEX);
117
- //sets either match arrtype or type to corresponding VersionType
118
- if (searcheeVersionType?.[0].toLowerCase() !==
119
- candidateVersionType?.[0].toLowerCase() ||
120
- (searcheeVersionType?.groups?.arrtype && candidateVersionType)) {
121
- return matchMode !== MatchMode.SAFE;
122
- }
123
- return searcheeVersionType === candidateVersionType;
131
+ function resolutionDoesMatch(searcheeTitle, candidateName) {
132
+ const searcheeRes = searcheeTitle
133
+ .match(RES_STRICT_REGEX)
134
+ ?.groups?.res?.trim()
135
+ ?.toLowerCase();
136
+ const candidateRes = candidateName
137
+ .match(RES_STRICT_REGEX)
138
+ ?.groups?.res?.trim()
139
+ ?.toLowerCase();
140
+ if (!searcheeRes || !candidateRes)
141
+ return true;
142
+ return extractInt(searcheeRes) === extractInt(candidateRes);
124
143
  }
125
- function releaseGroupDoesMatch(searcheeName, candidateName, matchMode) {
126
- const searcheeReleaseGroup = searcheeName
127
- .match(RELEASE_GROUP_REGEX)?.[0]
128
- ?.trim()
144
+ function releaseGroupDoesMatch(searcheeTitle, candidateName) {
145
+ const searcheeReleaseGroup = stripExtension(searcheeTitle)
146
+ .match(RELEASE_GROUP_REGEX)
147
+ ?.groups?.group?.trim()
148
+ ?.toLowerCase();
149
+ const candidateReleaseGroup = stripExtension(candidateName)
150
+ .match(RELEASE_GROUP_REGEX)
151
+ ?.groups?.group?.trim()
152
+ ?.toLowerCase();
153
+ if (!searcheeReleaseGroup || !candidateReleaseGroup) {
154
+ return true; // Pass if missing -GRP
155
+ }
156
+ if (searcheeReleaseGroup.startsWith(candidateReleaseGroup) ||
157
+ candidateReleaseGroup.startsWith(searcheeReleaseGroup)) {
158
+ return true; // -GRP matches
159
+ }
160
+ // Anime naming can cause weird things to match as release groups
161
+ const searcheeAnimeGroup = searcheeTitle
162
+ .match(ANIME_GROUP_REGEX)
163
+ ?.groups?.group?.trim()
129
164
  ?.toLowerCase();
130
- const candidateReleaseGroup = candidateName
131
- .match(RELEASE_GROUP_REGEX)?.[0]
132
- ?.trim()
165
+ const candidateAnimeGroup = candidateName
166
+ .match(ANIME_GROUP_REGEX)
167
+ ?.groups?.group?.trim()
133
168
  ?.toLowerCase();
134
- if (searcheeReleaseGroup === candidateReleaseGroup) {
169
+ if (!searcheeAnimeGroup && !candidateAnimeGroup) {
170
+ return false;
171
+ }
172
+ // Most checks will never get here, below are rare edge cases
173
+ if (searcheeAnimeGroup === candidateAnimeGroup) {
135
174
  return true;
136
175
  }
137
- // if we are unsure, pass in risky or partial mode but fail in safe mode
138
- if (!searcheeReleaseGroup || !candidateReleaseGroup) {
139
- return matchMode !== MatchMode.SAFE;
176
+ if (searcheeAnimeGroup && searcheeAnimeGroup === candidateReleaseGroup) {
177
+ return true;
178
+ }
179
+ if (candidateAnimeGroup && searcheeReleaseGroup === candidateAnimeGroup) {
180
+ return true;
140
181
  }
141
- return searcheeReleaseGroup.startsWith(candidateReleaseGroup);
182
+ return false;
183
+ }
184
+ function sourceDoesMatch(searcheeTitle, candidateName) {
185
+ const searcheeSource = parseSource(searcheeTitle);
186
+ const candidateSource = parseSource(candidateName);
187
+ if (!searcheeSource || !candidateSource)
188
+ return true;
189
+ return searcheeSource === candidateSource;
142
190
  }
143
- async function assessCandidateHelper({ link, size, name }, searchee, hashesToExclude) {
144
- const { matchMode, blockList } = getRuntimeConfig();
191
+ async function assessCandidateHelper(metaOrCandidate, searchee, hashesToExclude) {
192
+ const { blockList, includeSingleEpisodes, matchMode } = getRuntimeConfig();
193
+ // When metaOrCandidate is a Metafile, skip straight to the
194
+ // main matching algorithms as we don't need pre-download filtering.
195
+ const isCandidate = !(metaOrCandidate instanceof Metafile);
196
+ const name = metaOrCandidate.name;
197
+ const size = isCandidate ? metaOrCandidate.size : metaOrCandidate.length;
198
+ if (isCandidate) {
199
+ if (!releaseGroupDoesMatch(searchee.title, name)) {
200
+ return { decision: Decision.RELEASE_GROUP_MISMATCH };
201
+ }
202
+ if (!resolutionDoesMatch(searchee.title, name)) {
203
+ return { decision: Decision.RESOLUTION_MISMATCH };
204
+ }
205
+ if (!sourceDoesMatch(searchee.title, name)) {
206
+ return { decision: Decision.SOURCE_MISMATCH };
207
+ }
208
+ if (size && !fuzzySizeDoesMatch(size, searchee)) {
209
+ return { decision: Decision.FUZZY_SIZE_MISMATCH };
210
+ }
211
+ if (!metaOrCandidate.link) {
212
+ return { decision: Decision.NO_DOWNLOAD_LINK };
213
+ }
214
+ }
145
215
  if (findBlockedStringInReleaseMaybe(searchee, blockList)) {
146
216
  return { decision: Decision.BLOCKED_RELEASE };
147
217
  }
148
- if (size && !sizeDoesMatch(size, searchee)) {
149
- return { decision: Decision.SIZE_MISMATCH };
150
- }
151
- if (!link)
152
- return { decision: Decision.NO_DOWNLOAD_LINK };
153
- if (!releaseGroupDoesMatch(searchee.name, name, matchMode)) {
154
- return { decision: Decision.RELEASE_GROUP_MISMATCH };
218
+ let metafile;
219
+ if (isCandidate) {
220
+ let res = await snatch(metaOrCandidate);
221
+ if (res.isErr()) {
222
+ const e = res.unwrapErr();
223
+ if ([Label.ANNOUNCE, Label.RSS].includes(searchee.label) &&
224
+ ![SnatchError.RATE_LIMITED, SnatchError.MAGNET_LINK].includes(e)) {
225
+ await wait(ms("30 seconds"));
226
+ res = await snatch(metaOrCandidate);
227
+ }
228
+ if (res.isErr()) {
229
+ const err = res.unwrapErr();
230
+ return err === SnatchError.MAGNET_LINK
231
+ ? { decision: Decision.MAGNET_LINK }
232
+ : err === SnatchError.RATE_LIMITED
233
+ ? { decision: Decision.RATE_LIMITED }
234
+ : { decision: Decision.DOWNLOAD_FAILED };
235
+ }
236
+ }
237
+ metafile = res.unwrap();
238
+ cacheTorrentFile(metafile);
239
+ metaOrCandidate.size = metafile.length; // Trackers can be wrong
155
240
  }
156
- if (!releaseVersionDoesMatch(searchee.name, name, matchMode)) {
157
- return { decision: Decision.PROPER_REPACK_MISMATCH };
241
+ else {
242
+ metafile = metaOrCandidate;
158
243
  }
159
- const result = await parseTorrentFromURL(link);
160
- if (result.isErr()) {
161
- return result.unwrapErrOrThrow() === SnatchError.RATE_LIMITED
162
- ? { decision: Decision.RATE_LIMITED }
163
- : { decision: Decision.DOWNLOAD_FAILED };
244
+ if (hashesToExclude.includes(metafile.infoHash)) {
245
+ return { decision: Decision.INFO_HASH_ALREADY_EXISTS, metafile };
164
246
  }
165
- const candidateMeta = result.unwrapOrThrow();
166
- if (hashesToExclude.includes(candidateMeta.infoHash)) {
167
- return { decision: Decision.INFO_HASH_ALREADY_EXISTS };
247
+ // Prevent candidate episodes from matching searchee season packs
248
+ if (!includeSingleEpisodes &&
249
+ SEASON_REGEX.test(searchee.title) &&
250
+ isSingleEpisode(metafile, getMediaType(metafile))) {
251
+ return { decision: Decision.FILE_TREE_MISMATCH, metafile };
168
252
  }
169
- const sizeMatch = compareFileTreesIgnoringNames(candidateMeta, searchee);
170
- const perfectMatch = compareFileTrees(candidateMeta, searchee);
253
+ const perfectMatch = compareFileTrees(metafile, searchee);
171
254
  if (perfectMatch) {
172
- return { decision: Decision.MATCH, metafile: candidateMeta };
255
+ return { decision: Decision.MATCH, metafile };
173
256
  }
174
- if (sizeMatch &&
175
- matchMode !== MatchMode.SAFE &&
176
- searchee.files.length === 1) {
177
- return {
178
- decision: Decision.MATCH_SIZE_ONLY,
179
- metafile: candidateMeta,
180
- };
257
+ const sizeMatch = compareFileTreesIgnoringNames(metafile, searchee);
258
+ if (sizeMatch && matchMode !== MatchMode.SAFE) {
259
+ return { decision: Decision.MATCH_SIZE_ONLY, metafile };
181
260
  }
182
261
  if (matchMode === MatchMode.PARTIAL) {
183
- const partialSizeMatch = compareFileTreesPartialIgnoringNames(candidateMeta, searchee);
262
+ const partialSizeMatch = getPartialSizeRatio(metafile, searchee) >= getMinSizeRatio();
184
263
  if (!partialSizeMatch) {
185
- return { decision: Decision.SIZE_MISMATCH };
264
+ return { decision: Decision.PARTIAL_SIZE_MISMATCH, metafile };
186
265
  }
187
- const partialMatch = compareFileTreesPartial(candidateMeta, searchee);
266
+ const partialMatch = compareFileTreesPartial(metafile, searchee);
188
267
  if (partialMatch) {
189
- return {
190
- decision: Decision.MATCH_PARTIAL,
191
- metafile: candidateMeta,
192
- };
268
+ return { decision: Decision.MATCH_PARTIAL, metafile };
193
269
  }
194
270
  }
195
271
  else if (!sizeMatch) {
196
- return { decision: Decision.SIZE_MISMATCH };
272
+ return { decision: Decision.SIZE_MISMATCH, metafile };
197
273
  }
198
- return { decision: Decision.FILE_TREE_MISMATCH };
274
+ return { decision: Decision.FILE_TREE_MISMATCH, metafile };
199
275
  }
200
276
  function existsInTorrentCache(infoHash) {
201
- return existsSync(path.join(appDir(), TORRENT_CACHE_FOLDER, `${infoHash}.cached.torrent`));
277
+ const torrentPath = path.join(appDir(), TORRENT_CACHE_FOLDER, `${infoHash}.cached.torrent`);
278
+ if (!existsSync(torrentPath))
279
+ return false;
280
+ utimesSync(torrentPath, new Date(), statSync(torrentPath).mtime);
281
+ return true;
202
282
  }
203
283
  async function getCachedTorrentFile(infoHash) {
204
284
  return parseTorrentFromFilename(path.join(appDir(), TORRENT_CACHE_FOLDER, `${infoHash}.cached.torrent`));
205
285
  }
206
286
  function cacheTorrentFile(meta) {
207
- writeFileSync(path.join(appDir(), TORRENT_CACHE_FOLDER, `${meta.infoHash}.cached.torrent`), meta.encode());
287
+ const torrentPath = path.join(appDir(), TORRENT_CACHE_FOLDER, `${meta.infoHash}.cached.torrent`);
288
+ if (existsInTorrentCache(meta.infoHash))
289
+ return;
290
+ writeFileSync(torrentPath, meta.encode());
208
291
  }
209
- async function assessAndSaveResults(result, searchee, guid, infoHashesToExclude) {
210
- const assessment = await assessCandidateHelper(result, searchee, infoHashesToExclude);
211
- if (assessment.decision === Decision.MATCH ||
212
- assessment.decision === Decision.MATCH_SIZE_ONLY ||
213
- assessment.decision === Decision.MATCH_PARTIAL) {
214
- cacheTorrentFile(assessment.metafile);
215
- }
292
+ async function assessAndSaveResults(metaOrCandidate, searchee, guid, infoHashesToExclude, firstSeen) {
293
+ const assessment = await assessCandidateHelper(metaOrCandidate, searchee, infoHashesToExclude);
216
294
  await db.transaction(async (trx) => {
217
- const now = Date.now();
218
295
  const { id } = await trx("searchee")
219
296
  .select("id")
220
- .where({ name: searchee.name })
297
+ .where({ name: searchee.title })
221
298
  .first();
222
- await trx("decision").insert({
299
+ await trx("decision")
300
+ .insert({
223
301
  searchee_id: id,
224
302
  guid: guid,
303
+ info_hash: assessment.metafile?.infoHash ?? null,
225
304
  decision: assessment.decision,
226
- info_hash: assessment.decision === Decision.MATCH ||
227
- assessment.decision === Decision.MATCH_SIZE_ONLY ||
228
- assessment.decision === Decision.MATCH_PARTIAL
229
- ? assessment.metafile.infoHash
230
- : null,
231
- last_seen: now,
232
- first_seen: now,
233
- });
305
+ first_seen: firstSeen,
306
+ last_seen: Date.now(),
307
+ fuzzy_size_factor: getFuzzySizeFactor(),
308
+ })
309
+ .onConflict(["searchee_id", "guid"])
310
+ .merge();
234
311
  });
235
312
  return assessment;
236
313
  }
314
+ /**
315
+ * Some trackers have alt titles which get their own guid but resolve to same torrent
316
+ * @param guid The guid of the candidate
317
+ * @returns The info hash of the torrent if found
318
+ */
319
+ async function fuzzyGuidLookup(guid) {
320
+ if (!guid.includes(".tv/torrent/"))
321
+ return;
322
+ const torrentIdStr = guid.match(/\.tv\/torrent\/(\d+)\/group/)?.[1];
323
+ if (!torrentIdStr)
324
+ return;
325
+ return (await db("decision")
326
+ .select({ infoHash: "info_hash" })
327
+ .where("guid", "like", `%.tv/torrent/${torrentIdStr}/group%`)
328
+ .whereNotNull("info_hash")
329
+ .first())?.infoHash;
330
+ }
237
331
  async function assessCandidateCaching(candidate, searchee, infoHashesToExclude) {
238
332
  const { guid, name, tracker } = candidate;
239
- const logReason = createReasonLogger(name, tracker, searchee.name);
240
333
  const cacheEntry = await db("decision")
241
334
  .select({
242
- decision: "decision.decision",
243
- infoHash: "decision.info_hash",
244
335
  id: "decision.id",
336
+ infoHash: "decision.info_hash",
337
+ decision: "decision.decision",
338
+ firstSeen: "decision.first_seen",
339
+ fuzzySizeFactor: "decision.fuzzy_size_factor",
245
340
  })
246
341
  .join("searchee", "decision.searchee_id", "searchee.id")
247
- .where({ name: searchee.name, guid })
342
+ .where({ name: searchee.title, guid })
248
343
  .first();
344
+ const metaInfoHash = (await db("decision")
345
+ .select({ infoHash: "info_hash" })
346
+ .where({ guid })
347
+ .whereNotNull("info_hash")
348
+ .first())?.infoHash ?? (await fuzzyGuidLookup(guid));
349
+ const metaOrCandidate = metaInfoHash
350
+ ? existsInTorrentCache(metaInfoHash)
351
+ ? await getCachedTorrentFile(metaInfoHash)
352
+ : candidate
353
+ : candidate;
354
+ if (metaOrCandidate instanceof Metafile) {
355
+ logger.verbose({
356
+ label: Label.DECIDE,
357
+ message: `Using cached torrent ${sanitizeInfoHash(metaInfoHash)} for ${tracker} assessment ${name}`,
358
+ });
359
+ candidate.size = metaOrCandidate.length; // Trackers can be wrong
360
+ }
249
361
  let assessment;
250
- if (!cacheEntry?.decision ||
251
- cacheEntry.decision === Decision.DOWNLOAD_FAILED ||
252
- cacheEntry.decision === Decision.RATE_LIMITED) {
253
- assessment = await assessAndSaveResults(candidate, searchee, guid, infoHashesToExclude);
254
- logReason(assessment.decision, false, searchee, candidate);
362
+ if (!cacheEntry?.decision) {
363
+ // New candiate, could be metafile from cache
364
+ assessment = await assessAndSaveResults(metaOrCandidate, searchee, guid, infoHashesToExclude, Date.now());
255
365
  }
256
- else if ((cacheEntry.decision === Decision.MATCH ||
257
- cacheEntry.decision === Decision.MATCH_SIZE_ONLY ||
258
- cacheEntry.decision === Decision.MATCH_PARTIAL) &&
366
+ else if (isAnyMatchedDecision(cacheEntry.decision) &&
259
367
  infoHashesToExclude.includes(cacheEntry.infoHash)) {
260
- // has been added since the last run
368
+ // Already injected fast path, preserve match decision
261
369
  assessment = { decision: Decision.INFO_HASH_ALREADY_EXISTS };
262
- await db("decision")
263
- .where({ id: cacheEntry.id })
264
- .update({ decision: Decision.INFO_HASH_ALREADY_EXISTS });
265
- }
266
- else if ((cacheEntry.decision === Decision.MATCH ||
267
- cacheEntry.decision === Decision.MATCH_SIZE_ONLY ||
268
- cacheEntry.decision === Decision.MATCH_PARTIAL) &&
269
- existsInTorrentCache(cacheEntry.infoHash)) {
270
- // cached match
271
- assessment = {
272
- decision: cacheEntry.decision,
273
- metafile: await getCachedTorrentFile(cacheEntry.infoHash),
274
- };
370
+ await db("decision").where({ id: cacheEntry.id }).update({
371
+ last_seen: Date.now(),
372
+ });
275
373
  }
276
- else if (cacheEntry.decision === Decision.MATCH ||
277
- cacheEntry.decision === Decision.MATCH_SIZE_ONLY ||
278
- cacheEntry.decision === Decision.MATCH_PARTIAL) {
279
- assessment = await assessAndSaveResults(candidate, searchee, guid, infoHashesToExclude);
280
- logReason(assessment.decision, false, searchee, candidate);
374
+ else if (isStaticDecision(cacheEntry.decision)) {
375
+ // These decisions will never change unless we update their logic
376
+ assessment = { decision: cacheEntry.decision };
377
+ await db("decision").where({ id: cacheEntry.id }).update({
378
+ last_seen: Date.now(),
379
+ });
281
380
  }
282
381
  else {
283
- // cached rejection
284
- assessment = { decision: cacheEntry.decision };
285
- logReason(cacheEntry.decision, true, searchee, candidate);
382
+ // Re-assess decisions using Metafile if cached
383
+ if (cacheEntry.decision !== Decision.FUZZY_SIZE_MISMATCH ||
384
+ cacheEntry.fuzzySizeFactor < getFuzzySizeFactor()) {
385
+ assessment = await assessAndSaveResults(metaOrCandidate, searchee, guid, infoHashesToExclude, cacheEntry.firstSeen);
386
+ }
387
+ else {
388
+ assessment = { decision: Decision.FUZZY_SIZE_MISMATCH };
389
+ await db("decision").where({ id: cacheEntry.id }).update({
390
+ last_seen: Date.now(),
391
+ });
392
+ }
286
393
  }
287
- // if previously known
288
- if (cacheEntry) {
289
- await db("decision")
290
- .where({ id: cacheEntry.id })
291
- .update({ last_seen: Date.now() });
394
+ const wasCached = cacheEntry?.decision === assessment.decision;
395
+ if (!wasCached) {
396
+ logDecision(searchee, candidate, assessment.decision, assessment.metafile, tracker);
292
397
  }
293
398
  return assessment;
294
399
  }