@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,413 +1 @@
1
- import { Client, StatusDisplayType } from '@xhayper/discord-rpc';
2
- import logger from './logger.js';
3
- import { CONFIG } from './config.js';
4
- import { discordGatewayService } from './discord-gateway.js';
5
- const log = logger.child({ module: 'DiscordRPC' });
6
- class DiscordRPCService {
7
- client = null;
8
- isEnabled = false;
9
- hideMature = true;
10
- reconnectTimeout = null;
11
- lastActivity = null;
12
- currentSessionId = null;
13
- hasPagePresence = false;
14
- hasEverConnected = false;
15
- permanentlyDisabled = false;
16
- heartbeats = new Map();
17
- heartbeatTimer = null;
18
- retryCount = 0;
19
- MAX_RETRIES = 5;
20
- INITIAL_RECONNECT_DELAY = 15000;
21
- MAX_RECONNECT_DELAY = 300000;
22
- HEARTBEAT_TIMEOUT = 40000;
23
- HEARTBEAT_CHECK_INTERVAL = 10000;
24
- async setEnabled(enabled) {
25
- if (this.isEnabled === enabled)
26
- return;
27
- this.isEnabled = enabled;
28
- if (enabled) {
29
- if (this.permanentlyDisabled) {
30
- log.debug('Discord Rich Presence is unavailable on this device/session. Not retrying.');
31
- this.isEnabled = false;
32
- return;
33
- }
34
- this.retryCount = 0;
35
- discordGatewayService.setEnabled(true);
36
- if (!discordGatewayService.serviceEnabled) {
37
- this.connect();
38
- }
39
- }
40
- else {
41
- await this.disconnect();
42
- discordGatewayService.setEnabled(false);
43
- }
44
- }
45
- get isServiceEnabled() {
46
- return this.isEnabled && (this.client !== null || discordGatewayService.serviceEnabled);
47
- }
48
- setHideMature(hide) {
49
- this.hideMature = hide;
50
- }
51
- heartbeat(sessionId) {
52
- this.heartbeats.set(sessionId, Date.now());
53
- if (!this.heartbeatTimer && this.isEnabled) {
54
- this.heartbeatTimer = setInterval(() => {
55
- void this.checkHeartbeats();
56
- }, this.HEARTBEAT_CHECK_INTERVAL);
57
- }
58
- }
59
- removeHeartbeat(sessionId) {
60
- this.heartbeats.delete(sessionId);
61
- void this.checkHeartbeats();
62
- }
63
- async checkHeartbeats() {
64
- if (!this.isEnabled) {
65
- if (this.heartbeatTimer) {
66
- clearInterval(this.heartbeatTimer);
67
- this.heartbeatTimer = null;
68
- }
69
- return;
70
- }
71
- const now = Date.now();
72
- for (const [sessionId, lastSeen] of this.heartbeats) {
73
- if (now - lastSeen > this.HEARTBEAT_TIMEOUT) {
74
- this.heartbeats.delete(sessionId);
75
- }
76
- }
77
- if (this.heartbeats.size === 0) {
78
- if (this.heartbeatTimer) {
79
- clearInterval(this.heartbeatTimer);
80
- this.heartbeatTimer = null;
81
- }
82
- if (this.hasPagePresence || this.lastActivity || this.currentSessionId) {
83
- this.lastActivity = null;
84
- this.currentSessionId = null;
85
- this.hasPagePresence = false;
86
- await this.setIdleStatus('idle');
87
- }
88
- }
89
- }
90
- async connect() {
91
- if (!this.isEnabled || this.client || this.permanentlyDisabled)
92
- return;
93
- if (discordGatewayService.serviceEnabled) {
94
- log.debug('Using Discord Gateway for presence (token configured). Skipping local RPC.');
95
- return;
96
- }
97
- const isTermux = !!process.env.TERMUX_VERSION;
98
- const isAndroid = process.platform === 'android';
99
- if (isTermux || isAndroid) {
100
- log.debug('Discord Rich Presence is not supported on Android/Termux. Skipping connection.');
101
- this.isEnabled = false;
102
- this.permanentlyDisabled = true;
103
- return;
104
- }
105
- const clientId = CONFIG.DISCORD_CLIENT_ID;
106
- if (!clientId) {
107
- log.debug('DISCORD_CLIENT_ID is not configured. Discord Rich Presence is disabled.');
108
- this.isEnabled = false;
109
- this.permanentlyDisabled = true;
110
- return;
111
- }
112
- try {
113
- this.client = new Client({ clientId });
114
- this.client.on('ready', () => {
115
- log.info('Connected to Discord client successfully');
116
- this.hasEverConnected = true;
117
- this.retryCount = 0;
118
- if (this.lastActivity) {
119
- this.updatePresence(this.lastActivity);
120
- }
121
- else {
122
- this.setIdleStatus('idle');
123
- }
124
- });
125
- this.client.on('disconnected', () => {
126
- log.warn('Disconnected from Discord client, scheduling reconnect...');
127
- this.cleanup();
128
- this.scheduleReconnect();
129
- });
130
- this.client.on('ERROR', (err) => {
131
- const errMsg = err instanceof Error ? err.message : String(err);
132
- if (errMsg.includes('ENOENT') || errMsg.includes('ECONNREFUSED')) {
133
- this.cleanup();
134
- if (!this.hasEverConnected) {
135
- log.debug('Discord client not detected. Rich Presence disabled for this session.');
136
- this.isEnabled = false;
137
- this.permanentlyDisabled = true;
138
- return;
139
- }
140
- log.debug('Discord client not running or connection refused.');
141
- this.scheduleReconnect();
142
- }
143
- else {
144
- log.error({ err }, 'Discord RPC client error');
145
- this.cleanup();
146
- this.scheduleReconnect();
147
- }
148
- });
149
- const loginPromise = this.client.login();
150
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Login timeout')), 5000));
151
- await Promise.race([loginPromise, timeoutPromise]);
152
- this.hasEverConnected = true;
153
- }
154
- catch (err) {
155
- this.cleanup();
156
- if (!this.hasEverConnected) {
157
- log.debug('Discord client not available at startup. Rich Presence disabled for the rest of this session.');
158
- this.isEnabled = false;
159
- this.permanentlyDisabled = true;
160
- return;
161
- }
162
- this.scheduleReconnect();
163
- }
164
- }
165
- scheduleReconnect() {
166
- if (!this.isEnabled || this.reconnectTimeout || this.permanentlyDisabled)
167
- return;
168
- const delay = Math.min(this.INITIAL_RECONNECT_DELAY * Math.pow(1.5, this.retryCount), this.MAX_RECONNECT_DELAY);
169
- this.retryCount++;
170
- if (this.retryCount > this.MAX_RETRIES) {
171
- log.debug(`Discord RPC login failed ${this.retryCount} times. Retrying in ${Math.round(delay / 1000)}s...`);
172
- }
173
- else {
174
- log.info(`Discord RPC login failed. Retrying in ${Math.round(delay / 1000)}s...`);
175
- }
176
- this.reconnectTimeout = setTimeout(() => {
177
- this.reconnectTimeout = null;
178
- this.connect();
179
- }, delay);
180
- }
181
- cleanup() {
182
- if (this.client) {
183
- try {
184
- this.client.removeAllListeners();
185
- this.client.destroy();
186
- }
187
- catch (err) {
188
- // ignore
189
- }
190
- this.client = null;
191
- }
192
- }
193
- async disconnect() {
194
- if (this.reconnectTimeout) {
195
- clearTimeout(this.reconnectTimeout);
196
- this.reconnectTimeout = null;
197
- }
198
- if (this.client?.user) {
199
- try {
200
- await this.client.user.clearActivity();
201
- }
202
- catch {
203
- // ignore
204
- }
205
- }
206
- this.cleanup();
207
- this.lastActivity = null;
208
- this.currentSessionId = null;
209
- this.heartbeats.clear();
210
- if (this.heartbeatTimer) {
211
- clearInterval(this.heartbeatTimer);
212
- this.heartbeatTimer = null;
213
- }
214
- discordGatewayService.disconnect();
215
- }
216
- async updatePresence(data) {
217
- this.lastActivity = data;
218
- if (data.sessionId) {
219
- this.currentSessionId = data.sessionId;
220
- }
221
- if (!this.isEnabled)
222
- return;
223
- if (this.hideMature && data.isAdult) {
224
- if (discordGatewayService.serviceEnabled) {
225
- discordGatewayService.clearPresence();
226
- }
227
- else if (this.client?.user) {
228
- try {
229
- await this.client.user.clearActivity();
230
- }
231
- catch {
232
- // ignore
233
- }
234
- }
235
- return;
236
- }
237
- if (discordGatewayService.serviceEnabled) {
238
- discordGatewayService.updatePresence(data);
239
- return;
240
- }
241
- if (!this.client || !this.client.user)
242
- return;
243
- const isSafeUrl = (url) => {
244
- if (!url.startsWith('https://')) {
245
- return false;
246
- }
247
- if (url.includes('localhost') || url.includes('127.0.0.1')) {
248
- return false;
249
- }
250
- if (url.includes('s4.anilist.co') || url.includes('anilistcdn')) {
251
- return true;
252
- }
253
- const blockedDomains = [
254
- 'youtube-anime.com',
255
- 'animepahe',
256
- 'animeya.cc',
257
- 'gogocdn.net',
258
- 'weeabo0.xyz',
259
- 'japaneseasmr.com',
260
- ];
261
- return !blockedDomains.some((domain) => url.includes(domain));
262
- };
263
- let imageKey = 'logo';
264
- if (data.thumbnail && data.providerName !== 'AnimePahe') {
265
- let thumbUrl = data.thumbnail;
266
- if (thumbUrl.includes('/api/image-proxy')) {
267
- const match = thumbUrl.match(/url=([^&]+)/);
268
- if (match) {
269
- thumbUrl = decodeURIComponent(match[1]);
270
- }
271
- }
272
- if (isSafeUrl(thumbUrl)) {
273
- imageKey = thumbUrl;
274
- }
275
- else if (data.thumbnails) {
276
- const safeThumb = data.thumbnails.find((t) => t && (t.includes('s4.anilist.co') || t.includes('anilistcdn')));
277
- if (safeThumb)
278
- imageKey = safeThumb;
279
- }
280
- }
281
- try {
282
- if (!data.isPlaying) {
283
- await this.client.user.clearActivity();
284
- await this.client.user.setActivity({
285
- details: data.title,
286
- state: data.stateLine
287
- ? `${data.stateLine} (Paused)`
288
- : `Episode ${data.episode}${data.totalEpisodes ? `/${data.totalEpisodes}` : ''} (Paused)`,
289
- largeImageKey: imageKey,
290
- largeImageText: data.title,
291
- ...(imageKey !== 'logo' ? { smallImageKey: 'small', smallImageText: 'dango' } : {}),
292
- type: data.providerName === 'ASMR' ||
293
- data.providerName === 'Radio' ||
294
- data.providerName === 'Music'
295
- ? 2
296
- : 3,
297
- statusDisplayType: StatusDisplayType.DETAILS,
298
- buttons: [
299
- {
300
- label: 'Learn More',
301
- url: 'https://github.com/serifpersia/dango',
302
- },
303
- ],
304
- });
305
- this.hasPagePresence = false;
306
- return;
307
- }
308
- const nowSeconds = Math.round(Date.now() / 1000);
309
- const activity = {
310
- details: data.title,
311
- state: data.stateLine
312
- ? data.stateLine
313
- : `Episode ${data.episode}${data.totalEpisodes ? `/${data.totalEpisodes}` : ''}`,
314
- largeImageKey: imageKey,
315
- largeImageText: data.title,
316
- ...(imageKey !== 'logo' ? { smallImageKey: 'small', smallImageText: 'dango' } : {}),
317
- type: data.providerName === 'ASMR' ||
318
- data.providerName === 'Radio' ||
319
- data.providerName === 'Music'
320
- ? 2
321
- : 3,
322
- statusDisplayType: StatusDisplayType.DETAILS,
323
- buttons: [
324
- {
325
- label: 'Learn More',
326
- url: 'https://github.com/serifpersia/dango',
327
- },
328
- ],
329
- };
330
- if (data.currentTime && data.currentTime > 0) {
331
- activity.startTimestamp = Math.round(nowSeconds - data.currentTime);
332
- }
333
- if (data.duration && data.duration > data.currentTime) {
334
- activity.endTimestamp = Math.round(nowSeconds + (data.duration - data.currentTime));
335
- }
336
- await this.client.user.setActivity(activity);
337
- this.hasPagePresence = false;
338
- }
339
- catch (err) {
340
- log.error({ err }, 'Failed to set Discord activity');
341
- }
342
- }
343
- async setIdleStatus(page) {
344
- if (!this.isEnabled)
345
- return;
346
- if (discordGatewayService.serviceEnabled) {
347
- discordGatewayService.setIdleStatus(page);
348
- return;
349
- }
350
- if (!this.client || !this.client.user) {
351
- log.warn('Discord client not connected, skipping idle status update');
352
- return;
353
- }
354
- const pageLabels = {
355
- home: { details: 'Home', state: 'Browsing anime' },
356
- search: { details: 'Search', state: 'Exploring titles...' },
357
- watchlist: { details: 'Watchlist', state: 'Reviewing the watchlist' },
358
- anime: { details: 'Anime Info', state: 'Reading show details' },
359
- insights: { details: 'Insights', state: 'Reviewing stats' },
360
- settings: { details: 'Settings', state: 'Tweaking preferences' },
361
- mal: { details: 'MAL Sync', state: 'Syncing with MyAnimeList' },
362
- map: { details: 'Map', state: 'Exploring the global user map' },
363
- asmr: { details: 'ASMR', state: 'Browsing ASMR works' },
364
- radio: { details: 'Radio', state: 'Browsing radio stations' },
365
- music: { details: 'Music', state: 'Browsing music' },
366
- tv: { details: 'TV', state: 'Browsing movies & shows' },
367
- manga: { details: 'Manga', state: 'Browsing manga' },
368
- };
369
- const label = pageLabels[page] ?? { details: 'dango', state: 'Idle' };
370
- try {
371
- if (!this.client || !this.client.user) {
372
- log.warn('Discord client not connected, skipping idle status update');
373
- return;
374
- }
375
- await this.client.user.setActivity({
376
- details: label.details,
377
- state: label.state,
378
- largeImageKey: 'logo',
379
- largeImageText: 'dango',
380
- type: page === 'asmr' || page === 'radio' || page === 'music' ? 2 : 3,
381
- statusDisplayType: StatusDisplayType.DETAILS,
382
- buttons: [
383
- {
384
- label: 'Learn More',
385
- url: 'https://github.com/serifpersia/dango',
386
- },
387
- ],
388
- });
389
- this.hasPagePresence = page in pageLabels;
390
- }
391
- catch (err) {
392
- if (err.message === 'Closed by Discord') {
393
- log.warn('Discord connection ended, skipping idle status update');
394
- }
395
- else {
396
- log.error({ err }, 'Failed to set Discord idle status');
397
- }
398
- }
399
- }
400
- async clearPresence(sessionId) {
401
- if (sessionId && this.currentSessionId && sessionId !== this.currentSessionId) {
402
- return;
403
- }
404
- this.lastActivity = null;
405
- this.currentSessionId = null;
406
- if (discordGatewayService.serviceEnabled) {
407
- discordGatewayService.clearPresence();
408
- return;
409
- }
410
- await this.setIdleStatus('idle');
411
- }
412
- }
413
- export const discordRPCService = new DiscordRPCService();
1
+ import{Client as u,StatusDisplayType as c}from"@xhayper/discord-rpc";import h from"./logger.js";import{CONFIG as d}from"./config.js";import{discordGatewayService as n}from"./discord-gateway.js";const i=h.child({module:"DiscordRPC"});class m{client=null;isEnabled=!1;hideMature=!0;reconnectTimeout=null;lastActivity=null;currentSessionId=null;hasPagePresence=!1;hasEverConnected=!1;permanentlyDisabled=!1;heartbeats=new Map;heartbeatTimer=null;retryCount=0;MAX_RETRIES=5;INITIAL_RECONNECT_DELAY=15e3;MAX_RECONNECT_DELAY=3e5;HEARTBEAT_TIMEOUT=4e4;HEARTBEAT_CHECK_INTERVAL=1e4;async setEnabled(e){if(this.isEnabled!==e)if(this.isEnabled=e,e){if(this.permanentlyDisabled){i.debug("Discord Rich Presence is unavailable on this device/session. Not retrying."),this.isEnabled=!1;return}this.retryCount=0,n.setEnabled(!0),n.serviceEnabled||this.connect()}else await this.disconnect(),n.setEnabled(!1)}get isServiceEnabled(){return this.isEnabled&&(this.client!==null||n.serviceEnabled)}setHideMature(e){this.hideMature=e}heartbeat(e){this.heartbeats.set(e,Date.now()),!this.heartbeatTimer&&this.isEnabled&&(this.heartbeatTimer=setInterval(()=>{this.checkHeartbeats()},this.HEARTBEAT_CHECK_INTERVAL))}removeHeartbeat(e){this.heartbeats.delete(e),this.checkHeartbeats()}async checkHeartbeats(){if(!this.isEnabled){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null);return}const e=Date.now();for(const[l,s]of this.heartbeats)e-s>this.HEARTBEAT_TIMEOUT&&this.heartbeats.delete(l);this.heartbeats.size===0&&(this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null),(this.hasPagePresence||this.lastActivity||this.currentSessionId)&&(this.lastActivity=null,this.currentSessionId=null,this.hasPagePresence=!1,await this.setIdleStatus("idle")))}async connect(){if(!this.isEnabled||this.client||this.permanentlyDisabled)return;if(n.serviceEnabled){i.debug("Using Discord Gateway for presence (token configured). Skipping local RPC.");return}const e=!!process.env.TERMUX_VERSION,l=process.platform==="android";if(e||l){i.debug("Discord Rich Presence is not supported on Android/Termux. Skipping connection."),this.isEnabled=!1,this.permanentlyDisabled=!0;return}const s=d.DISCORD_CLIENT_ID;if(!s){i.debug("DISCORD_CLIENT_ID is not configured. Discord Rich Presence is disabled."),this.isEnabled=!1,this.permanentlyDisabled=!0;return}try{this.client=new u({clientId:s}),this.client.on("ready",()=>{i.info("Connected to Discord client successfully"),this.hasEverConnected=!0,this.retryCount=0,this.lastActivity?this.updatePresence(this.lastActivity):this.setIdleStatus("idle")}),this.client.on("disconnected",()=>{i.warn("Disconnected from Discord client, scheduling reconnect..."),this.cleanup(),this.scheduleReconnect()}),this.client.on("ERROR",a=>{const o=a instanceof Error?a.message:String(a);if(o.includes("ENOENT")||o.includes("ECONNREFUSED")){if(this.cleanup(),!this.hasEverConnected){i.debug("Discord client not detected. Rich Presence disabled for this session."),this.isEnabled=!1,this.permanentlyDisabled=!0;return}i.debug("Discord client not running or connection refused."),this.scheduleReconnect()}else i.error({err:a},"Discord RPC client error"),this.cleanup(),this.scheduleReconnect()});const t=this.client.login(),r=new Promise((a,o)=>setTimeout(()=>o(new Error("Login timeout")),5e3));await Promise.race([t,r]),this.hasEverConnected=!0}catch{if(this.cleanup(),!this.hasEverConnected){i.debug("Discord client not available at startup. Rich Presence disabled for the rest of this session."),this.isEnabled=!1,this.permanentlyDisabled=!0;return}this.scheduleReconnect()}}scheduleReconnect(){if(!this.isEnabled||this.reconnectTimeout||this.permanentlyDisabled)return;const e=Math.min(this.INITIAL_RECONNECT_DELAY*Math.pow(1.5,this.retryCount),this.MAX_RECONNECT_DELAY);this.retryCount++,this.retryCount>this.MAX_RETRIES?i.debug(`Discord RPC login failed ${this.retryCount} times. Retrying in ${Math.round(e/1e3)}s...`):i.info(`Discord RPC login failed. Retrying in ${Math.round(e/1e3)}s...`),this.reconnectTimeout=setTimeout(()=>{this.reconnectTimeout=null,this.connect()},e)}cleanup(){if(this.client){try{this.client.removeAllListeners(),this.client.destroy()}catch{}this.client=null}}async disconnect(){if(this.reconnectTimeout&&(clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null),this.client?.user)try{await this.client.user.clearActivity()}catch{}this.cleanup(),this.lastActivity=null,this.currentSessionId=null,this.heartbeats.clear(),this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null),n.disconnect()}async updatePresence(e){if(this.lastActivity=e,e.sessionId&&(this.currentSessionId=e.sessionId),!this.isEnabled)return;if(this.hideMature&&e.isAdult){if(n.serviceEnabled)n.clearPresence();else if(this.client?.user)try{await this.client.user.clearActivity()}catch{}return}if(n.serviceEnabled){n.updatePresence(e);return}if(!this.client||!this.client.user)return;const l=t=>!t.startsWith("https://")||t.includes("localhost")||t.includes("127.0.0.1")?!1:t.includes("s4.anilist.co")||t.includes("anilistcdn")?!0:!["youtube-anime.com","animepahe","animeya.cc","gogocdn.net","weeabo0.xyz","japaneseasmr.com"].some(a=>t.includes(a));let s="logo";if(e.thumbnail&&e.providerName!=="AnimePahe"){let t=e.thumbnail;if(t.includes("/api/image-proxy")){const r=t.match(/url=([^&]+)/);r&&(t=decodeURIComponent(r[1]))}if(l(t))s=t;else if(e.thumbnails){const r=e.thumbnails.find(a=>a&&(a.includes("s4.anilist.co")||a.includes("anilistcdn")));r&&(s=r)}}try{if(!e.isPlaying){await this.client.user.clearActivity(),await this.client.user.setActivity({details:e.title,state:e.stateLine?`${e.stateLine} (Paused)`:`Episode ${e.episode}${e.totalEpisodes?`/${e.totalEpisodes}`:""} (Paused)`,largeImageKey:s,largeImageText:e.title,...s!=="logo"?{smallImageKey:"small",smallImageText:"dango"}:{},type:e.providerName==="ASMR"||e.providerName==="Radio"||e.providerName==="Music"?2:3,statusDisplayType:c.DETAILS,buttons:[{label:"Learn More",url:"https://github.com/serifpersia/dango"}]}),this.hasPagePresence=!1;return}const t=Math.round(Date.now()/1e3),r={details:e.title,state:e.stateLine?e.stateLine:`Episode ${e.episode}${e.totalEpisodes?`/${e.totalEpisodes}`:""}`,largeImageKey:s,largeImageText:e.title,...s!=="logo"?{smallImageKey:"small",smallImageText:"dango"}:{},type:e.providerName==="ASMR"||e.providerName==="Radio"||e.providerName==="Music"?2:3,statusDisplayType:c.DETAILS,buttons:[{label:"Learn More",url:"https://github.com/serifpersia/dango"}]};e.currentTime&&e.currentTime>0&&(r.startTimestamp=Math.round(t-e.currentTime)),e.duration&&e.duration>e.currentTime&&(r.endTimestamp=Math.round(t+(e.duration-e.currentTime))),await this.client.user.setActivity(r),this.hasPagePresence=!1}catch(t){i.error({err:t},"Failed to set Discord activity")}}async setIdleStatus(e){if(!this.isEnabled)return;if(n.serviceEnabled){n.setIdleStatus(e);return}if(!this.client||!this.client.user){i.warn("Discord client not connected, skipping idle status update");return}const l={home:{details:"Home",state:"Browsing anime"},search:{details:"Search",state:"Exploring titles..."},watchlist:{details:"Watchlist",state:"Reviewing the watchlist"},anime:{details:"Anime Info",state:"Reading show details"},insights:{details:"Insights",state:"Reviewing stats"},settings:{details:"Settings",state:"Tweaking preferences"},mal:{details:"MAL Sync",state:"Syncing with MyAnimeList"},map:{details:"Map",state:"Exploring the global user map"},asmr:{details:"ASMR",state:"Browsing ASMR works"},radio:{details:"Radio",state:"Browsing radio stations"},music:{details:"Music",state:"Browsing music"},tv:{details:"TV",state:"Browsing movies & shows"},manga:{details:"Manga",state:"Browsing manga"}},s=l[e]??{details:"dango",state:"Idle"};try{if(!this.client||!this.client.user){i.warn("Discord client not connected, skipping idle status update");return}await this.client.user.setActivity({details:s.details,state:s.state,largeImageKey:"logo",largeImageText:"dango",type:e==="asmr"||e==="radio"||e==="music"?2:3,statusDisplayType:c.DETAILS,buttons:[{label:"Learn More",url:"https://github.com/serifpersia/dango"}]}),this.hasPagePresence=e in l}catch(t){t.message==="Closed by Discord"?i.warn("Discord connection ended, skipping idle status update"):i.error({err:t},"Failed to set Discord idle status")}}async clearPresence(e){if(!(e&&this.currentSessionId&&e!==this.currentSessionId)){if(this.lastActivity=null,this.currentSessionId=null,n.serviceEnabled){n.clearPresence();return}await this.setIdleStatus("idle")}}}const E=new m;export{E as discordRPCService};