@serifpersia/dango 3.3.4

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 (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/client/dist/assets/AnimeCard-B5A20jAI.js +1 -0
  4. package/client/dist/assets/AnimeInfo-0huPAq0O.js +1 -0
  5. package/client/dist/assets/AnimeInfo-BFWtHlPC.css +1 -0
  6. package/client/dist/assets/AnimeInfoPage-Dqr5QH6R.js +2 -0
  7. package/client/dist/assets/AnimeMetaDetails-BPvv5xIl.js +1 -0
  8. package/client/dist/assets/AnimeMetaDetails-BwNQnRCd.css +1 -0
  9. package/client/dist/assets/Asmr-B6ZiqkmU.js +2 -0
  10. package/client/dist/assets/Asmr-Cq2ua6Zk.css +1 -0
  11. package/client/dist/assets/Asmr.module-ChUXDZKx.js +1 -0
  12. package/client/dist/assets/AsmrResetProgressModal-Cqsoo3XC.js +1 -0
  13. package/client/dist/assets/AvSyncCalibrator-C1_iBSyL.css +1 -0
  14. package/client/dist/assets/AvSyncCalibrator-C8ZoIwMY.js +10 -0
  15. package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
  16. package/client/dist/assets/ErrorMessage-z4f4WGgP.js +1 -0
  17. package/client/dist/assets/Home-DtRtTuI9.css +1 -0
  18. package/client/dist/assets/Home-thJhPfs1.js +1 -0
  19. package/client/dist/assets/Insights-DKtoVe6L.css +1 -0
  20. package/client/dist/assets/Insights-ZlgPrzGD.js +1 -0
  21. package/client/dist/assets/LibraryListPage-CCiee-Ot.js +1 -0
  22. package/client/dist/assets/ListeningList-ctAz_6Zi.js +1 -0
  23. package/client/dist/assets/Manga-BI9WMTx5.js +1 -0
  24. package/client/dist/assets/Manga-Dp_-l1Pw.css +1 -0
  25. package/client/dist/assets/Manga.module-BcfOdsNP.js +1 -0
  26. package/client/dist/assets/MangaInfoPage-DcrEr7Sk.js +1 -0
  27. package/client/dist/assets/MangaReadPage-SYg63m48.js +1 -0
  28. package/client/dist/assets/MangaResetProgressModal-DX-1PS4h.js +1 -0
  29. package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
  30. package/client/dist/assets/Map-Dj51g5zL.js +1 -0
  31. package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
  32. package/client/dist/assets/Mature-Dqh_9SA3.js +1 -0
  33. package/client/dist/assets/MediaCard-Be1ST9oj.js +1 -0
  34. package/client/dist/assets/MediaCard-DB6Yjgky.css +1 -0
  35. package/client/dist/assets/Music-BGH_0voi.css +1 -0
  36. package/client/dist/assets/Music-zsPwT8_5.js +1 -0
  37. package/client/dist/assets/OptionListMenu-Drc_vybW.js +1 -0
  38. package/client/dist/assets/Player-B036Ic3m.css +1 -0
  39. package/client/dist/assets/Player-BEtAYps2.js +56 -0
  40. package/client/dist/assets/PlayerRedirect-D70t8VUW.js +1 -0
  41. package/client/dist/assets/PlayerSettings-Bj6SsmQ2.css +1 -0
  42. package/client/dist/assets/PlayerSettings-Cwoji-P7.js +1 -0
  43. package/client/dist/assets/QueueOptionsButton-B6BMq66e.js +1 -0
  44. package/client/dist/assets/QueueOptionsButton-CM15uQn8.css +1 -0
  45. package/client/dist/assets/QueueRail-BUYEjdaq.css +1 -0
  46. package/client/dist/assets/QueueRail-VxpUDBSm.js +1 -0
  47. package/client/dist/assets/Radio-B2Q_Egj7.js +1 -0
  48. package/client/dist/assets/Radio-CCHNFN7R.css +1 -0
  49. package/client/dist/assets/Radio.module-DGSZLpKM.js +1 -0
  50. package/client/dist/assets/ReadingList-O0QPwXu7.js +1 -0
  51. package/client/dist/assets/ResetProgressModal-CR_ovLAu.js +1 -0
  52. package/client/dist/assets/Search-C8e2nlDD.js +1 -0
  53. package/client/dist/assets/Search-Cs45oAwI.css +1 -0
  54. package/client/dist/assets/Search.module-CMEai-B-.js +1 -0
  55. package/client/dist/assets/Settings-AuFxQcJF.css +1 -0
  56. package/client/dist/assets/Settings-BgPWBl2V.js +21 -0
  57. package/client/dist/assets/SkeletonGrid-CtHKKMLj.js +1 -0
  58. package/client/dist/assets/SkeletonGrid-DhPYefXr.css +1 -0
  59. package/client/dist/assets/SynopsisText-BcpwT5iw.js +1 -0
  60. package/client/dist/assets/SynopsisText-CYv2W3yR.css +1 -0
  61. package/client/dist/assets/Trackers-Bfoiyuzy.js +2 -0
  62. package/client/dist/assets/Trackers-D5XBOCDp.css +1 -0
  63. package/client/dist/assets/Tv-Ca0fCdko.js +3 -0
  64. package/client/dist/assets/Tv-CuEl3Iol.css +1 -0
  65. package/client/dist/assets/TvInfo-Bu36xJyO.css +1 -0
  66. package/client/dist/assets/TvInfo-D_scgw_h.js +1 -0
  67. package/client/dist/assets/TvPopup-YTI7dEbJ.js +1 -0
  68. package/client/dist/assets/TvSearch-CxU33UOf.js +1 -0
  69. package/client/dist/assets/TvWatchlist-B4X1fWEH.js +1 -0
  70. package/client/dist/assets/VirtualKeyboard-6ZNTym4R.js +1 -0
  71. package/client/dist/assets/VirtualKeyboard-KIPc5ZGH.css +1 -0
  72. package/client/dist/assets/Watchlist-BiDEU0iV.css +1 -0
  73. package/client/dist/assets/Watchlist-P6YMZUdF.js +1 -0
  74. package/client/dist/assets/Watchlist.module-XGtVQr1d.js +1 -0
  75. package/client/dist/assets/hls-BOLXTnjL.js +42 -0
  76. package/client/dist/assets/hls-C4cG9wQw.js +1 -0
  77. package/client/dist/assets/index-BOKlZEHq.js +30354 -0
  78. package/client/dist/assets/index-CUm1meqr.css +1 -0
  79. package/client/dist/assets/index-IZgh1beL.js +178 -0
  80. package/client/dist/assets/useAnimeInfoData-DeuwDSnI.js +1 -0
  81. package/client/dist/assets/useAsmr-CbMVRjmV.js +1 -0
  82. package/client/dist/assets/useAsmrPopup-D91Q3CBg.js +1 -0
  83. package/client/dist/assets/useIsMobile-Dm_b6Ob3.js +1 -0
  84. package/client/dist/assets/useMangaLibrary-BPvFb1lU.js +1 -0
  85. package/client/dist/assets/useMangaPopup-BOL2mQJs.js +1 -0
  86. package/client/dist/assets/useMatureConsent-CT7-KfzG.js +1 -0
  87. package/client/dist/assets/useProviders-BsRGONMA.js +1 -0
  88. package/client/dist/assets/useTv-D37MfTHA.js +1 -0
  89. package/client/dist/assets/useTvLibrary-Cwxgy_Bg.js +1 -0
  90. package/client/dist/assets/vendor-core-8R2fFbyN.js +1 -0
  91. package/client/dist/assets/vendor-icons-CDMuNDeL.js +1 -0
  92. package/client/dist/assets/vendor-query-BXRECSI6.js +1 -0
  93. package/client/dist/assets/vendor-ui-CUDCvRS_.js +1 -0
  94. package/client/dist/av-sync-test.mp4 +0 -0
  95. package/client/dist/favicon.ico +0 -0
  96. package/client/dist/index.html +46 -0
  97. package/client/dist/logo-mochi.png +0 -0
  98. package/client/dist/logo-sakura.png +0 -0
  99. package/client/dist/logo.png +0 -0
  100. package/client/dist/placeholder.svg +4 -0
  101. package/client/dist/robots.txt +3 -0
  102. package/orchestrator.js +534 -0
  103. package/package.json +85 -0
  104. package/server/dist/app-auth.js +177 -0
  105. package/server/dist/config.js +175 -0
  106. package/server/dist/controllers/asmr-library.controller.js +288 -0
  107. package/server/dist/controllers/auth.controller.js +225 -0
  108. package/server/dist/controllers/data.controller.js +766 -0
  109. package/server/dist/controllers/insights.controller.js +198 -0
  110. package/server/dist/controllers/manga-library.controller.js +296 -0
  111. package/server/dist/controllers/music.controller.js +698 -0
  112. package/server/dist/controllers/proxy.controller.js +820 -0
  113. package/server/dist/controllers/settings.controller.js +609 -0
  114. package/server/dist/controllers/tv-library.controller.js +310 -0
  115. package/server/dist/controllers/watchlist.controller.js +1015 -0
  116. package/server/dist/db.js +145 -0
  117. package/server/dist/discord-gateway.js +391 -0
  118. package/server/dist/discord-rpc.js +427 -0
  119. package/server/dist/github-sync.js +512 -0
  120. package/server/dist/google.js +748 -0
  121. package/server/dist/lib/anilist.js +1549 -0
  122. package/server/dist/lib/anischedule.js +194 -0
  123. package/server/dist/lib/discord-roles-sync.service.js +130 -0
  124. package/server/dist/lib/ipc.js +24 -0
  125. package/server/dist/lib/kitsu.js +544 -0
  126. package/server/dist/lib/mal.js +934 -0
  127. package/server/dist/lib/migration.js +329 -0
  128. package/server/dist/lib/offline-db.js +401 -0
  129. package/server/dist/lib/temp-ids.js +3 -0
  130. package/server/dist/lib/tmdb.js +72 -0
  131. package/server/dist/lib/tracker/anilist-tracker.js +245 -0
  132. package/server/dist/lib/tracker/sync.service.js +496 -0
  133. package/server/dist/lib/ytmusic.js +125 -0
  134. package/server/dist/logger.js +16 -0
  135. package/server/dist/providers/manga/manga.types.js +7 -0
  136. package/server/dist/providers/provider.interface.js +1 -0
  137. package/server/dist/providers/remote-loader.js +574 -0
  138. package/server/dist/providers/remote-types.js +117 -0
  139. package/server/dist/providers/title-matching.js +225 -0
  140. package/server/dist/providers/tv.types.js +1 -0
  141. package/server/dist/rclone.js +120 -0
  142. package/server/dist/repositories/asmr.repository.js +123 -0
  143. package/server/dist/repositories/insights.repository.js +85 -0
  144. package/server/dist/repositories/library.repository.js +108 -0
  145. package/server/dist/repositories/mal-cache.repository.js +55 -0
  146. package/server/dist/repositories/manga.repository.js +138 -0
  147. package/server/dist/repositories/notifications.repository.js +41 -0
  148. package/server/dist/repositories/queue.repository.js +61 -0
  149. package/server/dist/repositories/settings.repository.js +12 -0
  150. package/server/dist/repositories/shows-meta.repository.js +42 -0
  151. package/server/dist/repositories/temp-show-ids.repository.js +51 -0
  152. package/server/dist/repositories/tv.repository.js +151 -0
  153. package/server/dist/repositories/watched-episodes.repository.js +52 -0
  154. package/server/dist/repositories/watchlist.repository.js +71 -0
  155. package/server/dist/request-logger.js +16 -0
  156. package/server/dist/routes/asmr-library.routes.js +23 -0
  157. package/server/dist/routes/asmr.routes.js +81 -0
  158. package/server/dist/routes/auth.routes.js +25 -0
  159. package/server/dist/routes/data.routes.js +47 -0
  160. package/server/dist/routes/discord-gateway.routes.js +51 -0
  161. package/server/dist/routes/insights.routes.js +34 -0
  162. package/server/dist/routes/lan-auth.routes.js +46 -0
  163. package/server/dist/routes/manga-library.routes.js +23 -0
  164. package/server/dist/routes/manga.routes.js +141 -0
  165. package/server/dist/routes/music.routes.js +20 -0
  166. package/server/dist/routes/providers.routes.js +17 -0
  167. package/server/dist/routes/proxy.routes.js +12 -0
  168. package/server/dist/routes/radio.routes.js +138 -0
  169. package/server/dist/routes/settings.routes.js +24 -0
  170. package/server/dist/routes/tracker.routes.js +164 -0
  171. package/server/dist/routes/translate.routes.js +88 -0
  172. package/server/dist/routes/tv-library.routes.js +23 -0
  173. package/server/dist/routes/tv.routes.js +629 -0
  174. package/server/dist/routes/watchlist.routes.js +341 -0
  175. package/server/dist/scripts/backfill-episode-duration.js +42 -0
  176. package/server/dist/server.js +529 -0
  177. package/server/dist/shipped-defaults.js +7 -0
  178. package/server/dist/sync-config.js +23 -0
  179. package/server/dist/sync-payload.js +91 -0
  180. package/server/dist/sync.js +1128 -0
  181. package/server/dist/utils/cache.utils.js +25 -0
  182. package/server/dist/utils/cookie.utils.js +13 -0
  183. package/server/dist/utils/db-utils.js +9 -0
  184. package/server/dist/utils/env.utils.js +77 -0
  185. package/server/dist/utils/http.utils.js +70 -0
  186. package/server/dist/utils/machine-id.js +40 -0
  187. package/server/dist/utils/megaplay.utils.js +41 -0
  188. package/server/dist/utils/request-context.js +2 -0
  189. package/server/dist/utils/security.utils.js +94 -0
  190. package/server/package.json +44 -0
@@ -0,0 +1,427 @@
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
+ formatTime(seconds) {
217
+ if (isNaN(seconds) || seconds <= 0)
218
+ return '00:00';
219
+ const h = Math.floor(seconds / 3600);
220
+ const m = Math.floor((seconds % 3600) / 60);
221
+ const s = Math.floor(seconds % 60);
222
+ const mm = String(m).padStart(2, '0');
223
+ const ss = String(s).padStart(2, '0');
224
+ if (h > 0) {
225
+ const hh = String(h).padStart(2, '0');
226
+ return `${hh}:${mm}:${ss}`;
227
+ }
228
+ return `${mm}:${ss}`;
229
+ }
230
+ async updatePresence(data) {
231
+ this.lastActivity = data;
232
+ if (data.sessionId) {
233
+ this.currentSessionId = data.sessionId;
234
+ }
235
+ if (!this.isEnabled)
236
+ return;
237
+ if (this.hideMature && data.isAdult) {
238
+ if (discordGatewayService.serviceEnabled) {
239
+ discordGatewayService.clearPresence();
240
+ }
241
+ else if (this.client?.user) {
242
+ try {
243
+ await this.client.user.clearActivity();
244
+ }
245
+ catch {
246
+ // ignore
247
+ }
248
+ }
249
+ return;
250
+ }
251
+ if (discordGatewayService.serviceEnabled) {
252
+ discordGatewayService.updatePresence(data);
253
+ return;
254
+ }
255
+ if (!this.client || !this.client.user)
256
+ return;
257
+ const isSafeUrl = (url) => {
258
+ if (!url.startsWith('https://')) {
259
+ return false;
260
+ }
261
+ if (url.includes('localhost') || url.includes('127.0.0.1')) {
262
+ return false;
263
+ }
264
+ if (url.includes('s4.anilist.co') || url.includes('anilistcdn')) {
265
+ return true;
266
+ }
267
+ const blockedDomains = [
268
+ 'youtube-anime.com',
269
+ 'animepahe',
270
+ 'animeya.cc',
271
+ 'gogocdn.net',
272
+ 'weeabo0.xyz',
273
+ 'japaneseasmr.com',
274
+ ];
275
+ return !blockedDomains.some((domain) => url.includes(domain));
276
+ };
277
+ let imageKey = 'logo';
278
+ if (data.thumbnail && data.providerName !== 'AnimePahe') {
279
+ let thumbUrl = data.thumbnail;
280
+ if (thumbUrl.includes('/api/image-proxy')) {
281
+ const match = thumbUrl.match(/url=([^&]+)/);
282
+ if (match) {
283
+ thumbUrl = decodeURIComponent(match[1]);
284
+ }
285
+ }
286
+ if (isSafeUrl(thumbUrl)) {
287
+ imageKey = thumbUrl;
288
+ }
289
+ else if (data.thumbnails) {
290
+ const safeThumb = data.thumbnails.find((t) => t && (t.includes('s4.anilist.co') || t.includes('anilistcdn')));
291
+ if (safeThumb)
292
+ imageKey = safeThumb;
293
+ }
294
+ }
295
+ try {
296
+ if (!data.isPlaying) {
297
+ await this.client.user.clearActivity();
298
+ await this.client.user.setActivity({
299
+ details: data.title,
300
+ state: data.stateLine
301
+ ? `${data.stateLine} (Paused)`
302
+ : `Episode ${data.episode}${data.totalEpisodes ? `/${data.totalEpisodes}` : ''} (Paused)`,
303
+ largeImageKey: imageKey,
304
+ largeImageText: data.title,
305
+ ...(imageKey !== 'logo' ? { smallImageKey: 'small', smallImageText: 'dango' } : {}),
306
+ type: data.providerName === 'ASMR' ||
307
+ data.providerName === 'Radio' ||
308
+ data.providerName === 'Music'
309
+ ? 2
310
+ : 3,
311
+ statusDisplayType: StatusDisplayType.DETAILS,
312
+ buttons: [
313
+ {
314
+ label: 'Learn More',
315
+ url: 'https://github.com/serifpersia/dango',
316
+ },
317
+ ],
318
+ });
319
+ this.hasPagePresence = false;
320
+ return;
321
+ }
322
+ const nowSeconds = Math.round(Date.now() / 1000);
323
+ const activity = {
324
+ details: data.title,
325
+ state: data.stateLine
326
+ ? data.stateLine
327
+ : `Episode ${data.episode}${data.totalEpisodes ? `/${data.totalEpisodes}` : ''}`,
328
+ largeImageKey: imageKey,
329
+ largeImageText: data.title,
330
+ ...(imageKey !== 'logo' ? { smallImageKey: 'small', smallImageText: 'dango' } : {}),
331
+ type: data.providerName === 'ASMR' ||
332
+ data.providerName === 'Radio' ||
333
+ data.providerName === 'Music'
334
+ ? 2
335
+ : 3,
336
+ statusDisplayType: StatusDisplayType.DETAILS,
337
+ buttons: [
338
+ {
339
+ label: 'Learn More',
340
+ url: 'https://github.com/serifpersia/dango',
341
+ },
342
+ ],
343
+ };
344
+ if (data.currentTime && data.currentTime > 0) {
345
+ activity.startTimestamp = Math.round(nowSeconds - data.currentTime);
346
+ }
347
+ if (data.duration && data.duration > data.currentTime) {
348
+ activity.endTimestamp = Math.round(nowSeconds + (data.duration - data.currentTime));
349
+ }
350
+ await this.client.user.setActivity(activity);
351
+ this.hasPagePresence = false;
352
+ }
353
+ catch (err) {
354
+ log.error({ err }, 'Failed to set Discord activity');
355
+ }
356
+ }
357
+ async setIdleStatus(page) {
358
+ if (!this.isEnabled)
359
+ return;
360
+ if (discordGatewayService.serviceEnabled) {
361
+ discordGatewayService.setIdleStatus(page);
362
+ return;
363
+ }
364
+ if (!this.client || !this.client.user) {
365
+ log.warn('Discord client not connected, skipping idle status update');
366
+ return;
367
+ }
368
+ const pageLabels = {
369
+ home: { details: 'Home', state: 'Browsing anime' },
370
+ search: { details: 'Search', state: 'Exploring titles...' },
371
+ watchlist: { details: 'Watchlist', state: 'Reviewing the watchlist' },
372
+ anime: { details: 'Anime Info', state: 'Reading show details' },
373
+ insights: { details: 'Insights', state: 'Reviewing stats' },
374
+ settings: { details: 'Settings', state: 'Tweaking preferences' },
375
+ mal: { details: 'MAL Sync', state: 'Syncing with MyAnimeList' },
376
+ map: { details: 'Map', state: 'Exploring the global user map' },
377
+ asmr: { details: 'ASMR', state: 'Browsing ASMR works' },
378
+ radio: { details: 'Radio', state: 'Browsing radio stations' },
379
+ music: { details: 'Music', state: 'Browsing music' },
380
+ tv: { details: 'TV', state: 'Browsing movies & shows' },
381
+ manga: { details: 'Manga', state: 'Browsing manga' },
382
+ };
383
+ const label = pageLabels[page] ?? { details: 'dango', state: 'Idle' };
384
+ try {
385
+ if (!this.client || !this.client.user) {
386
+ log.warn('Discord client not connected, skipping idle status update');
387
+ return;
388
+ }
389
+ await this.client.user.setActivity({
390
+ details: label.details,
391
+ state: label.state,
392
+ largeImageKey: 'logo',
393
+ largeImageText: 'dango',
394
+ type: page === 'asmr' || page === 'radio' || page === 'music' ? 2 : 3,
395
+ statusDisplayType: StatusDisplayType.DETAILS,
396
+ buttons: [
397
+ {
398
+ label: 'Learn More',
399
+ url: 'https://github.com/serifpersia/dango',
400
+ },
401
+ ],
402
+ });
403
+ this.hasPagePresence = page in pageLabels;
404
+ }
405
+ catch (err) {
406
+ if (err.message === 'Closed by Discord') {
407
+ log.warn('Discord connection ended, skipping idle status update');
408
+ }
409
+ else {
410
+ log.error({ err }, 'Failed to set Discord idle status');
411
+ }
412
+ }
413
+ }
414
+ async clearPresence(sessionId) {
415
+ if (sessionId && this.currentSessionId && sessionId !== this.currentSessionId) {
416
+ return;
417
+ }
418
+ this.lastActivity = null;
419
+ this.currentSessionId = null;
420
+ if (discordGatewayService.serviceEnabled) {
421
+ discordGatewayService.clearPresence();
422
+ return;
423
+ }
424
+ await this.setIdleStatus('idle');
425
+ }
426
+ }
427
+ export const discordRPCService = new DiscordRPCService();