@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,21 +0,0 @@
1
- import { SettingsRepository } from '../../repositories/settings.repository.js';
2
- export async function readTrackerSyncState(db, key) {
3
- const row = await SettingsRepository.getByKey(db, key);
4
- if (!row?.value)
5
- return {};
6
- try {
7
- return JSON.parse(row.value);
8
- }
9
- catch {
10
- return {};
11
- }
12
- }
13
- export function resolveStatus(localStatus, remoteStatus, remoteUpdated, localLastSync) {
14
- if (remoteUpdated > localLastSync && remoteStatus !== localStatus) {
15
- return { status: remoteStatus, pull: true };
16
- }
17
- if (remoteStatus !== localStatus) {
18
- return { status: localStatus, pull: false };
19
- }
20
- return { status: localStatus, pull: false };
21
- }
@@ -1,476 +0,0 @@
1
- import { AniListTracker } from './anilist-tracker.js';
2
- import { WatchlistRepository } from '../../repositories/watchlist.repository.js';
3
- import { WatchedEpisodesRepository } from '../../repositories/watched-episodes.repository.js';
4
- import { ShowsMetaRepository } from '../../repositories/shows-meta.repository.js';
5
- import { SettingsRepository } from '../../repositories/settings.repository.js';
6
- import { NotificationsRepository } from '../../repositories/notifications.repository.js';
7
- import { performWriteTransactionAsync } from '../../sync.js';
8
- import { dbGet } from '../../utils/db-utils.js';
9
- import logger from '../../logger.js';
10
- import { fetchMalUserList } from '../mal.js';
11
- import { offlineDb } from '../offline-db.js';
12
- import { getShowMetaById, getShowMetaByMalId, isAnilistRateLimited, searchAnilistByTitle, } from '../anilist.js';
13
- import { kitsuSearchAnime } from '../kitsu.js';
14
- import { readTrackerSyncState, resolveStatus, } from './sync-state.js';
15
- const TOKEN_KEY = 'tracker_anilist_token';
16
- const SYNC_STATE_KEY = 'tracker_anilist_sync_state';
17
- async function getWatchedCount(db, showId) {
18
- const row = await dbGet(db, 'SELECT COUNT(DISTINCT episodeNumber) as total FROM watched_episodes WHERE showId = ?', [showId]);
19
- return row?.total ?? 0;
20
- }
21
- export async function syncAniList(db) {
22
- const tokenRow = await SettingsRepository.getByKey(db, TOKEN_KEY);
23
- const token = tokenRow?.value;
24
- if (!token)
25
- throw new Error('AniList is not connected. Please log in first.');
26
- const tracker = new AniListTracker(token);
27
- const viewer = await tracker.getViewer();
28
- const remoteEntries = await tracker.fetchUserAnimeList(viewer.id);
29
- const localItems = await WatchlistRepository.getAll(db);
30
- const syncState = await readTrackerSyncState(db, SYNC_STATE_KEY);
31
- const remoteByMediaId = new Map();
32
- for (const entry of remoteEntries)
33
- remoteByMediaId.set(entry.mediaId, entry);
34
- const localByMediaId = new Map();
35
- for (const item of localItems) {
36
- const meta = (await ShowsMetaRepository.getById(db, item.id));
37
- const anilistId = meta?.anilistId;
38
- if (anilistId == null || !Number.isFinite(anilistId) || String(anilistId) !== String(item.id)) {
39
- continue;
40
- }
41
- if (!localByMediaId.has(anilistId)) {
42
- localByMediaId.set(anilistId, item);
43
- }
44
- }
45
- const allMediaIds = new Set([
46
- ...localByMediaId.keys(),
47
- ...remoteByMediaId.keys(),
48
- ...Object.keys(syncState)
49
- .map((k) => Number.parseInt(k, 10))
50
- .filter((n) => Number.isFinite(n)),
51
- ]);
52
- const summary = { pushed: 0, pulled: 0, merged: 0, unchanged: 0, errors: [] };
53
- const watchlistUpserts = [];
54
- const metaUpserts = [];
55
- const watchedInserts = [];
56
- const stateUpdates = {};
57
- const watchlistDeletes = [];
58
- const pushUpdates = [];
59
- const remoteDeleteEntryIds = [];
60
- const now = Math.floor(Date.now() / 1000);
61
- for (const mediaId of allMediaIds) {
62
- const local = localByMediaId.get(mediaId);
63
- const remote = remoteByMediaId.get(mediaId);
64
- const showId = String(mediaId);
65
- try {
66
- if (local && !remote) {
67
- if (syncState[showId] && remoteEntries.length > 0) {
68
- watchlistDeletes.push(showId);
69
- stateUpdates[showId] = undefined;
70
- summary.pulled++;
71
- continue;
72
- }
73
- const watchedCount = await getWatchedCount(db, showId);
74
- pushUpdates.push({
75
- mediaId,
76
- status: DANGO_STATUS_OR_FALLBACK(local.status),
77
- progress: watchedCount,
78
- });
79
- stateUpdates[showId] = { lastSyncedAt: now, remoteUpdatedAt: now };
80
- summary.pushed++;
81
- continue;
82
- }
83
- if (!local && remote) {
84
- if (syncState[showId]) {
85
- if (remote.entryId) {
86
- remoteDeleteEntryIds.push({ showId, entryId: remote.entryId });
87
- stateUpdates[showId] = undefined;
88
- summary.pushed++;
89
- continue;
90
- }
91
- stateUpdates[showId] = undefined;
92
- summary.pushed++;
93
- continue;
94
- }
95
- logger.debug({ mediaId, remoteStatus: remote.status }, '[AniList Sync] Remote only -> pulling to local');
96
- const title = remote.title.english || remote.title.romaji || `Anime #${mediaId}`;
97
- watchlistUpserts.push({
98
- id: showId,
99
- name: title,
100
- thumbnail: remote.coverImage ?? '',
101
- status: remote.status,
102
- nativeName: remote.title.native ?? '',
103
- englishName: remote.title.english ?? '',
104
- type: 'TV',
105
- });
106
- metaUpserts.push({
107
- id: showId,
108
- name: title,
109
- thumbnail: remote.coverImage,
110
- nativeName: remote.title.native,
111
- englishName: remote.title.english,
112
- episodeCount: remote.totalEpisodes,
113
- anilistId: mediaId,
114
- episodeDuration: remote.episodeDuration,
115
- });
116
- if (remote.progress > 0) {
117
- watchedInserts.push({ showId, from: 1, to: remote.progress, updatedAt: remote.updatedAt });
118
- }
119
- stateUpdates[showId] = { lastSyncedAt: now, remoteUpdatedAt: remote.updatedAt };
120
- summary.pulled++;
121
- continue;
122
- }
123
- if (!local && !remote && syncState[showId]) {
124
- stateUpdates[showId] = undefined;
125
- continue;
126
- }
127
- if (local && remote) {
128
- const watchedCount = await getWatchedCount(db, showId);
129
- const localLastSync = syncState[showId]?.lastSyncedAt ?? 0;
130
- const remoteUpdated = remote.updatedAt || 0;
131
- const targetProgress = Math.max(watchedCount, remote.progress);
132
- const shouldPushProgress = watchedCount > remote.progress;
133
- const shouldPullProgress = remote.progress > watchedCount;
134
- const { status: targetStatus, pull: pullStatus } = resolveStatus(local.status, remote.status, remoteUpdated, localLastSync);
135
- const statusDiffers = remote.status !== local.status;
136
- if (shouldPushProgress || (!pullStatus && statusDiffers)) {
137
- pushUpdates.push({
138
- mediaId,
139
- status: targetStatus,
140
- progress: targetProgress,
141
- });
142
- if (targetStatus !== local.status) {
143
- watchlistUpserts.push({
144
- id: showId,
145
- name: local.name,
146
- thumbnail: local.thumbnail ?? '',
147
- status: targetStatus,
148
- nativeName: local.nativeName ?? '',
149
- englishName: local.englishName ?? '',
150
- type: local.type ?? '',
151
- });
152
- }
153
- if (shouldPullProgress) {
154
- watchedInserts.push({
155
- showId,
156
- from: watchedCount + 1,
157
- to: targetProgress,
158
- updatedAt: remoteUpdated,
159
- });
160
- }
161
- stateUpdates[showId] = { lastSyncedAt: now, remoteUpdatedAt: now };
162
- summary.merged++;
163
- }
164
- else if (pullStatus || shouldPullProgress) {
165
- if (targetStatus !== local.status) {
166
- watchlistUpserts.push({
167
- id: showId,
168
- name: local.name,
169
- thumbnail: local.thumbnail ?? '',
170
- status: targetStatus,
171
- nativeName: local.nativeName ?? '',
172
- englishName: local.englishName ?? '',
173
- type: local.type ?? '',
174
- });
175
- }
176
- if (shouldPullProgress) {
177
- watchedInserts.push({
178
- showId,
179
- from: watchedCount + 1,
180
- to: targetProgress,
181
- updatedAt: remoteUpdated,
182
- });
183
- }
184
- stateUpdates[showId] = { lastSyncedAt: now, remoteUpdatedAt: remoteUpdated };
185
- summary.merged++;
186
- }
187
- else {
188
- summary.unchanged++;
189
- }
190
- }
191
- }
192
- catch (err) {
193
- const message = err instanceof Error ? err.message : String(err);
194
- logger.warn({ err, mediaId }, '[AniList Sync] Entry failed');
195
- summary.errors.push(`${local?.name ?? `Media ${mediaId}`}: ${message}`);
196
- }
197
- }
198
- if (pushUpdates.length > 0 || remoteDeleteEntryIds.length > 0 || watchlistDeletes.length > 0) {
199
- const deleteCap = Math.max(5, Math.ceil(localItems.length * 0.1));
200
- const totalDeletes = watchlistDeletes.length + remoteDeleteEntryIds.length;
201
- if (totalDeletes > deleteCap) {
202
- throw new Error(`AniList sync would delete ${totalDeletes} shows but only ${remoteEntries.length} were fetched. Aborting to protect your library — if you really removed that many shows on AniList, delete them locally first and sync again.`);
203
- }
204
- }
205
- if (pushUpdates.length > 0) {
206
- const batchSize = 10;
207
- for (let i = 0; i < pushUpdates.length; i += batchSize) {
208
- const batch = pushUpdates.slice(i, i + batchSize);
209
- await tracker.batchUpdateMediaEntries(batch);
210
- }
211
- }
212
- if (remoteDeleteEntryIds.length > 0) {
213
- const deleteBatch = remoteDeleteEntryIds.map((r) => r.entryId);
214
- const batchSize = 10;
215
- for (let i = 0; i < deleteBatch.length; i += batchSize) {
216
- const batch = deleteBatch.slice(i, i + batchSize);
217
- await tracker.batchDeleteMediaEntries(batch);
218
- }
219
- }
220
- if (watchlistUpserts.length > 0 ||
221
- metaUpserts.length > 0 ||
222
- watchedInserts.length > 0 ||
223
- watchlistDeletes.length > 0 ||
224
- Object.keys(stateUpdates).length > 0) {
225
- await performWriteTransactionAsync(db, async (tx) => {
226
- for (const item of watchlistUpserts) {
227
- await WatchlistRepository.upsert(tx, item);
228
- }
229
- for (const meta of metaUpserts) {
230
- await ShowsMetaRepository.upsert(tx, meta);
231
- }
232
- for (const w of watchedInserts) {
233
- for (let ep = w.from; ep <= w.to; ep++) {
234
- await WatchedEpisodesRepository.insertIfMissing(tx, {
235
- showId: w.showId,
236
- episodeNumber: String(ep),
237
- watchedAt: toSqliteDatetime(w.updatedAt),
238
- });
239
- }
240
- }
241
- for (const id of watchlistDeletes) {
242
- await WatchlistRepository.delete(tx, id);
243
- await WatchedEpisodesRepository.deleteByShow(tx, id);
244
- await NotificationsRepository.deleteByShow(tx, id);
245
- }
246
- const mergedState = { ...syncState };
247
- for (const [k, v] of Object.entries(stateUpdates)) {
248
- if (v === undefined)
249
- delete mergedState[k];
250
- else
251
- mergedState[k] = v;
252
- }
253
- await SettingsRepository.upsert(tx, SYNC_STATE_KEY, JSON.stringify(mergedState));
254
- });
255
- }
256
- return summary;
257
- }
258
- function toSqliteDatetime(unixSeconds) {
259
- if (!unixSeconds || unixSeconds <= 0)
260
- return undefined;
261
- return new Date(unixSeconds * 1000).toISOString().slice(0, 19).replace('T', ' ');
262
- }
263
- function DANGO_STATUS_OR_FALLBACK(status) {
264
- const known = ['Watching', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
265
- return known.includes(status) ? status : undefined;
266
- }
267
- export async function importFromUsername(db, username, erase = false) {
268
- const tokenRow = await SettingsRepository.getByKey(db, TOKEN_KEY);
269
- const token = tokenRow?.value;
270
- const tracker = new AniListTracker(token);
271
- let entries;
272
- if (token) {
273
- const viewer = await tracker.getViewer();
274
- entries = await tracker.fetchUserAnimeList(viewer.id);
275
- }
276
- else {
277
- entries = await tracker.fetchUserAnimeList(username);
278
- }
279
- if (entries.length === 0)
280
- return 0;
281
- const now = Math.floor(Date.now() / 1000);
282
- const syncState = await readTrackerSyncState(db, SYNC_STATE_KEY);
283
- await performWriteTransactionAsync(db, async (tx) => {
284
- if (erase)
285
- await SettingsRepository.clearWatchlist(tx);
286
- for (const remote of entries) {
287
- const showId = String(remote.mediaId);
288
- const title = remote.title.english || remote.title.romaji || `Anime #${remote.mediaId}`;
289
- await WatchlistRepository.upsert(tx, {
290
- id: showId,
291
- name: title,
292
- thumbnail: remote.coverImage ?? '',
293
- status: remote.status,
294
- nativeName: remote.title.native ?? '',
295
- englishName: remote.title.english ?? '',
296
- type: 'TV',
297
- });
298
- await ShowsMetaRepository.upsert(tx, {
299
- id: showId,
300
- name: title,
301
- thumbnail: remote.coverImage,
302
- nativeName: remote.title.native,
303
- englishName: remote.title.english,
304
- episodeCount: remote.totalEpisodes,
305
- anilistId: remote.mediaId,
306
- });
307
- if (remote.progress > 0) {
308
- for (let ep = 1; ep <= remote.progress; ep++) {
309
- await WatchedEpisodesRepository.insertIfMissing(tx, {
310
- showId,
311
- episodeNumber: String(ep),
312
- watchedAt: toSqliteDatetime(remote.updatedAt),
313
- });
314
- }
315
- }
316
- syncState[showId] = { lastSyncedAt: now, remoteUpdatedAt: remote.updatedAt };
317
- }
318
- await SettingsRepository.upsert(tx, SYNC_STATE_KEY, JSON.stringify(syncState));
319
- });
320
- return entries.length;
321
- }
322
- function mapMalListStatus(status) {
323
- switch (status) {
324
- case 1:
325
- return 'Watching';
326
- case 2:
327
- return 'Completed';
328
- case 3:
329
- return 'On-Hold';
330
- case 4:
331
- return 'Dropped';
332
- case 6:
333
- return 'Planned';
334
- default:
335
- return null;
336
- }
337
- }
338
- async function searchTitleForMalImport(title) {
339
- const result = await searchAnilistByTitle(title);
340
- if (result)
341
- return result.id;
342
- if (isAnilistRateLimited()) {
343
- try {
344
- const fb = await kitsuSearchAnime({ query: title, page: 1, perPage: 5 });
345
- if (fb.length > 0)
346
- return fb[0].id > 0 ? fb[0].id : null;
347
- }
348
- catch {
349
- // ignore
350
- }
351
- }
352
- return null;
353
- }
354
- export async function importFromMalUsername(db, username, options = {}) {
355
- const { erase = false, useOfflineDb = true, skipFallback = false } = options;
356
- const entries = await fetchMalUserList(username);
357
- if (entries.length === 0)
358
- return 0;
359
- const shows = [];
360
- const metas = [];
361
- const watched = [];
362
- const fallbackQueue = [];
363
- for (const entry of entries) {
364
- const status = mapMalListStatus(entry.status);
365
- if (!status)
366
- continue;
367
- const offline = useOfflineDb ? offlineDb.getByMalId(entry.malId) : null;
368
- if (offline) {
369
- const showId = String(offline.anilistId);
370
- const title = offline.title || entry.titleEnglish || entry.title || `MAL #${entry.malId}`;
371
- shows.push({
372
- id: showId,
373
- name: title,
374
- thumbnail: offline.thumbnail ?? entry.imageUrl ?? undefined,
375
- status,
376
- englishName: entry.titleEnglish ?? undefined,
377
- type: offline.type ?? entry.type ?? undefined,
378
- });
379
- if (offline.thumbnail || offline.type || offline.genres) {
380
- metas.push({
381
- id: showId,
382
- thumbnail: offline.thumbnail,
383
- type: offline.type,
384
- genres: offline.genres,
385
- });
386
- }
387
- if (entry.watchedEpisodes > 0)
388
- watched.push({ showId, progress: entry.watchedEpisodes });
389
- }
390
- else {
391
- fallbackQueue.push(entry);
392
- }
393
- }
394
- if (fallbackQueue.length > 0 && !skipFallback) {
395
- const BATCH_SIZE = 5;
396
- for (let i = 0; i < fallbackQueue.length; i += BATCH_SIZE) {
397
- const batch = fallbackQueue.slice(i, i + BATCH_SIZE);
398
- const results = await Promise.allSettled(batch.map(async (entry) => {
399
- const byMal = await getShowMetaByMalId(entry.malId).catch(() => null);
400
- if (byMal?.anilistId)
401
- return byMal;
402
- const title = entry.titleEnglish || entry.title;
403
- if (!title)
404
- return null;
405
- const id = await searchTitleForMalImport(title);
406
- if (!id)
407
- return null;
408
- return getShowMetaById(String(id)).catch(() => null);
409
- }));
410
- results.forEach((r, idx) => {
411
- if (r.status !== 'fulfilled' || !r.value)
412
- return;
413
- const meta = r.value;
414
- const entry = batch[idx];
415
- const status = mapMalListStatus(entry.status);
416
- if (!status)
417
- return;
418
- const showId = meta.anilistId ? String(meta.anilistId) : meta._id;
419
- const title = meta.names?.english ||
420
- meta.names?.romaji ||
421
- entry.titleEnglish ||
422
- entry.title ||
423
- `MAL #${entry.malId}`;
424
- shows.push({
425
- id: showId,
426
- name: title,
427
- thumbnail: meta.thumbnail || entry.imageUrl || undefined,
428
- status,
429
- englishName: entry.titleEnglish ?? undefined,
430
- type: meta.type ?? entry.type ?? 'TV',
431
- });
432
- if (meta.thumbnail || meta.type) {
433
- metas.push({ id: showId, thumbnail: meta.thumbnail, type: meta.type });
434
- }
435
- if (entry.watchedEpisodes > 0)
436
- watched.push({ showId, progress: entry.watchedEpisodes });
437
- });
438
- }
439
- }
440
- if (shows.length > 0 || erase) {
441
- await performWriteTransactionAsync(db, async (tx) => {
442
- if (erase)
443
- await SettingsRepository.clearWatchlist(tx);
444
- for (const show of shows) {
445
- await WatchlistRepository.upsert(tx, {
446
- id: show.id,
447
- name: show.name,
448
- thumbnail: show.thumbnail ?? '',
449
- status: show.status,
450
- nativeName: '',
451
- englishName: show.englishName ?? '',
452
- type: show.type ?? 'TV',
453
- });
454
- }
455
- for (const meta of metas) {
456
- if (meta.thumbnail || meta.type || meta.genres) {
457
- await ShowsMetaRepository.upsert(tx, {
458
- id: meta.id,
459
- thumbnail: meta.thumbnail,
460
- type: meta.type,
461
- genres: meta.genres,
462
- });
463
- }
464
- }
465
- for (const w of watched) {
466
- for (let ep = 1; ep <= w.progress; ep++) {
467
- await WatchedEpisodesRepository.insertIfMissing(tx, {
468
- showId: w.showId,
469
- episodeNumber: String(ep),
470
- });
471
- }
472
- }
473
- });
474
- }
475
- return shows.length;
476
- }
@@ -1,79 +0,0 @@
1
- import { AppCache } from '../utils/cache.utils.js';
2
- import { parseJsonBody } from '../utils/http.utils.js';
3
- const cache = new AppCache({
4
- ttlSeconds: 7 * 24 * 60 * 60,
5
- maxKeys: 5000,
6
- });
7
- function toTitleCase(s) {
8
- return s
9
- .split(' ')
10
- .map((w, i) => {
11
- if (i !== 0 &&
12
- ['x', 'and', 'or', 'the', 'a', 'an', 'of', 'in', 'on', 'with'].includes(w.toLowerCase())) {
13
- return w.toLowerCase();
14
- }
15
- return w.charAt(0).toUpperCase() + w.slice(1);
16
- })
17
- .join(' ')
18
- .replace(/\bX\b/g, 'x');
19
- }
20
- function normalize(s) {
21
- const t = s.trim();
22
- if (!t)
23
- return t;
24
- return toTitleCase(t);
25
- }
26
- export async function translateTexts(texts, source, target) {
27
- const jobs = texts.map((t) => String(t).trim()).filter(Boolean);
28
- const result = {};
29
- const toFetch = [];
30
- for (const t of jobs) {
31
- const key = `${source}:${target}:${t}`;
32
- const cached = cache.get(key);
33
- if (cached)
34
- result[t] = cached;
35
- else
36
- toFetch.push(t);
37
- }
38
- if (toFetch.length > 0) {
39
- const chunks = [];
40
- for (let i = 0; i < toFetch.length; i += 15)
41
- chunks.push(toFetch.slice(i, i + 15));
42
- for (const chunk of chunks) {
43
- const q = chunk.join('\n');
44
- const url = `https://clients5.google.com/translate_a/t?client=dict-chrome-ex&sl=${encodeURIComponent(source)}&tl=${encodeURIComponent(target)}&dt=t&q=${encodeURIComponent(q)}`;
45
- try {
46
- const r = await fetch(url, {
47
- headers: { 'User-Agent': 'Mozilla/5.0' },
48
- signal: AbortSignal.timeout(5000),
49
- });
50
- if (!r.ok)
51
- throw new Error(`translate ${r.status}`);
52
- const data = (await parseJsonBody(r));
53
- let translated = '';
54
- if (Array.isArray(data) && typeof data[0] === 'string') {
55
- translated = data[0];
56
- }
57
- else if (Array.isArray(data) && Array.isArray(data[0])) {
58
- const outer = data;
59
- translated = outer[0]?.map((p) => p[0]).join('') || '';
60
- }
61
- const parts = translated.split('\n');
62
- chunk.forEach((orig, i) => {
63
- const trans = (parts[i] ?? '').trim();
64
- const norm = trans ? normalize(trans) : orig;
65
- result[orig] = norm;
66
- cache.set(`${source}:${target}:${orig}`, norm);
67
- });
68
- }
69
- catch {
70
- for (const orig of chunk) {
71
- if (!result[orig])
72
- result[orig] = orig;
73
- }
74
- }
75
- await new Promise((r) => setTimeout(r, 80));
76
- }
77
- }
78
- return result;
79
- }
@@ -1,106 +0,0 @@
1
- const MEDIA_HEADER = 20;
2
- const MEDIA = 21;
3
- const STREAM_PROTECTION_STATUS = 58;
4
- function readVarint(buf, pos) {
5
- if (pos >= buf.length)
6
- return null;
7
- const prefix = buf[pos];
8
- let size = 0;
9
- for (let shift = 1; shift <= 5; shift++) {
10
- if ((prefix & (128 >> (shift - 1))) === 0) {
11
- size = shift;
12
- break;
13
- }
14
- }
15
- if (size < 1 || pos + size > buf.length)
16
- return null;
17
- if (size === 1)
18
- return [prefix, pos + 1];
19
- if (size === 5) {
20
- const value = buf[pos + 1] + buf[pos + 2] * 256 + buf[pos + 3] * 65536 + buf[pos + 4] * 16777216;
21
- return [value >>> 0, pos + 5];
22
- }
23
- const lowBits = 8 - size;
24
- let value = prefix & ((1 << lowBits) - 1);
25
- for (let i = 1; i < size; i++) {
26
- value += buf[pos + i] * 2 ** (lowBits + 8 * (i - 1));
27
- }
28
- return [value, pos + size];
29
- }
30
- function readStatusField(payload) {
31
- let pos = 0;
32
- while (pos < payload.length) {
33
- const key = payload[pos++];
34
- const field = key >> 3;
35
- const wire = key & 7;
36
- if (wire !== 0)
37
- break;
38
- const decoded = readVarint(payload, pos);
39
- if (!decoded)
40
- break;
41
- if (field === 1)
42
- return decoded[0];
43
- pos = decoded[1];
44
- }
45
- return 1;
46
- }
47
- export function parseUmpResponse(buf, carry) {
48
- const mediaChunks = [];
49
- let status = 1;
50
- let nextCarry = carry;
51
- let pos = 0;
52
- if (nextCarry) {
53
- const t = readVarint(buf, pos);
54
- const s = t && readVarint(buf, t[1]);
55
- if (t && s && t[0] === MEDIA_HEADER) {
56
- pos = s[1] + s[0];
57
- }
58
- const ct = readVarint(buf, pos);
59
- const cs = ct && readVarint(buf, ct[1]);
60
- if (ct && cs && ct[0] === nextCarry.pendingType) {
61
- pos = cs[1];
62
- const take = Math.min(cs[0], buf.length - pos);
63
- nextCarry.chunks.push(buf.subarray(pos, pos + take));
64
- nextCarry.remaining = cs[0] - take;
65
- pos += take;
66
- if (nextCarry.remaining <= 0) {
67
- const full = Buffer.concat(nextCarry.chunks);
68
- nextCarry = null;
69
- if (ct[0] === MEDIA && full.length > 1)
70
- mediaChunks.push(full.subarray(1));
71
- }
72
- }
73
- else {
74
- nextCarry = null;
75
- }
76
- }
77
- while (pos < buf.length) {
78
- const t = readVarint(buf, pos);
79
- if (!t)
80
- break;
81
- const s = readVarint(buf, t[1]);
82
- if (!s)
83
- break;
84
- const [type, size] = [t[0], s[0]];
85
- pos = s[1];
86
- const available = buf.length - pos;
87
- if (size > available) {
88
- nextCarry = {
89
- pendingType: type,
90
- remaining: size - available,
91
- chunks: [buf.subarray(pos)],
92
- };
93
- break;
94
- }
95
- const payload = buf.subarray(pos, pos + size);
96
- pos += size;
97
- if (type === MEDIA) {
98
- if (payload.length > 1)
99
- mediaChunks.push(payload.subarray(1));
100
- }
101
- else if (type === STREAM_PROTECTION_STATUS) {
102
- status = readStatusField(payload);
103
- }
104
- }
105
- return { media: Buffer.concat(mediaChunks), status, carry: nextCarry };
106
- }