@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,21 +1 @@
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
+ import{SettingsRepository as a}from"../../repositories/settings.repository.js";async function p(r,e){const t=await a.getByKey(r,e);if(!t?.value)return{};try{return JSON.parse(t.value)}catch{return{}}}function i(r,e,t,n){return t>n&&e!==r?{status:e,pull:!0}:e!==r?{status:r,pull:!1}:{status:r,pull:!1}}export{p as readTrackerSyncState,i as resolveStatus};
@@ -1,476 +1 @@
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
+ import{AniListTracker as _}from"./anilist-tracker.js";import{WatchlistRepository as v}from"../../repositories/watchlist.repository.js";import{WatchedEpisodesRepository as U}from"../../repositories/watched-episodes.repository.js";import{ShowsMetaRepository as D}from"../../repositories/shows-meta.repository.js";import{SettingsRepository as M}from"../../repositories/settings.repository.js";import{NotificationsRepository as Y}from"../../repositories/notifications.repository.js";import{performWriteTransactionAsync as B}from"../../sync.js";import{dbGet as Z}from"../../utils/db-utils.js";import P from"../../logger.js";import{fetchMalUserList as q}from"../mal.js";import{offlineDb as G}from"../offline-db.js";import{getShowMetaById as J,getShowMetaByMalId as Q,isAnilistRateLimited as X,searchAnilistByTitle as x}from"../anilist.js";import{kitsuSearchAnime as tt}from"../kitsu.js";import{readTrackerSyncState as F,resolveStatus as et}from"./sync-state.js";const $="tracker_anilist_token",L="tracker_anilist_sync_state";async function j(r,y){return(await Z(r,"SELECT COUNT(DISTINCT episodeNumber) as total FROM watched_episodes WHERE showId = ?",[y]))?.total??0}async function Nt(r){const g=(await M.getByKey(r,$))?.value;if(!g)throw new Error("AniList is not connected. Please log in first.");const N=new _(g),E=await N.getViewer(),I=await N.fetchUserAnimeList(E.id),f=await v.getAll(r),p=await F(r,L),w=new Map;for(const e of I)w.set(e.mediaId,e);const d=new Map;for(const e of f){const t=(await D.getById(r,e.id))?.anilistId;t==null||!Number.isFinite(t)||String(t)!==String(e.id)||d.has(t)||d.set(t,e)}const l=new Set([...d.keys(),...w.keys(),...Object.keys(p).map(e=>Number.parseInt(e,10)).filter(e=>Number.isFinite(e))]),i={pushed:0,pulled:0,merged:0,unchanged:0,errors:[]},s=[],o=[],b=[],c={},S=[],u=[],m=[],A=Math.floor(Date.now()/1e3);for(const e of l){const n=d.get(e),t=w.get(e),a=String(e);try{if(n&&!t){if(p[a]&&I.length>0){S.push(a),c[a]=void 0,i.pulled++;continue}const h=await j(r,a);u.push({mediaId:e,status:nt(n.status),progress:h}),c[a]={lastSyncedAt:A,remoteUpdatedAt:A},i.pushed++;continue}if(!n&&t){if(p[a]){if(t.entryId){m.push({showId:a,entryId:t.entryId}),c[a]=void 0,i.pushed++;continue}c[a]=void 0,i.pushed++;continue}P.debug({mediaId:e,remoteStatus:t.status},"[AniList Sync] Remote only -> pulling to local");const h=t.title.english||t.title.romaji||`Anime #${e}`;s.push({id:a,name:h,thumbnail:t.coverImage??"",status:t.status,nativeName:t.title.native??"",englishName:t.title.english??"",type:"TV"}),o.push({id:a,name:h,thumbnail:t.coverImage,nativeName:t.title.native,englishName:t.title.english,episodeCount:t.totalEpisodes,anilistId:e,episodeDuration:t.episodeDuration}),t.progress>0&&b.push({showId:a,from:1,to:t.progress,updatedAt:t.updatedAt}),c[a]={lastSyncedAt:A,remoteUpdatedAt:t.updatedAt},i.pulled++;continue}if(!n&&!t&&p[a]){c[a]=void 0;continue}if(n&&t){const h=await j(r,a),R=p[a]?.lastSyncedAt??0,T=t.updatedAt||0,C=Math.max(h,t.progress),V=h>t.progress,O=t.progress>h,{status:k,pull:W}=et(n.status,t.status,T,R),z=t.status!==n.status;V||!W&&z?(u.push({mediaId:e,status:k,progress:C}),k!==n.status&&s.push({id:a,name:n.name,thumbnail:n.thumbnail??"",status:k,nativeName:n.nativeName??"",englishName:n.englishName??"",type:n.type??""}),O&&b.push({showId:a,from:h+1,to:C,updatedAt:T}),c[a]={lastSyncedAt:A,remoteUpdatedAt:A},i.merged++):W||O?(k!==n.status&&s.push({id:a,name:n.name,thumbnail:n.thumbnail??"",status:k,nativeName:n.nativeName??"",englishName:n.englishName??"",type:n.type??""}),O&&b.push({showId:a,from:h+1,to:C,updatedAt:T}),c[a]={lastSyncedAt:A,remoteUpdatedAt:T},i.merged++):i.unchanged++}}catch(h){const R=h instanceof Error?h.message:String(h);P.warn({err:h,mediaId:e},"[AniList Sync] Entry failed"),i.errors.push(`${n?.name??`Media ${e}`}: ${R}`)}}if(u.length>0||m.length>0||S.length>0){const e=Math.max(5,Math.ceil(f.length*.1)),n=S.length+m.length;if(n>e)throw new Error(`AniList sync would delete ${n} shows but only ${I.length} were fetched. Aborting to protect your library \u2014 if you really removed that many shows on AniList, delete them locally first and sync again.`)}if(u.length>0)for(let n=0;n<u.length;n+=10){const t=u.slice(n,n+10);await N.batchUpdateMediaEntries(t)}if(m.length>0){const e=m.map(t=>t.entryId),n=10;for(let t=0;t<e.length;t+=n){const a=e.slice(t,t+n);await N.batchDeleteMediaEntries(a)}}return(s.length>0||o.length>0||b.length>0||S.length>0||Object.keys(c).length>0)&&await B(r,async e=>{for(const t of s)await v.upsert(e,t);for(const t of o)await D.upsert(e,t);for(const t of b)for(let a=t.from;a<=t.to;a++)await U.insertIfMissing(e,{showId:t.showId,episodeNumber:String(a),watchedAt:H(t.updatedAt)});for(const t of S)await v.delete(e,t),await U.deleteByShow(e,t),await Y.deleteByShow(e,t);const n={...p};for(const[t,a]of Object.entries(c))a===void 0?delete n[t]:n[t]=a;await M.upsert(e,L,JSON.stringify(n))}),i}function H(r){if(!(!r||r<=0))return new Date(r*1e3).toISOString().slice(0,19).replace("T"," ")}function nt(r){return["Watching","Completed","On-Hold","Dropped","Planned"].includes(r)?r:void 0}async function Et(r,y,g=!1){const E=(await M.getByKey(r,$))?.value,I=new _(E);let f;if(E){const d=await I.getViewer();f=await I.fetchUserAnimeList(d.id)}else f=await I.fetchUserAnimeList(y);if(f.length===0)return 0;const p=Math.floor(Date.now()/1e3),w=await F(r,L);return await B(r,async d=>{g&&await M.clearWatchlist(d);for(const l of f){const i=String(l.mediaId),s=l.title.english||l.title.romaji||`Anime #${l.mediaId}`;if(await v.upsert(d,{id:i,name:s,thumbnail:l.coverImage??"",status:l.status,nativeName:l.title.native??"",englishName:l.title.english??"",type:"TV"}),await D.upsert(d,{id:i,name:s,thumbnail:l.coverImage,nativeName:l.title.native,englishName:l.title.english,episodeCount:l.totalEpisodes,anilistId:l.mediaId}),l.progress>0)for(let o=1;o<=l.progress;o++)await U.insertIfMissing(d,{showId:i,episodeNumber:String(o),watchedAt:H(l.updatedAt)});w[i]={lastSyncedAt:p,remoteUpdatedAt:l.updatedAt}}await M.upsert(d,L,JSON.stringify(w))}),f.length}function K(r){switch(r){case 1:return"Watching";case 2:return"Completed";case 3:return"On-Hold";case 4:return"Dropped";case 6:return"Planned";default:return null}}async function st(r){const y=await x(r);if(y)return y.id;if(X())try{const g=await tt({query:r,page:1,perPage:5});if(g.length>0)return g[0].id>0?g[0].id:null}catch{}return null}async function Mt(r,y,g={}){const{erase:N=!1,useOfflineDb:E=!0,skipFallback:I=!1}=g,f=await q(y);if(f.length===0)return 0;const p=[],w=[],d=[],l=[];for(const i of f){const s=K(i.status);if(!s)continue;const o=E?G.getByMalId(i.malId):null;if(o){const b=String(o.anilistId),c=o.title||i.titleEnglish||i.title||`MAL #${i.malId}`;p.push({id:b,name:c,thumbnail:o.thumbnail??i.imageUrl??void 0,status:s,englishName:i.titleEnglish??void 0,type:o.type??i.type??void 0}),(o.thumbnail||o.type||o.genres)&&w.push({id:b,thumbnail:o.thumbnail,type:o.type,genres:o.genres}),i.watchedEpisodes>0&&d.push({showId:b,progress:i.watchedEpisodes})}else l.push(i)}if(l.length>0&&!I)for(let s=0;s<l.length;s+=5){const o=l.slice(s,s+5);(await Promise.allSettled(o.map(async c=>{const S=await Q(c.malId).catch(()=>null);if(S?.anilistId)return S;const u=c.titleEnglish||c.title;if(!u)return null;const m=await st(u);return m?J(String(m)).catch(()=>null):null}))).forEach((c,S)=>{if(c.status!=="fulfilled"||!c.value)return;const u=c.value,m=o[S],A=K(m.status);if(!A)return;const e=u.anilistId?String(u.anilistId):u._id,n=u.names?.english||u.names?.romaji||m.titleEnglish||m.title||`MAL #${m.malId}`;p.push({id:e,name:n,thumbnail:u.thumbnail||m.imageUrl||void 0,status:A,englishName:m.titleEnglish??void 0,type:u.type??m.type??"TV"}),(u.thumbnail||u.type)&&w.push({id:e,thumbnail:u.thumbnail,type:u.type}),m.watchedEpisodes>0&&d.push({showId:e,progress:m.watchedEpisodes})})}return(p.length>0||N)&&await B(r,async i=>{N&&await M.clearWatchlist(i);for(const s of p)await v.upsert(i,{id:s.id,name:s.name,thumbnail:s.thumbnail??"",status:s.status,nativeName:"",englishName:s.englishName??"",type:s.type??"TV"});for(const s of w)(s.thumbnail||s.type||s.genres)&&await D.upsert(i,{id:s.id,thumbnail:s.thumbnail,type:s.type,genres:s.genres});for(const s of d)for(let o=1;o<=s.progress;o++)await U.insertIfMissing(i,{showId:s.showId,episodeNumber:String(o)})}),p.length}export{Mt as importFromMalUsername,Et as importFromUsername,Nt as syncAniList};
@@ -1,79 +1,3 @@
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
+ import{AppCache as A}from"../utils/cache.utils.js";import{parseJsonBody as $}from"../utils/http.utils.js";const h=new A({ttlSeconds:10080*60,maxKeys:5e3});function k(e){return e.split(" ").map((t,i)=>i!==0&&["x","and","or","the","a","an","of","in","on","with"].includes(t.toLowerCase())?t.toLowerCase():t.charAt(0).toUpperCase()+t.slice(1)).join(" ").replace(/\bX\b/g,"x")}function C(e){const t=e.trim();return t&&k(t)}async function U(e,t,i){const d=e.map(r=>String(r).trim()).filter(Boolean),a={},c=[];for(const r of d){const n=`${t}:${i}:${r}`,l=h.get(n);l?a[r]=l:c.push(r)}if(c.length>0){const r=[];for(let n=0;n<c.length;n+=15)r.push(c.slice(n,n+15));for(const n of r){const l=n.join(`
2
+ `),w=`https://clients5.google.com/translate_a/t?client=dict-chrome-ex&sl=${encodeURIComponent(t)}&tl=${encodeURIComponent(i)}&dt=t&q=${encodeURIComponent(l)}`;try{const o=await fetch(w,{headers:{"User-Agent":"Mozilla/5.0"},signal:AbortSignal.timeout(5e3)});if(!o.ok)throw new Error(`translate ${o.status}`);const s=await $(o);let g="";Array.isArray(s)&&typeof s[0]=="string"?g=s[0]:Array.isArray(s)&&Array.isArray(s[0])&&(g=s[0]?.map(u=>u[0]).join("")||"");const y=g.split(`
3
+ `);n.forEach((f,u)=>{const m=(y[u]??"").trim(),p=m?C(m):f;a[f]=p,h.set(`${t}:${i}:${f}`,p)})}catch{for(const o of n)a[o]||(a[o]=o)}await new Promise(o=>setTimeout(o,80))}}return a}export{U as translateTexts};