@serifpersia/dango 3.3.7 → 3.3.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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-WlhU88fn.js → AnimeCard-URCiiy5W.js} +1 -1
- package/client/dist/assets/{AnimeInfo-BCLagpbu.js → AnimeInfo-e3z3er9t.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-o0U_OXKP.js → AnimeInfoPage-B7Djlmy_.js} +2 -2
- package/client/dist/assets/{Asmr-ieGJ_BcQ.js → Asmr-C28TfSvX.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-BwaoVski.js → AsmrResetProgressModal-6Xh71AgR.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-CXvBuY4l.js → AvSyncCalibrator-BwCtRW0P.js} +1 -1
- package/client/dist/assets/{Home-DjLpvsp8.js → Home-BJdbHYja.js} +1 -1
- package/client/dist/assets/Insights-CjRy42YE.js +1 -0
- package/client/dist/assets/{LibraryListPage-B2nhFFrc.js → LibraryListPage-B0qWqhb_.js} +1 -1
- package/client/dist/assets/{ListeningList-okIpsKCr.js → ListeningList-CSqD8j1C.js} +1 -1
- package/client/dist/assets/{Manga-3u9nVNZC.js → Manga-Cg2YuObF.js} +1 -1
- package/client/dist/assets/MangaInfoPage-DcArhEsy.js +1 -0
- package/client/dist/assets/MangaReadPage-YCETZRte.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-Ch8RjwbO.js → MangaResetProgressModal-D_yT3d1v.js} +1 -1
- package/client/dist/assets/{Mature-pINwl5wj.js → Mature-BGxRWkV5.js} +1 -1
- package/client/dist/assets/{MediaCard-DDiYEAm8.js → MediaCard-CAzoxsRS.js} +1 -1
- package/client/dist/assets/Music-Cgo-yQ7E.js +1 -0
- package/client/dist/assets/{OptionListMenu-CzH2VSPh.js → OptionListMenu--Owe9Jle.js} +1 -1
- package/client/dist/assets/Player-yRXG2BrP.js +56 -0
- package/client/dist/assets/{PlayerSettings-jrmY95-i.js → PlayerSettings-DrZLa0CC.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-CcVP7Vi6.js → QueueOptionsButton-BVZyPyKf.js} +1 -1
- package/client/dist/assets/{QueueRail-Cj-PU9EV.js → QueueRail-B7f1-e6i.js} +1 -1
- package/client/dist/assets/{Radio-CRlbaxQt.js → Radio-BFjxlzuF.js} +1 -1
- package/client/dist/assets/{ReadingList-wRtHGNo8.js → ReadingList-BaIem8H1.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-BokfoYFB.js → ResetProgressModal-DES5Y-Fl.js} +1 -1
- package/client/dist/assets/{Search-BOOBeTVA.js → Search-BOBo0Ogh.js} +1 -1
- package/client/dist/assets/{Settings-BgrcswJD.js → Settings-DOvNQ0gX.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-C6S7OYHn.js → SkeletonGrid-BxfgjP4i.js} +1 -1
- package/client/dist/assets/Trackers-DZ9KqOWv.js +2 -0
- package/client/dist/assets/{Tv-ByVckozq.js → Tv-5mATEU4p.js} +1 -1
- package/client/dist/assets/{TvInfo-fJ9SFMK8.js → TvInfo-CF-5C5Fs.js} +1 -1
- package/client/dist/assets/{TvPopup-yfH7p0y0.js → TvPopup-Dfqifj3s.js} +1 -1
- package/client/dist/assets/{TvSearch-CBNWDByW.js → TvSearch-BWDwWnHe.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BqjZ2-4A.js → TvWatchlist-D3n5_4MI.js} +1 -1
- package/client/dist/assets/{Watchlist-B_Q2hEiJ.js → Watchlist-B8MHNsAU.js} +1 -1
- package/client/dist/assets/{hls-kIF7bMpF.js → hls-BO_L1WoN.js} +1 -1
- package/client/dist/assets/index-D2RkUJIN.js +178 -0
- package/client/dist/assets/{useAnimeInfoData-BKSrS8eN.js → useAnimeInfoData-BWShDHqE.js} +1 -1
- package/client/dist/assets/{useAsmr-Byj8Qiwp.js → useAsmr-FyfD9G7b.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-CLcWCCXf.js → useAsmrPopup-D_o9-QlR.js} +1 -1
- package/client/dist/assets/useMangaLibrary-BJU5x8Vz.js +1 -0
- package/client/dist/assets/{useMangaPopup-ZonOP5_c.js → useMangaPopup-TFzkC-tG.js} +1 -1
- package/client/dist/assets/{useProviders-CymA42ZB.js → useProviders-DDRxa8vl.js} +1 -1
- package/client/dist/assets/{useTv-CPj6Cf42.js → useTv-Bh6WmNnZ.js} +1 -1
- package/client/dist/assets/{useTvLibrary-COsyN8yu.js → useTvLibrary-_Pqm8re1.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/dist/controllers/data.controller.js +1 -0
- package/server/dist/controllers/insights.controller.js +3 -2
- package/server/dist/controllers/manga-library.controller.js +54 -0
- package/server/dist/controllers/music.controller.js +123 -50
- package/server/dist/controllers/watchlist.controller.js +13 -1
- package/server/dist/lib/anilist.js +36 -3
- package/server/dist/lib/mal.js +69 -0
- package/server/dist/lib/tracker/anilist-tracker.js +66 -0
- package/server/dist/lib/tracker/manga-sync.service.js +792 -0
- package/server/dist/lib/ytmusic.js +15 -2
- package/server/dist/repositories/manga.repository.js +17 -2
- package/server/dist/routes/manga-library.routes.js +2 -0
- package/server/dist/routes/tracker.routes.js +123 -0
- package/server/dist/sync.js +8 -1
- package/server/package.json +1 -1
- package/client/dist/assets/Insights-lQcQoeES.js +0 -1
- package/client/dist/assets/MangaInfoPage-Cat-B487.js +0 -1
- package/client/dist/assets/MangaReadPage-CyLRxzvl.js +0 -1
- package/client/dist/assets/Music-B1swlVLI.js +0 -1
- package/client/dist/assets/Player-SKPWb-o8.js +0 -56
- package/client/dist/assets/Trackers-DLcTEJJW.js +0 -2
- package/client/dist/assets/index-C65i4HWv.js +0 -178
- package/client/dist/assets/useMangaLibrary-U7tUo8nx.js +0 -1
|
@@ -72,19 +72,32 @@ export function getInnertube() {
|
|
|
72
72
|
return publicPromise;
|
|
73
73
|
}
|
|
74
74
|
/** Authenticated session built from the saved cookie. Null when not signed in. */
|
|
75
|
-
export async function getAuthedInnertube() {
|
|
75
|
+
export async function getAuthedInnertube(forceRefresh = false) {
|
|
76
76
|
const cookie = readCookie();
|
|
77
77
|
if (!cookie) {
|
|
78
78
|
authedTube = null;
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
|
-
if (authedTube)
|
|
81
|
+
if (authedTube && !forceRefresh)
|
|
82
82
|
return authedTube;
|
|
83
83
|
await ensureShim();
|
|
84
84
|
const { Innertube } = await loadYoutube();
|
|
85
85
|
authedTube = await Innertube.create({ cookie });
|
|
86
86
|
return authedTube;
|
|
87
87
|
}
|
|
88
|
+
export function invalidateAuthedSession() {
|
|
89
|
+
authedTube = null;
|
|
90
|
+
}
|
|
91
|
+
export async function refreshAuthedInnertube() {
|
|
92
|
+
invalidateAuthedSession();
|
|
93
|
+
try {
|
|
94
|
+
return await getAuthedInnertube();
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
logger.warn({ err }, '[ytmusic] saved-cookie session refresh failed');
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
88
101
|
export function getMusicAuthStatus() {
|
|
89
102
|
return { authenticated: readCookie() !== null };
|
|
90
103
|
}
|
|
@@ -40,8 +40,8 @@ export const MangaLibraryRepository = {
|
|
|
40
40
|
return row?.total || 0;
|
|
41
41
|
},
|
|
42
42
|
getIds: (db) => dbAll(db, 'SELECT id FROM manga_library').map((r) => r.id),
|
|
43
|
-
upsert: (db, data) => dbRun(db, `INSERT INTO manga_library (id, provider, mangaId, title, cover, status, author, altTitle, contentRating, updatedAt)
|
|
44
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'))
|
|
43
|
+
upsert: (db, data) => dbRun(db, `INSERT INTO manga_library (id, provider, mangaId, title, cover, status, author, altTitle, contentRating, anilistId, anilistIdSource, updatedAt)
|
|
44
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'))
|
|
45
45
|
ON CONFLICT(id) DO UPDATE SET
|
|
46
46
|
provider = COALESCE(NULLIF(EXCLUDED.provider, ''), manga_library.provider),
|
|
47
47
|
title = COALESCE(NULLIF(EXCLUDED.title, ''), manga_library.title),
|
|
@@ -50,6 +50,8 @@ export const MangaLibraryRepository = {
|
|
|
50
50
|
author = COALESCE(NULLIF(EXCLUDED.author, ''), manga_library.author),
|
|
51
51
|
altTitle = COALESCE(NULLIF(EXCLUDED.altTitle, ''), manga_library.altTitle),
|
|
52
52
|
contentRating = COALESCE(EXCLUDED.contentRating, manga_library.contentRating),
|
|
53
|
+
anilistId = COALESCE(EXCLUDED.anilistId, manga_library.anilistId),
|
|
54
|
+
anilistIdSource = COALESCE(EXCLUDED.anilistIdSource, manga_library.anilistIdSource),
|
|
53
55
|
updatedAt = strftime('%s', 'now')`, [
|
|
54
56
|
data.id,
|
|
55
57
|
data.provider,
|
|
@@ -60,7 +62,17 @@ export const MangaLibraryRepository = {
|
|
|
60
62
|
data.author || null,
|
|
61
63
|
data.altTitle || null,
|
|
62
64
|
data.contentRating || null,
|
|
65
|
+
data.anilistId ?? null,
|
|
66
|
+
data.anilistIdSource ?? null,
|
|
63
67
|
]),
|
|
68
|
+
setAnilistId: (db, id, anilistId, source) => source
|
|
69
|
+
? dbRun(db, 'UPDATE manga_library SET anilistId = ?, anilistIdSource = ? WHERE id = ?', [
|
|
70
|
+
anilistId,
|
|
71
|
+
source,
|
|
72
|
+
id,
|
|
73
|
+
])
|
|
74
|
+
: dbRun(db, 'UPDATE manga_library SET anilistId = ? WHERE id = ?', [anilistId, id]),
|
|
75
|
+
getByAnilistId: (db, anilistId) => dbAll(db, 'SELECT * FROM manga_library WHERE anilistId = ?', [anilistId]),
|
|
64
76
|
updateStatus: (db, id, status) => dbRun(db, "UPDATE manga_library SET status = ?, updatedAt = strftime('%s', 'now') WHERE id = ?", [status, id]),
|
|
65
77
|
updateStatusMany: (db, ids, status) => {
|
|
66
78
|
if (ids.length === 0)
|
|
@@ -69,6 +81,7 @@ export const MangaLibraryRepository = {
|
|
|
69
81
|
dbRun(db, `UPDATE manga_library SET status = ?, updatedAt = strftime('%s', 'now') WHERE id IN (${placeholders})`, [status, ...ids]);
|
|
70
82
|
},
|
|
71
83
|
touchProgress: (db, id, progress) => dbRun(db, `UPDATE manga_library SET lastChapterId = ?, lastChapterNumber = ?, lastPage = ?, updatedAt = strftime('%s', 'now') WHERE id = ?`, [progress.chapterId, progress.chapterNumber, progress.page, id]),
|
|
84
|
+
setProgressPointer: (db, id, chapterNumber) => dbRun(db, `UPDATE manga_library SET lastChapterNumber = ?, updatedAt = strftime('%s', 'now') WHERE id = ?`, [chapterNumber, id]),
|
|
72
85
|
delete: (db, id) => dbRun(db, 'DELETE FROM manga_library WHERE id = ?', [id]),
|
|
73
86
|
deleteMany: (db, ids) => {
|
|
74
87
|
if (ids.length === 0)
|
|
@@ -115,6 +128,8 @@ export const MangaProgressRepository = {
|
|
|
115
128
|
mangaId,
|
|
116
129
|
chapterId,
|
|
117
130
|
]),
|
|
131
|
+
deleteSyntheticChapters: (db, mangaId, keepChapterId) => dbRun(db, `DELETE FROM manga_progress WHERE mangaId = ? AND chapterId LIKE 'anilist:ch:%' AND chapterId != ?`, [mangaId, keepChapterId]),
|
|
132
|
+
moveSyntheticChapters: (db, fromMangaId, toMangaId) => dbRun(db, `UPDATE manga_progress SET mangaId = ?, updatedAt = strftime('%s', 'now') WHERE mangaId = ? AND chapterId LIKE 'anilist:ch:%'`, [toMangaId, fromMangaId]),
|
|
118
133
|
getContinueReading: (db, limit) => {
|
|
119
134
|
const limitClause = typeof limit === 'number' ? `LIMIT ${limit}` : '';
|
|
120
135
|
return dbAll(db, `SELECT p.mangaId as id,
|
|
@@ -6,9 +6,11 @@ export function createMangaLibraryRouter() {
|
|
|
6
6
|
router.get('/manga/library', controller.getLibrary);
|
|
7
7
|
router.get('/manga/library/ids', controller.getLibraryIds);
|
|
8
8
|
router.get('/manga/library/check/:id', controller.checkLibrary);
|
|
9
|
+
router.get('/manga/library/entry/:id', controller.getEntry);
|
|
9
10
|
router.post('/manga/library/add', controller.addToLibrary);
|
|
10
11
|
router.post('/manga/library/remove', controller.removeFromLibrary);
|
|
11
12
|
router.post('/manga/library/status', controller.updateStatus);
|
|
13
|
+
router.post('/manga/library/link', controller.linkAnilist);
|
|
12
14
|
router.post('/manga/library/batch-status', controller.batchUpdateStatus);
|
|
13
15
|
router.post('/manga/library/remove-many', controller.batchRemove);
|
|
14
16
|
router.post('/manga/progress/remove-many', controller.batchRemoveProgress);
|
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
+
import multer from 'multer';
|
|
3
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
2
4
|
import { AniListTracker } from '../lib/tracker/anilist-tracker.js';
|
|
3
5
|
import { syncAniList, importFromMalUsername, importFromUsername, } from '../lib/tracker/sync.service.js';
|
|
6
|
+
import { syncAniListManga, importFromUsernameManga, importMangaFromMalUsername, importMangaFromMalXmlItems, } from '../lib/tracker/manga-sync.service.js';
|
|
4
7
|
import { SettingsRepository } from '../repositories/settings.repository.js';
|
|
5
8
|
import { performWriteTransaction } from '../sync.js';
|
|
6
9
|
const TOKEN_KEY = 'tracker_anilist_token';
|
|
7
10
|
const USER_KEY = 'tracker_anilist_user';
|
|
8
11
|
const CLIENT_ID_KEY = 'tracker_anilist_client_id';
|
|
12
|
+
const malMangaXmlParser = new XMLParser({
|
|
13
|
+
isArray: (name) => name === 'manga',
|
|
14
|
+
parseTagValue: false,
|
|
15
|
+
trimValues: true,
|
|
16
|
+
});
|
|
17
|
+
function mangaXmlText(value) {
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return String(value[0] ?? '');
|
|
20
|
+
return String(value ?? '');
|
|
21
|
+
}
|
|
22
|
+
function mapMalMangaXmlStatus(status) {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'Plan to Read':
|
|
25
|
+
return 'Planned';
|
|
26
|
+
case 'On Hold':
|
|
27
|
+
return 'On-Hold';
|
|
28
|
+
case 'Reading':
|
|
29
|
+
case 'Completed':
|
|
30
|
+
case 'Dropped':
|
|
31
|
+
return status;
|
|
32
|
+
default:
|
|
33
|
+
return 'Planned';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
9
36
|
export function createTrackerRouter() {
|
|
10
37
|
const router = Router();
|
|
11
38
|
router.get('/tracker/anilist/callback', (_req, res) => {
|
|
@@ -160,5 +187,101 @@ export function createTrackerRouter() {
|
|
|
160
187
|
res.status(500).json({ error: message });
|
|
161
188
|
}
|
|
162
189
|
});
|
|
190
|
+
router.post('/tracker/manga/sync', async (req, res) => {
|
|
191
|
+
const { direction } = req.body ?? {};
|
|
192
|
+
try {
|
|
193
|
+
if (!req.mangaDb || req.mangaDb.isClosedCheck()) {
|
|
194
|
+
return res.status(503).json({ error: 'Manga database is not ready' });
|
|
195
|
+
}
|
|
196
|
+
const syncDirection = direction === 'pull-only' ? 'pull-only' : 'two-way';
|
|
197
|
+
const result = await syncAniListManga(req.db, req.mangaDb, {
|
|
198
|
+
direction: syncDirection,
|
|
199
|
+
});
|
|
200
|
+
res.json({
|
|
201
|
+
success: true,
|
|
202
|
+
summary: result.summary,
|
|
203
|
+
details: result.details,
|
|
204
|
+
direction: syncDirection,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
const message = err instanceof Error ? err.message : 'Manga sync failed';
|
|
209
|
+
res.status(500).json({ error: message });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
router.post('/tracker/manga/import', async (req, res) => {
|
|
213
|
+
const { username, erase } = req.body ?? {};
|
|
214
|
+
if (!username || typeof username !== 'string') {
|
|
215
|
+
return res.status(400).json({ error: 'Username is required' });
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
if (!req.mangaDb || req.mangaDb.isClosedCheck()) {
|
|
219
|
+
return res.status(503).json({ error: 'Manga database is not ready' });
|
|
220
|
+
}
|
|
221
|
+
const count = await importFromUsernameManga(req.db, req.mangaDb, username.trim(), erase === true);
|
|
222
|
+
res.json({ success: true, count });
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
const message = err instanceof Error ? err.message : 'Import failed';
|
|
226
|
+
res.status(500).json({ error: message });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
router.post('/tracker/manga/mal-import', async (req, res) => {
|
|
230
|
+
const { username, erase } = req.body ?? {};
|
|
231
|
+
if (!username || typeof username !== 'string') {
|
|
232
|
+
return res.status(400).json({ error: 'MAL username is required' });
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
if (!req.mangaDb || req.mangaDb.isClosedCheck()) {
|
|
236
|
+
return res.status(503).json({ error: 'Manga database is not ready' });
|
|
237
|
+
}
|
|
238
|
+
const result = await importMangaFromMalUsername(req.db, req.mangaDb, username.trim(), erase === true);
|
|
239
|
+
res.json({ success: true, ...result });
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
const message = err instanceof Error ? err.message : 'Import failed';
|
|
243
|
+
if (message.includes('private') || message.includes('not found')) {
|
|
244
|
+
return res.status(404).json({ error: message });
|
|
245
|
+
}
|
|
246
|
+
if (message.includes('blocked') || message.includes('HTTP 429')) {
|
|
247
|
+
return res.status(429).json({ error: message });
|
|
248
|
+
}
|
|
249
|
+
res.status(500).json({ error: message });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
router.post('/import/mal-xml-manga', multer().single('xmlfile'), async (req, res) => {
|
|
253
|
+
if (!req.file)
|
|
254
|
+
return res.status(400).json({ error: 'No file' });
|
|
255
|
+
const eraseRaw = req.body?.erase;
|
|
256
|
+
const erase = eraseRaw === true || eraseRaw === 'true' || eraseRaw === '1';
|
|
257
|
+
try {
|
|
258
|
+
if (!req.mangaDb || req.mangaDb.isClosedCheck()) {
|
|
259
|
+
return res.status(503).json({ error: 'Manga database is not ready' });
|
|
260
|
+
}
|
|
261
|
+
let result;
|
|
262
|
+
try {
|
|
263
|
+
result = malMangaXmlParser.parse(req.file.buffer.toString());
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return res.status(400).json({ error: 'Invalid XML' });
|
|
267
|
+
}
|
|
268
|
+
const mangaList = result?.myanimelist?.manga || [];
|
|
269
|
+
if (mangaList.length === 0) {
|
|
270
|
+
return res.status(400).json({ error: 'No manga found in XML' });
|
|
271
|
+
}
|
|
272
|
+
const items = mangaList.map((item) => ({
|
|
273
|
+
malId: parseInt(mangaXmlText(item.series_mangadb_id), 10) || 0,
|
|
274
|
+
title: mangaXmlText(item.series_title),
|
|
275
|
+
status: mapMalMangaXmlStatus(mangaXmlText(item.my_status)),
|
|
276
|
+
chapters: Math.max(parseInt(mangaXmlText(item.my_read_chapters), 10) || 0, 0),
|
|
277
|
+
}));
|
|
278
|
+
const { imported, skipped } = await importMangaFromMalXmlItems(req.db, req.mangaDb, items, erase);
|
|
279
|
+
res.json({ success: true, imported, skipped });
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
const message = err instanceof Error ? err.message : 'Import failed';
|
|
283
|
+
res.status(500).json({ error: message });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
163
286
|
return router;
|
|
164
287
|
}
|
package/server/dist/sync.js
CHANGED
|
@@ -713,7 +713,7 @@ export async function initializeMangaDatabase(dbPath) {
|
|
|
713
713
|
db.run('PRAGMA temp_store = MEMORY;');
|
|
714
714
|
db.run('PRAGMA mmap_size = 268435456;');
|
|
715
715
|
db.run('PRAGMA foreign_keys = ON;');
|
|
716
|
-
db.run(`CREATE TABLE IF NOT EXISTS manga_library (id TEXT PRIMARY KEY, provider TEXT NOT NULL, mangaId TEXT NOT NULL, title TEXT, cover TEXT, status TEXT DEFAULT 'Reading', author TEXT, contentRating TEXT, lastChapterId TEXT, lastChapterNumber TEXT, lastPage INTEGER, updatedAt INTEGER, altTitle TEXT)`);
|
|
716
|
+
db.run(`CREATE TABLE IF NOT EXISTS manga_library (id TEXT PRIMARY KEY, provider TEXT NOT NULL, mangaId TEXT NOT NULL, title TEXT, cover TEXT, status TEXT DEFAULT 'Reading', author TEXT, contentRating TEXT, lastChapterId TEXT, lastChapterNumber TEXT, lastPage INTEGER, updatedAt INTEGER, altTitle TEXT, anilistId INTEGER, anilistIdSource TEXT)`);
|
|
717
717
|
db.run(`CREATE TABLE IF NOT EXISTS manga_progress (mangaId TEXT NOT NULL, chapterId TEXT NOT NULL, chapterNumber TEXT, page INTEGER DEFAULT 0, pageCount INTEGER DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (mangaId, chapterId))`);
|
|
718
718
|
db.run(`CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)`);
|
|
719
719
|
db.run(`INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)`);
|
|
@@ -724,6 +724,13 @@ export async function initializeMangaDatabase(dbPath) {
|
|
|
724
724
|
if (!mangaColumns.some((c) => c.name === 'altTitle')) {
|
|
725
725
|
db.run(`ALTER TABLE manga_library ADD COLUMN altTitle TEXT`);
|
|
726
726
|
}
|
|
727
|
+
if (!mangaColumns.some((c) => c.name === 'anilistId')) {
|
|
728
|
+
db.run(`ALTER TABLE manga_library ADD COLUMN anilistId INTEGER`);
|
|
729
|
+
}
|
|
730
|
+
if (!mangaColumns.some((c) => c.name === 'anilistIdSource')) {
|
|
731
|
+
db.run(`ALTER TABLE manga_library ADD COLUMN anilistIdSource TEXT`);
|
|
732
|
+
}
|
|
733
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_manga_library_anilist ON manga_library(anilistId)`);
|
|
727
734
|
const mangaProgressColumns = db.all(`PRAGMA table_info(manga_progress)`);
|
|
728
735
|
if (!mangaProgressColumns.some((c) => c.name === 'title')) {
|
|
729
736
|
db.run(`ALTER TABLE manga_progress ADD COLUMN title TEXT`);
|
package/server/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{h as G,T as i,d as R,u as e,L as W}from"./vendor-core-8R2fFbyN.js";import{b as j,u as P,a as O}from"./vendor-query-BXRECSI6.js";import{X as F,d as A,z as S,q as V,f as C}from"./index-C65i4HWv.js";import{I as o}from"./vendor-icons-CDMuNDeL.js";const Y="_header_1j07c_1",q="_fadeIn_1j07c_1",H="_personaBadge_1j07c_9",E="_personaInfo_1j07c_37",z="_personaLabel_1j07c_42",U="_personaValue_1j07c_51",K="_statsGrid_1j07c_57",Q="_slideUpIn_1j07c_1",J="_statCard_1j07c_65",X="_statIcon_1j07c_82",Z="_statInfo_1j07c_98",ee="_statValue_1j07c_103",ae="_statLabel_1j07c_112",te="_wideSection_1j07c_120",se="_sectionHeader_1j07c_132",ne="_yearSelect_1j07c_147",re="_heatmapLayout_1j07c_176",ce="_dayLabels_1j07c_183",oe="_heatmapWrapper_1j07c_195",le="_heatmapMonthLabels_1j07c_201",ie="_heatmapGrid_1j07c_212",de="_heatBox_1j07c_220",he="_hiddenBox_1j07c_229",me="_level0_1j07c_240",pe="_level1_1j07c_243",_e="_level2_1j07c_247",ue="_level3_1j07c_251",ge="_level4_1j07c_255",Ne="_heatmapFooter_1j07c_260",ve="_heatLegend_1j07c_267",ye="_chartsContainer_1j07c_277",be="_chartWrapper_1j07c_285",fe="_hourlyChart_1j07c_312",je="_hourlyBarContainer_1j07c_320",Se="_hourlyBar_1j07c_320",Ce="_hourlyLabel_1j07c_346",ke="_chartSubtext_1j07c_353",Le="_popScale_1j07c_365",we="_popTrack_1j07c_369",Be="_popThumb_1j07c_377",Ie="_popLabels_1j07c_389",De="_popDescription_1j07c_399",xe="_seasonalChart_1j07c_414",Te="_seasonalBarContainer_1j07c_422",Me="_seasonalBar_1j07c_422",$e="_seasonalLabel_1j07c_444",Ge="_genreList_1j07c_452",Re="_genreRow_1j07c_459",We="_genreInfo_1j07c_465",Pe="_genreName_1j07c_472",Oe="_genreCount_1j07c_479",Fe="_genreBarBg_1j07c_486",Ae="_genreBar_1j07c_486",Ve="_warningSection_1j07c_499",Ye="_warningHeader_1j07c_510",qe="_droppedGrid_1j07c_526",He="_droppedCard_1j07c_532",Ee="_droppedName_1j07c_549",ze="_droppedDate_1j07c_556",Ue="_genreGrid_1j07c_597",Ke="_genreCard_1j07c_609",Qe="_rankBadge_1j07c_618",Je="_rankNumber_1j07c_631",Xe="_genreTitle_1j07c_637",Ze="_genreStats_1j07c_645",ea="_posterRow_1j07c_653",aa="_miniPoster_1j07c_660",ta="_genreCardSkeleton_1j07c_674",sa="_pulse_1j07c_1",na="_skeletonRank_1j07c_682",ra="_skeletonTitle_1j07c_696",ca="_skeletonStats_1j07c_709",oa="_skeletonPosters_1j07c_721",la="_recSection_1j07c_752",ia="_recSubtext_1j07c_763",da="_recGrid_1j07c_769",ha="_recCard_1j07c_775",ma="_recCover_1j07c_790",pa="_recInfo_1j07c_798",_a="_recTitle_1j07c_805",ua="_recMatch_1j07c_816",ga="_recMeta_1j07c_822",Na="_recWhy_1j07c_827",va="_recNote_1j07c_832",ya="_recAddBtn_1j07c_838",a={header:Y,fadeIn:q,personaBadge:H,personaInfo:E,personaLabel:z,personaValue:U,statsGrid:K,slideUpIn:Q,statCard:J,statIcon:X,statInfo:Z,statValue:ee,statLabel:ae,wideSection:te,sectionHeader:se,yearSelect:ne,heatmapLayout:re,dayLabels:ce,heatmapWrapper:oe,heatmapMonthLabels:le,heatmapGrid:ie,heatBox:de,hiddenBox:he,level0:me,level1:pe,level2:_e,level3:ue,level4:ge,heatmapFooter:Ne,heatLegend:ve,chartsContainer:ye,chartWrapper:be,hourlyChart:fe,hourlyBarContainer:je,hourlyBar:Se,hourlyLabel:Ce,chartSubtext:ke,popScale:Le,popTrack:we,popThumb:Be,popLabels:Ie,popDescription:De,seasonalChart:xe,seasonalBarContainer:Te,seasonalBar:Me,seasonalLabel:$e,genreList:Ge,genreRow:Re,genreInfo:We,genreName:Pe,genreCount:Oe,genreBarBg:Fe,genreBar:Ae,warningSection:Ve,warningHeader:Ye,droppedGrid:qe,droppedCard:He,droppedName:Ee,droppedDate:ze,genreGrid:Ue,genreCard:Ke,rankBadge:Qe,rankNumber:Je,genreTitle:Xe,genreStats:Ze,posterRow:ea,miniPoster:aa,genreCardSkeleton:ta,pulse:sa,skeletonRank:na,skeletonTitle:ra,skeletonStats:ca,skeletonPosters:oa,recSection:la,recSubtext:ia,recGrid:da,recCard:ha,recCover:ma,recInfo:pa,recTitle:_a,recMatch:ua,recMeta:ga,recWhy:Na,recNote:va,recAddBtn:ya},Ca=()=>{G(()=>{document.title="Insights - dango"},[]);const{data:s,isLoading:k,isError:L}=j({queryKey:["insights"],queryFn:async()=>{const t=await fetch("/api/insights");if(!t.ok)throw new Error("Failed to fetch insights");return t.json()}}),{data:w,isLoading:p}=F(),{titlePreference:B}=A(),_=P(),{data:u}=j({queryKey:["insights-recommendations"],queryFn:async()=>{const t=await fetch("/api/insights/recommendations");if(!t.ok)throw new Error("Failed to fetch recommendations");return t.json()},staleTime:300*1e3,retry:1}),g=O({mutationFn:async t=>{await V("/api/watchlist/add",{method:"POST",body:JSON.stringify({id:String(t.id),name:t.title,thumbnail:t.cover||"",status:"Plan to Watch",type:t.format})})},onSuccess:()=>{_.invalidateQueries({queryKey:["watchlist"]}),_.invalidateQueries({queryKey:["insights-recommendations"]}),S.success("Added to Plan to Watch")},onError:()=>S.error("Could not add to watchlist")}),N=i(()=>[...u?.candidates||[]].sort((t,n)=>n.matchPct-t.matchPct).slice(0,6),[u]),v=t=>{switch(B){case"nativeName":return t.nativeName||t.name;case"englishName":return t.englishName||t.name;default:return t.name}},[d,I]=R(new Date().getFullYear()),D=i(()=>{if(!s?.activityGrid)return[new Date().getFullYear()];const t=new Set(s.activityGrid.map(n=>parseInt(n.day.split("-")[0])));return t.add(new Date().getFullYear()),Array.from(t).sort((n,r)=>r-n)},[s]),y=i(()=>{if(!s?.activityGrid)return[];const t=new Map(s.activityGrid.map(m=>[m.day,m.count])),n=[],r=new Date(d,0,1),l=new Date(d,11,31),h=r.getDay(),c=new Date(r);for(c.setDate(c.getDate()-h);(c<=l||c.getDay()!==0)&&!(c>l&&c.getDay()===0);){const m=c.getFullYear(),M=String(c.getMonth()+1).padStart(2,"0"),$=String(c.getDate()).padStart(2,"0"),b=`${m}-${M}-${$}`,f=c<r||c>l;n.push({date:b,count:f?0:t.get(b)||0,month:c.getMonth(),isOutOfRange:f}),c.setDate(c.getDate()+1)}return n},[s,d]),x=i(()=>s?.seasonality?.length?Math.max(...s.seasonality.map(t=>t.seconds),1):0,[s]),T=i(()=>s?.hourlyDist?.length?Math.max(...s.hourlyDist.map(t=>t.count),1):0,[s]);return k||p?e("div",{className:a.loading,children:"Analyzing your anime lifestyle..."}):L?e("div",{className:a.error,children:"Could not load insights. Data might be syncing."}):s?e("div",{className:"page-container",children:[e("div",{className:a.header,children:[e("h2",{className:"section-title",children:"Watch Insights"}),e("div",{className:a.personaBadge,children:[e(o,{name:"user-astronaut"}),e("div",{className:a.personaInfo,children:[e("span",{className:a.personaLabel,children:"Your Persona"}),e("span",{className:a.personaValue,children:s.persona})]})]})]}),e("div",{className:a.statsGrid,children:[e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(138, 79, 255, 0.2)",color:"var(--accent)"},children:e(o,{name:"clock"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:[s.totalHours,"h"]}),e("span",{className:a.statLabel,children:"Watch Time"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(167, 139, 250, 0.2)",color:"#a78bfa"},children:e(o,{name:"bookmark"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:s.totalAnime}),e("span",{className:a.statLabel,children:"Total Anime"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(34, 197, 94, 0.2)",color:"#22c55e"},children:e(o,{name:"check-circle"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:s.totalEpisodes}),e("span",{className:a.statLabel,children:"Episodes Watched"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(34, 197, 94, 0.2)",color:"#22c55e"},children:e(o,{name:"check-circle"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:[s.completionRate,"%"]}),e("span",{className:a.statLabel,children:"Completion Rate"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(45, 212, 191, 0.2)",color:"#2dd4bf"},children:e(o,{name:"tv"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:s.completedAnime}),e("span",{className:a.statLabel,children:"Completed Shows"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(239, 68, 68, 0.2)",color:"#ef4444"},children:e(o,{name:"fire"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:s.bingeFactor}),e("span",{className:a.statLabel,children:"Max Daily Binge"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(59, 130, 246, 0.2)",color:"#3b82f6"},children:e(o,{name:"history"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:[s.avgSessionMinutes,"m"]}),e("span",{className:a.statLabel,children:"Avg. Session"})]})]}),e("div",{className:a.statCard,children:[e("div",{className:a.statIcon,style:{background:"rgba(251, 191, 36, 0.2)",color:"#fbbf24"},children:e(o,{name:"layer-group"})}),e("div",{className:a.statInfo,children:[e("span",{className:a.statValue,children:[s.avgCompletionDays,"d"]}),e("span",{className:a.statLabel,children:"Avg. Speed"})]})]})]}),e("div",{className:a.wideSection,children:[e("div",{className:a.sectionHeader,children:[e("h3",{children:"Activity"}),e("select",{className:a.yearSelect,value:d,onChange:t=>I(Number(t.currentTarget.value)),children:D.map(t=>e("option",{value:t,children:t},t))})]}),e("div",{className:a.heatmapLayout,children:[e("div",{className:a.dayLabels,children:[e("span",{style:{gridRow:2},children:"Mon"}),e("span",{style:{gridRow:4},children:"Wed"}),e("span",{style:{gridRow:6},children:"Fri"})]}),e("div",{className:a.heatmapWrapper,children:[e("div",{className:a.heatmapMonthLabels,children:(()=>{const t=[];let n=-1;return y.forEach((r,l)=>{if(r.month!==n&&!r.isOutOfRange){const h=Math.floor(l/7)+1;h>(t.length>0?t[t.length-1].props.style.gridColumn+2:-1)&&(t.push(e("span",{style:{gridColumn:h,whiteSpace:"nowrap"},children:new Date(0,r.month).toLocaleString("default",{month:"short"})},l)),n=r.month)}}),t})()}),e("div",{className:a.heatmapGrid,children:y.map((t,n)=>{let r=0;return t.count>0&&!t.isOutOfRange&&(r=1),t.count>2&&!t.isOutOfRange&&(r=2),t.count>5&&!t.isOutOfRange&&(r=3),t.count>10&&!t.isOutOfRange&&(r=4),e("div",{className:`${a.heatBox} ${a[`level${r}`]} ${t.isOutOfRange?a.hiddenBox:""}`,title:t.isOutOfRange?"":`${t.count} episodes on ${t.date}`},n)})}),e("div",{className:a.heatmapFooter,children:e("div",{className:a.heatLegend,children:[e("span",{children:"Less"}),e("div",{className:`${a.heatBox} ${a.level0}`}),e("div",{className:`${a.heatBox} ${a.level1}`}),e("div",{className:`${a.heatBox} ${a.level2}`}),e("div",{className:`${a.heatBox} ${a.level3}`}),e("div",{className:`${a.heatBox} ${a.level4}`}),e("span",{children:"More"})]})})]})]})]}),e("div",{className:a.chartsContainer,children:[e("div",{className:a.chartWrapper,children:[e("h3",{children:"Time Distribution (24h)"}),e("div",{className:a.hourlyChart,children:s.hourlyDist?.map((t,n)=>e("div",{className:a.hourlyBarContainer,children:[e("div",{className:a.hourlyBar,style:{height:`${t.count/T*100||2}%`,width:"100%",maxWidth:"12px"},title:`${t.count} watches at ${t.hour}:00`}),e("span",{className:a.hourlyLabel,style:{visibility:n%4===0?"visible":"hidden"},children:t.hour})]},n))}),e("div",{className:a.chartSubtext,children:(()=>{if(!s.hourlyDist?.length)return"Watch more to see your peak hours!";const t=parseInt([...s.hourlyDist].sort((n,r)=>r.count-n.count)[0].hour);return t>=6&&t<19?"You prefer daytime watching ☀️":"You're a confirmed Night Owl 🦉"})()})]}),e("div",{className:a.chartWrapper,children:[e("h3",{children:"Popularity Bias"}),e("div",{className:a.popScale,children:[e("div",{className:a.popTrack,children:e("div",{className:a.popThumb,style:{left:`${Math.max(0,Math.min(100,100-s.popularityScore*10))}%`}})}),e("div",{className:a.popLabels,children:[e("span",{children:"Mainstream"}),e("span",{children:"Underground"})]})]}),e("p",{className:a.popDescription,children:["Your taste score is ",e("strong",{children:[s.popularityScore,"/10"]}),".",s.popularityScore>=5?" You follow the big hits!":" You're a connoisseur of the obscure!"]})]}),e("div",{className:a.chartWrapper,children:[e("h3",{children:"Monthly Activity"}),e("div",{className:a.seasonalChart,children:s.seasonality?.map((t,n)=>e("div",{className:a.seasonalBarContainer,children:[e("div",{className:a.seasonalBar,style:{height:`${t.seconds/x*100||5}%`,width:"100%",maxWidth:"24px"},title:`${Math.round(t.seconds/3600)}h in ${new Date(0,n).toLocaleString("default",{month:"long"})}`}),e("span",{className:a.seasonalLabel,children:new Date(0,n).toLocaleString("default",{month:"short"})})]},n))})]})]}),e("div",{className:a.genreGrid,children:p?Array.from({length:4}).map((t,n)=>e("div",{className:a.genreCardSkeleton,children:[e("div",{className:a.skeletonRank}),e("div",{className:a.skeletonTitle}),e("div",{className:a.skeletonStats}),e("div",{className:a.skeletonPosters})]},n)):w?.map(t=>e("div",{className:a.genreCard,children:[e("div",{className:a.rankBadge,children:e("span",{className:a.rankNumber,children:t.rank})}),e("h3",{className:a.genreTitle,children:t.name}),e("div",{className:a.genreStats,children:[e("span",{children:[t.count," episodes"]}),e("span",{children:[t.meanScore.toFixed(1)," avg score"]}),e("span",{children:t.timeWatched})]}),e("div",{className:a.posterRow,children:t.topShows.map(n=>e("img",{src:C(n.thumbnail),alt:v(n),className:a.miniPoster,title:v(n),loading:"lazy",decoding:"async"},n.id))})]},t.name))}),N.length>0&&e("div",{className:a.recSection,children:[e("h3",{children:"Recommended For You"}),e("p",{className:a.recSubtext,children:"From your library taste — updates automatically as you watch"}),e("div",{className:a.recGrid,children:N.map(t=>e("div",{className:a.recCard,children:[t.cover&&e("img",{src:C(t.cover),alt:t.title,className:a.recCover,loading:"lazy",decoding:"async"}),e("div",{className:a.recInfo,children:[e(W,{to:`/anime/${t.id}`,className:a.recTitle,children:t.title}),e("span",{className:a.recMatch,children:[t.matchPct,"% match"]}),e("span",{className:a.recMeta,children:[t.year,t.format,t.episodes?`${t.episodes}ep`:null].filter(Boolean).join(" · ")}),e("span",{className:a.recWhy,children:["Shares ",t.sharedGenres.join(", "),t.exemplars.length>0&&` — like ${t.exemplars.join(", ")}`]}),t.kind==="continue"&&e("span",{className:a.recNote,children:"Continue — you watched the earlier part."}),t.kind==="sequel"&&t.prequelTitle&&e("span",{className:a.recNote,children:['Sequel — start with "',t.prequelTitle,'" first.']}),e("button",{className:a.recAddBtn,onClick:()=>g.mutate(t),disabled:g.isPending,children:"+ Plan to Watch"})]})]},t.id))})]}),s.droppedShows?.length>0&&e("div",{className:a.warningSection,children:[e("div",{className:a.warningHeader,children:[e(o,{name:"exclamation-triangle"}),e("h3",{children:"Dusty Watchlist (Inactive 90+ days)"})]}),e("div",{className:a.droppedGrid,children:s.droppedShows.map(t=>e("div",{className:a.droppedCard,children:[e("span",{className:a.droppedName,children:t.name}),e("span",{className:a.droppedDate,children:["Last watched: ",new Date(t.lastActivity).toLocaleDateString()]})]},t.id))})]})]}):null};export{Ca as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{u as e,n as E,o as z,a as D,d as Q,h as L,T as b,S as _}from"./vendor-core-8R2fFbyN.js";import{I as N}from"./vendor-icons-CDMuNDeL.js";import{d as j,B as I}from"./index-C65i4HWv.js";import{E as V}from"./ErrorMessage-z4f4WGgP.js";import{b as F,m as G,g as H,h as O,j as W,r as Y,k as q,l as J,n as K,M as X}from"./useMangaLibrary-U7tUo8nx.js";import{s as t}from"./Manga.module-BcfOdsNP.js";import"./vendor-query-BXRECSI6.js";const Z=({detail:r,altTitle:l,onBack:C,onOpenChapter:u,bookmarkButton:c,statusSelect:v,primaryAction:d,progressByChapter:y})=>{const i=[r.type,r.status,r.year?String(r.year):"",r.author?`by ${r.author}`:""].filter(Boolean);return e("div",{children:[e("button",{className:t.backBtn,onClick:C,children:[e(N,{name:"chevron-left",size:12})," Back to browse"]}),e("div",{className:t.detail,children:[r.cover&&e("img",{className:t.detailCover,src:F(r.provider,r.cover),alt:r.title,decoding:"async",draggable:!1}),e("div",{className:t.detailInfo,children:[e("h2",{className:t.detailTitle,children:r.title}),l&&e("p",{className:t.detailAltTitle,children:["Also known as: ",l]}),i.length>0&&e("div",{className:t.detailMeta,children:i.map(n=>e("span",{className:t.badge,children:n},n))}),r.genres&&r.genres.length>0&&e("div",{className:t.detailMeta,children:r.genres.map(n=>e("span",{className:t.badge,children:n},n))}),r.description&&e("p",{className:t.detailDesc,children:r.description}),(c||v||d)&&e("div",{className:t.detailActions,children:[d,c,v]})]})]}),e("h3",{className:t.detailTitle,style:{fontSize:"1rem"},children:["Chapters (",r.chapters.length,")"]}),r.chapters.length===0?e("p",{className:t.statusMsg,children:"No chapters found for this title on this provider."}):e("div",{className:t.chapterList,children:r.chapters.map(n=>{const a=y?.get(n.id),p=!!a&&a.pageCount>0&&a.page>=a.pageCount;return e("button",{className:`${t.chapterRow} ${p?t.chapterDone:""}`,onClick:()=>u(n),title:n.externalUrl?"External only — opens on source site":void 0,children:[e("span",{children:["Ch. ",n.number,n.title?` — ${n.title}`:""]}),e("span",{className:t.chapterMeta,children:[a&&a.pageCount>0?p?"read":`p. ${a.page}/${a.pageCount}`:"",n.group,n.externalUrl?"external":""].filter(Boolean).join(" · ")})]},n.id)})})]})};function le(){const{provider:r="",id:l=""}=E(),[C]=z(),u=D(),[c,v]=Q(()=>localStorage.getItem("agreedToViewMature")==="true");L(()=>{const s=()=>v(localStorage.getItem("agreedToViewMature")==="true");return window.addEventListener("focus",s),window.addEventListener("storage",s),()=>{window.removeEventListener("focus",s),window.removeEventListener("storage",s)}},[]);const d=C.get("rating")||"safe",y=c?d:"safe",i=r&&l?G(r,l):void 0,n=H(r||null,l||null,y,c),a=n.data,p=O(i),f=W(i),{titlePreference:w}=j(),m=b(()=>a&&{...a,title:Y(a,w)},[a,w]),M=q(),B=J(),P=K();L(()=>{m&&(document.title=`${m.title} - dango`)},[m]);const S=b(()=>{const s=new Map;for(const o of f.data?.progress??[])s.set(o.chapterId,o);return s},[f.data]),k=p.data?.inLibrary??!1,R=p.data?.status??"Reading",$=b(()=>(a?.chapters??[]).filter(s=>!s.externalUrl),[a]),g=b(()=>{if(!a)return null;const s=[...f.data?.progress??[]].sort((o,h)=>h.updatedAt-o.updatedAt);for(const o of s){const h=a.chapters.find(U=>U.id===o.chapterId);if(h&&!h.externalUrl)return h}return $[0]??null},[a,f.data,$]),x=(g?S.get(g.id):void 0)?`Continue Ch. ${g?.number}`:"Start Reading",T=s=>{if(s.externalUrl){window.open(s.externalUrl,"_blank","noopener,noreferrer");return}const o=new URLSearchParams;o.set("chapter",s.id),c&&d!=="safe"&&o.set("rating",d),u(`/manga/${r}/${encodeURIComponent(l)}/read?${o.toString()}`)},A=()=>{!a||!i||(k?B.mutate(i):M.mutate({provider:r,mangaId:l,title:a.title,cover:a.cover,author:a.author,altTitle:a.altTitle,contentRating:a.contentRating}))};return e("div",{className:t.page,children:n.isLoading?e("div",{"aria-hidden":!0,children:[e("div",{className:`${t.skeletonThumb} ${t.shimmer}`,style:{maxWidth:"12rem"}}),e("div",{className:`${t.skeletonLine} ${t.shimmer}`,style:{width:"40%"}}),e("div",{className:`${t.skeletonLine} ${t.shimmer}`,style:{width:"70%"}})]}):n.isError||!a||!m?e(_,{children:[e("button",{className:t.backBtn,onClick:()=>u("/manga"),children:[e(N,{name:"chevron-left",size:12})," Back to browse"]}),e(V,{message:"Failed to load this title. Please try again."})]}):e(Z,{detail:m,altTitle:a.altTitle!==m.title?a.altTitle:void 0,onBack:()=>u("/manga"),onOpenChapter:T,progressByChapter:S,primaryAction:g?e(I,{onClick:()=>g&&T(g),children:[e(N,{name:"book",size:13})," ",x]}):void 0,bookmarkButton:e(I,{variant:k?"secondary":"primary",onClick:A,disabled:M.isPending||B.isPending,children:[e(N,{name:"bookmark",size:13})," ",k?"Bookmarked":"Bookmark"]}),statusSelect:k?e("select",{className:t.detailSelect,value:R,onChange:s=>i&&P.mutate({id:i,status:s.target.value}),"aria-label":"Reading status",children:X.map(s=>e("option",{value:s,children:s},s))}):void 0})})}export{le as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{d as A,A as v,q,h as g,u as e,S as J,n as ie,o as ce,a as le,T as de}from"./vendor-core-8R2fFbyN.js";import{E as ge}from"./ErrorMessage-z4f4WGgP.js";import{I as C}from"./vendor-icons-CDMuNDeL.js";import{u as ue}from"./vendor-query-BXRECSI6.js";import{o as he,p as me,m as pe,g as fe,r as ve,j as be,q as Ne}from"./useMangaLibrary-U7tUo8nx.js";import{s as n}from"./Manga.module-BcfOdsNP.js";import{d as ye}from"./index-C65i4HWv.js";const Y="mangaReadMode",G="mangaFitMode";function we(){try{return localStorage.getItem(Y)==="page"?"page":"strip"}catch{return"strip"}}function Se(){try{return localStorage.getItem(G)==="height"?"height":"width"}catch{return"width"}}const Ie=({provider:p,mangaTitle:b,chapter:u,chapters:R,onBack:$,onOpenChapter:f,initialPage:l=0,onProgress:z,savedPage:w=0})=>{const[i,s]=A(we),[B,F]=A(Se),[d,x]=A(0),[h,S]=A(new Set),[D,N]=A(0),y=v(null),c=v([]),O=ue(),{data:T,isLoading:K,isError:U}=he(p,u.id),t=T?.pages??[],o=t.map(a=>me(p,a)),M=v(0),ee=v(0),L=v(!1);ee.current=t.length;const _=R.findIndex(a=>a.id===u.id),I=_>0?[...R.slice(0,_)].reverse().find(a=>!a.externalUrl)??null:null,k=_>=0?R.slice(_+1).find(a=>!a.externalUrl)??null:null,P=q(a=>{L.current=!0,x(r=>{const m=r+a;return m<0?(I&&f(I),0):m>=t.length?(k&&f(k),r):m})},[t.length,I,k,f]);g(()=>{L.current=!1,M.current=Math.max(0,l),x(Math.max(0,l)),S(new Set),c.current=[],l<=0&&window.scrollTo(0,0)},[u.id,l]);const Q=v(z);Q.current=z;const W=v("");g(()=>{M.current=d,t.length!==0&&Q.current?.(Math.min(d+1,t.length),t.length)},[d,t.length]),g(()=>{l<=0||L.current||M.current===0&&(L.current=!1,M.current=l,x(l))},[l]),g(()=>{if(i!=="strip"||t.length===0||l<=0)return;const a=`${u.id}:${l}`;if(W.current===a)return;W.current=a;const r=window.setTimeout(()=>{c.current[Math.min(l,t.length-1)]?.scrollIntoView({block:"start"})},150);return()=>window.clearTimeout(r)},[i,t.length,l,u.id]),g(()=>{document.title=`${b} Ch. ${u.number} - dango`},[b,u.number]),g(()=>{try{localStorage.setItem(Y,i)}catch{}},[i]),g(()=>{try{localStorage.setItem(G,B)}catch{}},[B]),g(()=>{i==="page"&&window.scrollTo(0,0)},[i]),g(()=>{if(i!=="page"||t.length===0)return;const a=r=>{r.key==="ArrowRight"||r.key===" "?(r.preventDefault(),P(1)):r.key==="ArrowLeft"&&(r.preventDefault(),P(-1))};return window.addEventListener("keydown",a),()=>window.removeEventListener("keydown",a)},[i,t.length,P]),g(()=>{if(i!=="strip"||t.length===0)return;const a=new IntersectionObserver(r=>{for(const m of r)if(m.isIntersecting){const j=Number(m.target.dataset.pageIndex);Number.isNaN(j)||(j!==M.current&&(L.current=!0),x(j))}},{rootMargin:"-45% 0px -45% 0px",threshold:0});return c.current.forEach(r=>r&&a.observe(r)),()=>a.disconnect()},[i,t.length,T]);const te=a=>{y.current=a.touches[0].clientX},ne=a=>{if(y.current===null)return;const r=a.changedTouches[0].clientX-y.current;y.current=null,!(Math.abs(r)<50)&&P(r<0?1:-1)},V=a=>{a&&f(a)},Z=a=>S(r=>{if(r.has(a))return r;const m=new Set(r);return m.add(a),m}),ae=a=>{S(r=>{const m=new Set(r);return m.delete(a),m}),N(r=>r+1)},re=()=>{S(new Set),N(a=>a+1),O.invalidateQueries({queryKey:["mangaPages",p,u.id]})},se=t.length>0&&h.size>=t.length,H=a=>D>0?`${a}&n=${D}`:a,X=a=>e("div",{className:n.deadPage,role:"alert",children:[e("span",{children:["Page ",a+1," failed to load"]}),e("button",{className:n.pageBtn,onClick:()=>ae(a),children:[e(C,{name:"sync-alt",size:12})," Retry"]})]}),E=B==="height",oe=w>0?w-1:-1;return e("div",{className:n.readerShell,children:[e("div",{className:n.readerBar,children:[e("button",{className:n.readerIconBtn,onClick:$,"aria-label":"Back to chapters",children:e(C,{name:"chevron-left",size:14})}),e("span",{className:n.readerTitle,children:[b," — Ch. ",u.number,u.title?` ${u.title}`:""]}),t.length>0&&e("span",{className:n.readerCounter,children:[Math.min(d+1,t.length)," / ",t.length]}),e("div",{className:n.readerSegment,role:"group","aria-label":"Reading mode",children:[e("button",{type:"button","aria-pressed":i==="strip",className:i==="strip"?n.readerSegmentActive:"",onClick:()=>s("strip"),children:"Strip"}),e("button",{type:"button","aria-pressed":i==="page",className:i==="page"?n.readerSegmentActive:"",onClick:()=>s("page"),children:"Page"})]}),e("div",{className:n.readerSegment,role:"group","aria-label":"Fit mode",children:[e("button",{type:"button","aria-pressed":!E,className:E?"":n.readerSegmentActive,onClick:()=>F("width"),title:"Fit width — best for portrait / phones",children:"Wide"}),e("button",{type:"button","aria-pressed":E,className:E?n.readerSegmentActive:"",onClick:()=>F("height"),title:"Fit height — best for landscape / desktop",children:"Tall"})]}),e("button",{className:n.readerIconBtn,disabled:!I,onClick:()=>V(I),"aria-label":"Previous chapter",children:e(C,{name:"chevron-left",size:14})}),e("button",{className:n.readerIconBtn,disabled:!k,onClick:()=>V(k),"aria-label":"Next chapter",children:e(C,{name:"chevron-right",size:14})})]}),U?e("p",{className:n.statusMsg,children:"Failed to load pages. Please try again."}):K?e("div",{className:n.readerPages,"aria-hidden":!0,children:Array.from({length:3}).map((a,r)=>e("div",{className:`${n.skeletonThumb} ${n.shimmer}`,style:{width:"100%",maxWidth:"56rem",aspectRatio:"3 / 4"}},r))}):t.length===0?e("p",{className:n.statusMsg,children:"No pages found for this chapter."}):se?e("div",{className:n.failedBox,children:[e("p",{className:n.statusMsg,children:"Page images failed to load (expired or blocked links)."}),e("button",{className:n.pageBtn,onClick:re,children:[e(C,{name:"sync-alt",size:12})," Reload pages"]})]}):i==="strip"?e("div",{className:`${n.readerPages} ${E?n.readerPagesTall:""}`,children:o.map((a,r)=>e("div",{ref:m=>{c.current[r]=m},"data-page-index":r,className:n.readerPageWrap,children:[r===oe&&e("div",{className:n.saveMarker,role:"note","aria-label":"Saved reading position",children:[e(C,{name:"bookmark",size:12}),e("span",{children:["Saved · p. ",w]})]}),h.has(r)?X(r):e("img",{className:n.readerPage,src:H(a),alt:`Page ${r+1}`,loading:r<3?"eager":"lazy",decoding:"async",draggable:!1,onError:()=>Z(r)})]},`${r}-${t[r].slice(-32)}`))}):e(J,{children:[e("div",{className:`${n.pageView} ${E?n.pageViewTall:""}`,onTouchStart:te,onTouchEnd:ne,children:[e("button",{className:`${n.tapZone} ${n.tapZoneLeft}`,onClick:()=>P(-1),"aria-label":"Previous page",children:e("span",{className:n.tapChevron,children:e(C,{name:"chevron-left",size:16})})}),h.has(d)?X(d):e("img",{className:n.pageImage,src:H(o[d]),alt:`Page ${d+1} of ${t.length}`,decoding:"async",draggable:!1,onError:()=>Z(d)},`${d}-${t[d].slice(-32)}`),e("button",{className:`${n.tapZone} ${n.tapZoneRight}`,onClick:()=>P(1),"aria-label":"Next page",children:e("span",{className:n.tapChevron,children:e(C,{name:"chevron-right",size:16})})})]}),e("nav",{className:n.pagination,"aria-label":"Page navigation",children:[e("button",{className:n.pageBtn,disabled:d<=0,onClick:()=>P(-1),children:"Previous page"}),e("span",{className:n.pageIndicator,children:[d+1," / ",t.length]}),e("button",{className:n.pageBtn,disabled:d>=t.length-1,onClick:()=>P(1),children:"Next page"})]})]}),(I||k)&&e("nav",{className:n.pagination,children:[e("button",{className:n.pageBtn,disabled:!I,onClick:()=>V(I),children:"Previous chapter"}),e("button",{className:n.pageBtn,disabled:!k,onClick:()=>V(k),children:"Next chapter"})]})]})},ke=800,Pe=5e3;function Be(){const{provider:p="",id:b=""}=ie(),[u]=ce(),R=le(),$=u.get("chapter")||"",f=u.get("rating")||"safe",l=typeof localStorage<"u"&&localStorage.getItem("agreedToViewMature")==="true",z=l?f:"safe",w=p&&b?pe(p,b):"",i=fe(p||null,b||null,z,l),s=i.data,{titlePreference:B}=ye(),F=s?ve(s,B):"Manga",d=be(w||void 0),x=Ne(),h=s&&$?s.chapters.find(t=>t.id===$)??null:null,S=de(()=>{const t=(d.data?.progress??[]).find(o=>o.chapterId===$);return t&&t.page>0?t.page:0},[d.data,$]),D=S>1?S-1:0,N=v(null),y=v(null),c=q(()=>{N.current&&(clearTimeout(N.current),N.current=null);const t=y.current;y.current=null,t&&w&&x.mutate({mangaId:w,chapterId:t.chapterId,chapterNumber:t.chapterNumber,page:t.page,pageCount:t.pageCount,title:s?.title??null,cover:s?.cover??null,provider:p||null,altTitle:s?.altTitle??null,contentRating:s?.contentRating??null})},[w,x,s?.title,s?.cover,s?.altTitle,s?.contentRating,p]),O=q((t,o)=>{h&&(y.current={chapterId:h.id,chapterNumber:h.number,page:t,pageCount:o},N.current&&clearTimeout(N.current),N.current=setTimeout(c,ke))},[h,c]);g(()=>{c()},[$,c]),g(()=>()=>c(),[c]),g(()=>{const t=window.setInterval(()=>{y.current&&c()},Pe);return()=>window.clearInterval(t)},[c]),g(()=>{const t=()=>{document.visibilityState==="hidden"&&c()},o=()=>c();return document.addEventListener("visibilitychange",t),window.addEventListener("pagehide",o),()=>{document.removeEventListener("visibilitychange",t),window.removeEventListener("pagehide",o)}},[c]);const T=v("");T.current||(T.current=`manga-${Date.now()}-${Math.random().toString(36).slice(2)}`),g(()=>{if(!s||!h)return;const t=s.contentRating==="erotica"||s.contentRating==="pornographic"||s.type==="doujinshi",o=()=>{fetch("/api/discord/manga",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:s.title,chapterLabel:`Chapter ${h.number}`,isPlaying:!0,thumbnail:s.cover||"",thumbnails:[s.cover].filter(Boolean),isAdult:t,sessionId:T.current})}).catch(()=>{}),fetch("/api/discord/heartbeat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionId:T.current})}).catch(()=>{})};o();const M=window.setInterval(o,6e4);return()=>window.clearInterval(M)},[s,h]);const K=t=>{if(c(),t.externalUrl){window.open(t.externalUrl,"_blank","noopener,noreferrer");return}const o=new URLSearchParams;o.set("chapter",t.id),l&&f!=="safe"&&o.set("rating",f),R(`/manga/${p}/${encodeURIComponent(b)}/read?${o.toString()}`)},U=()=>{c();const t=new URLSearchParams;l&&f!=="safe"&&t.set("rating",f);const o=t.toString()?`?${t.toString()}`:"";R(`/manga/${p}/${encodeURIComponent(b)}${o}`)};return e("div",{className:n.page,children:i.isLoading?e("div",{className:n.readerPages,"aria-hidden":!0,children:Array.from({length:3}).map((t,o)=>e("div",{className:`${n.skeletonThumb} ${n.shimmer}`,style:{width:"100%",maxWidth:"56rem",aspectRatio:"3 / 4"}},o))}):!s||!h?e(J,{children:[e("button",{className:n.backBtn,onClick:U,children:"Back to title"}),e(ge,{message:"Failed to load this chapter. Please try again."})]}):e(Ie,{provider:p,mangaTitle:F,chapter:h,chapters:s.chapters,onBack:U,onOpenChapter:K,initialPage:D,onProgress:O,savedPage:S},h.id)})}export{Be as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{A as L,d as b,h as F,g as Ke,u as e,S as w,$ as Qe,o as ze,T as z}from"./vendor-core-8R2fFbyN.js";import{q as P,z as C}from"./index-C65i4HWv.js";import{I as p}from"./vendor-icons-CDMuNDeL.js";import{a as i}from"./Asmr.module-ChUXDZKx.js";import{r as m}from"./Radio.module-DGSZLpKM.js";import{b as J,u as Ce,a as Le}from"./vendor-query-BXRECSI6.js";function Ie(n){if(!Number.isFinite(n)||n<0)return"0:00";const h=Math.floor(n/60),g=Math.floor(n%60);return`${h}:${String(g).padStart(2,"0")}`}const Ye=({track:n,shuffle:h,onToggleShuffle:g,onTrackStep:O,onTrackFailed:be,onClose:ie,liked:Y,likeVisible:W,likePending:ae,onToggleLike:V})=>{const M=L(null),Z=L(!1),[q,R]=b(!1),[he,l]=b(!0),[N,D]=b(!1),[I,B]=b(0),[U,se]=b(0),[T,H]=b(()=>{const r=parseFloat(localStorage.getItem("musicVolume")||"");return Number.isFinite(r)?r:1}),[$,re]=b(()=>localStorage.getItem("musicAutoplay")!=="false"),j=L(T);j.current=T;const S=L($);S.current=$;const f=L(0),E=L(0),A=L(null),K=L("");K.current||(K.current=typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID():`music-${Date.now()}-${Math.random().toString(36).slice(2)}`);const X=`/api/music/audio?videoId=${encodeURIComponent(n.id)}`,G=()=>{A.current&&(window.clearTimeout(A.current),A.current=null)},ee=r=>{const a=M.current;if(!a)return;G(),l(!0),D(!1);const k=()=>{if(a.removeEventListener("loadedmetadata",k),r>5&&Number.isFinite(a.duration)&&r<a.duration-5)try{a.currentTime=r}catch{}};a.addEventListener("loadedmetadata",k),a.src=X,a.load(),a.volume=j.current,a.play().catch(()=>{})},ve=()=>{l(!1),D(!0),be?.(n),S.current&&O(1)},oe=()=>{if(!A.current){if(E.current>=3){ve();return}E.current+=1,A.current=window.setTimeout(()=>{A.current=null,ee(f.current)},5e3)}},ke=()=>{E.current=0,ee(f.current)};F(()=>{f.current=0,E.current=0,R(!1),B(0),se(0);const r=M.current;return ee(f.current),()=>{G(),r&&r.removeAttribute("src")}},[X]),F(()=>{const r=M.current;r&&(r.volume=T)},[T]),F(()=>(document.body.classList.add("asmr-player-open"),()=>document.body.classList.remove("asmr-player-open")),[]);const _=n.thumbnails?.[n.thumbnails.length-1]?.url??null,te=Ke.useCallback(r=>{const a=M.current;fetch("/api/discord/music",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:n.title,artistLabel:n.artists,isPlaying:r,thumbnail:_||"",currentTime:a?.currentTime??0,duration:Number.isFinite(a?.duration??NaN)?a?.duration:0,sessionId:K.current})}).catch(()=>{}),fetch("/api/discord/heartbeat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionId:K.current})}).catch(()=>{})},[n,_]);F(()=>{te(q)},[te,q,n.id]),F(()=>{if(!q)return;const r=window.setInterval(()=>te(!0),15e3);return()=>window.clearInterval(r)},[q,te]),F(()=>{const r=K.current,a=()=>{if(!r)return;const ye=JSON.stringify({sessionId:r});navigator.sendBeacon?(navigator.sendBeacon("/api/discord/clear",new Blob([ye],{type:"application/json"})),navigator.sendBeacon("/api/discord/heartbeat",new Blob([JSON.stringify({sessionId:r,bye:!0})],{type:"application/json"}))):(fetch("/api/discord/clear",{method:"POST",headers:{"Content-Type":"application/json"},body:ye,keepalive:!0}).catch(()=>{}),fetch("/api/discord/heartbeat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionId:r,bye:!0}),keepalive:!0}).catch(()=>{}))},k=()=>a(),v=()=>{document.visibilityState==="hidden"&&a()};return window.addEventListener("pagehide",k),window.addEventListener("beforeunload",k),document.addEventListener("visibilitychange",v),()=>{window.removeEventListener("pagehide",k),window.removeEventListener("beforeunload",k),document.removeEventListener("visibilitychange",v),a()}},[]);const pe=()=>{const r=M.current;r&&(r.paused?r.play().catch(()=>{}):r.pause())},[Ne,le]=b(!1);F(()=>{le(!1)},[_]);const ce=`${n.title} — ${n.artists}`,ge=e(w,{children:[e("div",{className:i.playerButtons,children:[e("button",{className:i.playerBtn,onClick:()=>O(-1),"aria-label":"Previous track",children:e(p,{name:"step-backward"})}),e("button",{className:`${i.playerBtn} ${i.playBtn}`,onClick:pe,"aria-label":q?"Pause":"Play",children:q?e(p,{name:"pause"}):e(p,{name:"play"})}),e("button",{className:i.playerBtn,onClick:()=>O(1),"aria-label":"Next track",children:e(p,{name:"step-forward"})}),N&&e("button",{className:i.playerBtn,onClick:ke,title:"Retry playback","aria-label":"Retry playback",children:e(p,{name:"redo"})})]}),e("div",{className:i.seekRow,children:[e("span",{className:i.timeLabel,children:Ie(I)}),e("input",{className:i.seekBar,type:"range",min:0,max:U||0,step:.1,value:Math.min(I,U||0),onChange:r=>{const a=M.current;!a||!Number.isFinite(a.duration)||(a.currentTime=parseFloat(r.target.value),B(a.currentTime))},onPointerDown:()=>{Z.current=!0},onPointerUp:()=>{Z.current=!1},style:{"--played-percent":`${U>0?I/U*100:0}%`},"aria-label":"Seek"}),e("span",{className:i.timeLabel,children:Ie(U)}),he&&!N&&e("span",{className:i.timeLabel,children:"loading…"})]})]}),ue=e(w,{children:[n.thumbnails?.[0]?.url&&!Ne&&e("img",{src:n.thumbnails[0].url,alt:"",className:m.stationThumb,loading:"lazy",decoding:"async",onError:()=>le(!0)}),e("div",{className:i.playerInfo,children:[e("p",{className:i.playerTitle,title:ce,children:ce}),e("p",{className:i.playerTrack,children:n.artists})]}),e("div",{className:i.playerControls,children:ge}),e("div",{className:i.playerRight,children:[T===0?e(p,{name:"volume-mute",className:i.volumeIcon}):T<.5?e(p,{name:"volume-down",className:i.volumeIcon}):e(p,{name:"volume-up",className:i.volumeIcon}),e("input",{className:i.volumeBar,type:"range",min:0,max:1,step:.01,value:T,onChange:r=>{const a=parseFloat(r.target.value);H(a),localStorage.setItem("musicVolume",String(a))},style:{"--volume-percent":`${T*100}%`},"aria-label":"Volume"})]}),e("div",{className:i.playerActions,children:[W&&V&&e("button",{className:`${i.playerBtn} ${Y?i.playerBtnActive:""}`,onClick:V,disabled:ae,title:Y?"Remove from Liked Music":"Add to Liked Music","aria-label":Y?"Remove from Liked Music":"Add to Liked Music","aria-pressed":!!Y,children:e(p,{name:"heart"})}),e("button",{className:`${i.playerBtn} ${h?i.playerBtnActive:""}`,onClick:g,title:h?"Shuffle on":"Shuffle off","aria-label":"Toggle shuffle","aria-pressed":h,children:e(p,{name:"shuffle"})}),e("button",{className:`${i.playerBtn} ${$?i.playerBtnActive:""}`,onClick:()=>{re(r=>(localStorage.setItem("musicAutoplay",String(!r)),!r))},title:$?"Autoplay on: next track plays automatically":"Autoplay off","aria-label":"Toggle autoplay","aria-pressed":$,children:e(p,{name:"forward"})}),e("a",{href:`https://music.youtube.com/watch?v=${encodeURIComponent(n.id)}`,target:"_blank",rel:"noreferrer",title:"Open in YouTube Music","aria-label":"Open in YouTube Music",className:i.playerBtn,style:{display:"inline-flex",alignItems:"center"},children:e(p,{name:"external-link-alt"})}),e("button",{className:i.playerBtn,onClick:ie,"aria-label":"Close player",children:e(p,{name:"times"})})]})]});return Qe(e(w,{children:[e("audio",{ref:M,preload:"none",onPlay:()=>{R(!0),l(!1),D(!1),E.current=0,G()},onPause:()=>R(!1),onWaiting:()=>{N||l(!0)},onPlaying:()=>{l(!1),E.current=0,G()},onTimeUpdate:r=>{const a=r.target.currentTime;f.current=a,G(),Z.current||B(a)},onStalled:()=>{oe()},onLoadedMetadata:r=>{const a=r.target.duration;Number.isFinite(a)&&se(a)},onDurationChange:r=>{const a=r.target.duration;Number.isFinite(a)&&se(a)},onError:()=>{oe()},onEnded:()=>{S.current&&O(1)}}),e("div",{className:i.playerBar,children:ue})]}),document.body)},je=()=>e("div",{style:{marginTop:"0.9rem",paddingTop:"0.9rem",borderTop:"1px dashed var(--border-color)"},children:[e("h4",{style:{margin:"0 0 0.35rem",fontSize:"0.95rem"},children:"Where to get the cookie"}),e("p",{style:{margin:"0 0 0.6rem",fontSize:"0.85rem",color:"var(--text-secondary)"},children:"Google marks its login cookies unreadable to page scripts, so this has to come from DevTools — it takes about 20 seconds:"}),e("ol",{style:{margin:"0 0 0.75rem",paddingLeft:"1.25rem",fontSize:"0.85rem",color:"var(--text-secondary)",display:"grid",gap:"0.3rem"},children:[e("li",{children:["Open"," ",e("a",{href:"https://music.youtube.com/",target:"_blank",rel:"noopener noreferrer",children:"music.youtube.com"})," ","and make sure you are logged in."]}),e("li",{children:["Press ",e("kbd",{children:"F12"}),", go to the ",e("strong",{children:"Network"})," tab, and type ",e("kbd",{children:"browse"})," ","in the filter box."]}),e("li",{children:["Click any ",e("kbd",{children:"POST"})," browse entry, then scroll to Request Headers."]}),e("li",{children:[e("strong",{children:"Right-click"})," the ",e("kbd",{children:"Cookie"})," value and choose"," ",e("strong",{children:"Copy value"})," — right-click copies the full unwrapped value, selecting the text by hand truncates it."]}),e("li",{children:"Paste it into the sign-in box above and press Sign in."})]})]}),me=300*1e3,Ge=()=>J({queryKey:["music-auth"],queryFn:()=>P("/api/music/auth/status"),staleTime:30*1e3,refetchInterval:n=>n.state.data?.authenticated?!1:3e3,retry:1}),Je=()=>{const n=Ce();return Le({mutationFn:h=>P("/api/music/auth/start",{method:"POST",body:JSON.stringify({cookie:h})}),onSuccess:()=>{n.invalidateQueries({queryKey:["music-auth"]}),n.invalidateQueries({queryKey:["music-library"]})}})},We=()=>{const n=Ce();return Le({mutationFn:()=>P("/api/music/auth/signout",{method:"POST"}),onSuccess:()=>{n.invalidateQueries({queryKey:["music-auth"]}),n.invalidateQueries({queryKey:["music-library"]})}})},Ve=n=>J({queryKey:["music-search",n],queryFn:()=>P(`/api/music/search?q=${encodeURIComponent(n)}`),enabled:n.trim().length>0,staleTime:me,retry:1}),Ze=n=>J({queryKey:["music-library"],queryFn:()=>P("/api/music/library"),enabled:n,staleTime:me,retry:1}),Xe=n=>J({queryKey:["music-track",n],queryFn:()=>P(`/api/music/track?id=${encodeURIComponent(n||"")}`),enabled:!!n,staleTime:me,retry:1}),et=n=>J({queryKey:["music-playlist",n],queryFn:()=>P(`/api/music/playlist?id=${encodeURIComponent(n||"")}`),enabled:!!n,staleTime:me,retry:1}),tt=n=>J({queryKey:["music-upnext",n],queryFn:()=>P(`/api/music/upnext?id=${encodeURIComponent(n||"")}`),enabled:!!n,staleTime:me,retry:1}),nt=n=>J({queryKey:["music-likes"],queryFn:()=>P("/api/music/likes"),enabled:n,staleTime:120*1e3,retry:1}),it=()=>{const n=Ce();return Le({mutationFn:({id:h,like:g})=>P("/api/music/like",{method:"POST",body:JSON.stringify({id:h,like:g})}),onSuccess:()=>{n.invalidateQueries({queryKey:["music-likes"]}),n.invalidateQueries({queryKey:["music-library"]}),n.invalidateQueries({queryKey:["music-upnext"]})}})},at="_authRow_10tvv_1",st="_btn_10tvv_9",rt="_btnGhost_10tvv_42",ot="_cookieForm_10tvv_74",lt="_cookieInput_10tvv_83",ct="_link_10tvv_99",ut="_contentLayout_10tvv_109",dt="_mainCol_10tvv_116",mt="_upnextAside_10tvv_120",ht="_upnextHeader_10tvv_131",pt="_upnextTitle_10tvv_139",gt="_upnextActions_10tvv_149",yt="_nowPlayingLabel_10tvv_156",ft="_nowPlayingWrap_10tvv_168",bt="_likeBtn_10tvv_172",vt="_likeActive_10tvv_196",y={authRow:at,btn:st,btnGhost:rt,cookieForm:ot,cookieInput:lt,link:ct,contentLayout:ut,mainCol:dt,upnextAside:mt,upnextHeader:ht,upnextTitle:pt,upnextActions:gt,nowPlayingLabel:yt,nowPlayingWrap:ft,likeBtn:bt,likeActive:vt};function $e(n){const h=n.trim();if(!h)return null;const g=h.match(/[?&]v=([A-Za-z0-9_-]{11})/)?.[1]??h.match(/youtu\.be\/([A-Za-z0-9_-]{11})/)?.[1]??null;return g||(/^[A-Za-z0-9_-]{11}$/.test(h)?h:null)}const kt="https://github.com/serifpersia/dango/releases/latest/download/dango-extension.zip",Lt=()=>{const[n,h]=ze(),g=$e(n.get("v")??""),[O,be]=b(""),[ie,Y]=b(""),[W,ae]=b(""),[V,M]=b(!1),[Z,q]=b(()=>localStorage.getItem("musicShuffle")==="true"),[R,he]=b(()=>localStorage.getItem("musicRadio")!=="false"),[l,N]=b(null),[D,I]=b([]),[B,U]=b(null),[se,T]=b({}),H=L([]);H.current=D;const $=L(null);$.current=l;const re=L(!1),j=L(new Set),{data:S}=Ge(),f=Je(),E=We(),A=typeof navigator<"u"?navigator.userAgent:"",K=/android/i.test(A),X=/firefox|fxios/i.test(A)&&!K,G=!X&&!K&&/chrome|chromium|edg|brave/i.test(A),ee=X||G,ve=()=>{if(V)return;M(!0);const t=window.setTimeout(()=>{window.removeEventListener("dango:ytmusic-cookie",s),M(!1),C.error("Helper not detected — install the Dango Helper extension first.")},3e3),s=c=>{window.clearTimeout(t),window.removeEventListener("dango:ytmusic-cookie",s),M(!1);try{const o=JSON.parse(c.detail);if(o?.ok&&o.cookie){const d=o.cookie.replace(/^cookie\s*:\s*/i,"").replace(/^["']+|["']+$/g,"").trim();if(d.includes("…")||d.includes("...")){C.error(`Helper returned a truncated cookie (${d.length} chars) — reload music.youtube.com and Extract again.`);return}ae(d),C.success("Cookie extracted from helper — signing in…"),f.mutate(d)}else o?.error==="NO_COOKIE"?C.error("Helper found no YouTube login — log in on a music.youtube.com tab first."):C.error("Helper request failed — try again.")}catch{C.error("Helper request failed — try again.")}};window.addEventListener("dango:ytmusic-cookie",s),window.dispatchEvent(new CustomEvent("dango:get-ytmusic-cookie"))},{data:oe,isLoading:ke}=Ve(ie),{data:_,isLoading:te}=Ze(S?.authenticated===!0),{data:pe,isLoading:Ne}=et(B?.id??null),{data:le,isLoading:ce}=tt(l?.id??null),{data:ge}=nt(S?.authenticated===!0),ue=it(),Q=z(()=>oe?.tracks||[],[oe]),r=z(()=>_?.playlists||[],[_]),a=z(()=>_?.tracks||[],[_]),k=z(()=>pe?.tracks||[],[pe]),v=z(()=>{const t=le?.tracks||[];return l?t.filter(s=>s.id!==l.id):t},[le,l]),ye=z(()=>new Set(ge?.likedIds||[]),[ge]),Se=t=>se[t.id]??(ye.has(t.id)||t.liked===!0);F(()=>{S?.authenticated!==!0&&T({})},[S?.authenticated]);const Ae=z(()=>[...Q,...k,...a,...v],[Q,k,a,v]),Fe=g&&l?.id!==g&&!Ae.some(t=>t.id===g)?g:null,{data:Me}=Xe(Fe),we=l?.id;F(()=>{document.title=l?`${l.title} - dango`:"Music - dango"},[l]),F(()=>{if(!g){we&&N(null);return}if(we===g)return;const t=o=>{const d=o.find(u=>u.id===g);return d?(N(d),I(u=>u.some(ne=>ne.id===g)?u:o.length>0?o:[d]),!0):!1};if(t(Q)||t(k)||t(a))return;const s=v.find(o=>o.id===g);if(s){N(s),I(o=>o.some(d=>d.id===s.id)?o:[s]);return}const c=Me?.track;c&&c.id===g&&(N(c),I(o=>o.some(d=>d.id===c.id)?o:[c]))},[g,we,Q,k,a,v,Me]);const Te=(t,s)=>{j.current.delete(t.id),N(t),I(s.length>0?s:[t]),h({v:t.id})},Be=t=>{j.current.add(t.id),C.error(`Couldn't play “${t.title}” — skipped.`)},Ee=async()=>{if(re.current)return;const t=H.current,s=t[t.length-1]??$.current;if(s){re.current=!0;try{const c=await P(`/api/music/upnext?id=${encodeURIComponent(s.id)}`),o=new Set(t.map(u=>u.id)),d=(c.tracks||[]).filter(u=>!o.has(u.id)&&!j.current.has(u.id));if(d.length===0){const u=H.current[0];u&&(N(u),h({v:u.id}));return}I([...H.current,...d]),N(d[0]),h({v:d[0].id})}catch{const c=H.current[0];c&&(N(c),h({v:c.id}))}finally{re.current=!1}}},Oe=t=>{const s=H.current,c=$.current;if(!c||s.length===0)return;const o=s.findIndex(x=>x.id===c.id);if(t>0&&R&&(o<0||o===s.length-1)){Ee();return}const d=x=>j.current.has(x.id),u=s.length;let ne;if(Z&&u>1){let x=Math.floor(Math.random()*u);for(let xe=0;xe<u&&(x===o||d(s[x]));xe++)x=Math.floor(Math.random()*u);ne=s[x]}else{const x=t>0?1:-1;let fe=(((o<0?x>0?-1:0:o)+x)%u+u)%u;for(let Re=1;Re<u&&d(s[fe]);Re++)fe=((fe+x)%u+u)%u;ne=s[fe]}N(ne),h({v:ne.id})},qe=()=>{if(R){he(!1),localStorage.setItem("musicRadio","false");return}const t=$.current;if(!t)return;if(v.length===0){C.error("No radio suggestions for this song yet.");return}const s=new Set,c=[];for(const o of[t,...v])s.has(o.id)||(s.add(o.id),c.push(o));he(!0),localStorage.setItem("musicRadio","true"),I(c),C.success(`Radio started — ${v.length} songs queued.`)},_e=t=>{if(S?.authenticated!==!0){C.error("Sign in to like songs.");return}const s=!Se(t);T(c=>({...c,[t.id]:s})),ue.mutate({id:t.id,like:s},{onError:()=>{T(c=>{const o={...c};return delete o[t.id],o}),C.error("Like action failed — try again.")}})},De=()=>{q(t=>(localStorage.setItem("musicShuffle",String(!t)),!t))},Ue=t=>e("button",{className:m.stationRow,onClick:()=>U(t),"aria-label":`Open ${t.title}`,children:[t.thumbnails?.[0]?.url?e("img",{src:t.thumbnails[0].url,alt:"",className:m.stationThumb,loading:"lazy",decoding:"async"}):e("span",{className:m.stationThumbPlaceholder,children:e(p,{name:"headphones"})}),e("span",{className:m.stationMeta,children:[e("span",{className:m.stationName,children:t.title}),t.subtitle&&e("span",{className:m.stationSub,children:t.subtitle})]})]},t.id),de=(t,s,c)=>{const o=l?.id===t.id,d=Se(t),u=[t.album,t.duration].filter(Boolean).join(" · ");return e("div",{className:`${m.stationRow} ${o?m.stationRowActive:""}`,style:{cursor:"default"},children:[e("button",{onClick:()=>c?c(t):Te(t,s),className:m.stationRow,style:{flex:1,minWidth:0,background:"transparent",border:0,padding:0},"aria-label":`Play ${t.title}`,children:[t.thumbnails?.[0]?.url?e("img",{src:t.thumbnails[0].url,alt:"",className:m.stationThumb,loading:"lazy",decoding:"async"}):e("span",{className:m.stationThumbPlaceholder,children:e(p,{name:"headphones"})}),e("span",{className:m.stationMeta,children:[e("span",{className:m.stationName,children:t.title}),e("span",{className:m.stationSub,children:[t.artists,u?` · ${u}`:""]})]})]}),S?.authenticated===!0&&e("button",{className:`${y.likeBtn} ${d?y.likeActive:""}`,onClick:()=>_e(t),disabled:ue.isPending,title:d?"Remove from Liked Music":"Add to Liked Music","aria-label":d?`Remove ${t.title} from Liked Music`:`Add ${t.title} to Liked Music`,"aria-pressed":d,children:e(p,{name:"heart"})}),e("a",{href:`https://music.youtube.com/watch?v=${encodeURIComponent(t.id)}`,target:"_blank",rel:"noreferrer",title:"Open in YouTube Music","aria-label":`Open ${t.title} in YouTube Music`,style:{padding:8,color:"inherit",opacity:.7,flexShrink:0},children:e(p,{name:"external-link-alt"})})]},t.id)},Pe=z(()=>{if(!l)return v;const t=new Set,s=[];for(const c of[l,...v])t.has(c.id)||(t.add(c.id),s.push(c));return s},[l,v]),He=t=>{Te(t,Pe)};return e("div",{className:`${i.page} ${l?i.pageWithPlayer:""}`,children:[e("div",{className:i.header,children:[e("h1",{className:i.pageTitle,children:[e(p,{name:"headphones"})," Music"]}),e("form",{className:i.searchForm,onSubmit:t=>{t.preventDefault();const s=$e(O);s?(Y(""),h({v:s})):Y(O)},children:[e("input",{className:i.searchInput,value:O,onChange:t=>be(t.target.value),placeholder:"Search YouTube Music…","aria-label":"Search music"}),e("button",{className:i.searchBtn,type:"submit","aria-label":"Search",children:e(p,{name:"search"})})]})]}),e("div",{className:y.authRow,children:[S?.authenticated?e(w,{children:[e("span",{className:i.statusMsg,children:["Signed in to YouTube Music",_&&` — ${_.tracks.length} songs, ${_.playlists.length} playlists`]}),e("button",{className:y.btnGhost,onClick:()=>E.mutate(),disabled:E.isPending,children:"Sign out"})]}):e(w,{children:[e("form",{onSubmit:t=>{t.preventDefault();const s=W.replace(/^cookie\s*:\s*/i,"").replace(/^["']+|["']+$/g,"").trim();if(s){if(s.includes("…")||s.includes("...")){C.error("That cookie is truncated (contains …). Right-click the Cookie header → Copy value, not the wrapped preview.");return}s!==W&&ae(s),f.mutate(s)}},className:y.cookieForm,children:[e("input",{className:y.cookieInput,value:W,onChange:t=>ae(t.target.value),placeholder:"Paste music.youtube.com Cookie header…","aria-label":"YouTube Music cookie",type:"password"}),e("button",{className:y.btn,type:"submit",disabled:f.isPending||!W.trim(),children:f.isPending?"Checking…":"Sign in"}),ee&&e("button",{className:y.btnGhost,type:"button",onClick:ve,disabled:V||f.isPending,title:"Read the YouTube Music cookie via the Dango Helper extension (same zip as AnimePahe/JapaneseASMR)",children:V?"Extracting…":"Extract from helper"})]}),ee&&e("p",{className:i.statusMsg,children:["Two ways to sign in: paste the Cookie header manually (steps below), or download"," ",e("a",{href:kt,target:"_blank",rel:"noreferrer",className:y.link,children:"dango-extension.zip"})," ","then"," ",X?e(w,{children:[e("code",{children:"about:debugging"})," → ",e("strong",{children:"This Firefox"})," →"," ",e("strong",{children:"Load Temporary Add-on"})," →"," ",e("code",{children:"dango-extension/firefox/manifest.json"})]}):e(w,{children:[e("code",{children:"chrome://extensions"})," → ",e("strong",{children:"Developer mode"})," →"," ",e("strong",{children:"Load unpacked"})," → ",e("code",{children:"dango-extension/chrome"})]}),", log in on a music.youtube.com tab, and click Extract."]}),e(je,{})]}),f.isSuccess&&e("p",{className:i.statusMsg,children:["Signed in — found ",f.data.tracks," songs and ",f.data.playlists," ","playlists."]}),f.isError&&e("p",{className:i.statusMsg,children:f.error?.message||"Sign-in failed. Paste a fresh Cookie header from music.youtube.com."}),!S?.authenticated&&!f.isError&&!f.isSuccess&&e("p",{className:i.statusMsg,children:"Sign in with your YouTube Music cookie to see your library. Search and playback work without sign-in."})]}),e("div",{className:l?y.contentLayout:void 0,children:[e("div",{className:y.mainCol,children:ie?e(w,{children:[e("h2",{className:m.sectionTitle,children:["Results for “",ie,"”"]}),ke?e("p",{className:i.statusMsg,children:"Searching…"}):Q.length===0?e("p",{className:i.statusMsg,children:"No tracks found."}):e("div",{className:m.stationList,children:Q.map(t=>de(t,Q))})]}):S?.authenticated?e(w,{children:[e("h2",{className:m.sectionTitle,children:"Your library"}),te?e("p",{className:i.statusMsg,children:"Loading library…"}):e(w,{children:[r.length>0&&!B&&e(w,{children:[e("h3",{className:m.sectionTitle,children:"Playlists"}),e("div",{className:m.stationList,children:r.map(t=>Ue(t))})]}),a.length>0&&!B&&e(w,{children:[e("h3",{className:m.sectionTitle,children:["Liked songs (",a.length,")"]}),e("div",{style:{marginBottom:8},children:e("button",{className:y.btnGhost,onClick:()=>Te(a[0],a),"aria-label":"Play all liked songs",children:[e(p,{name:"play"})," Play all"]})}),e("div",{className:m.stationList,children:a.map(t=>de(t,a))})]}),B?e(w,{children:[e("button",{className:y.btnGhost,onClick:()=>U(null),style:{marginBottom:8},children:"← Back to library"}),e("h3",{className:m.sectionTitle,children:B.title}),Ne?e("p",{className:i.statusMsg,children:"Loading playlist…"}):k.length===0?e("p",{className:i.statusMsg,children:"No tracks in this playlist."}):e("div",{className:m.stationList,children:k.map(t=>de(t,k))})]}):null]})]}):e("p",{className:i.statusMsg,children:"Search above, or sign in to see your library."})}),l&&e("aside",{className:y.upnextAside,"aria-label":"Up next",children:[e("div",{className:y.upnextHeader,children:e("span",{className:y.upnextTitle,children:[e(p,{name:"list-ul"})," Up Next"]})}),e("div",{className:y.nowPlayingWrap,children:[e("p",{className:y.nowPlayingLabel,children:"Now playing"}),de(l,D.length>0?D:[l])]}),e("div",{className:y.upnextActions,children:e("button",{className:y.btnGhost,onClick:qe,disabled:!R&&(ce||v.length===0),"aria-pressed":R,title:R?"Stop the radio — the queue will loop instead of fetching more songs":"Queue these suggestions after the current song and keep the mix going",children:R?"Stop radio":"Start radio"})}),ce?e("p",{className:i.statusMsg,children:"Loading suggestions…"}):v.length===0?e("p",{className:i.statusMsg,children:"No suggestions for this song."}):e("div",{className:m.stationList,children:v.map(t=>de(t,Pe,He))})]})]}),l&&e(Ye,{track:l,queue:D,shuffle:Z,onToggleShuffle:De,onTrackStep:Oe,onTrackFailed:Be,onClose:()=>{N(null),h({})},liked:l?Se(l):!1,likeVisible:S?.authenticated===!0,likePending:ue.isPending,onToggleLike:()=>_e(l)})]})};export{Lt as default};
|