@serifpersia/dango 3.6.5 → 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 (151) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-DraA_TF9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-BJ7pzs-P.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-B_esUr5C.js} +2 -2
  5. package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-Ckuh2xfW.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
  7. package/client/dist/assets/AvSyncCalibrator-DwHCd9yB.js +10 -0
  8. package/client/dist/assets/{Home-DxMBHFmc.js → Home-DuM1EuvN.js} +1 -1
  9. package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-D3Pd9c9V.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-Dm2yVn0-.js} +1 -1
  11. package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-D-zt6EXZ.js} +1 -1
  12. package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-JwprMKBk.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-DaeyJl2p.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-COg0fHI9.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
  16. package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-IHDaasrs.js} +1 -1
  17. package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard-CpwsrXo5.js} +1 -1
  18. package/client/dist/assets/{Music-DULtVsAM.js → Music-Dks4tPeK.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-11CcAsy8.js} +1 -1
  20. package/client/dist/assets/{Player-B8XeOmso.js → Player-DQoEQkHL.js} +6 -6
  21. package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-D4Ud4kv4.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
  23. package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-B4Dok7U3.js} +1 -1
  24. package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-BHK6LSk4.js} +1 -1
  25. package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-BCsxIz57.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-BXHxSfy-.js} +1 -1
  27. package/client/dist/assets/{Search-DvgVDlUR.js → Search-PK1ds07O.js} +1 -1
  28. package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-Bxk_x55l.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-CpJCZSRN.js} +1 -1
  30. package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-RieCh_84.js} +1 -1
  31. package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-DLytPJUj.js} +4 -4
  32. package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-C8djKebR.js} +1 -1
  33. package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-40JGGJDk.js} +1 -1
  34. package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-A3hbospf.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-DuXjqmZM.js} +1 -1
  36. package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-B9wGBh0u.js} +1 -1
  37. package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-CZ5X3Ya-.js} +1 -1
  38. package/client/dist/assets/{index-BTnhzofk.js → index-dRhc9-ol.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
  40. package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-CKOdHgol.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SFHPhpcl.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-CKATe31P.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-C75QuYgY.js} +1 -1
  44. package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-CMtvZbEi.js} +1 -1
  45. package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DUX4zYMr.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-a_Y0c2FS.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 -905
  66. package/server/dist/hono/tv.js +5 -169
  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 -605
  92. package/server/dist/providers/remote-types.js +1 -124
  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 -307
  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
  150. package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
  151. package/server/dist/utils/megaplay.utils.js +0 -40
