@serifpersia/dango 3.6.7 → 3.6.9

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 (156) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
  5. package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
  6. package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
  8. package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
  9. package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
  11. package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
  12. package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
  16. package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
  17. package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
  18. package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
  19. package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
  20. package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
  21. package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
  22. package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
  23. package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
  24. package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
  25. package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
  26. package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
  27. package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
  28. package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
  29. package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
  30. package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
  31. package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
  32. package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
  33. package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
  34. package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
  35. package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
  36. package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
  37. package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
  38. package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
  39. package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
  40. package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
  41. package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
  42. package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
  43. package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
  44. package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
  45. package/client/dist/index.html +1 -1
  46. package/package.json +3 -5
  47. package/server/dist/config.js +1 -174
  48. package/server/dist/server.js +1068 -289
  49. package/server/dist/shipped-defaults.js +1 -6
  50. package/server/package.json +4 -5
  51. package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
  52. package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
  53. package/client/dist/assets/Music-BGH_0voi.css +0 -1
  54. package/client/dist/assets/Music-CQpyXTLH.js +0 -1
  55. package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
  56. package/client/dist/assets/Radio-DF6g6odd.js +0 -1
  57. package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
  58. package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
  59. package/server/dist/app-auth.js +0 -157
  60. package/server/dist/app-hono.js +0 -598
  61. package/server/dist/db/drizzle.js +0 -16
  62. package/server/dist/db/schema-anime.js +0 -61
  63. package/server/dist/db/schema-asmr.js +0 -13
  64. package/server/dist/db/schema-manga.js +0 -31
  65. package/server/dist/db/schema-tv.js +0 -18
  66. package/server/dist/db.js +0 -217
  67. package/server/dist/discord-gateway.js +0 -385
  68. package/server/dist/discord-rpc.js +0 -413
  69. package/server/dist/github-sync.js +0 -508
  70. package/server/dist/google.js +0 -741
  71. package/server/dist/hono/asmr.js +0 -10
  72. package/server/dist/hono/auth.js +0 -49
  73. package/server/dist/hono/music.js +0 -209
  74. package/server/dist/hono/proxy.js +0 -827
  75. package/server/dist/hono/tv.js +0 -200
  76. package/server/dist/hono/watchlist.js +0 -341
  77. package/server/dist/lib/anilist.js +0 -1583
  78. package/server/dist/lib/anischedule.js +0 -191
  79. package/server/dist/lib/insights.js +0 -185
  80. package/server/dist/lib/ipc.js +0 -23
  81. package/server/dist/lib/kitsu.js +0 -517
  82. package/server/dist/lib/mal-import.js +0 -291
  83. package/server/dist/lib/mal.js +0 -957
  84. package/server/dist/lib/migration.js +0 -310
  85. package/server/dist/lib/offline-db.js +0 -395
  86. package/server/dist/lib/potoken.js +0 -135
  87. package/server/dist/lib/recommendations.service.js +0 -216
  88. package/server/dist/lib/temp-ids.js +0 -3
  89. package/server/dist/lib/tmdb.js +0 -72
  90. package/server/dist/lib/tracker/anilist-tracker.js +0 -308
  91. package/server/dist/lib/tracker/manga-sync.service.js +0 -773
  92. package/server/dist/lib/tracker/sync-state.js +0 -21
  93. package/server/dist/lib/tracker/sync.service.js +0 -476
  94. package/server/dist/lib/translate.js +0 -79
  95. package/server/dist/lib/ump.js +0 -106
  96. package/server/dist/lib/ytmusic.js +0 -162
  97. package/server/dist/logger.js +0 -16
  98. package/server/dist/providers/manga/manga.types.js +0 -1
  99. package/server/dist/providers/provider.interface.js +0 -1
  100. package/server/dist/providers/remote-loader.js +0 -614
  101. package/server/dist/providers/remote-types.js +0 -159
  102. package/server/dist/providers/title-matching.js +0 -226
  103. package/server/dist/providers/tv.types.js +0 -1
  104. package/server/dist/rclone.js +0 -120
  105. package/server/dist/repositories/asmr.repository.js +0 -77
  106. package/server/dist/repositories/insights.repository.js +0 -106
  107. package/server/dist/repositories/library.repository.js +0 -94
  108. package/server/dist/repositories/mal-cache.repository.js +0 -52
  109. package/server/dist/repositories/manga.repository.js +0 -95
  110. package/server/dist/repositories/notifications.repository.js +0 -60
  111. package/server/dist/repositories/queue.repository.js +0 -72
  112. package/server/dist/repositories/settings.repository.js +0 -30
  113. package/server/dist/repositories/shows-meta.repository.js +0 -58
  114. package/server/dist/repositories/temp-show-ids.repository.js +0 -63
  115. package/server/dist/repositories/tv.repository.js +0 -93
  116. package/server/dist/repositories/watched-episodes.repository.js +0 -82
  117. package/server/dist/repositories/watchlist.repository.js +0 -60
  118. package/server/dist/scripts/backfill-episode-duration.js +0 -42
  119. package/server/dist/sync-payload.js +0 -91
  120. package/server/dist/sync.js +0 -657
  121. package/server/dist/trpc/context.js +0 -7
  122. package/server/dist/trpc/errors.js +0 -15
  123. package/server/dist/trpc/index.js +0 -18
  124. package/server/dist/trpc/router.js +0 -49
  125. package/server/dist/trpc/routers/asmr-library.js +0 -112
  126. package/server/dist/trpc/routers/asmr-progress.js +0 -121
  127. package/server/dist/trpc/routers/asmr.js +0 -87
  128. package/server/dist/trpc/routers/auth.js +0 -194
  129. package/server/dist/trpc/routers/continue-watching.js +0 -81
  130. package/server/dist/trpc/routers/data.js +0 -1026
  131. package/server/dist/trpc/routers/discord.js +0 -285
  132. package/server/dist/trpc/routers/discovery.js +0 -11
  133. package/server/dist/trpc/routers/insights.js +0 -14
  134. package/server/dist/trpc/routers/manga-library.js +0 -263
  135. package/server/dist/trpc/routers/manga-progress.js +0 -169
  136. package/server/dist/trpc/routers/manga.js +0 -201
  137. package/server/dist/trpc/routers/music.js +0 -655
  138. package/server/dist/trpc/routers/notifications.js +0 -80
  139. package/server/dist/trpc/routers/progress.js +0 -109
  140. package/server/dist/trpc/routers/queue.js +0 -93
  141. package/server/dist/trpc/routers/radio.js +0 -145
  142. package/server/dist/trpc/routers/settings.js +0 -96
  143. package/server/dist/trpc/routers/tracker.js +0 -213
  144. package/server/dist/trpc/routers/tv-library.js +0 -135
  145. package/server/dist/trpc/routers/tv-progress.js +0 -159
  146. package/server/dist/trpc/routers/tv.js +0 -551
  147. package/server/dist/trpc/routers/watchlist.js +0 -550
  148. package/server/dist/trpc/validation.js +0 -258
  149. package/server/dist/utils/cache.utils.js +0 -25
  150. package/server/dist/utils/cookie.utils.js +0 -13
  151. package/server/dist/utils/db-utils.js +0 -17
  152. package/server/dist/utils/env.utils.js +0 -77
  153. package/server/dist/utils/http.utils.js +0 -82
  154. package/server/dist/utils/request-context.js +0 -2
  155. package/server/dist/utils/security.utils.js +0 -76
  156. /package/client/dist/assets/{Asmr-D1KuQwQy.css → useAsmrPopup-D1KuQwQy.css} +0 -0
