@serifpersia/dango 3.6.7 → 3.6.8

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 (149) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DraA_TF9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-BJ7pzs-P.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_esUr5C.js} +2 -2
  5. package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-Ckuh2xfW.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-DwHCd9yB.js} +1 -1
  8. package/client/dist/assets/{Home-DUkUTTTy.js → Home-DuM1EuvN.js} +1 -1
  9. package/client/dist/assets/{Insights-QCh4xW12.js → Insights-D3Pd9c9V.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-Dm2yVn0-.js} +1 -1
  11. package/client/dist/assets/{ListeningList-DguKjI_t.js → ListeningList-D-zt6EXZ.js} +1 -1
  12. package/client/dist/assets/{Manga-HydR-L7o.js → Manga-JwprMKBk.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DaeyJl2p.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-COg0fHI9.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
  16. package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-IHDaasrs.js} +1 -1
  17. package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CpwsrXo5.js} +1 -1
  18. package/client/dist/assets/{Music-CQpyXTLH.js → Music-Dks4tPeK.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-11CcAsy8.js} +1 -1
  20. package/client/dist/assets/{Player-CsH4x_I_.js → Player-DQoEQkHL.js} +2 -2
  21. package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-D4Ud4kv4.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
  23. package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-B4Dok7U3.js} +1 -1
  24. package/client/dist/assets/{Radio-DF6g6odd.js → Radio-BHK6LSk4.js} +1 -1
  25. package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-BCsxIz57.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BXHxSfy-.js} +1 -1
  27. package/client/dist/assets/{Search-DFkhuk-x.js → Search-PK1ds07O.js} +1 -1
  28. package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-Bxk_x55l.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CpJCZSRN.js} +1 -1
  30. package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-RieCh_84.js} +1 -1
  31. package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-DLytPJUj.js} +1 -1
  32. package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-C8djKebR.js} +1 -1
  33. package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-40JGGJDk.js} +1 -1
  34. package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-A3hbospf.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-DuXjqmZM.js} +1 -1
  36. package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-B9wGBh0u.js} +1 -1
  37. package/client/dist/assets/{hls-BGCX7mJy.js → hls-CZ5X3Ya-.js} +1 -1
  38. package/client/dist/assets/{index-CnfZwsds.js → index-dRhc9-ol.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
  40. package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CKOdHgol.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-SymV1J-b.js → useAsmrPopup-SFHPhpcl.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary-CKATe31P.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-C75QuYgY.js} +1 -1
  44. package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-CMtvZbEi.js} +1 -1
  45. package/client/dist/assets/{useTv-DGflCsSI.js → useTv-DUX4zYMr.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-By4Gt-R_.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +2 -2
  49. package/server/dist/app-auth.js +1 -157
  50. package/server/dist/app-hono.js +7 -557
  51. package/server/dist/config.js +1 -174
  52. package/server/dist/db/drizzle.js +1 -16
  53. package/server/dist/db/schema-anime.js +1 -61
  54. package/server/dist/db/schema-asmr.js +1 -13
  55. package/server/dist/db/schema-manga.js +1 -31
  56. package/server/dist/db/schema-tv.js +1 -18
  57. package/server/dist/db.js +1 -217
  58. package/server/dist/discord-gateway.js +1 -385
  59. package/server/dist/discord-rpc.js +1 -413
  60. package/server/dist/github-sync.js +1 -508
  61. package/server/dist/google.js +1 -741
  62. package/server/dist/hono/asmr.js +1 -10
  63. package/server/dist/hono/auth.js +3 -26
  64. package/server/dist/hono/music.js +1 -209
  65. package/server/dist/hono/proxy.js +36 -783
  66. package/server/dist/hono/tv.js +5 -200
  67. package/server/dist/hono/watchlist.js +2 -338
  68. package/server/dist/lib/anilist.js +26 -1458
  69. package/server/dist/lib/anischedule.js +1 -191
  70. package/server/dist/lib/insights.js +1 -185
  71. package/server/dist/lib/ipc.js +1 -23
  72. package/server/dist/lib/kitsu.js +1 -517
  73. package/server/dist/lib/mal-import.js +1 -291
  74. package/server/dist/lib/mal.js +1 -957
  75. package/server/dist/lib/migration.js +1 -310
  76. package/server/dist/lib/offline-db.js +4 -384
  77. package/server/dist/lib/potoken.js +1 -135
  78. package/server/dist/lib/recommendations.service.js +1 -216
  79. package/server/dist/lib/temp-ids.js +1 -3
  80. package/server/dist/lib/tmdb.js +1 -72
  81. package/server/dist/lib/tracker/anilist-tracker.js +8 -245
  82. package/server/dist/lib/tracker/manga-sync.service.js +1 -773
  83. package/server/dist/lib/tracker/sync-state.js +1 -21
  84. package/server/dist/lib/tracker/sync.service.js +1 -476
  85. package/server/dist/lib/translate.js +3 -79
  86. package/server/dist/lib/ump.js +1 -106
  87. package/server/dist/lib/ytmusic.js +1 -162
  88. package/server/dist/logger.js +1 -16
  89. package/server/dist/providers/manga/manga.types.js +0 -1
  90. package/server/dist/providers/provider.interface.js +0 -1
  91. package/server/dist/providers/remote-loader.js +2 -614
  92. package/server/dist/providers/remote-types.js +1 -159
  93. package/server/dist/providers/title-matching.js +1 -226
  94. package/server/dist/providers/tv.types.js +0 -1
  95. package/server/dist/rclone.js +2 -120
  96. package/server/dist/repositories/asmr.repository.js +11 -46
  97. package/server/dist/repositories/insights.repository.js +11 -25
  98. package/server/dist/repositories/library.repository.js +3 -94
  99. package/server/dist/repositories/mal-cache.repository.js +1 -52
  100. package/server/dist/repositories/manga.repository.js +14 -58
  101. package/server/dist/repositories/notifications.repository.js +6 -52
  102. package/server/dist/repositories/queue.repository.js +5 -58
  103. package/server/dist/repositories/settings.repository.js +2 -29
  104. package/server/dist/repositories/shows-meta.repository.js +3 -56
  105. package/server/dist/repositories/temp-show-ids.repository.js +3 -63
  106. package/server/dist/repositories/tv.repository.js +11 -54
  107. package/server/dist/repositories/watched-episodes.repository.js +6 -55
  108. package/server/dist/repositories/watchlist.repository.js +3 -54
  109. package/server/dist/scripts/backfill-episode-duration.js +1 -42
  110. package/server/dist/server.js +1 -310
  111. package/server/dist/shipped-defaults.js +1 -6
  112. package/server/dist/sync-payload.js +1 -91
  113. package/server/dist/sync.js +3 -647
  114. package/server/dist/trpc/context.js +1 -7
  115. package/server/dist/trpc/errors.js +1 -15
  116. package/server/dist/trpc/index.js +1 -18
  117. package/server/dist/trpc/router.js +1 -49
  118. package/server/dist/trpc/routers/asmr-library.js +1 -112
  119. package/server/dist/trpc/routers/asmr-progress.js +1 -121
  120. package/server/dist/trpc/routers/asmr.js +1 -87
  121. package/server/dist/trpc/routers/auth.js +1 -194
  122. package/server/dist/trpc/routers/continue-watching.js +2 -58
  123. package/server/dist/trpc/routers/data.js +1 -1026
  124. package/server/dist/trpc/routers/discord.js +1 -285
  125. package/server/dist/trpc/routers/discovery.js +1 -11
  126. package/server/dist/trpc/routers/insights.js +1 -14
  127. package/server/dist/trpc/routers/manga-library.js +1 -263
  128. package/server/dist/trpc/routers/manga-progress.js +1 -169
  129. package/server/dist/trpc/routers/manga.js +1 -201
  130. package/server/dist/trpc/routers/music.js +1 -655
  131. package/server/dist/trpc/routers/notifications.js +1 -80
  132. package/server/dist/trpc/routers/progress.js +1 -109
  133. package/server/dist/trpc/routers/queue.js +1 -93
  134. package/server/dist/trpc/routers/radio.js +1 -145
  135. package/server/dist/trpc/routers/settings.js +1 -96
  136. package/server/dist/trpc/routers/tracker.js +1 -213
  137. package/server/dist/trpc/routers/tv-library.js +1 -135
  138. package/server/dist/trpc/routers/tv-progress.js +1 -159
  139. package/server/dist/trpc/routers/tv.js +1 -551
  140. package/server/dist/trpc/routers/watchlist.js +2 -549
  141. package/server/dist/trpc/validation.js +1 -258
  142. package/server/dist/utils/cache.utils.js +1 -25
  143. package/server/dist/utils/cookie.utils.js +1 -13
  144. package/server/dist/utils/db-utils.js +5 -17
  145. package/server/dist/utils/env.utils.js +5 -77
  146. package/server/dist/utils/http.utils.js +1 -82
  147. package/server/dist/utils/request-context.js +1 -2
  148. package/server/dist/utils/security.utils.js +1 -76
  149. package/server/package.json +3 -2
@@ -1,395 +1,15 @@
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(`
1
+ import C from"crypto";import U from"../logger.js";import{SettingsRepository as i}from"../repositories/settings.repository.js";import{decodeMaybeGzip as k}from"../utils/http.utils.js";const o=U.child({module:"OfflineDb"}),$=["https://github.com/cedya77/anime-offline-database/releases/latest/download/anime-offline-database-minified.json","https://raw.githubusercontent.com/cedya77/anime-offline-database/master/anime-offline-database-minified.json","https://github.com/manami-project/anime-offline-database/releases/latest/download/anime-offline-database-minified.json","https://raw.githubusercontent.com/manami-project/anime-offline-database/master/anime-offline-database-minified.json"],v=6e4,B=1800*1e3,I=100*1024*1024,_=1e3,F=3600*1e3,P=["Action","Adventure","Comedy","Drama","Fantasy","Horror","Mahou Shoujo","Mecha","Music","Mystery","Psychological","Romance","Sci-Fi","Slice of Life","Sports","Supernatural","Thriller"],W=new Map(P.map(r=>[r.toLowerCase(),r])),z=1,p="anime_id_map",A="anime_id_map_new";function y(r,e=p){if(!r.get("SELECT name FROM sqlite_master WHERE type='table' AND name=?",[e]))r.run(`CREATE TABLE ${e} (id INTEGER PRIMARY KEY AUTOINCREMENT, anilist_id INTEGER, mal_id INTEGER, title TEXT, thumbnail TEXT, type TEXT, genres TEXT)`);else{const t=r.all(`PRAGMA table_info(${e})`),n=new Set(t.map(l=>l.name));n.has("mal_id")||r.run(`ALTER TABLE ${e} ADD COLUMN mal_id INTEGER`),n.has("type")||r.run(`ALTER TABLE ${e} ADD COLUMN type TEXT`),n.has("genres")||r.run(`ALTER TABLE ${e} ADD COLUMN genres TEXT`)}r.run(`CREATE INDEX IF NOT EXISTS idx_anime_id_map_anilist ON ${e}(anilist_id)`),r.run(`CREATE INDEX IF NOT EXISTS idx_anime_id_map_mal ON ${e}(mal_id)`)}function M(){const r=new Date,e=(r.getUTCDay()+1)%7;return new Date(Date.UTC(r.getUTCFullYear(),r.getUTCMonth(),r.getUTCDate()-e,0,0,0,0))}async function G(r){const e=new AbortController,a=setTimeout(()=>e.abort(),B);let t;const n=()=>{t&&clearTimeout(t),t=setTimeout(()=>e.abort(),v)};try{const l=await fetch(r,{signal:e.signal,headers:{"User-Agent":"Dango-Media-Client/1.0",Accept:"application/json"}});if(!l.ok)throw new Error(`HTTP ${l.status}`);const d=l.headers.get("content-length");if(d&&Number(d)>I)throw new Error("Response too large");if(!l.body)throw new Error("Empty response body");n();const T=[];let g=0;const w=l.body.getReader();try{for(;;){const{done:c,value:D}=await w.read();if(c)break;if(n(),g+=D.byteLength,g>I)throw new Error("Response too large");T.push(Buffer.from(D))}}finally{w.releaseLock()}const b=k(Buffer.concat(T)).toString("utf8");if(b.length>I)throw new Error("Response too large");return b}finally{t&&clearTimeout(t),clearTimeout(a)}}class x{malToEntry=new Map;anilistToMal=new Map;anilistToEntry=new Map;isInitialized=!1;isRefreshing=!1;async init(e){try{y(e),this.loadCache(e),o.info(`OfflineDb loaded ${this.malToEntry.size} MAL mappings from SQLite`);const a=(await i.getByKey(e,"offlineDbLastStatus"))?.value==="updating",t=await i.getByKey(e,"offlineDbAutoUpdateEnabled"),n=!t||t.value!=="false";this.backfillShowsMetaGenres(e),this.malToEntry.size===0?(o.info("OfflineDb cache is empty. Starting background download..."),this.refreshDatabase(e).catch(l=>{o.warn({err:l?.message},"Background offline database download failed")})):a&&n&&await this.interruptedRetryDue(e)?(o.info("Previous offline database refresh was interrupted. Retrying in background..."),await i.upsert(e,"offlineDbLastStatus","idle"),await i.upsert(e,"offlineDbLastMessage","Previous refresh was interrupted by a restart. Retrying automatically..."),this.refreshDatabase(e).catch(l=>{o.warn({err:l?.message},"Interrupted offline database retry failed")})):a&&(await i.upsert(e,"offlineDbLastStatus","idle"),await i.upsert(e,"offlineDbLastMessage","Previous refresh was interrupted by a restart. Trigger an update to retry.")),this.isInitialized=!0}catch(a){o.error({err:a},"Failed to initialize OfflineDb")}}backfillShowsMetaGenres(e){try{return y(e),e.run(`
157
2
  UPDATE shows_meta
158
3
  SET genres = (
159
- SELECT genres FROM ${TABLE}
4
+ SELECT genres FROM ${p}
160
5
  WHERE anilist_id = CAST(shows_meta.id AS INTEGER)
161
6
  AND genres IS NOT NULL AND genres != '[]'
162
7
  LIMIT 1
163
8
  )
164
9
  WHERE (genres IS NULL OR genres = '' OR genres = '[]')
165
10
  AND EXISTS (
166
- SELECT 1 FROM ${TABLE}
11
+ SELECT 1 FROM ${p}
167
12
  WHERE anilist_id = CAST(shows_meta.id AS INTEGER)
168
13
  AND genres IS NOT NULL AND genres != '[]'
169
14
  )
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();
15
+ `),1}catch(a){return o.warn({err:a},"Failed to backfill shows_meta genres from offline db"),0}}loadCache(e){try{y(e);const a=e.all(`SELECT anilist_id, mal_id, title, thumbnail, type, genres FROM ${p} WHERE anilist_id IS NOT NULL AND mal_id IS NOT NULL`);this.malToEntry.clear(),this.anilistToMal.clear(),this.anilistToEntry.clear();for(const t of a){if(!t.anilist_id||!t.mal_id)continue;const n={anilistId:t.anilist_id,malId:t.mal_id,title:t.title||"",thumbnail:t.thumbnail||void 0,type:t.type||void 0,genres:t.genres||void 0};this.malToEntry.set(t.mal_id,n),this.anilistToMal.set(t.anilist_id,t.mal_id),this.anilistToEntry.set(t.anilist_id,n)}}catch(a){o.error({err:a},"Failed to load offline mappings into memory")}}getByMalId(e){return this.malToEntry.get(e)??null}getByAnilistId(e){return this.anilistToEntry.get(e)??null}async getOfflineDbInfo(e){const a=async d=>(await i.getByKey(e,d))?.value??null,t=await a("offlineDbAutoUpdateEnabled"),n=await a("offlineDbLastStatus"),l=Number(await a("offlineDbEntryCount")??this.malToEntry.size)||0;return{totalMapped:this.malToEntry.size,totalMalMapped:this.malToEntry.size,isInitialized:this.isInitialized,isRefreshing:this.isRefreshing,autoUpdateEnabled:t?t!=="false":!0,lastCheckedAt:await a("offlineDbLastCheckedAt"),lastUpdatedAt:await a("offlineDbLastUpdatedAt"),lastStatus:n||(this.isRefreshing?"updating":"idle"),lastMessage:await a("offlineDbLastMessage"),sourceUrl:await a("offlineDbSourceUrl"),releaseTag:await a("offlineDbReleaseTag"),contentSha256:await a("offlineDbContentSha256"),entryCount:l,schemaVersion:z,nextDueAt:M().toISOString()}}async checkWeeklyUpdateDue(e){const a=await i.getByKey(e,"offlineDbAutoUpdateEnabled");if(a&&a.value==="false")return!1;const t=await i.getByKey(e,"offlineDbLastCheckedAt");if(!t?.value)return!0;const n=new Date(t.value).getTime();return Number.isNaN(n)?!0:n<M().getTime()}async interruptedRetryDue(e){const a=await i.getByKey(e,"offlineDbLastCheckedAt");if(!a?.value)return!0;const t=new Date(a.value).getTime();return Number.isNaN(t)?!0:Date.now()-t>F}async executeScheduledUpdate(e){if(this.isRefreshing){o.info("Scheduled offline database update skipped: refresh already in progress");return}o.info("Starting scheduled weekly offline database update...");const a=await this.refreshDatabase(e);a.success?o.info(`Scheduled offline database update succeeded: ${a.count} entries`):o.warn(`Scheduled offline database update failed: ${a.error}`)}async refreshDatabase(e){if(this.isRefreshing)return{success:!1,count:this.malToEntry.size,error:"Refresh already in progress"};this.isRefreshing=!0;const a=Date.now();await i.upsert(e,"offlineDbLastStatus","updating"),await i.upsert(e,"offlineDbLastCheckedAt",new Date().toISOString()),await i.upsert(e,"offlineDbLastMessage","Downloading and parsing database...");try{let t=null,n=null,l=null;for(const u of $)try{o.info(`Fetching anime-offline-database from ${u}...`),t=await G(u);const s=JSON.parse(t);if(s&&Array.isArray(s.data)){n=u;break}t=null}catch(s){l=s,o.warn(`Failed downloading from ${u}: ${s.message}`)}if(!t)throw l||new Error("Could not download anime-offline-database from any source");const d=C.createHash("sha256").update(t).digest("hex"),T=(await i.getByKey(e,"offlineDbContentSha256"))?.value,g=JSON.parse(t).data;o.info(`Parsing ${g.length} entries from anime-offline-database...`);const w=/anilist\.co\/anime\/(\d+)/i,b=/myanimelist\.net\/anime\/(\d+)/i,c=[];for(const u of g){const s=u;if(!Array.isArray(s.sources))continue;let f=null,L=null;for(const m of s.sources){if(typeof m!="string")continue;const h=m.match(w);h&&(f=parseInt(h[1],10));const E=m.match(b);E&&(L=parseInt(E[1],10))}if(!f||!L)continue;let S=typeof s.thumbnail=="string"?s.thumbnail:typeof s.picture=="string"?s.picture:"";S.includes("cdn.myanimelist.net/images/anime/")&&(S=S.replace(/t\.(jpe?g|png|webp)$/i,"l.$1"));let R;if(Array.isArray(s.tags)){const m=new Set;for(const h of s.tags){if(typeof h!="string")continue;const E=W.get(h.trim().toLowerCase());E&&m.add(E)}m.size>0&&(R=JSON.stringify([...m]))}c.push({anilistId:f,malId:L,title:typeof s.title=="string"?s.title:"",thumbnail:S||void 0,type:typeof s.type=="string"?s.type:void 0,genres:R})}o.info(`Saving ${c.length} mapped entries to SQLite...`),e.run(`DROP TABLE IF EXISTS ${A}`),y(e,A);const D=e.prepare(`INSERT INTO ${A} (anilist_id, mal_id, title, thumbnail, type, genres) VALUES (?, ?, ?, ?, ?, ?)`);for(let u=0;u<c.length;u+=_){const s=c.slice(u,u+_);e.serialize(()=>{for(const f of s)D.run(f.anilistId,f.malId,f.title,f.thumbnail??null,f.type??null,f.genres??null)}),await new Promise(f=>setImmediate(f))}e.run(`DROP TABLE IF EXISTS ${p}`),e.run(`ALTER TABLE ${A} RENAME TO ${p}`),y(e),this.loadCache(e),this.backfillShowsMetaGenres(e);const N=n?.includes("/releases/")?"latest":"raw";await i.upsert(e,"offlineDbLastStatus","success"),await i.upsert(e,"offlineDbLastUpdatedAt",new Date().toISOString()),await i.upsert(e,"offlineDbSourceUrl",n??""),await i.upsert(e,"offlineDbReleaseTag",N),await i.upsert(e,"offlineDbContentSha256",d),await i.upsert(e,"offlineDbEntryCount",String(c.length));const O=T===d;return await i.upsert(e,"offlineDbLastMessage",`Updated ${c.length} entries in ${Math.round((Date.now()-a)/1e3)}s${O?" (unchanged upstream hash)":""}.`),o.info(`OfflineDb refresh completed: ${c.length} entries from ${n}`),{success:!0,count:c.length}}catch(t){const n=t.message||"Failed to refresh offline database";return o.error({err:t},"Failed to refresh anime-offline-database"),await i.upsert(e,"offlineDbLastStatus","failed"),await i.upsert(e,"offlineDbLastMessage",n),{success:!1,count:this.malToEntry.size,error:n}}finally{this.isRefreshing=!1}}}const J=new x;export{J as offlineDb};
@@ -1,135 +1 @@
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
- }
1
+ import S from"../logger.js";import{parseJsonBody as E}from"../utils/http.utils.js";import{getInnertube as M}from"./ytmusic.js";const C="O43z0dpjhgX20SCx4KAo",A=600*60*1e3,R=360*60*1e3;let s=null,c=0,a=null,h=0;const k=60*1e3,l=new Map;async function D(){const t=globalThis;if("navigator"in t&&t.yt)return;const{JSDOM:e,VirtualConsole:i}=await import("jsdom"),o=new i,n=new e('<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>',{url:"https://www.youtube.com",referrer:"https://www.youtube.com/",virtualConsole:o});Object.assign(t,{window:n.window,document:n.window.document,location:n.window.location,origin:n.window.origin}),"navigator"in t||Object.defineProperty(t,"navigator",{value:n.window.navigator,writable:!0,configurable:!0})}async function N(){const[{BotGuardClient:t},{WebPoMinter:e},{buildURL:i,getHeaders:o,USER_AGENT:n}]=await Promise.all([import("bgutils-js/botguard"),import("bgutils-js/webpo"),import("bgutils-js/utils")]),f=await M(),g=(await(await fetch("https://www.youtube.com",{headers:{accept:"*/*","accept-language":"en-US,en;q=0.7","user-agent":n}})).text()).match(/ytcfg\.set\(({.+?})\);/s)?.[1];if(g){const d=globalThis.window;d&&(d.yt={config_:JSON.parse(g)})}const r=(await f.getAttestationChallenge("ENGAGEMENT_TYPE_UNBOUND")).bg_challenge,u=r?.interpreter_url?.private_do_not_access_or_else_trusted_resource_url_wrapped_value;if(!r?.program||!r.global_name||!u)throw new Error("No BotGuard challenge in response");const w=await(await fetch(`https:${u}`)).text();if(!w)throw new Error("Could not load BotGuard VM");new Function(w)();const _=await t.create({program:r.program,globalName:r.global_name,globalObject:globalThis}),p=[],y=await _.snapshot({webPoSignalOutput:p}),P=await fetch(i("GenerateIT",!0),{method:"POST",headers:o(),body:JSON.stringify([C,y])}),[m,b,T,O]=await E(P);if(!m)throw new Error("Integrity token request failed");return S.info("[pot] minter ready (integrity ttl %ss)",b),e.create({integrityToken:m,estimatedTtlSecs:b,mintRefreshThreshold:T,websafeFallbackToken:O},p)}async function W(){if(s&&Date.now()-c<A)return s;if(Date.now()-h<k)throw new Error("PoToken bootstrap cooling down");return a||(a=(async()=>{await D();const t=await N();return s=t,c=Date.now(),t})().catch(t=>{throw a=null,h=Date.now(),t})),a}async function F(t){const e=l.get(t);if(e&&Date.now()-e.fetchedAt<R)return e.pot;const o=await(await W()).mintAsWebsafeString(t);return l.set(t,{pot:o,fetchedAt:Date.now()}),o}function H(t){t?l.delete(t):(l.clear(),s=null,c=0,a=null)}export{F as mintPoToken,H as resetPoTokens};