@@ -1,508 +1 @@
1
- import logger from './logger.js';
2
- import path from 'path';
3
- import { dbAll, dbGet } from './utils/db-utils.js';
4
- import { isTempSyncRow } from './lib/temp-ids.js';
5
- import { updateEnvFile } from './utils/env.utils.js';
6
- import { CONFIG } from './config.js';
7
- import { SHIPPED_DEFAULTS } from './shipped-defaults.js';
8
- import { exportTables, importTables, MANGA_SYNC_TABLES, TV_SYNC_TABLES, ASMR_SYNC_TABLES, normalizePayload as normalizeGenericPayload, readPayloadVersion, } from './sync-payload.js';
9
- const log = logger.child({ module: 'GitHubSync' });
10
- const REPO_NAME = 'dango-sync-data';
11
- const LEGACY_REPO_NAME = 'aniweb-sync-data';
12
- const GITHUB_SCOPES = ['repo', 'delete_repo'];
13
- const GITHUB_API_HEADERS = {
14
- accept: 'application/vnd.github+json',
15
- 'x-github-api-version': '2026-03-10',
16
- };
17
- const SYNC_TABLES = [
18
- 'watchlist',
19
- 'watched_episodes',
20
- 'queue',
21
- 'settings',
22
- 'shows_meta',
23
- 'sync_metadata',
24
- 'dismissed_notifications',
25
- 'discovered_notifications',
26
- ];
27
- const nativeImport = new Function('specifier', 'return import(specifier)');
28
- function getGitHubClientId() {
29
- const id = process.env.GITHUB_CLIENT_ID || SHIPPED_DEFAULTS.GITHUB_CLIENT_ID || '';
30
- if (!id) {
31
- throw new Error('GITHUB_CLIENT_ID is not configured');
32
- }
33
- return id;
34
- }
35
- function getSyncFilename() {
36
- return CONFIG.IS_DEV ? 'sync.dev.json' : 'sync.json';
37
- }
38
- function getMangaSyncFilename() {
39
- return CONFIG.IS_DEV ? 'manga.sync.dev.json' : 'manga.sync.json';
40
- }
41
- function getTvSyncFilename() {
42
- return CONFIG.IS_DEV ? 'tv.sync.dev.json' : 'tv.sync.json';
43
- }
44
- function getAsmrSyncFilename() {
45
- return CONFIG.IS_DEV ? 'asmr.sync.dev.json' : 'asmr.sync.json';
46
- }
47
- async function loadOctokit(token) {
48
- const { Octokit } = await nativeImport('@octokit/rest');
49
- return new Octokit({
50
- auth: token,
51
- request: {
52
- headers: GITHUB_API_HEADERS,
53
- },
54
- });
55
- }
56
- function getErrorStatus(error) {
57
- if (typeof error === 'object' && error && 'status' in error) {
58
- return Number(error.status);
59
- }
60
- return undefined;
61
- }
62
- function quoteIdentifier(identifier) {
63
- return `"${identifier.replace(/"/g, '""')}"`;
64
- }
65
- function getRowsFromAll(db, sql) {
66
- return dbAll(db, sql);
67
- }
68
- function readVersion(payload) {
69
- const versionRow = payload.tables.sync_metadata.find((row) => row.key === 'db_version');
70
- const value = versionRow?.value;
71
- return typeof value === 'number' ? value : Number(value || payload.version || 0);
72
- }
73
- function normalizePayload(input) {
74
- if (!input || typeof input !== 'object' || !('tables' in input)) {
75
- throw new Error('Invalid GitHub sync payload.');
76
- }
77
- const payload = input;
78
- for (const table of SYNC_TABLES) {
79
- if (!Array.isArray(payload.tables?.[table])) {
80
- throw new Error(`Invalid GitHub sync payload: missing ${table}.`);
81
- }
82
- }
83
- return payload;
84
- }
85
- class GitHubSyncService {
86
- deviceState = { status: 'idle' };
87
- isAuthenticated() {
88
- return !!process.env.GITHUB_TOKEN;
89
- }
90
- async migrateFromAniWebSync() {
91
- if (!this.isAuthenticated())
92
- return;
93
- try {
94
- const octokit = await this.getOctokit();
95
- const { data: user } = await octokit.rest.users.getAuthenticated({
96
- headers: GITHUB_API_HEADERS,
97
- });
98
- try {
99
- await octokit.rest.repos.get({
100
- owner: user.login,
101
- repo: LEGACY_REPO_NAME,
102
- headers: GITHUB_API_HEADERS,
103
- });
104
- }
105
- catch (err) {
106
- if (getErrorStatus(err) === 404) {
107
- log.info('No legacy aniweb-sync-data repo found. Skipping migration.');
108
- return;
109
- }
110
- throw err;
111
- }
112
- log.info('Found legacy aniweb-sync-data repo. Renaming to dango-sync-data...');
113
- const existingNew = await octokit.rest.repos
114
- .get({
115
- owner: user.login,
116
- repo: REPO_NAME,
117
- headers: GITHUB_API_HEADERS,
118
- })
119
- .catch(() => null);
120
- if (existingNew) {
121
- log.info('dango-sync-data repo already exists. Deleting legacy repo.');
122
- await octokit.rest.repos.delete({
123
- owner: user.login,
124
- repo: LEGACY_REPO_NAME,
125
- headers: GITHUB_API_HEADERS,
126
- });
127
- log.info('Deleted legacy aniweb-sync-data repo.');
128
- return;
129
- }
130
- await octokit.rest.repos.update({
131
- owner: user.login,
132
- repo: LEGACY_REPO_NAME,
133
- name: REPO_NAME,
134
- description: 'Private dango synchronization data.',
135
- headers: GITHUB_API_HEADERS,
136
- });
137
- log.info('Renamed aniweb-sync-data to dango-sync-data.');
138
- }
139
- catch (err) {
140
- log.error({ err }, 'GitHub sync migration failed');
141
- }
142
- }
143
- getDeviceState() {
144
- return this.deviceState;
145
- }
146
- async startDeviceAuth(db, runSyncSequence) {
147
- if (this.isAuthenticated()) {
148
- const user = await this.getUserProfile();
149
- if (user) {
150
- this.deviceState = {
151
- status: 'success',
152
- user: user,
153
- };
154
- const { updateEnvFile } = await import('./utils/env.utils.js');
155
- await updateEnvFile({ SYNC_PROVIDER: 'github' });
156
- await runSyncSequence(db, 'github');
157
- return this.deviceState;
158
- }
159
- else {
160
- log.warn('Saved GitHub token is invalid or expired. Clearing for new auth.');
161
- await this.logout();
162
- }
163
- }
164
- if (this.deviceState.status === 'pending') {
165
- return this.deviceState;
166
- }
167
- this.deviceState = { status: 'pending' };
168
- let resolveVerification;
169
- const verificationReady = new Promise((resolve) => {
170
- resolveVerification = resolve;
171
- });
172
- void this.runDeviceAuth(db, runSyncSequence, resolveVerification);
173
- await verificationReady;
174
- return this.deviceState;
175
- }
176
- async getUserProfile() {
177
- if (!process.env.GITHUB_TOKEN)
178
- return null;
179
- try {
180
- const octokit = await loadOctokit(process.env.GITHUB_TOKEN);
181
- const { data } = await octokit.rest.users.getAuthenticated({
182
- headers: GITHUB_API_HEADERS,
183
- });
184
- return {
185
- login: data.login,
186
- name: data.name,
187
- avatarUrl: data.avatar_url,
188
- };
189
- }
190
- catch (err) {
191
- if (getErrorStatus(err) === 401) {
192
- log.warn('GitHub token is invalid or expired. Logging out.');
193
- await this.logout();
194
- }
195
- return null;
196
- }
197
- }
198
- async logout() {
199
- await updateEnvFile({ GITHUB_TOKEN: '' });
200
- delete process.env.GITHUB_TOKEN;
201
- this.deviceState = { status: 'idle' };
202
- }
203
- async getRemoteVersion() {
204
- const payload = await this.fetchSyncPayload();
205
- return payload ? readVersion(payload) : 0;
206
- }
207
- async getMangaRemoteVersion() {
208
- const payload = await this.fetchMangaSyncPayload();
209
- return payload ? readPayloadVersion(payload) : 0;
210
- }
211
- async getTvRemoteVersion() {
212
- const payload = await this.fetchTvSyncPayload();
213
- return payload ? readPayloadVersion(payload) : 0;
214
- }
215
- async getAsmrRemoteVersion() {
216
- const payload = await this.fetchAsmrSyncPayload();
217
- return payload ? readPayloadVersion(payload) : 0;
218
- }
219
- async syncUp(db) {
220
- const payload = await this.exportDatabase(db);
221
- const octokit = await this.getOctokit();
222
- const owner = await this.ensureRepo(octokit);
223
- const existing = await this.getSyncFile(octokit, owner);
224
- const content = Buffer.from(JSON.stringify(payload, null, 2), 'utf8').toString('base64');
225
- await octokit.rest.repos.createOrUpdateFileContents({
226
- owner,
227
- repo: REPO_NAME,
228
- path: getSyncFilename(),
229
- message: `Sync dango data v${payload.version}`,
230
- content,
231
- sha: existing?.sha,
232
- headers: GITHUB_API_HEADERS,
233
- });
234
- }
235
- async syncDown(db) {
236
- const payload = await this.fetchSyncPayload();
237
- if (!payload) {
238
- return 0;
239
- }
240
- this.importDatabase(db, payload);
241
- return readVersion(payload);
242
- }
243
- async syncMangaUp(db) {
244
- const payload = exportTables(db, MANGA_SYNC_TABLES);
245
- const octokit = await this.getOctokit();
246
- const owner = await this.ensureRepo(octokit);
247
- const existing = await this.getSyncFile(octokit, owner, getMangaSyncFilename());
248
- const content = Buffer.from(JSON.stringify(payload, null, 2), 'utf8').toString('base64');
249
- await octokit.rest.repos.createOrUpdateFileContents({
250
- owner,
251
- repo: REPO_NAME,
252
- path: getMangaSyncFilename(),
253
- message: `Sync dango manga data v${payload.version}`,
254
- content,
255
- sha: existing?.sha,
256
- headers: GITHUB_API_HEADERS,
257
- });
258
- }
259
- async syncMangaDown(db) {
260
- const payload = await this.fetchMangaSyncPayload();
261
- if (!payload) {
262
- return 0;
263
- }
264
- importTables(db, MANGA_SYNC_TABLES, payload, {
265
- libraryTables: ['manga_library', 'manga_progress'],
266
- backupName: 'pre-sync-manga-backup.db',
267
- });
268
- return readPayloadVersion(payload);
269
- }
270
- async syncTvUp(db) {
271
- const payload = exportTables(db, TV_SYNC_TABLES);
272
- const octokit = await this.getOctokit();
273
- const owner = await this.ensureRepo(octokit);
274
- const existing = await this.getSyncFile(octokit, owner, getTvSyncFilename());
275
- const content = Buffer.from(JSON.stringify(payload, null, 2), 'utf8').toString('base64');
276
- await octokit.rest.repos.createOrUpdateFileContents({
277
- owner,
278
- repo: REPO_NAME,
279
- path: getTvSyncFilename(),
280
- message: `Sync dango TV data v${payload.version}`,
281
- content,
282
- sha: existing?.sha,
283
- headers: GITHUB_API_HEADERS,
284
- });
285
- }
286
- async syncTvDown(db) {
287
- const payload = await this.fetchTvSyncPayload();
288
- if (!payload) {
289
- return 0;
290
- }
291
- importTables(db, TV_SYNC_TABLES, payload, {
292
- libraryTables: ['tv_library', 'tv_progress'],
293
- backupName: 'pre-sync-tv-backup.db',
294
- });
295
- return readPayloadVersion(payload);
296
- }
297
- async syncAsmrUp(db) {
298
- const payload = exportTables(db, ASMR_SYNC_TABLES);
299
- const octokit = await this.getOctokit();
300
- const owner = await this.ensureRepo(octokit);
301
- const existing = await this.getSyncFile(octokit, owner, getAsmrSyncFilename());
302
- const content = Buffer.from(JSON.stringify(payload, null, 2), 'utf8').toString('base64');
303
- await octokit.rest.repos.createOrUpdateFileContents({
304
- owner,
305
- repo: REPO_NAME,
306
- path: getAsmrSyncFilename(),
307
- message: `Sync dango ASMR data v${payload.version}`,
308
- content,
309
- sha: existing?.sha,
310
- headers: GITHUB_API_HEADERS,
311
- });
312
- }
313
- async syncAsmrDown(db) {
314
- const payload = await this.fetchAsmrSyncPayload();
315
- if (!payload) {
316
- return 0;
317
- }
318
- importTables(db, ASMR_SYNC_TABLES, payload, {
319
- libraryTables: ['asmr_library', 'asmr_progress'],
320
- backupName: 'pre-sync-asmr-backup.db',
321
- });
322
- return readPayloadVersion(payload);
323
- }
324
- async runDeviceAuth(db, runSyncSequence, resolveVerification) {
325
- try {
326
- const { createOAuthDeviceAuth } = await nativeImport('@octokit/auth-oauth-device');
327
- const auth = createOAuthDeviceAuth({
328
- clientId: getGitHubClientId(),
329
- scopes: GITHUB_SCOPES,
330
- onVerification: (verification) => {
331
- this.deviceState = {
332
- status: 'pending',
333
- verification: {
334
- user_code: verification.user_code,
335
- verification_uri: verification.verification_uri,
336
- expires_in: verification.expires_in,
337
- interval: verification.interval,
338
- },
339
- };
340
- resolveVerification();
341
- },
342
- });
343
- const authentication = await auth({ type: 'oauth' });
344
- await updateEnvFile({ GITHUB_TOKEN: authentication.token, SYNC_PROVIDER: 'github' });
345
- process.env.GITHUB_TOKEN = authentication.token;
346
- const user = await this.getUserProfile();
347
- this.deviceState = {
348
- status: 'success',
349
- user: user || undefined,
350
- };
351
- try {
352
- await runSyncSequence(db, 'github');
353
- }
354
- catch (err) {
355
- log.error({ err }, 'Post-GitHub-login sync failed');
356
- }
357
- }
358
- catch (err) {
359
- this.deviceState = {
360
- status: 'error',
361
- error: err instanceof Error ? err.message : 'GitHub device authentication failed.',
362
- };
363
- resolveVerification();
364
- log.error({ err }, 'GitHub device authentication failed');
365
- }
366
- }
367
- async getOctokit() {
368
- if (!process.env.GITHUB_TOKEN) {
369
- throw new Error('GitHub token is not configured.');
370
- }
371
- return loadOctokit(process.env.GITHUB_TOKEN);
372
- }
373
- async ensureRepo(octokit) {
374
- const { data: user } = await octokit.rest.users.getAuthenticated({
375
- headers: GITHUB_API_HEADERS,
376
- });
377
- try {
378
- await octokit.rest.repos.get({
379
- owner: user.login,
380
- repo: REPO_NAME,
381
- headers: GITHUB_API_HEADERS,
382
- });
383
- }
384
- catch (err) {
385
- if (getErrorStatus(err) !== 404) {
386
- throw err;
387
- }
388
- await octokit.rest.repos.createForAuthenticatedUser({
389
- name: REPO_NAME,
390
- private: true,
391
- auto_init: true,
392
- description: 'Private dango synchronization data.',
393
- headers: GITHUB_API_HEADERS,
394
- });
395
- }
396
- return user.login;
397
- }
398
- async getSyncFile(octokit, owner, filePath = getSyncFilename()) {
399
- try {
400
- const response = await octokit.rest.repos.getContent({
401
- owner,
402
- repo: REPO_NAME,
403
- path: filePath,
404
- headers: GITHUB_API_HEADERS,
405
- });
406
- const data = response.data;
407
- if (data.type !== 'file' || !data.content || !data.sha) {
408
- return null;
409
- }
410
- return {
411
- content: Buffer.from(data.content, 'base64').toString('utf8'),
412
- sha: data.sha,
413
- };
414
- }
415
- catch (err) {
416
- if (getErrorStatus(err) === 404) {
417
- return null;
418
- }
419
- throw err;
420
- }
421
- }
422
- async fetchSyncPayload() {
423
- const octokit = await this.getOctokit();
424
- const owner = await this.ensureRepo(octokit);
425
- const file = await this.getSyncFile(octokit, owner);
426
- if (!file) {
427
- return null;
428
- }
429
- return normalizePayload(JSON.parse(file.content));
430
- }
431
- async fetchMangaSyncPayload() {
432
- const octokit = await this.getOctokit();
433
- const owner = await this.ensureRepo(octokit);
434
- const file = await this.getSyncFile(octokit, owner, getMangaSyncFilename());
435
- if (!file) {
436
- return null;
437
- }
438
- return normalizeGenericPayload(JSON.parse(file.content), MANGA_SYNC_TABLES, 'GitHub manga');
439
- }
440
- async fetchTvSyncPayload() {
441
- const octokit = await this.getOctokit();
442
- const owner = await this.ensureRepo(octokit);
443
- const file = await this.getSyncFile(octokit, owner, getTvSyncFilename());
444
- if (!file) {
445
- return null;
446
- }
447
- return normalizeGenericPayload(JSON.parse(file.content), TV_SYNC_TABLES, 'GitHub TV');
448
- }
449
- async fetchAsmrSyncPayload() {
450
- const octokit = await this.getOctokit();
451
- const owner = await this.ensureRepo(octokit);
452
- const file = await this.getSyncFile(octokit, owner, getAsmrSyncFilename());
453
- if (!file) {
454
- return null;
455
- }
456
- return normalizeGenericPayload(JSON.parse(file.content), ASMR_SYNC_TABLES, 'GitHub ASMR');
457
- }
458
- async exportDatabase(db) {
459
- const tables = {};
460
- for (const table of SYNC_TABLES) {
461
- const rows = await getRowsFromAll(db, `SELECT * FROM ${quoteIdentifier(table)}`);
462
- tables[table] = rows.filter((row) => !isTempSyncRow(row));
463
- }
464
- return {
465
- version: readVersion({ version: 0, exportedAt: '', tables }),
466
- exportedAt: new Date().toISOString(),
467
- tables,
468
- };
469
- }
470
- importDatabase(db, payload) {
471
- const remoteLibraryRows = (payload.tables.watchlist?.length ?? 0) + (payload.tables.watched_episodes?.length ?? 0);
472
- const localLibraryRows = dbGet(db, 'SELECT (SELECT COUNT(*) FROM watchlist) + (SELECT COUNT(*) FROM watched_episodes) AS n')?.n ?? 0;
473
- if (remoteLibraryRows === 0 && localLibraryRows > 0) {
474
- throw new Error('Sync down refused: remote library is empty while local library has data');
475
- }
476
- try {
477
- db.backup(path.join(CONFIG.ROOT, 'pre-sync-backup.db'));
478
- }
479
- catch (err) {
480
- log.warn({ err }, 'Pre-sync backup failed, continuing without backup');
481
- }
482
- const localColumns = new Map();
483
- for (const table of SYNC_TABLES) {
484
- const cols = db.all(`PRAGMA table_info("${table}")`);
485
- localColumns.set(table, new Set(cols.map((c) => c.name)));
486
- }
487
- db.serialize(() => {
488
- for (const table of SYNC_TABLES) {
489
- db.run(`DELETE FROM ${quoteIdentifier(table)}`);
490
- }
491
- for (const table of SYNC_TABLES) {
492
- const known = localColumns.get(table);
493
- for (const row of payload.tables[table]) {
494
- if (isTempSyncRow(row))
495
- continue;
496
- const columns = Object.keys(row).filter((c) => known?.has(c));
497
- if (columns.length === 0)
498
- continue;
499
- const columnSql = columns.map(quoteIdentifier).join(', ');
500
- const placeholders = columns.map(() => '?').join(', ');
501
- const values = columns.map((column) => row[column]);
502
- db.run(`INSERT INTO ${quoteIdentifier(table)} (${columnSql}) VALUES (${placeholders})`, values);
503
- }
504
- }
505
- });
506
- }
507
- }
508
- export const githubSyncService = new GitHubSyncService();
1
+ import C from"./logger.js";import L from"path";import{dbAll as M,dbGet as x}from"./utils/db-utils.js";import{isTempSyncRow as O}from"./lib/temp-ids.js";import{updateEnvFile as R}from"./utils/env.utils.js";import{CONFIG as y}from"./config.js";import{SHIPPED_DEFAULTS as V}from"./shipped-defaults.js";import{exportTables as S,importTables as b,MANGA_SYNC_TABLES as v,TV_SYNC_TABLES as _,ASMR_SYNC_TABLES as P,normalizePayload as T,readPayloadVersion as p}from"./sync-payload.js";const u=C.child({module:"GitHubSync"}),l="dango-sync-data",E="aniweb-sync-data",W=["repo","delete_repo"],o={accept:"application/vnd.github+json","x-github-api-version":"2026-03-10"},g=["watchlist","watched_episodes","queue","settings","shows_meta","sync_metadata","dismissed_notifications","discovered_notifications"],G=new Function("specifier","return import(specifier)");function j(){const i=process.env.GITHUB_CLIENT_ID||V.GITHUB_CLIENT_ID||"";if(!i)throw new Error("GITHUB_CLIENT_ID is not configured");return i}function N(){return y.IS_DEV?"sync.dev.json":"sync.json"}function A(){return y.IS_DEV?"manga.sync.dev.json":"manga.sync.json"}function k(){return y.IS_DEV?"tv.sync.dev.json":"tv.sync.json"}function D(){return y.IS_DEV?"asmr.sync.dev.json":"asmr.sync.json"}async function H(i){const{Octokit:t}=await G("@octokit/rest");return new t({auth:i,request:{headers:o}})}function m(i){if(typeof i=="object"&&i&&"status"in i)return Number(i.status)}function f(i){return`"${i.replace(/"/g,'""')}"`}function $(i,t){return M(i,t)}function I(i){const e=i.tables.sync_metadata.find(a=>a.key==="db_version")?.value;return typeof e=="number"?e:Number(e||i.version||0)}function Y(i){if(!i||typeof i!="object"||!("tables"in i))throw new Error("Invalid GitHub sync payload.");const t=i;for(const e of g)if(!Array.isArray(t.tables?.[e]))throw new Error(`Invalid GitHub sync payload: missing ${e}.`);return t}class K{deviceState={status:"idle"};isAuthenticated(){return!!process.env.GITHUB_TOKEN}async migrateFromAniWebSync(){if(this.isAuthenticated())try{const t=await this.getOctokit(),{data:e}=await t.rest.users.getAuthenticated({headers:o});try{await t.rest.repos.get({owner:e.login,repo:E,headers:o})}catch(n){if(m(n)===404){u.info("No legacy aniweb-sync-data repo found. Skipping migration.");return}throw n}if(u.info("Found legacy aniweb-sync-data repo. Renaming to dango-sync-data..."),await t.rest.repos.get({owner:e.login,repo:l,headers:o}).catch(()=>null)){u.info("dango-sync-data repo already exists. Deleting legacy repo."),await t.rest.repos.delete({owner:e.login,repo:E,headers:o}),u.info("Deleted legacy aniweb-sync-data repo.");return}await t.rest.repos.update({owner:e.login,repo:E,name:l,description:"Private dango synchronization data.",headers:o}),u.info("Renamed aniweb-sync-data to dango-sync-data.")}catch(t){u.error({err:t},"GitHub sync migration failed")}}getDeviceState(){return this.deviceState}async startDeviceAuth(t,e){if(this.isAuthenticated()){const r=await this.getUserProfile();if(r){this.deviceState={status:"success",user:r};const{updateEnvFile:s}=await import("./utils/env.utils.js");return await s({SYNC_PROVIDER:"github"}),await e(t,"github"),this.deviceState}else u.warn("Saved GitHub token is invalid or expired. Clearing for new auth."),await this.logout()}if(this.deviceState.status==="pending")return this.deviceState;this.deviceState={status:"pending"};let a;const n=new Promise(r=>{a=r});return this.runDeviceAuth(t,e,a),await n,this.deviceState}async getUserProfile(){if(!process.env.GITHUB_TOKEN)return null;try{const t=await H(process.env.GITHUB_TOKEN),{data:e}=await t.rest.users.getAuthenticated({headers:o});return{login:e.login,name:e.name,avatarUrl:e.avatar_url}}catch(t){return m(t)===401&&(u.warn("GitHub token is invalid or expired. Logging out."),await this.logout()),null}}async logout(){await R({GITHUB_TOKEN:""}),delete process.env.GITHUB_TOKEN,this.deviceState={status:"idle"}}async getRemoteVersion(){const t=await this.fetchSyncPayload();return t?I(t):0}async getMangaRemoteVersion(){const t=await this.fetchMangaSyncPayload();return t?p(t):0}async getTvRemoteVersion(){const t=await this.fetchTvSyncPayload();return t?p(t):0}async getAsmrRemoteVersion(){const t=await this.fetchAsmrSyncPayload();return t?p(t):0}async syncUp(t){const e=await this.exportDatabase(t),a=await this.getOctokit(),n=await this.ensureRepo(a),r=await this.getSyncFile(a,n),s=Buffer.from(JSON.stringify(e,null,2),"utf8").toString("base64");await a.rest.repos.createOrUpdateFileContents({owner:n,repo:l,path:N(),message:`Sync dango data v${e.version}`,content:s,sha:r?.sha,headers:o})}async syncDown(t){const e=await this.fetchSyncPayload();return e?(this.importDatabase(t,e),I(e)):0}async syncMangaUp(t){const e=S(t,v),a=await this.getOctokit(),n=await this.ensureRepo(a),r=await this.getSyncFile(a,n,A()),s=Buffer.from(JSON.stringify(e,null,2),"utf8").toString("base64");await a.rest.repos.createOrUpdateFileContents({owner:n,repo:l,path:A(),message:`Sync dango manga data v${e.version}`,content:s,sha:r?.sha,headers:o})}async syncMangaDown(t){const e=await this.fetchMangaSyncPayload();return e?(b(t,v,e,{libraryTables:["manga_library","manga_progress"],backupName:"pre-sync-manga-backup.db"}),p(e)):0}async syncTvUp(t){const e=S(t,_),a=await this.getOctokit(),n=await this.ensureRepo(a),r=await this.getSyncFile(a,n,k()),s=Buffer.from(JSON.stringify(e,null,2),"utf8").toString("base64");await a.rest.repos.createOrUpdateFileContents({owner:n,repo:l,path:k(),message:`Sync dango TV data v${e.version}`,content:s,sha:r?.sha,headers:o})}async syncTvDown(t){const e=await this.fetchTvSyncPayload();return e?(b(t,_,e,{libraryTables:["tv_library","tv_progress"],backupName:"pre-sync-tv-backup.db"}),p(e)):0}async syncAsmrUp(t){const e=S(t,P),a=await this.getOctokit(),n=await this.ensureRepo(a),r=await this.getSyncFile(a,n,D()),s=Buffer.from(JSON.stringify(e,null,2),"utf8").toString("base64");await a.rest.repos.createOrUpdateFileContents({owner:n,repo:l,path:D(),message:`Sync dango ASMR data v${e.version}`,content:s,sha:r?.sha,headers:o})}async syncAsmrDown(t){const e=await this.fetchAsmrSyncPayload();return e?(b(t,P,e,{libraryTables:["asmr_library","asmr_progress"],backupName:"pre-sync-asmr-backup.db"}),p(e)):0}async runDeviceAuth(t,e,a){try{const{createOAuthDeviceAuth:n}=await G("@octokit/auth-oauth-device"),s=await n({clientId:j(),scopes:W,onVerification:c=>{this.deviceState={status:"pending",verification:{user_code:c.user_code,verification_uri:c.verification_uri,expires_in:c.expires_in,interval:c.interval}},a()}})({type:"oauth"});await R({GITHUB_TOKEN:s.token,SYNC_PROVIDER:"github"}),process.env.GITHUB_TOKEN=s.token;const d=await this.getUserProfile();this.deviceState={status:"success",user:d||void 0};try{await e(t,"github")}catch(c){u.error({err:c},"Post-GitHub-login sync failed")}}catch(n){this.deviceState={status:"error",error:n instanceof Error?n.message:"GitHub device authentication failed."},a(),u.error({err:n},"GitHub device authentication failed")}}async getOctokit(){if(!process.env.GITHUB_TOKEN)throw new Error("GitHub token is not configured.");return H(process.env.GITHUB_TOKEN)}async ensureRepo(t){const{data:e}=await t.rest.users.getAuthenticated({headers:o});try{await t.rest.repos.get({owner:e.login,repo:l,headers:o})}catch(a){if(m(a)!==404)throw a;await t.rest.repos.createForAuthenticatedUser({name:l,private:!0,auto_init:!0,description:"Private dango synchronization data.",headers:o})}return e.login}async getSyncFile(t,e,a=N()){try{const r=(await t.rest.repos.getContent({owner:e,repo:l,path:a,headers:o})).data;return r.type!=="file"||!r.content||!r.sha?null:{content:Buffer.from(r.content,"base64").toString("utf8"),sha:r.sha}}catch(n){if(m(n)===404)return null;throw n}}async fetchSyncPayload(){const t=await this.getOctokit(),e=await this.ensureRepo(t),a=await this.getSyncFile(t,e);return a?Y(JSON.parse(a.content)):null}async fetchMangaSyncPayload(){const t=await this.getOctokit(),e=await this.ensureRepo(t),a=await this.getSyncFile(t,e,A());return a?T(JSON.parse(a.content),v,"GitHub manga"):null}async fetchTvSyncPayload(){const t=await this.getOctokit(),e=await this.ensureRepo(t),a=await this.getSyncFile(t,e,k());return a?T(JSON.parse(a.content),_,"GitHub TV"):null}async fetchAsmrSyncPayload(){const t=await this.getOctokit(),e=await this.ensureRepo(t),a=await this.getSyncFile(t,e,D());return a?T(JSON.parse(a.content),P,"GitHub ASMR"):null}async exportDatabase(t){const e={};for(const a of g){const n=await $(t,`SELECT * FROM ${f(a)}`);e[a]=n.filter(r=>!O(r))}return{version:I({version:0,exportedAt:"",tables:e}),exportedAt:new Date().toISOString(),tables:e}}importDatabase(t,e){const a=(e.tables.watchlist?.length??0)+(e.tables.watched_episodes?.length??0),n=x(t,"SELECT (SELECT COUNT(*) FROM watchlist) + (SELECT COUNT(*) FROM watched_episodes) AS n")?.n??0;if(a===0&&n>0)throw new Error("Sync down refused: remote library is empty while local library has data");try{t.backup(L.join(y.ROOT,"pre-sync-backup.db"))}catch(s){u.warn({err:s},"Pre-sync backup failed, continuing without backup")}const r=new Map;for(const s of g){const d=t.all(`PRAGMA table_info("${s}")`);r.set(s,new Set(d.map(c=>c.name)))}t.serialize(()=>{for(const s of g)t.run(`DELETE FROM ${f(s)}`);for(const s of g){const d=r.get(s);for(const c of e.tables[s]){if(O(c))continue;const h=Object.keys(c).filter(w=>d?.has(w));if(h.length===0)continue;const U=h.map(f).join(", "),F=h.map(()=>"?").join(", "),B=h.map(w=>c[w]);t.run(`INSERT INTO ${f(s)} (${U}) VALUES (${F})`,B)}}})}}const rt=new K;export{rt as githubSyncService};