@@ -1,159 +0,0 @@
1
- const ID_RE = /^[a-z0-9][a-z0-9-]*$/i;
2
- const SHA_RE = /^[0-9a-f]{64}$/i;
3
- export function parseRegistry(raw) {
4
- if (!raw || typeof raw !== 'object')
5
- throw new Error('registry: not an object');
6
- const r = raw;
7
- if (typeof r.version !== 'number')
8
- throw new Error('registry.version must be a number');
9
- if (!Array.isArray(r.providers))
10
- throw new Error('registry.providers must be an array');
11
- const providers = r.providers.map((p, i) => parseEntry(p, i));
12
- const seen = new Set();
13
- for (const p of providers) {
14
- const key = p.id.toLowerCase();
15
- if (seen.has(key))
16
- throw new Error(`registry: duplicate provider id "${p.id}"`);
17
- seen.add(key);
18
- }
19
- return {
20
- version: r.version,
21
- updatedAt: typeof r.updatedAt === 'string' ? r.updatedAt : undefined,
22
- providers,
23
- };
24
- }
25
- function parseEntry(raw, index) {
26
- if (!raw || typeof raw !== 'object')
27
- throw new Error(`registry.providers[${index}]: not an object`);
28
- const p = raw;
29
- const id = p.id;
30
- const entry = p.entry;
31
- const label = p.label;
32
- const version = p.version;
33
- if (typeof id !== 'string' || !ID_RE.test(id.trim())) {
34
- throw new Error(`registry.providers[${index}].id must match ${ID_RE}`);
35
- }
36
- if (typeof label !== 'string' || !label.trim()) {
37
- throw new Error(`registry.providers[${index}].label must be a non-empty string`);
38
- }
39
- if (typeof version !== 'string' || !version.trim()) {
40
- throw new Error(`registry.providers[${index}].version must be a non-empty string`);
41
- }
42
- const embedBase = p.embedBase;
43
- if (embedBase !== undefined) {
44
- if (typeof embedBase !== 'string' || !/^https:\/\/[^/]+/i.test(embedBase.trim())) {
45
- throw new Error(`registry.providers[${index}].embedBase must be an https URL`);
46
- }
47
- }
48
- if ((typeof entry !== 'string' || !entry.trim()) && embedBase === undefined) {
49
- throw new Error(`registry.providers[${index}].entry must be a non-empty string`);
50
- }
51
- if (p.sha256 !== undefined && (typeof p.sha256 !== 'string' || !SHA_RE.test(p.sha256.trim()))) {
52
- throw new Error(`registry.providers[${index}].sha256 must be 64 hex chars`);
53
- }
54
- if (p.mature !== undefined && typeof p.mature !== 'boolean') {
55
- throw new Error(`registry.providers[${index}].mature must be boolean`);
56
- }
57
- if (p.kind !== undefined &&
58
- p.kind !== 'anime' &&
59
- p.kind !== 'asmr' &&
60
- p.kind !== 'manga' &&
61
- p.kind !== 'tv') {
62
- throw new Error(`registry.providers[${index}].kind must be anime|asmr|manga|tv`);
63
- }
64
- if (p.sub !== undefined && p.sub !== 'soft' && p.sub !== 'hard' && p.sub !== 'mixed') {
65
- throw new Error(`registry.providers[${index}].sub must be soft|hard|mixed`);
66
- }
67
- if (p.tier !== undefined && p.tier !== 'direct' && p.tier !== 'embed' && p.tier !== 'cookie') {
68
- throw new Error(`registry.providers[${index}].tier must be direct|embed|cookie`);
69
- }
70
- let modes;
71
- if (p.modes !== undefined) {
72
- if (!Array.isArray(p.modes))
73
- throw new Error(`registry.providers[${index}].modes must be an array`);
74
- modes = [];
75
- for (const m of p.modes) {
76
- if (m !== 'sub' && m !== 'dub') {
77
- throw new Error(`registry.providers[${index}].modes must contain only sub|dub`);
78
- }
79
- if (!modes.includes(m))
80
- modes.push(m);
81
- }
82
- }
83
- return {
84
- id: id.trim(),
85
- label: label.trim(),
86
- version: version.trim(),
87
- entry: typeof entry === 'string' ? entry.trim() : undefined,
88
- embedBase: typeof embedBase === 'string' ? embedBase.trim().replace(/\/+$/, '') : undefined,
89
- sha256: typeof p.sha256 === 'string' ? p.sha256.trim().toLowerCase() : undefined,
90
- minDango: typeof p.minDango === 'string' ? p.minDango : undefined,
91
- mature: typeof p.mature === 'boolean' ? p.mature : false,
92
- kind: p.kind === 'asmr' || p.kind === 'manga' || p.kind === 'tv' ? p.kind : 'anime',
93
- sub: p.sub,
94
- tier: p.tier,
95
- modes,
96
- browse: parseBrowseCaps(p.browse, index),
97
- proxy: parseProxyRule(p.proxy, index),
98
- auth: p.auth === 'cookie' ? 'cookie' : undefined,
99
- enabledByDefault: p.enabledByDefault === false ? false : true,
100
- };
101
- }
102
- function parseProxyRule(raw, index) {
103
- if (raw === undefined)
104
- return undefined;
105
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
106
- throw new Error(`registry.providers[${index}].proxy must be an object`);
107
- }
108
- const p = raw;
109
- const rule = {};
110
- for (const key of ['hosts', 'suffixes', 'mirrors']) {
111
- if (p[key] === undefined)
112
- continue;
113
- if (!Array.isArray(p[key]) || !p[key].every((v) => typeof v === 'string' && v)) {
114
- throw new Error(`registry.providers[${index}].proxy.${key} must be a non-empty string array`);
115
- }
116
- rule[key] = p[key].map((v) => v.trim().toLowerCase());
117
- }
118
- for (const key of ['referer', 'origin']) {
119
- if (p[key] === undefined)
120
- continue;
121
- if (typeof p[key] !== 'string' || !/^https:\/\//i.test(p[key].trim())) {
122
- throw new Error(`registry.providers[${index}].proxy.${key} must be an https URL`);
123
- }
124
- rule[key] = p[key].trim();
125
- }
126
- for (const key of ['originFromReferer', 'scrape', 'cfCookie', 'embedPatch']) {
127
- if (p[key] === undefined)
128
- continue;
129
- if (typeof p[key] !== 'boolean') {
130
- throw new Error(`registry.providers[${index}].proxy.${key} must be boolean`);
131
- }
132
- rule[key] = p[key];
133
- }
134
- return rule;
135
- }
136
- function parseBrowseCaps(raw, index) {
137
- if (raw === undefined)
138
- return undefined;
139
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
140
- throw new Error(`registry.providers[${index}].browse must be an object`);
141
- }
142
- const b = raw;
143
- const caps = {};
144
- for (const key of ['genre', 'order', 'studio', 'sort']) {
145
- if (b[key] === undefined)
146
- continue;
147
- if (typeof b[key] !== 'boolean') {
148
- throw new Error(`registry.providers[${index}].browse.${key} must be boolean`);
149
- }
150
- caps[key] = b[key];
151
- }
152
- if (b.pageSize !== undefined) {
153
- if (typeof b.pageSize !== 'number' || !Number.isInteger(b.pageSize) || b.pageSize < 1) {
154
- throw new Error(`registry.providers[${index}].browse.pageSize must be a positive integer`);
155
- }
156
- caps.pageSize = b.pageSize;
157
- }
158
- return caps;
159
- }
@@ -1,226 +0,0 @@
1
- function normalizeCompact(s) {
2
- return s.toLowerCase().replace(/[^a-z0-9]/g, '');
3
- }
4
- function tokenize(s) {
5
- return s
6
- .toLowerCase()
7
- .replace(/[^\p{L}\p{N}\s]/gu, ' ')
8
- .split(/\s+/)
9
- .filter((w) => w.length >= 2);
10
- }
11
- function bigramDice(a, b) {
12
- if (a.length < 2 || b.length < 2)
13
- return a === b ? 1 : 0;
14
- const grams = (s) => {
15
- const map = new Map();
16
- for (let i = 0; i < s.length - 1; i++) {
17
- const g = s.slice(i, i + 2);
18
- map.set(g, (map.get(g) || 0) + 1);
19
- }
20
- return map;
21
- };
22
- const A = grams(a);
23
- const B = grams(b);
24
- let totalA = 0;
25
- let totalB = 0;
26
- let shared = 0;
27
- A.forEach((c) => (totalA += c));
28
- B.forEach((c) => (totalB += c));
29
- A.forEach((c, g) => {
30
- const other = B.get(g);
31
- if (other)
32
- shared += Math.min(c, other);
33
- });
34
- return (2 * shared) / (totalA + totalB);
35
- }
36
- const STOPWORDS = new Set([
37
- 'the',
38
- 'animation',
39
- 'no',
40
- 'wa',
41
- 'ga',
42
- 'wo',
43
- 'ni',
44
- 'de',
45
- 'to',
46
- 'e',
47
- 'o',
48
- 'x',
49
- 'ii',
50
- 'yo',
51
- 'ova',
52
- 'oad',
53
- 'hd',
54
- 'ep',
55
- 'episode',
56
- 'season',
57
- 'part',
58
- 'hen',
59
- 'a',
60
- ]);
61
- const SEASON_TOKENS = new Set([
62
- 'season',
63
- '2nd',
64
- '3rd',
65
- '4th',
66
- '5th',
67
- 'ii',
68
- 'iii',
69
- 'iv',
70
- 'v',
71
- 'vi',
72
- 'vii',
73
- 'viii',
74
- 'ix',
75
- 'x',
76
- 'two',
77
- 'three',
78
- 'four',
79
- 'five',
80
- 'six',
81
- 'seven',
82
- 'eight',
83
- 'nine',
84
- 'ten',
85
- ]);
86
- function distinctiveTokensExcludingSeason(s) {
87
- return [...new Set(tokenize(s).filter((w) => !STOPWORDS.has(w) && !SEASON_TOKENS.has(w)))];
88
- }
89
- const ROMAN_TO_NUM = {
90
- ii: 2,
91
- iii: 3,
92
- iv: 4,
93
- v: 5,
94
- vi: 6,
95
- vii: 7,
96
- viii: 8,
97
- ix: 9,
98
- x: 10,
99
- };
100
- const WORD_TO_NUM = {
101
- two: 2,
102
- three: 3,
103
- four: 4,
104
- five: 5,
105
- six: 6,
106
- seven: 7,
107
- eight: 8,
108
- nine: 9,
109
- ten: 10,
110
- };
111
- function detectSeasonMarker(s) {
112
- const t = s.toLowerCase();
113
- // "season 2", "season ii", "season 2nd", "season three"
114
- const seasonNum = t.match(/\bseason\s*(\d+|ii|iii|iv|v|vi|vii|viii|ix|x|two|three|four|five|six|seven|eight|nine|ten)\b/);
115
- if (seasonNum) {
116
- const token = seasonNum[1];
117
- const n = ROMAN_TO_NUM[token];
118
- const w = WORD_TO_NUM[token];
119
- return String(n ?? w ?? token);
120
- }
121
- const ord = t.match(/\b(\d+)(st|nd|rd|th)\b/);
122
- if (ord)
123
- return ord[1];
124
- const trailing = t.match(/\b(ii|iii|iv|v|vi|vii|viii|ix|x)\b(?!\s*(st|nd|rd|th|season))/);
125
- if (trailing)
126
- return String(ROMAN_TO_NUM[trailing[1]]);
127
- const trailingNum = t.match(/\b(\d+)\s*(season)?\s*$/);
128
- if (trailingNum)
129
- return trailingNum[1];
130
- return '';
131
- }
132
- function distinctiveTokens(s) {
133
- return [...new Set(tokenize(s).filter((w) => !STOPWORDS.has(w)))];
134
- }
135
- function titleSimilarity(query, candidate) {
136
- const q = normalizeCompact(query);
137
- const c = normalizeCompact(candidate);
138
- if (!q || !c)
139
- return 0;
140
- if (q === c)
141
- return 1;
142
- let score = bigramDice(q, c);
143
- if (q.startsWith(c) || c.startsWith(q)) {
144
- const prefixScore = 0.5 + 0.5 * (Math.min(q.length, c.length) / Math.max(q.length, c.length));
145
- if (prefixScore > score)
146
- score = prefixScore;
147
- }
148
- const qD = distinctiveTokensExcludingSeason(query);
149
- const cD = distinctiveTokensExcludingSeason(candidate);
150
- if (qD.length > 0 && cD.length >= 2) {
151
- const cSet = new Set(cD);
152
- let hits = 0;
153
- for (const t of qD) {
154
- if (cSet.has(t))
155
- hits++;
156
- }
157
- const coverage = hits / Math.min(qD.length, cD.length);
158
- if (coverage > score)
159
- score = coverage;
160
- }
161
- const qMarker = detectSeasonMarker(query);
162
- if (qMarker) {
163
- const cMarker = detectSeasonMarker(candidate);
164
- if (cMarker === qMarker) {
165
- score += 0.25;
166
- }
167
- else if (!cMarker && score >= 0.5) {
168
- score -= 0.25;
169
- }
170
- }
171
- return score >= 1 ? 0.99 : score;
172
- }
173
- export function pickBestMatch(candidates, targets, minScore = 0.6) {
174
- if (!candidates.length)
175
- return null;
176
- const cleanTargets = targets.filter((t) => !!t && t.trim().length > 0);
177
- if (!cleanTargets.length)
178
- return null;
179
- let best = null;
180
- for (const item of candidates) {
181
- if (!item.title)
182
- continue;
183
- let score = 0;
184
- for (const target of cleanTargets) {
185
- const s = titleSimilarity(target, item.title);
186
- if (s > score)
187
- score = s;
188
- }
189
- if (!best || score > best.score)
190
- best = { item, score };
191
- }
192
- return best && best.score >= minScore ? best : null;
193
- }
194
- export function buildQueryVariants(title, romaji) {
195
- const seen = new Set();
196
- const out = [];
197
- const push = (q) => {
198
- const v = q.replace(/\s+/g, ' ').trim();
199
- if (v && !seen.has(v.toLowerCase())) {
200
- seen.add(v.toLowerCase());
201
- out.push(v);
202
- }
203
- };
204
- if (romaji && romaji.trim())
205
- push(romaji);
206
- push(title);
207
- if (/^re\s*zero$/i.test(title.trim()))
208
- push('Re Zero');
209
- const stripped = (s) => s.replace(/[^\p{L}\p{N}\s]/gu, ' ');
210
- push(stripped(title));
211
- if (romaji)
212
- push(stripped(romaji));
213
- const words = stripped(title).split(/\s+/).filter(Boolean);
214
- if (words.length > 3)
215
- push(words.slice(0, 3).join(' '));
216
- if (words.length > 4)
217
- push(words.slice(0, 4).join(' '));
218
- const tokens = distinctiveTokens(title)
219
- .concat(romaji ? distinctiveTokens(romaji) : [])
220
- .filter((w) => w.length >= 4);
221
- const unique = [...new Set(tokens)];
222
- unique.sort((a, b) => b.length - a.length);
223
- for (const t of unique.slice(0, 4))
224
- push(t);
225
- return out;
226
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,120 +0,0 @@
1
- import { spawn, execFile } from 'child_process';
2
- import logger from './logger.js';
3
- import { CONFIG } from './config.js';
4
- class RcloneService {
5
- activeRemote = null;
6
- executeCommand(args) {
7
- return new Promise((resolve, reject) => {
8
- execFile('rclone', args, (err, stdout, stderr) => {
9
- if (err) {
10
- if (stderr)
11
- logger.warn({ stderr }, 'Rclone command warning');
12
- return reject(new Error(stderr || err.message));
13
- }
14
- resolve(stdout.trim());
15
- });
16
- });
17
- }
18
- executeRcloneArgs(args) {
19
- return new Promise((resolve, reject) => {
20
- const process = spawn('rclone', args, { stdio: 'ignore' });
21
- process.on('close', (code) => {
22
- if (code === 0)
23
- resolve();
24
- else
25
- reject(new Error(`Rclone exited with code ${code}`));
26
- });
27
- process.on('error', (err) => reject(err));
28
- });
29
- }
30
- async listRemotes() {
31
- try {
32
- const remotesStr = await this.executeCommand(['listremotes']);
33
- return remotesStr
34
- .split('\n')
35
- .map((r) => r.trim())
36
- .filter((r) => r !== '')
37
- .map((r) => r.replace(/:$/, ''));
38
- }
39
- catch {
40
- return [];
41
- }
42
- }
43
- async init() {
44
- try {
45
- await this.executeCommand(['version']);
46
- const remotes = await this.listRemotes();
47
- if (CONFIG.RCLONE_REMOTE) {
48
- const found = remotes.find((r) => r.toLowerCase() === CONFIG.RCLONE_REMOTE?.toLowerCase());
49
- if (found) {
50
- this.activeRemote = found;
51
- logger.info(`Rclone initialized with manual remote: ${this.activeRemote}`);
52
- return true;
53
- }
54
- else {
55
- logger.warn(`Configured RCLONE_REMOTE '${CONFIG.RCLONE_REMOTE}' not found in rclone listremotes.`);
56
- }
57
- }
58
- if (remotes.length > 0 && !CONFIG.RCLONE_REMOTE) {
59
- logger.info({ remotes }, 'Rclone available but no manual remote is configured in settings.');
60
- return false;
61
- }
62
- return false;
63
- }
64
- catch (error) {
65
- logger.warn({ err: error }, 'Rclone initialization failed');
66
- return false;
67
- }
68
- }
69
- isActive() {
70
- return this.activeRemote !== null;
71
- }
72
- getRemoteName() {
73
- return this.activeRemote || 'unknown';
74
- }
75
- async downloadFile(remoteFolder, fileName, localPath) {
76
- if (!this.activeRemote)
77
- throw new Error('Rclone not active');
78
- const remotePath = `${this.activeRemote}:${remoteFolder}/${fileName}`;
79
- await this.executeRcloneArgs(['copyto', remotePath, localPath]);
80
- }
81
- async uploadFile(localPath, remoteFolder, fileName) {
82
- if (!this.activeRemote)
83
- throw new Error('Rclone not active');
84
- const remotePath = `${this.activeRemote}:${remoteFolder}/${fileName}`;
85
- await this.executeRcloneArgs(['copyto', localPath, remotePath]);
86
- }
87
- executeRcloneArgsWithOutput(args) {
88
- return new Promise((resolve, reject) => {
89
- const process = spawn('rclone', args, { stdio: 'pipe' });
90
- let stdout = '';
91
- let stderr = '';
92
- process.stdout?.on('data', (data) => (stdout += data));
93
- process.stderr?.on('data', (data) => (stderr += data));
94
- process.on('close', (code) => {
95
- if (code === 0)
96
- resolve(stdout.trim());
97
- else {
98
- if (stderr)
99
- logger.warn({ stderr }, 'Rclone command warning');
100
- reject(new Error(stderr || `Rclone exited with code ${code}`));
101
- }
102
- });
103
- process.on('error', (err) => reject(err));
104
- });
105
- }
106
- async fileExists(remoteFolder, fileName) {
107
- if (!this.activeRemote)
108
- return false;
109
- try {
110
- const remotePath = `${this.activeRemote}:${remoteFolder}/${fileName}`;
111
- const output = await this.executeRcloneArgsWithOutput(['lsjson', remotePath]);
112
- const json = JSON.parse(output);
113
- return json && json.length > 0;
114
- }
115
- catch {
116
- return false;
117
- }
118
- }
119
- }
120
- export const rcloneService = new RcloneService();
@@ -1,77 +0,0 @@
1
- import { eq, inArray, sql } from 'drizzle-orm';
2
- import { getDrizzle } from '../db/drizzle.js';
3
- import { asmrProgress } from '../db/schema-asmr.js';
4
- import { makeLibraryRepo } from './library.repository.js';
5
- export const ASMR_STATUSES = ['Listening', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
6
- export function buildAsmrId(rjCode) {
7
- return String(rjCode).trim().toUpperCase();
8
- }
9
- export const AsmrLibraryRepository = {
10
- ...makeLibraryRepo('asmr_library'),
11
- upsert: (db, data) => getDrizzle(db).run(sql `
12
- INSERT INTO asmr_library (id, rjCode, title, thumbnail, status, isAdult, updatedAt)
13
- VALUES (${data.id}, ${data.rjCode}, ${data.title}, ${data.thumbnail}, ${data.status}, ${data.isAdult ? 1 : 0}, strftime('%s', 'now'))
14
- ON CONFLICT(id) DO UPDATE SET
15
- rjCode = COALESCE(NULLIF(EXCLUDED.rjCode, ''), asmr_library.rjCode),
16
- title = COALESCE(NULLIF(EXCLUDED.title, ''), asmr_library.title),
17
- thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), asmr_library.thumbnail),
18
- status = COALESCE(NULLIF(EXCLUDED.status, ''), asmr_library.status),
19
- isAdult = COALESCE(EXCLUDED.isAdult, asmr_library.isAdult),
20
- updatedAt = strftime('%s', 'now')`),
21
- touchProgress: (db, id, progress) => getDrizzle(db).run(sql `
22
- UPDATE asmr_library SET lastTrackIndex = ${progress.trackIndex}, lastTrackLabel = ${progress.trackLabel}, lastPosition = ${progress.position}, updatedAt = strftime('%s', 'now') WHERE id = ${id}`),
23
- };
24
- export const AsmrProgressRepository = {
25
- getByWork: (db, workId) => getDrizzle(db).all(sql `
26
- SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${workId} ORDER BY updatedAt DESC`),
27
- getTrack: async (db, workId, trackIndex) => {
28
- const rows = await getDrizzle(db).all(sql `
29
- SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${workId} AND trackIndex = ${trackIndex}`);
30
- return rows[0];
31
- },
32
- getLatest: async (db, workId) => {
33
- const rows = await getDrizzle(db).all(sql `
34
- SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${workId} ORDER BY updatedAt DESC LIMIT 1`);
35
- return rows[0];
36
- },
37
- upsert: (db, data) => getDrizzle(db).run(sql `
38
- INSERT INTO asmr_progress (workId, trackIndex, trackLabel, currentTime, duration, title, thumbnail, rjCode, isAdult, updatedAt)
39
- VALUES (${data.workId}, ${data.trackIndex}, ${data.trackLabel}, ${data.currentTime}, ${data.duration}, ${data.title ?? null}, ${data.thumbnail ?? null}, ${data.rjCode ?? null}, ${data.isAdult ?? null}, strftime('%s', 'now'))
40
- ON CONFLICT(workId, trackIndex) DO UPDATE SET
41
- trackLabel = COALESCE(NULLIF(EXCLUDED.trackLabel, ''), asmr_progress.trackLabel),
42
- currentTime = EXCLUDED.currentTime,
43
- duration = EXCLUDED.duration,
44
- title = COALESCE(EXCLUDED.title, asmr_progress.title),
45
- thumbnail = COALESCE(EXCLUDED.thumbnail, asmr_progress.thumbnail),
46
- rjCode = COALESCE(EXCLUDED.rjCode, asmr_progress.rjCode),
47
- isAdult = COALESCE(EXCLUDED.isAdult, asmr_progress.isAdult),
48
- updatedAt = strftime('%s', 'now')`),
49
- deleteByWork: (db, workId) => getDrizzle(db).delete(asmrProgress).where(eq(asmrProgress.workId, workId)),
50
- deleteMany: (db, ids) => {
51
- if (ids.length === 0)
52
- return Promise.resolve();
53
- return getDrizzle(db).delete(asmrProgress).where(inArray(asmrProgress.workId, ids));
54
- },
55
- deleteTrack: (db, workId, trackIndex) => getDrizzle(db).run(sql `
56
- DELETE FROM asmr_progress WHERE workId = ${workId} AND trackIndex = ${trackIndex}`),
57
- getContinueListening: (db, limit) => {
58
- const limitClause = typeof limit === 'number' ? `LIMIT ${limit}` : '';
59
- return getDrizzle(db).all(sql `
60
- SELECT p.workId as id,
61
- COALESCE(l.rjCode, p.rjCode, p.workId) as rjCode,
62
- COALESCE(l.title, p.title) as title,
63
- COALESCE(l.thumbnail, p.thumbnail) as thumbnail,
64
- COALESCE(l.isAdult, p.isAdult) as isAdult,
65
- l.status as watchlistStatus,
66
- p.trackIndex, p.trackLabel, p.currentTime, p.duration, p.updatedAt as progressAt
67
- FROM (
68
- SELECT *, ROW_NUMBER() OVER (PARTITION BY workId ORDER BY updatedAt DESC) as rn
69
- FROM asmr_progress
70
- ) p
71
- LEFT JOIN asmr_library l ON p.workId = l.id
72
- WHERE p.rn = 1
73
- AND (l.status IS NULL OR l.status = 'Listening')
74
- ORDER BY p.updatedAt DESC
75
- ${sql.raw(limitClause)}`);
76
- },
77
- };
@@ -1,106 +0,0 @@
1
- import { sql } from 'drizzle-orm';
2
- import { getDrizzle } from '../db/drizzle.js';
3
- export const InsightsRepository = {
4
- getCoreStats: async (db) => (await getDrizzle(db).all(sql `
5
- SELECT
6
- (SELECT SUM(
7
- CASE WHEN we.currentTime > 0 THEN we.currentTime
8
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
9
- COALESCE(sm.episodeDuration * 60,
10
- CASE UPPER(COALESCE(sm.type, ''))
11
- WHEN 'MOVIE' THEN 6000
12
- WHEN 'TV_SHORT' THEN 720
13
- WHEN 'MUSIC' THEN 300
14
- ELSE 1440 END)
15
- ELSE 0 END)
16
- FROM watched_episodes we
17
- LEFT JOIN shows_meta sm ON sm.id = we.showId) as totalSeconds,
18
- (SELECT COUNT(*) FROM watched_episodes) as totalEpisodes,
19
- (SELECT COUNT(DISTINCT showId) FROM watched_episodes) as totalAnime,
20
- (SELECT COUNT(*) FROM watchlist WHERE status = 'Completed') as completedCount,
21
- (SELECT COUNT(*) FROM watchlist) as totalWatchlist`))[0],
22
- getActivityGrid: (db) => getDrizzle(db).all(sql `
23
- SELECT date(watchedAt) as day, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY day`),
24
- getHourlyDist: (db) => getDrizzle(db).all(sql `
25
- SELECT strftime('%H', watchedAt) as hour, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY hour`),
26
- getSeasonality: (db) => getDrizzle(db).all(sql `
27
- SELECT strftime('%m', we.watchedAt) as month, SUM(
28
- CASE WHEN we.currentTime > 0 THEN we.currentTime
29
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
30
- COALESCE(sm.episodeDuration * 60,
31
- CASE UPPER(COALESCE(sm.type, ''))
32
- WHEN 'MOVIE' THEN 6000
33
- WHEN 'TV_SHORT' THEN 720
34
- WHEN 'MUSIC' THEN 300
35
- ELSE 1440 END)
36
- ELSE 0 END) as seconds
37
- FROM watched_episodes we
38
- LEFT JOIN shows_meta sm ON sm.id = we.showId
39
- GROUP BY month`),
40
- getAllWatches: (db) => getDrizzle(db).all(sql `
41
- SELECT we.watchedAt, we.currentTime,
42
- CASE WHEN we.currentTime > 0 THEN we.currentTime
43
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
44
- COALESCE(sm.episodeDuration * 60,
45
- CASE UPPER(COALESCE(sm.type, ''))
46
- WHEN 'MOVIE' THEN 6000
47
- WHEN 'TV_SHORT' THEN 720
48
- WHEN 'MUSIC' THEN 300
49
- ELSE 1440 END)
50
- ELSE 0 END as effectiveSeconds
51
- FROM watched_episodes we
52
- LEFT JOIN shows_meta sm ON sm.id = we.showId
53
- ORDER BY we.watchedAt ASC`),
54
- getWatchedShowsMeta: (db) => getDrizzle(db).all(sql `
55
- SELECT DISTINCT sm.id, sm.genres, sm.popularityScore
56
- FROM shows_meta sm
57
- JOIN watched_episodes we ON sm.id = we.showId`),
58
- getDroppedShows: (db) => getDrizzle(db).all(sql `
59
- SELECT w.id, w.name, MAX(we.watchedAt) as lastActivity
60
- FROM watchlist w
61
- JOIN watched_episodes we ON w.id = we.showId
62
- WHERE w.status = 'Watching' AND NOT (we.currentTime = 0 AND we.duration = 0)
63
- GROUP BY w.id
64
- HAVING lastActivity < date('now', '-90 days')`),
65
- getCompletionVelocities: (db) => getDrizzle(db).all(sql `
66
- SELECT
67
- (julianday(MAX(we.watchedAt)) - julianday(MIN(we.watchedAt))) as daysToFinish
68
- FROM watchlist w
69
- JOIN watched_episodes we ON w.id = we.showId
70
- WHERE w.status = 'Completed'
71
- GROUP BY w.id
72
- HAVING SUM(we.currentTime) > 0`),
73
- getWatchedEpisodesWithMeta: (db) => getDrizzle(db).all(sql `
74
- SELECT
75
- we.showId,
76
- we.currentTime,
77
- we.duration,
78
- CASE WHEN we.currentTime > 0 THEN we.currentTime
79
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
80
- COALESCE(sm.episodeDuration * 60,
81
- CASE UPPER(COALESCE(sm.type, ''))
82
- WHEN 'MOVIE' THEN 6000
83
- WHEN 'TV_SHORT' THEN 720
84
- WHEN 'MUSIC' THEN 300
85
- ELSE 1440 END)
86
- ELSE 0 END as effectiveSeconds,
87
- sm.genres,
88
- sm.popularityScore,
89
- sm.name,
90
- sm.nativeName,
91
- sm.englishName,
92
- sm.thumbnail
93
- FROM watched_episodes we
94
- JOIN shows_meta sm ON we.showId = sm.id`),
95
- getLibraryShowsWithGenres: (db) => getDrizzle(db).all(sql `
96
- SELECT
97
- w.id,
98
- w.status,
99
- COALESCE(NULLIF(sm.englishName, ''), sm.name, w.name) as title,
100
- sm.genres,
101
- (SELECT COUNT(*) FROM watched_episodes we WHERE we.showId = w.id) as episodesWatched,
102
- COALESCE(sm.popularityScore, 0) as popularityScore
103
- FROM watchlist w
104
- LEFT JOIN shows_meta sm ON sm.id = w.id`),
105
- getAllAnilistIds: (db) => getDrizzle(db).all(sql `SELECT anilistId FROM shows_meta WHERE anilistId IS NOT NULL`),
106
- };