@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,395 +0,0 @@
1
- import crypto from 'crypto';
2
- import logger from '../logger.js';
3
- import { SettingsRepository } from '../repositories/settings.repository.js';
4
- import { decodeMaybeGzip } from '../utils/http.utils.js';
5
- const log = logger.child({ module: 'OfflineDb' });
6
- const OFFLINE_DB_URLS = [
7
- 'https://github.com/cedya77/anime-offline-database/releases/latest/download/anime-offline-database-minified.json',
8
- 'https://raw.githubusercontent.com/cedya77/anime-offline-database/master/anime-offline-database-minified.json',
9
- 'https://github.com/manami-project/anime-offline-database/releases/latest/download/anime-offline-database-minified.json',
10
- 'https://raw.githubusercontent.com/manami-project/anime-offline-database/master/anime-offline-database-minified.json',
11
- ];
12
- const FETCH_IDLE_TIMEOUT_MS = 60000;
13
- const FETCH_OVERALL_TIMEOUT_MS = 30 * 60 * 1000;
14
- const MAX_BODY_BYTES = 100 * 1024 * 1024;
15
- const CHUNK_SIZE = 1000;
16
- const INTERRUPTED_RETRY_COOLDOWN_MS = 60 * 60 * 1000;
17
- const CANONICAL_GENRES = [
18
- 'Action',
19
- 'Adventure',
20
- 'Comedy',
21
- 'Drama',
22
- 'Fantasy',
23
- 'Horror',
24
- 'Mahou Shoujo',
25
- 'Mecha',
26
- 'Music',
27
- 'Mystery',
28
- 'Psychological',
29
- 'Romance',
30
- 'Sci-Fi',
31
- 'Slice of Life',
32
- 'Sports',
33
- 'Supernatural',
34
- 'Thriller',
35
- ];
36
- const canonicalGenreMap = new Map(CANONICAL_GENRES.map((g) => [g.toLowerCase(), g]));
37
- const SCHEMA_VERSION = 1;
38
- const TABLE = 'anime_id_map';
39
- const STAGING_TABLE = 'anime_id_map_new';
40
- function ensureTable(db, table = TABLE) {
41
- const row = db.get("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [table]);
42
- if (!row) {
43
- db.run(`CREATE TABLE ${table} (id INTEGER PRIMARY KEY AUTOINCREMENT, anilist_id INTEGER, mal_id INTEGER, title TEXT, thumbnail TEXT, type TEXT, genres TEXT)`);
44
- }
45
- else {
46
- const columns = db.all(`PRAGMA table_info(${table})`);
47
- const names = new Set(columns.map((c) => c.name));
48
- if (!names.has('mal_id'))
49
- db.run(`ALTER TABLE ${table} ADD COLUMN mal_id INTEGER`);
50
- if (!names.has('type'))
51
- db.run(`ALTER TABLE ${table} ADD COLUMN type TEXT`);
52
- if (!names.has('genres'))
53
- db.run(`ALTER TABLE ${table} ADD COLUMN genres TEXT`);
54
- }
55
- db.run(`CREATE INDEX IF NOT EXISTS idx_anime_id_map_anilist ON ${table}(anilist_id)`);
56
- db.run(`CREATE INDEX IF NOT EXISTS idx_anime_id_map_mal ON ${table}(mal_id)`);
57
- }
58
- function latestSaturdayUtc() {
59
- const now = new Date();
60
- const daysSinceSaturday = (now.getUTCDay() + 1) % 7;
61
- return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - daysSinceSaturday, 0, 0, 0, 0));
62
- }
63
- async function fetchText(url) {
64
- const controller = new AbortController();
65
- const overall = setTimeout(() => controller.abort(), FETCH_OVERALL_TIMEOUT_MS);
66
- let idle;
67
- const touchIdle = () => {
68
- if (idle)
69
- clearTimeout(idle);
70
- idle = setTimeout(() => controller.abort(), FETCH_IDLE_TIMEOUT_MS);
71
- };
72
- try {
73
- const res = await fetch(url, {
74
- signal: controller.signal,
75
- headers: { 'User-Agent': 'Dango-Media-Client/1.0', Accept: 'application/json' },
76
- });
77
- if (!res.ok)
78
- throw new Error(`HTTP ${res.status}`);
79
- const length = res.headers.get('content-length');
80
- if (length && Number(length) > MAX_BODY_BYTES)
81
- throw new Error('Response too large');
82
- if (!res.body)
83
- throw new Error('Empty response body');
84
- touchIdle();
85
- const chunks = [];
86
- let total = 0;
87
- const reader = res.body.getReader();
88
- try {
89
- for (;;) {
90
- const { done, value } = await reader.read();
91
- if (done)
92
- break;
93
- touchIdle();
94
- total += value.byteLength;
95
- if (total > MAX_BODY_BYTES)
96
- throw new Error('Response too large');
97
- chunks.push(Buffer.from(value));
98
- }
99
- }
100
- finally {
101
- reader.releaseLock();
102
- }
103
- const text = decodeMaybeGzip(Buffer.concat(chunks)).toString('utf8');
104
- if (text.length > MAX_BODY_BYTES)
105
- throw new Error('Response too large');
106
- return text;
107
- }
108
- finally {
109
- if (idle)
110
- clearTimeout(idle);
111
- clearTimeout(overall);
112
- }
113
- }
114
- class OfflineDb {
115
- malToEntry = new Map();
116
- anilistToMal = new Map();
117
- anilistToEntry = new Map();
118
- isInitialized = false;
119
- isRefreshing = false;
120
- async init(db) {
121
- try {
122
- ensureTable(db);
123
- this.loadCache(db);
124
- log.info(`OfflineDb loaded ${this.malToEntry.size} MAL mappings from SQLite`);
125
- const wasInterrupted = (await SettingsRepository.getByKey(db, 'offlineDbLastStatus'))?.value === 'updating';
126
- const autoUpdateSetting = await SettingsRepository.getByKey(db, 'offlineDbAutoUpdateEnabled');
127
- const autoUpdateEnabled = !autoUpdateSetting || autoUpdateSetting.value !== 'false';
128
- this.backfillShowsMetaGenres(db);
129
- if (this.malToEntry.size === 0) {
130
- log.info('OfflineDb cache is empty. Starting background download...');
131
- this.refreshDatabase(db).catch((err) => {
132
- log.warn({ err: err?.message }, 'Background offline database download failed');
133
- });
134
- }
135
- else if (wasInterrupted && autoUpdateEnabled && (await this.interruptedRetryDue(db))) {
136
- log.info('Previous offline database refresh was interrupted. Retrying in background...');
137
- await SettingsRepository.upsert(db, 'offlineDbLastStatus', 'idle');
138
- await SettingsRepository.upsert(db, 'offlineDbLastMessage', 'Previous refresh was interrupted by a restart. Retrying automatically...');
139
- this.refreshDatabase(db).catch((err) => {
140
- log.warn({ err: err?.message }, 'Interrupted offline database retry failed');
141
- });
142
- }
143
- else if (wasInterrupted) {
144
- await SettingsRepository.upsert(db, 'offlineDbLastStatus', 'idle');
145
- await SettingsRepository.upsert(db, 'offlineDbLastMessage', 'Previous refresh was interrupted by a restart. Trigger an update to retry.');
146
- }
147
- this.isInitialized = true;
148
- }
149
- catch (e) {
150
- log.error({ err: e }, 'Failed to initialize OfflineDb');
151
- }
152
- }
153
- backfillShowsMetaGenres(db) {
154
- try {
155
- ensureTable(db);
156
- db.run(`
157
- UPDATE shows_meta
158
- SET genres = (
159
- SELECT genres FROM ${TABLE}
160
- WHERE anilist_id = CAST(shows_meta.id AS INTEGER)
161
- AND genres IS NOT NULL AND genres != '[]'
162
- LIMIT 1
163
- )
164
- WHERE (genres IS NULL OR genres = '' OR genres = '[]')
165
- AND EXISTS (
166
- SELECT 1 FROM ${TABLE}
167
- WHERE anilist_id = CAST(shows_meta.id AS INTEGER)
168
- AND genres IS NOT NULL AND genres != '[]'
169
- )
170
- `);
171
- return 1;
172
- }
173
- catch (err) {
174
- log.warn({ err }, 'Failed to backfill shows_meta genres from offline db');
175
- return 0;
176
- }
177
- }
178
- loadCache(db) {
179
- try {
180
- ensureTable(db);
181
- const rows = db.all(`SELECT anilist_id, mal_id, title, thumbnail, type, genres FROM ${TABLE} WHERE anilist_id IS NOT NULL AND mal_id IS NOT NULL`);
182
- this.malToEntry.clear();
183
- this.anilistToMal.clear();
184
- this.anilistToEntry.clear();
185
- for (const row of rows) {
186
- if (!row.anilist_id || !row.mal_id)
187
- continue;
188
- const entry = {
189
- anilistId: row.anilist_id,
190
- malId: row.mal_id,
191
- title: row.title || '',
192
- thumbnail: row.thumbnail || undefined,
193
- type: row.type || undefined,
194
- genres: row.genres || undefined,
195
- };
196
- this.malToEntry.set(row.mal_id, entry);
197
- this.anilistToMal.set(row.anilist_id, row.mal_id);
198
- this.anilistToEntry.set(row.anilist_id, entry);
199
- }
200
- }
201
- catch (err) {
202
- log.error({ err }, 'Failed to load offline mappings into memory');
203
- }
204
- }
205
- getByMalId(malId) {
206
- return this.malToEntry.get(malId) ?? null;
207
- }
208
- getByAnilistId(anilistId) {
209
- return this.anilistToEntry.get(anilistId) ?? null;
210
- }
211
- async getOfflineDbInfo(db) {
212
- const get = async (key) => (await SettingsRepository.getByKey(db, key))?.value ?? null;
213
- const autoUpdate = await get('offlineDbAutoUpdateEnabled');
214
- const lastStatus = (await get('offlineDbLastStatus'));
215
- const entryCount = Number((await get('offlineDbEntryCount')) ?? this.malToEntry.size) || 0;
216
- return {
217
- totalMapped: this.malToEntry.size,
218
- totalMalMapped: this.malToEntry.size,
219
- isInitialized: this.isInitialized,
220
- isRefreshing: this.isRefreshing,
221
- autoUpdateEnabled: autoUpdate ? autoUpdate !== 'false' : true,
222
- lastCheckedAt: await get('offlineDbLastCheckedAt'),
223
- lastUpdatedAt: await get('offlineDbLastUpdatedAt'),
224
- lastStatus: lastStatus || (this.isRefreshing ? 'updating' : 'idle'),
225
- lastMessage: await get('offlineDbLastMessage'),
226
- sourceUrl: await get('offlineDbSourceUrl'),
227
- releaseTag: await get('offlineDbReleaseTag'),
228
- contentSha256: await get('offlineDbContentSha256'),
229
- entryCount,
230
- schemaVersion: SCHEMA_VERSION,
231
- nextDueAt: latestSaturdayUtc().toISOString(),
232
- };
233
- }
234
- async checkWeeklyUpdateDue(db) {
235
- const autoUpdate = await SettingsRepository.getByKey(db, 'offlineDbAutoUpdateEnabled');
236
- if (autoUpdate && autoUpdate.value === 'false')
237
- return false;
238
- const lastChecked = await SettingsRepository.getByKey(db, 'offlineDbLastCheckedAt');
239
- if (!lastChecked?.value)
240
- return true;
241
- const lastCheckedTime = new Date(lastChecked.value).getTime();
242
- if (Number.isNaN(lastCheckedTime))
243
- return true;
244
- return lastCheckedTime < latestSaturdayUtc().getTime();
245
- }
246
- async interruptedRetryDue(db) {
247
- const lastChecked = await SettingsRepository.getByKey(db, 'offlineDbLastCheckedAt');
248
- if (!lastChecked?.value)
249
- return true;
250
- const lastCheckedTime = new Date(lastChecked.value).getTime();
251
- if (Number.isNaN(lastCheckedTime))
252
- return true;
253
- return Date.now() - lastCheckedTime > INTERRUPTED_RETRY_COOLDOWN_MS;
254
- }
255
- async executeScheduledUpdate(db) {
256
- if (this.isRefreshing) {
257
- log.info('Scheduled offline database update skipped: refresh already in progress');
258
- return;
259
- }
260
- log.info('Starting scheduled weekly offline database update...');
261
- const result = await this.refreshDatabase(db);
262
- if (result.success)
263
- log.info(`Scheduled offline database update succeeded: ${result.count} entries`);
264
- else
265
- log.warn(`Scheduled offline database update failed: ${result.error}`);
266
- }
267
- async refreshDatabase(db) {
268
- if (this.isRefreshing) {
269
- return { success: false, count: this.malToEntry.size, error: 'Refresh already in progress' };
270
- }
271
- this.isRefreshing = true;
272
- const startedAt = Date.now();
273
- await SettingsRepository.upsert(db, 'offlineDbLastStatus', 'updating');
274
- await SettingsRepository.upsert(db, 'offlineDbLastCheckedAt', new Date().toISOString());
275
- await SettingsRepository.upsert(db, 'offlineDbLastMessage', 'Downloading and parsing database...');
276
- try {
277
- let text = null;
278
- let sourceUrl = null;
279
- let lastErr = null;
280
- for (const url of OFFLINE_DB_URLS) {
281
- try {
282
- log.info(`Fetching anime-offline-database from ${url}...`);
283
- text = await fetchText(url);
284
- const parsed = JSON.parse(text);
285
- if (parsed && Array.isArray(parsed.data)) {
286
- sourceUrl = url;
287
- break;
288
- }
289
- text = null;
290
- }
291
- catch (err) {
292
- lastErr = err;
293
- log.warn(`Failed downloading from ${url}: ${err.message}`);
294
- }
295
- }
296
- if (!text)
297
- throw lastErr || new Error('Could not download anime-offline-database from any source');
298
- const sha256 = crypto.createHash('sha256').update(text).digest('hex');
299
- const storedSha = (await SettingsRepository.getByKey(db, 'offlineDbContentSha256'))?.value;
300
- const data = JSON.parse(text).data;
301
- log.info(`Parsing ${data.length} entries from anime-offline-database...`);
302
- const anilistRegex = /anilist\.co\/anime\/(\d+)/i;
303
- const malRegex = /myanimelist\.net\/anime\/(\d+)/i;
304
- const parsedEntries = [];
305
- for (const item of data) {
306
- const rec = item;
307
- if (!Array.isArray(rec.sources))
308
- continue;
309
- let anilistId = null;
310
- let malId = null;
311
- for (const src of rec.sources) {
312
- if (typeof src !== 'string')
313
- continue;
314
- const anilistMatch = src.match(anilistRegex);
315
- if (anilistMatch)
316
- anilistId = parseInt(anilistMatch[1], 10);
317
- const malMatch = src.match(malRegex);
318
- if (malMatch)
319
- malId = parseInt(malMatch[1], 10);
320
- }
321
- if (!anilistId || !malId)
322
- continue;
323
- let thumbnail = typeof rec.thumbnail === 'string'
324
- ? rec.thumbnail
325
- : typeof rec.picture === 'string'
326
- ? rec.picture
327
- : '';
328
- if (thumbnail.includes('cdn.myanimelist.net/images/anime/')) {
329
- thumbnail = thumbnail.replace(/t\.(jpe?g|png|webp)$/i, 'l.$1');
330
- }
331
- let genres;
332
- if (Array.isArray(rec.tags)) {
333
- const matched = new Set();
334
- for (const tag of rec.tags) {
335
- if (typeof tag !== 'string')
336
- continue;
337
- const canonical = canonicalGenreMap.get(tag.trim().toLowerCase());
338
- if (canonical)
339
- matched.add(canonical);
340
- }
341
- if (matched.size > 0)
342
- genres = JSON.stringify([...matched]);
343
- }
344
- parsedEntries.push({
345
- anilistId,
346
- malId,
347
- title: typeof rec.title === 'string' ? rec.title : '',
348
- thumbnail: thumbnail || undefined,
349
- type: typeof rec.type === 'string' ? rec.type : undefined,
350
- genres,
351
- });
352
- }
353
- log.info(`Saving ${parsedEntries.length} mapped entries to SQLite...`);
354
- db.run(`DROP TABLE IF EXISTS ${STAGING_TABLE}`);
355
- ensureTable(db, STAGING_TABLE);
356
- const stmt = db.prepare(`INSERT INTO ${STAGING_TABLE} (anilist_id, mal_id, title, thumbnail, type, genres) VALUES (?, ?, ?, ?, ?, ?)`);
357
- for (let i = 0; i < parsedEntries.length; i += CHUNK_SIZE) {
358
- const chunk = parsedEntries.slice(i, i + CHUNK_SIZE);
359
- db.serialize(() => {
360
- for (const entry of chunk) {
361
- stmt.run(entry.anilistId, entry.malId, entry.title, entry.thumbnail ?? null, entry.type ?? null, entry.genres ?? null);
362
- }
363
- });
364
- await new Promise((resolve) => setImmediate(resolve));
365
- }
366
- db.run(`DROP TABLE IF EXISTS ${TABLE}`);
367
- db.run(`ALTER TABLE ${STAGING_TABLE} RENAME TO ${TABLE}`);
368
- ensureTable(db);
369
- this.loadCache(db);
370
- this.backfillShowsMetaGenres(db);
371
- const releaseTag = sourceUrl?.includes('/releases/') ? 'latest' : 'raw';
372
- await SettingsRepository.upsert(db, 'offlineDbLastStatus', 'success');
373
- await SettingsRepository.upsert(db, 'offlineDbLastUpdatedAt', new Date().toISOString());
374
- await SettingsRepository.upsert(db, 'offlineDbSourceUrl', sourceUrl ?? '');
375
- await SettingsRepository.upsert(db, 'offlineDbReleaseTag', releaseTag);
376
- await SettingsRepository.upsert(db, 'offlineDbContentSha256', sha256);
377
- await SettingsRepository.upsert(db, 'offlineDbEntryCount', String(parsedEntries.length));
378
- const skipped = storedSha === sha256;
379
- await SettingsRepository.upsert(db, 'offlineDbLastMessage', `Updated ${parsedEntries.length} entries in ${Math.round((Date.now() - startedAt) / 1000)}s${skipped ? ' (unchanged upstream hash)' : ''}.`);
380
- log.info(`OfflineDb refresh completed: ${parsedEntries.length} entries from ${sourceUrl}`);
381
- return { success: true, count: parsedEntries.length };
382
- }
383
- catch (err) {
384
- const errMsg = err.message || 'Failed to refresh offline database';
385
- log.error({ err }, 'Failed to refresh anime-offline-database');
386
- await SettingsRepository.upsert(db, 'offlineDbLastStatus', 'failed');
387
- await SettingsRepository.upsert(db, 'offlineDbLastMessage', errMsg);
388
- return { success: false, count: this.malToEntry.size, error: errMsg };
389
- }
390
- finally {
391
- this.isRefreshing = false;
392
- }
393
- }
394
- }
395
- export const offlineDb = new OfflineDb();
@@ -1,135 +0,0 @@
1
- import logger from '../logger.js';
2
- import { parseJsonBody } from '../utils/http.utils.js';
3
- import { getInnertube } from './ytmusic.js';
4
- const REQUEST_KEY = 'O43z0dpjhgX20SCx4KAo';
5
- const INTEGRITY_TTL_MS = 10 * 60 * 60 * 1000;
6
- const POT_CACHE_MS = 6 * 60 * 60 * 1000;
7
- let minter = null;
8
- let minterFetchedAt = 0;
9
- let bootstrapPromise = null;
10
- let bootstrapFailedAt = 0;
11
- const BOOTSTRAP_FAIL_COOLDOWN_MS = 60 * 1000;
12
- const potCache = new Map();
13
- // BotGuard needs browser globals to execute. They are installed once and kept
14
- // for the process lifetime: the minter's callbacks read them lazily at mint
15
- // time, long after bootstrap completes, so scoping them to bootstrap breaks
16
- // minting with `window is not defined`.
17
- async function setupDom() {
18
- const g = globalThis;
19
- if ('navigator' in g && g.yt)
20
- return;
21
- const { JSDOM, VirtualConsole } = await import('jsdom');
22
- const virtualConsole = new VirtualConsole();
23
- const dom = new JSDOM('<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>', {
24
- url: 'https://www.youtube.com',
25
- referrer: 'https://www.youtube.com/',
26
- virtualConsole,
27
- });
28
- Object.assign(g, {
29
- window: dom.window,
30
- document: dom.window.document,
31
- location: dom.window.location,
32
- origin: dom.window.origin,
33
- });
34
- if (!('navigator' in g)) {
35
- Object.defineProperty(g, 'navigator', {
36
- value: dom.window.navigator,
37
- writable: true,
38
- configurable: true,
39
- });
40
- }
41
- }
42
- async function buildMinter() {
43
- const [{ BotGuardClient }, { WebPoMinter }, { buildURL, getHeaders, USER_AGENT }] = await Promise.all([
44
- import('bgutils-js/botguard'),
45
- import('bgutils-js/webpo'),
46
- import('bgutils-js/utils'),
47
- ]);
48
- const yt = await getInnertube();
49
- const pageResponse = await fetch('https://www.youtube.com', {
50
- headers: {
51
- accept: '*/*',
52
- 'accept-language': 'en-US,en;q=0.7',
53
- 'user-agent': USER_AGENT,
54
- },
55
- });
56
- const pageHtml = await pageResponse.text();
57
- const ytConfig = pageHtml.match(/ytcfg\.set\(({.+?})\);/s)?.[1];
58
- if (ytConfig) {
59
- const win = globalThis.window;
60
- if (win)
61
- win.yt = { config_: JSON.parse(ytConfig) };
62
- }
63
- const challenge = (await yt.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND'));
64
- const bgc = challenge.bg_challenge;
65
- const interpreterUrl = bgc?.interpreter_url?.private_do_not_access_or_else_trusted_resource_url_wrapped_value;
66
- if (!bgc?.program || !bgc.global_name || !interpreterUrl) {
67
- throw new Error('No BotGuard challenge in response');
68
- }
69
- const bgScript = await (await fetch(`https:${interpreterUrl}`)).text();
70
- if (!bgScript)
71
- throw new Error('Could not load BotGuard VM');
72
- new Function(bgScript)();
73
- const botGuard = await BotGuardClient.create({
74
- program: bgc.program,
75
- globalName: bgc.global_name,
76
- globalObject: globalThis,
77
- });
78
- const webPoSignalOutput = [];
79
- const botguardResponse = await botGuard.snapshot({ webPoSignalOutput });
80
- const itResponse = await fetch(buildURL('GenerateIT', true), {
81
- method: 'POST',
82
- headers: getHeaders(),
83
- body: JSON.stringify([REQUEST_KEY, botguardResponse]),
84
- });
85
- const [integrityToken, estimatedTtlSecs, mintRefreshThreshold, websafeFallbackToken] = await parseJsonBody(itResponse);
86
- if (!integrityToken)
87
- throw new Error('Integrity token request failed');
88
- logger.info('[pot] minter ready (integrity ttl %ss)', estimatedTtlSecs);
89
- return WebPoMinter.create({
90
- integrityToken,
91
- estimatedTtlSecs,
92
- mintRefreshThreshold,
93
- websafeFallbackToken,
94
- }, webPoSignalOutput);
95
- }
96
- async function getMinter() {
97
- if (minter && Date.now() - minterFetchedAt < INTEGRITY_TTL_MS)
98
- return minter;
99
- if (Date.now() - bootstrapFailedAt < BOOTSTRAP_FAIL_COOLDOWN_MS) {
100
- throw new Error('PoToken bootstrap cooling down');
101
- }
102
- if (!bootstrapPromise) {
103
- bootstrapPromise = (async () => {
104
- await setupDom();
105
- const m = await buildMinter();
106
- minter = m;
107
- minterFetchedAt = Date.now();
108
- return m;
109
- })().catch((err) => {
110
- bootstrapPromise = null;
111
- bootstrapFailedAt = Date.now();
112
- throw err;
113
- });
114
- }
115
- return bootstrapPromise;
116
- }
117
- export async function mintPoToken(videoId) {
118
- const cached = potCache.get(videoId);
119
- if (cached && Date.now() - cached.fetchedAt < POT_CACHE_MS)
120
- return cached.pot;
121
- const m = await getMinter();
122
- const pot = await m.mintAsWebsafeString(videoId);
123
- potCache.set(videoId, { pot, fetchedAt: Date.now() });
124
- return pot;
125
- }
126
- export function resetPoTokens(videoId) {
127
- if (videoId)
128
- potCache.delete(videoId);
129
- else {
130
- potCache.clear();
131
- minter = null;
132
- minterFetchedAt = 0;
133
- bootstrapPromise = null;
134
- }
135
- }