@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,31 +0,0 @@
1
- import { integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
- export const mangaLibrary = sqliteTable('manga_library', {
3
- id: text('id').notNull(),
4
- provider: text('provider').notNull(),
5
- mangaId: text('mangaId').notNull(),
6
- title: text('title'),
7
- cover: text('cover'),
8
- status: text('status'),
9
- author: text('author'),
10
- altTitle: text('altTitle'),
11
- contentRating: text('contentRating'),
12
- lastChapterId: text('lastChapterId'),
13
- lastChapterNumber: text('lastChapterNumber'),
14
- lastPage: integer('lastPage'),
15
- updatedAt: integer('updatedAt'),
16
- anilistId: integer('anilistId'),
17
- anilistIdSource: text('anilistIdSource'),
18
- }, (t) => [primaryKey({ columns: [t.id] })]);
19
- export const mangaProgress = sqliteTable('manga_progress', {
20
- mangaId: text('mangaId').notNull(),
21
- chapterId: text('chapterId').notNull(),
22
- chapterNumber: text('chapterNumber'),
23
- page: integer('page'),
24
- pageCount: integer('pageCount'),
25
- updatedAt: integer('updatedAt'),
26
- title: text('title'),
27
- cover: text('cover'),
28
- provider: text('provider'),
29
- altTitle: text('altTitle'),
30
- contentRating: text('contentRating'),
31
- }, (t) => [primaryKey({ columns: [t.mangaId, t.chapterId] })]);
@@ -1,18 +0,0 @@
1
- import { integer, primaryKey, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
- export const tvProgress = sqliteTable('tv_progress', {
3
- mediaId: text('mediaId').notNull(),
4
- season: integer('season').notNull(),
5
- episode: integer('episode').notNull(),
6
- currentTime: real('currentTime'),
7
- duration: real('duration'),
8
- updatedAt: integer('updatedAt'),
9
- title: text('title'),
10
- poster: text('poster'),
11
- backdrop: text('backdrop'),
12
- year: text('year'),
13
- overview: text('overview'),
14
- tmdbId: integer('tmdbId'),
15
- mediaType: text('mediaType'),
16
- adult: integer('adult'),
17
- completed: integer('completed'),
18
- }, (t) => [primaryKey({ columns: [t.mediaId, t.season, t.episode] })]);
package/server/dist/db.js DELETED
@@ -1,217 +0,0 @@
1
- import { DatabaseSync } from 'node:sqlite';
2
- import { AsyncLocalStorage } from 'node:async_hooks';
3
- import fs from 'fs';
4
- import path from 'path';
5
- import logger from './logger.js';
6
- const txOwner = new AsyncLocalStorage();
7
- export class DatabaseWrapper {
8
- db;
9
- isClosed = false;
10
- statementCache = new Map();
11
- txDepth = 0;
12
- txQueue = Promise.resolve();
13
- constructor(_dbPath, db) {
14
- this.db = db;
15
- }
16
- static async create(dbPath) {
17
- try {
18
- const dir = path.dirname(dbPath);
19
- if (!fs.existsSync(dir)) {
20
- fs.mkdirSync(dir, { recursive: true });
21
- }
22
- const db = new DatabaseSync(dbPath);
23
- return new DatabaseWrapper(dbPath, db);
24
- }
25
- catch (e) {
26
- logger.error({ err: e }, `Failed to initialize database at ${dbPath}`);
27
- throw e;
28
- }
29
- }
30
- getClient() {
31
- return this.db;
32
- }
33
- configure(option, value) {
34
- if (option === 'busyTimeout') {
35
- this.db.exec(`PRAGMA busy_timeout = ${value}`);
36
- }
37
- }
38
- serialize(cb) {
39
- const outer = this.txDepth === 0;
40
- const level = this.txDepth;
41
- this.run(outer ? 'BEGIN IMMEDIATE' : `SAVEPOINT dango_sp_${level}`);
42
- this.txDepth = level + 1;
43
- try {
44
- txOwner.run(this, cb);
45
- }
46
- catch (e) {
47
- this.txDepth = level;
48
- try {
49
- this.run(outer ? 'ROLLBACK' : `ROLLBACK TO SAVEPOINT dango_sp_${level}`);
50
- }
51
- catch {
52
- // ignore
53
- }
54
- throw e;
55
- }
56
- this.txDepth = level;
57
- this.run(outer ? 'COMMIT' : `RELEASE SAVEPOINT dango_sp_${level}`);
58
- }
59
- acquireTxQueue() {
60
- const prev = this.txQueue;
61
- let release;
62
- const current = new Promise((res) => {
63
- release = res;
64
- });
65
- this.txQueue = prev.then(() => current);
66
- return prev.then(() => release);
67
- }
68
- async transact(fn) {
69
- if (txOwner.getStore() === this) {
70
- const level = this.txDepth;
71
- this.run(`SAVEPOINT dango_sp_${level}`);
72
- this.txDepth = level + 1;
73
- try {
74
- await fn(this);
75
- }
76
- catch (e) {
77
- this.txDepth = level;
78
- try {
79
- this.run(`ROLLBACK TO SAVEPOINT dango_sp_${level}`);
80
- }
81
- catch {
82
- // ignore
83
- }
84
- throw e;
85
- }
86
- this.txDepth = level;
87
- this.run(`RELEASE SAVEPOINT dango_sp_${level}`);
88
- return;
89
- }
90
- const release = await this.acquireTxQueue();
91
- try {
92
- await txOwner.run(this, async () => {
93
- this.run('BEGIN IMMEDIATE');
94
- this.txDepth = 1;
95
- try {
96
- await fn(this);
97
- }
98
- catch (e) {
99
- this.txDepth = 0;
100
- try {
101
- this.run('ROLLBACK');
102
- }
103
- catch {
104
- // ignore
105
- }
106
- throw e;
107
- }
108
- this.txDepth = 0;
109
- this.run('COMMIT');
110
- });
111
- }
112
- finally {
113
- release();
114
- }
115
- }
116
- close(cb) {
117
- if (this.isClosed) {
118
- if (cb)
119
- cb(null);
120
- return;
121
- }
122
- try {
123
- this.isClosed = true;
124
- this.statementCache.clear();
125
- this.db.close();
126
- if (cb)
127
- cb(null);
128
- }
129
- catch (e) {
130
- logger.error({ err: e }, 'Error during database close');
131
- if (cb)
132
- cb(e);
133
- }
134
- }
135
- isClosedCheck() {
136
- return this.isClosed;
137
- }
138
- getPreparedStatement(query) {
139
- let stmt = this.statementCache.get(query);
140
- if (!stmt) {
141
- if (this.statementCache.size > 100) {
142
- this.statementCache.clear();
143
- }
144
- stmt = this.db.prepare(query);
145
- this.statementCache.set(query, stmt);
146
- }
147
- return stmt;
148
- }
149
- run(query, params = []) {
150
- if (this.isClosed) {
151
- throw new Error('Database is closed');
152
- }
153
- const stmt = this.getPreparedStatement(query);
154
- if (params.length > 0) {
155
- stmt.run(...params);
156
- }
157
- else {
158
- stmt.run();
159
- }
160
- }
161
- get(query, params = []) {
162
- if (this.isClosed) {
163
- throw new Error('Database is closed');
164
- }
165
- const stmt = this.getPreparedStatement(query);
166
- if (params.length > 0) {
167
- return stmt.get(...params);
168
- }
169
- return stmt.get();
170
- }
171
- all(query, params = []) {
172
- if (this.isClosed) {
173
- throw new Error('Database is closed');
174
- }
175
- const stmt = this.getPreparedStatement(query);
176
- if (params.length > 0) {
177
- return stmt.all(...params);
178
- }
179
- return stmt.all();
180
- }
181
- prepare(query) {
182
- const stmt = this.getPreparedStatement(query);
183
- return {
184
- run: (...args) => {
185
- stmt.run(...args);
186
- },
187
- all: () => {
188
- return stmt.all();
189
- },
190
- get: () => {
191
- return stmt.get();
192
- },
193
- finalize: () => { },
194
- };
195
- }
196
- backup(backupPath) {
197
- try {
198
- if (fs.existsSync(backupPath)) {
199
- fs.rmSync(backupPath, { force: true });
200
- }
201
- this.db.exec(`VACUUM INTO '${backupPath}'`);
202
- }
203
- catch (e) {
204
- logger.error({ err: e, backupPath }, 'Database backup failed via VACUUM INTO');
205
- throw e;
206
- }
207
- }
208
- checkpoint() {
209
- try {
210
- this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
211
- }
212
- catch (e) {
213
- logger.error({ err: e }, 'Database WAL checkpoint failed');
214
- throw e;
215
- }
216
- }
217
- }
@@ -1,385 +0,0 @@
1
- import WebSocket from 'ws';
2
- import logger from './logger.js';
3
- import { CONFIG } from './config.js';
4
- import dotenv from 'dotenv';
5
- const log = logger.child({ module: 'DiscordGateway' });
6
- class DiscordGatewayService {
7
- gateway = null;
8
- heartbeat = null;
9
- token = null;
10
- reconnectTimeout = null;
11
- lastActivity = null;
12
- isEnabled = false;
13
- RECONNECT_DELAY = 15000;
14
- externalAssetCache = new Map();
15
- setEnabled(enabled) {
16
- this.isEnabled = enabled;
17
- if (enabled) {
18
- this.loadToken();
19
- this.connect();
20
- }
21
- else {
22
- this.disconnect();
23
- }
24
- }
25
- get serviceEnabled() {
26
- return this.isEnabled && !!this.token && this.token.length > 30;
27
- }
28
- hasToken() {
29
- return !!this.token && this.token.length > 30;
30
- }
31
- loadToken() {
32
- this.token = process.env.DISCORD_GATEWAY_TOKEN || null;
33
- }
34
- reloadToken() {
35
- dotenv.config({ path: CONFIG.ENV_PATH, override: true });
36
- this.loadToken();
37
- if (this.serviceEnabled) {
38
- this.disconnect();
39
- this.connect();
40
- }
41
- }
42
- async getExternalAsset(url) {
43
- if (!CONFIG.DISCORD_CLIENT_ID || !this.token)
44
- return null;
45
- const cached = this.externalAssetCache.get(url);
46
- if (cached)
47
- return cached;
48
- try {
49
- const res = await fetch(`https://discord.com/api/v9/applications/${CONFIG.DISCORD_CLIENT_ID}/external-assets`, {
50
- method: 'POST',
51
- headers: {
52
- Authorization: this.token,
53
- 'Content-Type': 'application/json',
54
- },
55
- body: JSON.stringify({ urls: [url] }),
56
- });
57
- const text = await res.text();
58
- if (!res.ok) {
59
- log.warn(`external-assets proxy failed ${res.status}: ${text.slice(0, 200)}`);
60
- return null;
61
- }
62
- const data = JSON.parse(text);
63
- if (!data[0]?.external_asset_path)
64
- return null;
65
- const mp = `mp:${data[0].external_asset_path}`;
66
- this.externalAssetCache.set(url, mp);
67
- return mp;
68
- }
69
- catch (err) {
70
- log.warn({ err }, 'external-assets proxy error');
71
- return null;
72
- }
73
- }
74
- resolveImageUrl(data) {
75
- const isSafeUrl = (url) => {
76
- if (!url.startsWith('https://'))
77
- return false;
78
- if (url.includes('localhost') || url.includes('127.0.0.1'))
79
- return false;
80
- if (url.includes('s4.anilist.co') || url.includes('anilistcdn'))
81
- return true;
82
- const blocked = [
83
- 'youtube-anime.com',
84
- 'animepahe',
85
- 'animeya.cc',
86
- 'gogocdn.net',
87
- 'weeabo0.xyz',
88
- 'japaneseasmr.com',
89
- ];
90
- return !blocked.some((d) => url.includes(d));
91
- };
92
- let thumb = data.thumbnail;
93
- if (thumb && thumb.includes('/api/image-proxy')) {
94
- const m = thumb.match(/url=([^&]+)/);
95
- if (m) {
96
- try {
97
- thumb = decodeURIComponent(m[1]);
98
- }
99
- catch (_e) {
100
- void _e;
101
- }
102
- }
103
- }
104
- if (thumb && data.providerName !== 'AnimePahe' && isSafeUrl(thumb))
105
- return thumb;
106
- if (data.thumbnails) {
107
- const safe = data.thumbnails.find((t) => t && (t.includes('s4.anilist.co') || t.includes('anilistcdn')));
108
- if (safe)
109
- return safe;
110
- }
111
- if (thumb && isSafeUrl(thumb))
112
- return thumb;
113
- return null;
114
- }
115
- async updatePresence(data) {
116
- this.lastActivity = data;
117
- if (!this.isEnabled || !this.gateway || this.gateway.readyState !== WebSocket.OPEN)
118
- return;
119
- const name = 'dango';
120
- const details = data.title;
121
- const state = data.stateLine
122
- ? data.stateLine
123
- : `Episode ${data.episode}${data.totalEpisodes ? `/${data.totalEpisodes}` : ''}`;
124
- const type = data.providerName === 'ASMR' || data.providerName === 'Radio' || data.providerName === 'Music'
125
- ? 2
126
- : 3;
127
- const imageUrl = this.resolveImageUrl(data);
128
- let assets;
129
- if (imageUrl) {
130
- if (CONFIG.DISCORD_CLIENT_ID) {
131
- const proxied = await this.getExternalAsset(imageUrl);
132
- if (proxied) {
133
- assets = {
134
- large_image: proxied,
135
- large_text: data.title.slice(0, 128),
136
- small_image: 'small',
137
- small_text: 'dango',
138
- };
139
- }
140
- else {
141
- assets = { large_image: 'logo', large_text: data.title.slice(0, 128) };
142
- }
143
- }
144
- }
145
- else if (CONFIG.DISCORD_CLIENT_ID) {
146
- assets = { large_image: 'logo', large_text: data.title.slice(0, 128) };
147
- }
148
- const presence = {
149
- since: null,
150
- activities: [
151
- {
152
- name,
153
- type,
154
- state: data.isPlaying ? state : `${state} (Paused)`,
155
- details,
156
- ...(CONFIG.DISCORD_CLIENT_ID ? { application_id: CONFIG.DISCORD_CLIENT_ID } : {}),
157
- ...(assets ? { assets } : {}),
158
- buttons: ['Learn More'],
159
- metadata: { button_urls: ['https://github.com/serifpersia/dango'] },
160
- status_display_type: 2,
161
- },
162
- ],
163
- status: 'online',
164
- afk: false,
165
- };
166
- const nowMs = Date.now();
167
- if (data.isPlaying) {
168
- if (data.currentTime && data.currentTime > 0) {
169
- presence.since = nowMs - Math.round(data.currentTime * 1000);
170
- }
171
- if (data.duration && data.duration > data.currentTime) {
172
- presence.activities[0].timestamps = {
173
- start: nowMs - Math.round(data.currentTime * 1000),
174
- end: nowMs + Math.round((data.duration - data.currentTime) * 1000),
175
- };
176
- }
177
- }
178
- try {
179
- this.gateway.send(JSON.stringify({ op: 3, d: presence }));
180
- }
181
- catch (err) {
182
- log.error({ err }, 'Failed to send gateway presence update');
183
- }
184
- }
185
- setIdleStatus(page) {
186
- if (!this.isEnabled || !this.gateway || this.gateway.readyState !== WebSocket.OPEN)
187
- return;
188
- const pageLabels = {
189
- home: { details: 'Home', state: 'Browsing anime' },
190
- search: { details: 'Search', state: 'Exploring titles...' },
191
- watchlist: { details: 'Watchlist', state: 'Reviewing the watchlist' },
192
- anime: { details: 'Anime Info', state: 'Reading show details' },
193
- insights: { details: 'Insights', state: 'Reviewing stats' },
194
- settings: { details: 'Settings', state: 'Tweaking preferences' },
195
- trackers: { details: 'Trackers', state: 'Syncing watchlists' },
196
- map: { details: 'Map', state: 'Exploring the global user map' },
197
- asmr: { details: 'ASMR', state: 'Browsing ASMR works' },
198
- radio: { details: 'Radio', state: 'Browsing radio stations' },
199
- music: { details: 'Music', state: 'Browsing music' },
200
- tv: { details: 'TV', state: 'Browsing movies & shows' },
201
- manga: { details: 'Manga', state: 'Browsing manga' },
202
- };
203
- const label = pageLabels[page] ?? { details: 'dango', state: 'Idle' };
204
- try {
205
- this.gateway.send(JSON.stringify({
206
- op: 3,
207
- d: {
208
- since: null,
209
- activities: [
210
- {
211
- name: 'dango',
212
- type: page === 'asmr' || page === 'radio' || page === 'music' ? 2 : 3,
213
- state: label.state,
214
- details: label.details,
215
- ...(CONFIG.DISCORD_CLIENT_ID ? { application_id: CONFIG.DISCORD_CLIENT_ID } : {}),
216
- ...(CONFIG.DISCORD_CLIENT_ID
217
- ? { assets: { large_image: 'logo', large_text: 'dango' } }
218
- : {}),
219
- buttons: ['Learn More'],
220
- metadata: { button_urls: ['https://github.com/serifpersia/dango'] },
221
- status_display_type: 2,
222
- },
223
- ],
224
- status: 'online',
225
- afk: false,
226
- },
227
- }));
228
- }
229
- catch (err) {
230
- log.error({ err }, 'Failed to send gateway idle status');
231
- }
232
- }
233
- clearPresence() {
234
- if (!this.isEnabled)
235
- return;
236
- if (this.gateway && this.gateway.readyState === WebSocket.OPEN) {
237
- try {
238
- this.gateway.send(JSON.stringify({
239
- op: 3,
240
- d: { since: null, activities: [], status: 'online', afk: false },
241
- }));
242
- }
243
- catch {
244
- // ignore
245
- }
246
- }
247
- this.lastActivity = null;
248
- }
249
- shutdown() {
250
- this.isEnabled = false;
251
- this.token = null;
252
- this.disconnect();
253
- }
254
- disconnect() {
255
- if (this.reconnectTimeout) {
256
- clearTimeout(this.reconnectTimeout);
257
- this.reconnectTimeout = null;
258
- }
259
- if (this.gateway) {
260
- try {
261
- if (this.gateway.readyState === WebSocket.OPEN) {
262
- this.gateway.send(JSON.stringify({
263
- op: 3,
264
- d: { since: null, activities: [], status: 'online', afk: false },
265
- }));
266
- }
267
- this.gateway.close();
268
- }
269
- catch {
270
- // ignore
271
- }
272
- this.gateway = null;
273
- }
274
- if (this.heartbeat) {
275
- clearInterval(this.heartbeat);
276
- this.heartbeat = null;
277
- }
278
- }
279
- connect() {
280
- if (!this.isEnabled || !this.token || this.gateway)
281
- return;
282
- const wsUrl = 'wss://gateway.discord.gg/?encoding=json&v=10';
283
- log.debug('Connecting Discord gateway');
284
- this.gateway = new WebSocket(wsUrl);
285
- let heartbeatInterval = 41250;
286
- this.gateway.on('message', (data) => {
287
- try {
288
- const msg = JSON.parse(data.toString());
289
- if (msg.op === 10) {
290
- heartbeatInterval = msg.d.heartbeat_interval;
291
- this.heartbeat = setInterval(() => {
292
- try {
293
- if (this.gateway && this.gateway.readyState === WebSocket.OPEN) {
294
- this.gateway.send(JSON.stringify({ op: 1, d: null }));
295
- }
296
- }
297
- catch {
298
- // ignore
299
- }
300
- }, heartbeatInterval);
301
- const identify = {
302
- op: 2,
303
- d: {
304
- token: this.token,
305
- capabilities: 1021,
306
- client_state: {
307
- guild_hashes: {},
308
- highest_last_message_id: '0',
309
- private_channels_version: '0',
310
- read_state_version: 0,
311
- user_guild_settings_version: -1,
312
- user_settings_version: -1,
313
- },
314
- compress: false,
315
- presence: {
316
- since: null,
317
- activities: [],
318
- status: 'online',
319
- afk: false,
320
- },
321
- properties: {
322
- os: 'Windows',
323
- os_version: '10.0.19044',
324
- os_arch: 'x64',
325
- browser: 'Discord Client',
326
- client_version: '0.0.20',
327
- client_build_number: 152131,
328
- client_event_source: null,
329
- release_channel: 'stable',
330
- system_locale: 'en-US',
331
- },
332
- },
333
- };
334
- if (this.gateway) {
335
- this.gateway.send(JSON.stringify(identify));
336
- }
337
- }
338
- else if (msg.op === 11) {
339
- // ignore
340
- }
341
- else if (msg.op === 0) {
342
- if (msg.t === 'READY') {
343
- log.info(`Gateway READY as ${msg.d.user?.username || 'unknown'}`);
344
- if (this.lastActivity) {
345
- void this.updatePresence(this.lastActivity).catch((err) => log.warn({ err }, 'failed to restore presence after READY'));
346
- }
347
- }
348
- }
349
- else if (msg.op === 7) {
350
- log.warn('Gateway reconnect requested by Discord');
351
- this.handleDisconnect();
352
- }
353
- else if (msg.op === 9) {
354
- log.warn('Gateway invalid session, reconnecting...');
355
- this.handleDisconnect();
356
- }
357
- }
358
- catch (err) {
359
- log.debug({ err }, 'Gateway message parse error');
360
- }
361
- });
362
- this.gateway.on('close', (code, reason) => {
363
- log.warn(`Gateway closed ${code} ${reason.toString().slice(0, 100)}`);
364
- this.handleDisconnect();
365
- });
366
- this.gateway.on('error', (err) => {
367
- log.error(err, 'Gateway WebSocket error');
368
- this.handleDisconnect();
369
- });
370
- }
371
- handleDisconnect() {
372
- if (this.heartbeat) {
373
- clearInterval(this.heartbeat);
374
- this.heartbeat = null;
375
- }
376
- this.gateway = null;
377
- if (this.isEnabled && this.token) {
378
- this.reconnectTimeout = setTimeout(() => {
379
- this.reconnectTimeout = null;
380
- this.connect();
381
- }, this.RECONNECT_DELAY);
382
- }
383
- }
384
- }
385
- export const discordGatewayService = new DiscordGatewayService();