@serifpersia/dango 3.3.8 → 3.4.0
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-D4qo4289.js → AnimeCard-CJy1SXH3.js} +1 -1
- package/client/dist/assets/{AnimeInfo-BdRRm4So.js → AnimeInfo-BoXlVvZ-.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DeJNAKMR.js → AnimeInfoPage-BxbA_7AV.js} +2 -2
- package/client/dist/assets/Asmr--Zc8G9zv.js +2 -0
- package/client/dist/assets/AsmrResetProgressModal-CKYm-8cp.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-3yjZS7s6.js +10 -0
- package/client/dist/assets/Home-rrhOGQcg.js +1 -0
- package/client/dist/assets/Insights-Bay90Yeg.js +1 -0
- package/client/dist/assets/{LibraryListPage-D3hTqTRU.js → LibraryListPage-HzuDavbb.js} +1 -1
- package/client/dist/assets/{ListeningList-BgWsfu40.js → ListeningList-DYcQnnsG.js} +1 -1
- package/client/dist/assets/{Manga-Dn-CYins.js → Manga-D8CT9aO0.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-EmQoK7Bv.js → MangaInfoPage-DGK6CYp1.js} +1 -1
- package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-Dian37PQ.js → MangaResetProgressModal-QjZyIAjl.js} +1 -1
- package/client/dist/assets/Mature-CHt_-Hz5.js +1 -0
- package/client/dist/assets/MediaCard-B5584Cq9.js +1 -0
- package/client/dist/assets/Music-HeLrEZ2d.js +1 -0
- package/client/dist/assets/OptionListMenu-CJ90CdwI.js +1 -0
- package/client/dist/assets/Player-BYtEax_z.js +56 -0
- package/client/dist/assets/PlayerSettings-4-cUAY4B.js +1 -0
- package/client/dist/assets/QueueOptionsButton-Ba0dg_82.js +1 -0
- package/client/dist/assets/{QueueRail-B7DeSFoA.js → QueueRail-O7f4RqjB.js} +1 -1
- package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
- package/client/dist/assets/{ReadingList-BT8kUjCB.js → ReadingList-n6rr8ygy.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-DWeiO-hs.js → ResetProgressModal-CcStlbhP.js} +1 -1
- package/client/dist/assets/Search-C2wrWOdy.js +1 -0
- package/client/dist/assets/Settings-CluUYs8U.js +21 -0
- package/client/dist/assets/{SkeletonGrid-B5loz7ii.js → SkeletonGrid-CLEPY9nP.js} +1 -1
- package/client/dist/assets/Trackers-CTGPxs0V.js +2 -0
- package/client/dist/assets/Tv-BwHlDdi8.js +4 -0
- package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
- package/client/dist/assets/TvInfo-CXnrLekr.js +1 -0
- package/client/dist/assets/TvPopup-BekfiNcX.js +1 -0
- package/client/dist/assets/{TvSearch-BYwCf5kO.js → TvSearch-DuqR-z3v.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BiJU6pKT.js → TvWatchlist-CBvgFrA0.js} +1 -1
- package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
- package/client/dist/assets/{hls-C11FOWHj.js → hls-Boje3IOE.js} +1 -1
- package/client/dist/assets/index-BXwhFWB_.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DJ7A4B4T.js +1 -0
- package/client/dist/assets/useAsmr-DIwIsJXF.js +1 -0
- package/client/dist/assets/{useAsmrPopup-cT_Uu41e.js → useAsmrPopup-e4zNN9ig.js} +1 -1
- package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
- package/client/dist/assets/{useMangaPopup-PnmSctHu.js → useMangaPopup-B8nnpvyi.js} +1 -1
- package/client/dist/assets/useProviders-B5Qr4I1y.js +1 -0
- package/client/dist/assets/useTv-Dm8MCYlL.js +1 -0
- package/client/dist/assets/useTvLibrary-B7jEk2Q7.js +1 -0
- package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
- package/client/dist/index.html +2 -2
- package/orchestrator.js +1 -2
- package/package.json +2 -6
- package/server/dist/app-auth.js +9 -29
- package/server/dist/app-hono.js +621 -0
- package/server/dist/config.js +0 -1
- package/server/dist/db/drizzle.js +32 -0
- package/server/dist/db/schema-anime.js +64 -0
- package/server/dist/db/schema-asmr.js +25 -0
- package/server/dist/db/schema-manga.js +31 -0
- package/server/dist/db/schema-tv.js +33 -0
- package/server/dist/db.js +75 -4
- package/server/dist/discord-gateway.js +1 -1
- package/server/dist/google.js +0 -7
- package/server/dist/hono/asmr.js +1 -0
- package/server/dist/hono/auth.js +56 -0
- package/server/dist/hono/music.js +419 -0
- package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
- package/server/dist/hono/tv.js +197 -0
- package/server/dist/hono/watchlist.js +733 -0
- package/server/dist/lib/anilist.js +3 -3
- package/server/dist/lib/insights.js +185 -0
- package/server/dist/lib/ipc.js +1 -2
- package/server/dist/lib/kitsu.js +1 -28
- package/server/dist/lib/mal-import.js +291 -0
- package/server/dist/lib/migration.js +20 -34
- package/server/dist/lib/offline-db.js +36 -36
- package/server/dist/lib/tracker/manga-sync.service.js +21 -21
- package/server/dist/lib/tracker/sync.service.js +20 -20
- package/server/dist/lib/translate.js +79 -0
- package/server/dist/lib/ytmusic.js +15 -4
- package/server/dist/providers/title-matching.js +0 -1
- package/server/dist/repositories/asmr.repository.js +90 -87
- package/server/dist/repositories/insights.repository.js +24 -13
- package/server/dist/repositories/library.repository.js +33 -93
- package/server/dist/repositories/manga.repository.js +108 -116
- package/server/dist/repositories/notifications.repository.js +50 -31
- package/server/dist/repositories/queue.repository.js +36 -25
- package/server/dist/repositories/settings.repository.js +18 -8
- package/server/dist/repositories/shows-meta.repository.js +54 -41
- package/server/dist/repositories/temp-show-ids.repository.js +43 -31
- package/server/dist/repositories/tv.repository.js +98 -115
- package/server/dist/repositories/watched-episodes.repository.js +52 -20
- package/server/dist/repositories/watchlist.repository.js +36 -48
- package/server/dist/server.js +59 -151
- package/server/dist/sync.js +41 -4
- package/server/dist/trpc/context.js +7 -0
- package/server/dist/trpc/errors.js +21 -0
- package/server/dist/trpc/index.js +19 -0
- package/server/dist/trpc/router.js +49 -0
- package/server/dist/trpc/routers/asmr-library.js +122 -0
- package/server/dist/trpc/routers/asmr-progress.js +133 -0
- package/server/dist/trpc/routers/asmr.js +87 -0
- package/server/dist/trpc/routers/auth.js +247 -0
- package/server/dist/trpc/routers/continue-watching.js +83 -0
- package/server/dist/trpc/routers/data.js +1039 -0
- package/server/dist/trpc/routers/discord.js +288 -0
- package/server/dist/trpc/routers/discovery.js +15 -0
- package/server/dist/trpc/routers/insights.js +45 -0
- package/server/dist/trpc/routers/manga-library.js +280 -0
- package/server/dist/trpc/routers/manga-progress.js +181 -0
- package/server/dist/trpc/routers/manga.js +201 -0
- package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +235 -295
- package/server/dist/trpc/routers/notifications.js +80 -0
- package/server/dist/trpc/routers/progress.js +15 -0
- package/server/dist/trpc/routers/queue.js +95 -0
- package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
- package/server/dist/trpc/routers/settings.js +105 -0
- package/server/dist/trpc/routers/tracker.js +219 -0
- package/server/dist/trpc/routers/tv-library.js +145 -0
- package/server/dist/trpc/routers/tv-progress.js +171 -0
- package/server/dist/trpc/routers/tv.js +565 -0
- package/server/dist/trpc/routers/watchlist.js +564 -0
- package/server/dist/trpc/validation.js +159 -0
- package/server/dist/utils/security.utils.js +0 -18
- package/server/package.json +5 -10
- package/client/dist/assets/Asmr-D0EGRYsv.js +0 -2
- package/client/dist/assets/AsmrResetProgressModal-Gs0GmVaT.js +0 -1
- package/client/dist/assets/AvSyncCalibrator-D8hqarVB.js +0 -10
- package/client/dist/assets/Home-BnBzjFkW.js +0 -1
- package/client/dist/assets/Insights-i9QgY3nE.js +0 -1
- package/client/dist/assets/MangaReadPage-xJB8K2YU.js +0 -1
- package/client/dist/assets/Mature-DQtXKOXc.js +0 -1
- package/client/dist/assets/MediaCard-DpKoY-1j.js +0 -1
- package/client/dist/assets/Music-BP43Zqmq.js +0 -1
- package/client/dist/assets/OptionListMenu-Bj9ebsW3.js +0 -1
- package/client/dist/assets/Player-O2Cy2GF1.js +0 -56
- package/client/dist/assets/PlayerSettings-DYZ272Yp.js +0 -1
- package/client/dist/assets/QueueOptionsButton-lv26-6jp.js +0 -1
- package/client/dist/assets/Radio-qQBYnVfY.js +0 -1
- package/client/dist/assets/Search-BkvPYN33.js +0 -1
- package/client/dist/assets/Settings-CyZEBF6S.js +0 -21
- package/client/dist/assets/Trackers-JK-aXBYC.js +0 -2
- package/client/dist/assets/Tv-BwXWJxMy.js +0 -4
- package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
- package/client/dist/assets/TvInfo-Dzyq6ucH.js +0 -1
- package/client/dist/assets/TvPopup-PlCU30FO.js +0 -1
- package/client/dist/assets/Watchlist-BOEeGXH9.js +0 -1
- package/client/dist/assets/index-CtoKRNLc.js +0 -178
- package/client/dist/assets/useAnimeInfoData-kr3WOIWH.js +0 -1
- package/client/dist/assets/useAsmr-CB9THf0m.js +0 -1
- package/client/dist/assets/useMangaLibrary-rM6z3_0E.js +0 -1
- package/client/dist/assets/useProviders-B3VB7A9M.js +0 -1
- package/client/dist/assets/useTv-BTUKMH7Q.js +0 -1
- package/client/dist/assets/useTvLibrary-C0sX-ilZ.js +0 -1
- package/server/dist/controllers/asmr-library.controller.js +0 -288
- package/server/dist/controllers/auth.controller.js +0 -225
- package/server/dist/controllers/data.controller.js +0 -767
- package/server/dist/controllers/insights.controller.js +0 -203
- package/server/dist/controllers/manga-library.controller.js +0 -350
- package/server/dist/controllers/settings.controller.js +0 -609
- package/server/dist/controllers/tv-library.controller.js +0 -314
- package/server/dist/controllers/watchlist.controller.js +0 -1069
- package/server/dist/request-logger.js +0 -16
- package/server/dist/routes/asmr-library.routes.js +0 -23
- package/server/dist/routes/asmr.routes.js +0 -81
- package/server/dist/routes/auth.routes.js +0 -25
- package/server/dist/routes/data.routes.js +0 -47
- package/server/dist/routes/discord-gateway.routes.js +0 -51
- package/server/dist/routes/insights.routes.js +0 -35
- package/server/dist/routes/lan-auth.routes.js +0 -46
- package/server/dist/routes/manga-library.routes.js +0 -25
- package/server/dist/routes/manga.routes.js +0 -141
- package/server/dist/routes/music.routes.js +0 -20
- package/server/dist/routes/providers.routes.js +0 -17
- package/server/dist/routes/proxy.routes.js +0 -12
- package/server/dist/routes/settings.routes.js +0 -24
- package/server/dist/routes/tracker.routes.js +0 -287
- package/server/dist/routes/translate.routes.js +0 -88
- package/server/dist/routes/tv-library.routes.js +0 -23
- package/server/dist/routes/tv.routes.js +0 -629
- package/server/dist/routes/watchlist.routes.js +0 -341
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { protectedProcedure, router } from '../index.js';
|
|
2
|
+
import { performAsmrWriteTransactionAsync } from '../../sync.js';
|
|
3
|
+
import { dbAll } from '../../utils/db-utils.js';
|
|
4
|
+
import { SettingsRepository } from '../../repositories/settings.repository.js';
|
|
5
|
+
import { AsmrLibraryRepository, AsmrProgressRepository, } from '../../repositories/asmr.repository.js';
|
|
6
|
+
import { defineSchema, reqObj, reqStr, reqStrArray } from '../validation.js';
|
|
7
|
+
async function getAdultNonListWorkIds(db) {
|
|
8
|
+
const rows = await dbAll(db, `SELECT DISTINCT p.workId as workId
|
|
9
|
+
FROM asmr_progress p
|
|
10
|
+
LEFT JOIN asmr_library l ON l.id = p.workId
|
|
11
|
+
WHERE COALESCE(l.isAdult, p.isAdult) = 1
|
|
12
|
+
AND l.id IS NULL`);
|
|
13
|
+
return rows.map((r) => r.workId);
|
|
14
|
+
}
|
|
15
|
+
const asmrProgressIdsInput = () => defineSchema((value) => ({
|
|
16
|
+
ids: reqStrArray(reqObj(value), 'ids', true),
|
|
17
|
+
}));
|
|
18
|
+
const asmrProgressWorkInput = () => defineSchema((value) => ({
|
|
19
|
+
workId: reqStr(reqObj(value), 'workId'),
|
|
20
|
+
}));
|
|
21
|
+
const asmrProgressSaveInput = () => defineSchema((value) => {
|
|
22
|
+
const obj = reqObj(value);
|
|
23
|
+
return {
|
|
24
|
+
workId: reqStr(obj, 'workId'),
|
|
25
|
+
trackIndex: obj.trackIndex,
|
|
26
|
+
trackLabel: obj.trackLabel,
|
|
27
|
+
currentTime: obj.currentTime,
|
|
28
|
+
duration: obj.duration,
|
|
29
|
+
title: obj.title ?? null,
|
|
30
|
+
thumbnail: obj.thumbnail ?? null,
|
|
31
|
+
rjCode: obj.rjCode ?? null,
|
|
32
|
+
isAdult: obj.isAdult ?? null,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
const asmrProgressRemoveInput = () => defineSchema((value) => {
|
|
36
|
+
const obj = reqObj(value);
|
|
37
|
+
return { workId: reqStr(obj, 'workId'), trackIndex: obj.trackIndex ?? undefined };
|
|
38
|
+
});
|
|
39
|
+
const asmrContinueListeningInput = () => defineSchema((value) => {
|
|
40
|
+
const obj = reqObj(value);
|
|
41
|
+
const out = {};
|
|
42
|
+
if (obj.limit !== undefined && obj.limit !== null && obj.limit !== '') {
|
|
43
|
+
const limit = Number(obj.limit);
|
|
44
|
+
if (Number.isFinite(limit))
|
|
45
|
+
out.limit = limit;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
});
|
|
49
|
+
export const asmrProgressRouter = router({
|
|
50
|
+
getByWork: protectedProcedure.input(asmrProgressWorkInput()).query(async ({ ctx, input }) => {
|
|
51
|
+
const rows = await AsmrProgressRepository.getByWork(ctx.asmrDb, input.workId);
|
|
52
|
+
return { progress: rows };
|
|
53
|
+
}),
|
|
54
|
+
latest: protectedProcedure.input(asmrProgressWorkInput()).query(async ({ ctx, input }) => {
|
|
55
|
+
const row = await AsmrProgressRepository.getLatest(ctx.asmrDb, input.workId);
|
|
56
|
+
return row || { currentTime: 0, duration: 0 };
|
|
57
|
+
}),
|
|
58
|
+
save: protectedProcedure.input(asmrProgressSaveInput()).mutation(async ({ ctx, input }) => {
|
|
59
|
+
const trackIndexNum = Math.max(Math.round(Number(input.trackIndex)) || 0, 0);
|
|
60
|
+
const timeNum = Math.max(Number(input.currentTime) || 0, 0);
|
|
61
|
+
const durationNum = Math.max(Number(input.duration) || 0, 0);
|
|
62
|
+
await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
|
|
63
|
+
await AsmrProgressRepository.upsert(tx, {
|
|
64
|
+
workId: String(input.workId),
|
|
65
|
+
trackIndex: trackIndexNum,
|
|
66
|
+
trackLabel: String(input.trackLabel || ''),
|
|
67
|
+
currentTime: timeNum,
|
|
68
|
+
duration: durationNum,
|
|
69
|
+
title: (input.title ?? null),
|
|
70
|
+
thumbnail: (input.thumbnail ?? null),
|
|
71
|
+
rjCode: (input.rjCode ?? null),
|
|
72
|
+
isAdult: input.isAdult != null ? Number(input.isAdult) : null,
|
|
73
|
+
});
|
|
74
|
+
await AsmrLibraryRepository.touchProgress(tx, String(input.workId), {
|
|
75
|
+
trackIndex: trackIndexNum,
|
|
76
|
+
trackLabel: String(input.trackLabel || ''),
|
|
77
|
+
position: timeNum,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
return { success: true };
|
|
81
|
+
}),
|
|
82
|
+
remove: protectedProcedure.input(asmrProgressRemoveInput()).mutation(async ({ ctx, input }) => {
|
|
83
|
+
await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
|
|
84
|
+
if (input.trackIndex !== undefined && input.trackIndex !== null) {
|
|
85
|
+
await AsmrProgressRepository.deleteTrack(tx, String(input.workId), Number(input.trackIndex));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
await AsmrProgressRepository.deleteByWork(tx, String(input.workId));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return { success: true };
|
|
92
|
+
}),
|
|
93
|
+
removeMany: protectedProcedure.input(asmrProgressIdsInput()).mutation(async ({ ctx, input }) => {
|
|
94
|
+
const ids = input.ids.map((id) => String(id));
|
|
95
|
+
await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
|
|
96
|
+
await AsmrProgressRepository.deleteMany(tx, ids);
|
|
97
|
+
});
|
|
98
|
+
return { success: true, removed: ids.length };
|
|
99
|
+
}),
|
|
100
|
+
continueListening: protectedProcedure
|
|
101
|
+
.input(asmrContinueListeningInput())
|
|
102
|
+
.query(async ({ ctx, input }) => {
|
|
103
|
+
const limit = Math.min(Math.max(parseInt(String(input.limit ?? 24)) || 24, 1), 100);
|
|
104
|
+
const ignoreAdultRow = await SettingsRepository.getByKey(ctx.db, 'asmrIgnoreAdultContent');
|
|
105
|
+
const ignoreAdult = ignoreAdultRow ? ignoreAdultRow.value !== 'false' : true;
|
|
106
|
+
const listOnlyRow = await SettingsRepository.getByKey(ctx.db, 'asmrCwWatchlistOnly');
|
|
107
|
+
const listOnly = listOnlyRow
|
|
108
|
+
? listOnlyRow.value === 'true' || listOnlyRow.value === '1'
|
|
109
|
+
: false;
|
|
110
|
+
const rows = (await AsmrProgressRepository.getContinueListening(ctx.asmrDb, limit)).filter((row) => {
|
|
111
|
+
if (ignoreAdult && row.isAdult === 1)
|
|
112
|
+
return false;
|
|
113
|
+
if (listOnly && row.watchlistStatus !== 'Listening')
|
|
114
|
+
return false;
|
|
115
|
+
return true;
|
|
116
|
+
});
|
|
117
|
+
return { data: rows, total: rows.length };
|
|
118
|
+
}),
|
|
119
|
+
adultCount: protectedProcedure.query(async ({ ctx }) => {
|
|
120
|
+
const ids = await getAdultNonListWorkIds(ctx.asmrDb);
|
|
121
|
+
return { count: ids.length };
|
|
122
|
+
}),
|
|
123
|
+
purgeAdult: protectedProcedure.mutation(async ({ ctx }) => {
|
|
124
|
+
const ids = await getAdultNonListWorkIds(ctx.asmrDb);
|
|
125
|
+
if (ids.length > 0) {
|
|
126
|
+
await performAsmrWriteTransactionAsync(ctx.asmrDb, async (tx) => {
|
|
127
|
+
for (const id of ids)
|
|
128
|
+
await AsmrProgressRepository.deleteByWork(tx, id);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return { success: true, removed: ids.length };
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { protectedProcedure, router } from '../index.js';
|
|
2
|
+
import { defineSchema, optStr, reqObj, reqStr } from '../validation.js';
|
|
3
|
+
import { authRequiredError } from '../errors.js';
|
|
4
|
+
import logger from '../../logger.js';
|
|
5
|
+
async function cached(ctx, key, ttl, produce) {
|
|
6
|
+
const hit = ctx.apiCache.get(key);
|
|
7
|
+
if (hit)
|
|
8
|
+
return hit;
|
|
9
|
+
const body = await produce();
|
|
10
|
+
if (ttl !== undefined)
|
|
11
|
+
ctx.apiCache.set(key, body, ttl);
|
|
12
|
+
else
|
|
13
|
+
ctx.apiCache.set(key, body);
|
|
14
|
+
return body;
|
|
15
|
+
}
|
|
16
|
+
const asmrBrowseInput = () => defineSchema((value) => {
|
|
17
|
+
if (value === undefined || value === null)
|
|
18
|
+
return {};
|
|
19
|
+
const obj = reqObj(value);
|
|
20
|
+
const out = {};
|
|
21
|
+
const q = optStr(obj, 'q');
|
|
22
|
+
if (q !== undefined)
|
|
23
|
+
out.q = q;
|
|
24
|
+
if (obj.page !== undefined)
|
|
25
|
+
out.page = parseInt(String(obj.page), 10) || 1;
|
|
26
|
+
const sort = optStr(obj, 'sort');
|
|
27
|
+
if (sort !== undefined)
|
|
28
|
+
out.sort = sort;
|
|
29
|
+
const rating = optStr(obj, 'rating');
|
|
30
|
+
if (rating !== undefined)
|
|
31
|
+
out.rating = rating;
|
|
32
|
+
return out;
|
|
33
|
+
});
|
|
34
|
+
const asmrWorkInput = () => defineSchema((value) => ({ rj: reqStr(reqObj(value), 'rj') }));
|
|
35
|
+
export const asmrRouter = router({
|
|
36
|
+
browse: protectedProcedure.input(asmrBrowseInput()).query(async ({ ctx, input }) => {
|
|
37
|
+
const q = input.q || '';
|
|
38
|
+
const page = input.page || 1;
|
|
39
|
+
const sort = input.sort || '';
|
|
40
|
+
const rating = input.rating || '';
|
|
41
|
+
return cached(ctx, `route-asmr-browse-${q}-${page}-${sort}-${rating}-${sort === 'random' ? Date.now() : ''}`, 300, async () => {
|
|
42
|
+
try {
|
|
43
|
+
const provider = ctx.getJasmr();
|
|
44
|
+
if (!provider)
|
|
45
|
+
return { shows: [], hasNext: false };
|
|
46
|
+
return await provider.browse({ query: q, page, sort, rating });
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err.message === 'AUTH_REQUIRED')
|
|
50
|
+
throw authRequiredError('jasmr');
|
|
51
|
+
logger.error({ err }, '[Asmr] browse failed');
|
|
52
|
+
return { shows: [], hasNext: false };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}),
|
|
56
|
+
work: protectedProcedure.input(asmrWorkInput()).query(async ({ ctx, input }) => {
|
|
57
|
+
const rj = input.rj;
|
|
58
|
+
return cached(ctx, `route-asmr-work-${rj}`, 1800, async () => {
|
|
59
|
+
try {
|
|
60
|
+
const provider = ctx.getJasmr();
|
|
61
|
+
if (!provider) {
|
|
62
|
+
return { rjCode: rj, description: '', tracks: [], images: [], chapters: [] };
|
|
63
|
+
}
|
|
64
|
+
const rjCode = String(rj).trim().toUpperCase();
|
|
65
|
+
const episodes = await provider.getEpisodes(rjCode);
|
|
66
|
+
const [streams, images, chapters] = await Promise.all([
|
|
67
|
+
provider.getStreamUrls(rjCode, '1'),
|
|
68
|
+
provider.getImages(rjCode),
|
|
69
|
+
provider.getChapters(rjCode),
|
|
70
|
+
]);
|
|
71
|
+
return {
|
|
72
|
+
rjCode,
|
|
73
|
+
description: episodes?.description || '',
|
|
74
|
+
tracks: streams?.[0]?.links || [],
|
|
75
|
+
images,
|
|
76
|
+
chapters,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err.message === 'AUTH_REQUIRED')
|
|
81
|
+
throw authRequiredError('jasmr');
|
|
82
|
+
logger.error({ err, rj }, '[Asmr] work fetch failed');
|
|
83
|
+
return { rjCode: rj, description: '', tracks: [], images: [], chapters: [] };
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import logger from '../../logger.js';
|
|
3
|
+
import { googleDriveService } from '../../google.js';
|
|
4
|
+
import { githubSyncService } from '../../github-sync.js';
|
|
5
|
+
import { initSyncProvider, getActiveProvider, syncDownOnBoot, syncUp, mangaSyncDownOnBoot, mangaSyncUp, tvSyncDownOnBoot, tvSyncUp, asmrSyncDownOnBoot, asmrSyncUp, initializeDatabase, } from '../../sync.js';
|
|
6
|
+
import { CONFIG } from '../../config.js';
|
|
7
|
+
import { rcloneService } from '../../rclone.js';
|
|
8
|
+
import { updateEnvFile } from '../../utils/env.utils.js';
|
|
9
|
+
import { protectedProcedure, router } from '../index.js';
|
|
10
|
+
import { defineSchema, reqObj } from '../validation.js';
|
|
11
|
+
async function runSyncFromCtx(ctx, database, preferredProvider) {
|
|
12
|
+
const dbName = CONFIG.IS_DEV ? CONFIG.DB_NAME_DEV : CONFIG.DB_NAME_PROD;
|
|
13
|
+
const dbPath = path.join(CONFIG.ROOT, dbName);
|
|
14
|
+
const remoteFolder = CONFIG.IS_DEV ? CONFIG.REMOTE_FOLDER_DEV : CONFIG.REMOTE_FOLDER_PROD;
|
|
15
|
+
await initSyncProvider(preferredProvider);
|
|
16
|
+
if (getActiveProvider() === 'github' && githubSyncService.isAuthenticated()) {
|
|
17
|
+
try {
|
|
18
|
+
await githubSyncService.migrateFromAniWebSync();
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
logger.error({ err }, 'GitHub sync migration from ani-web failed');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const didDownload = await syncDownOnBoot(database, dbPath, remoteFolder, () => {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
if (database && !database.isClosedCheck()) {
|
|
27
|
+
database.checkpoint();
|
|
28
|
+
database.close(() => resolve());
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
resolve();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
let currentDb = database;
|
|
36
|
+
if (didDownload) {
|
|
37
|
+
currentDb = await initializeDatabase(dbPath);
|
|
38
|
+
logger.info('Database re-initialized after sync.');
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await syncUp(currentDb, dbPath, remoteFolder);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
logger.error({ err }, 'Sync up on boot failed');
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await mangaSyncDownOnBoot(ctx.mangaDb, remoteFolder);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
logger.error({ err }, 'Manga sync down on boot failed');
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await mangaSyncUp(ctx.mangaDb, remoteFolder);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
logger.error({ err }, 'Manga sync up on boot failed');
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await tvSyncDownOnBoot(ctx.tvDb, remoteFolder);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logger.error({ err }, 'TV sync down on boot failed');
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await tvSyncUp(ctx.tvDb, remoteFolder);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
logger.error({ err }, 'TV sync up on boot failed');
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await asmrSyncDownOnBoot(ctx.asmrDb, remoteFolder);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
logger.error({ err }, 'ASMR sync down on boot failed');
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await asmrSyncUp(ctx.asmrDb, remoteFolder);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
logger.error({ err }, 'ASMR sync up on boot failed');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const googleAuthSaveInput = () => defineSchema((value) => {
|
|
84
|
+
const obj = reqObj(value);
|
|
85
|
+
return { clientId: obj.clientId, clientSecret: obj.clientSecret, workerUrl: obj.workerUrl };
|
|
86
|
+
});
|
|
87
|
+
const githubAuthSaveInput = () => defineSchema((value) => {
|
|
88
|
+
const obj = reqObj(value);
|
|
89
|
+
if (typeof obj.clientId !== 'string')
|
|
90
|
+
throw new Error('clientId required');
|
|
91
|
+
return { clientId: obj.clientId };
|
|
92
|
+
});
|
|
93
|
+
const syncProviderInput = () => defineSchema((value) => ({
|
|
94
|
+
provider: reqObj(value).provider,
|
|
95
|
+
}));
|
|
96
|
+
const rcloneSaveInput = () => defineSchema((value) => ({
|
|
97
|
+
remote: reqObj(value).remote,
|
|
98
|
+
}));
|
|
99
|
+
export const authRouter = router({
|
|
100
|
+
configStatus: protectedProcedure.query(async () => {
|
|
101
|
+
const useWorker = !!CONFIG.GOOGLE_AUTH_WORKER_URL;
|
|
102
|
+
const hasLegacyConfig = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET;
|
|
103
|
+
return { hasConfig: useWorker || hasLegacyConfig, useWorker };
|
|
104
|
+
}),
|
|
105
|
+
googleAuth: protectedProcedure.query(async () => {
|
|
106
|
+
return {
|
|
107
|
+
useWorker: !!CONFIG.GOOGLE_AUTH_WORKER_URL,
|
|
108
|
+
hasCustomWorkerUrl: !!process.env.GOOGLE_AUTH_WORKER_URL,
|
|
109
|
+
hasCustomClientId: !!process.env.GOOGLE_CLIENT_ID,
|
|
110
|
+
hasClientSecret: !!process.env.GOOGLE_CLIENT_SECRET,
|
|
111
|
+
};
|
|
112
|
+
}),
|
|
113
|
+
saveGoogleAuth: protectedProcedure.input(googleAuthSaveInput()).mutation(async ({ input }) => {
|
|
114
|
+
const { clientId, clientSecret, workerUrl } = input;
|
|
115
|
+
const updates = {};
|
|
116
|
+
if (typeof clientId === 'string') {
|
|
117
|
+
updates.GOOGLE_CLIENT_ID = clientId;
|
|
118
|
+
}
|
|
119
|
+
if (typeof clientSecret === 'string') {
|
|
120
|
+
updates.GOOGLE_CLIENT_SECRET = clientSecret;
|
|
121
|
+
}
|
|
122
|
+
if (typeof workerUrl === 'string') {
|
|
123
|
+
updates.GOOGLE_AUTH_WORKER_URL = workerUrl;
|
|
124
|
+
}
|
|
125
|
+
await updateEnvFile(updates);
|
|
126
|
+
if (typeof workerUrl === 'string') {
|
|
127
|
+
;
|
|
128
|
+
CONFIG.GOOGLE_AUTH_WORKER_URL = workerUrl;
|
|
129
|
+
}
|
|
130
|
+
if (typeof clientId === 'string') {
|
|
131
|
+
;
|
|
132
|
+
CONFIG.GOOGLE_CLIENT_ID = clientId || undefined;
|
|
133
|
+
}
|
|
134
|
+
if (typeof clientSecret === 'string') {
|
|
135
|
+
;
|
|
136
|
+
CONFIG.GOOGLE_CLIENT_SECRET =
|
|
137
|
+
clientSecret || undefined;
|
|
138
|
+
}
|
|
139
|
+
await initSyncProvider();
|
|
140
|
+
return { success: true };
|
|
141
|
+
}),
|
|
142
|
+
githubAuth: protectedProcedure.query(async () => {
|
|
143
|
+
return { hasCustomClientId: !!process.env.GITHUB_CLIENT_ID };
|
|
144
|
+
}),
|
|
145
|
+
saveGithubAuth: protectedProcedure.input(githubAuthSaveInput()).mutation(async ({ input }) => {
|
|
146
|
+
await updateEnvFile({ GITHUB_CLIENT_ID: input.clientId });
|
|
147
|
+
return { success: true };
|
|
148
|
+
}),
|
|
149
|
+
rcloneSettings: protectedProcedure.query(async () => {
|
|
150
|
+
const remotes = await rcloneService.listRemotes();
|
|
151
|
+
return {
|
|
152
|
+
remote: CONFIG.RCLONE_REMOTE || '',
|
|
153
|
+
availableRemotes: remotes,
|
|
154
|
+
activeRemote: rcloneService.isActive() ? rcloneService.getRemoteName() : null,
|
|
155
|
+
};
|
|
156
|
+
}),
|
|
157
|
+
syncSettings: protectedProcedure.query(async () => {
|
|
158
|
+
return {
|
|
159
|
+
activeProvider: process.env.SYNC_PROVIDER || 'default',
|
|
160
|
+
actualActiveProvider: getActiveProvider(),
|
|
161
|
+
authenticatedProviders: {
|
|
162
|
+
github: githubSyncService.isAuthenticated(),
|
|
163
|
+
google: googleDriveService.isAuthenticated(),
|
|
164
|
+
rclone: rcloneService.isActive(),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}),
|
|
168
|
+
saveSyncProvider: protectedProcedure.input(syncProviderInput()).mutation(async ({ input }) => {
|
|
169
|
+
const { provider } = input;
|
|
170
|
+
const value = provider === 'default' ? '' : provider;
|
|
171
|
+
await updateEnvFile({ SYNC_PROVIDER: value });
|
|
172
|
+
await initSyncProvider();
|
|
173
|
+
return { success: true, activeProvider: process.env.SYNC_PROVIDER || 'default' };
|
|
174
|
+
}),
|
|
175
|
+
githubStatus: protectedProcedure.query(async () => {
|
|
176
|
+
try {
|
|
177
|
+
const user = await githubSyncService.getUserProfile();
|
|
178
|
+
return {
|
|
179
|
+
authenticated: !!user,
|
|
180
|
+
user,
|
|
181
|
+
device: githubSyncService.getDeviceState(),
|
|
182
|
+
hasCustomClientId: !!process.env.GITHUB_CLIENT_ID,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
logger.error({ err: error }, 'Failed to fetch GitHub auth status');
|
|
187
|
+
return {
|
|
188
|
+
authenticated: false,
|
|
189
|
+
user: null,
|
|
190
|
+
device: githubSyncService.getDeviceState(),
|
|
191
|
+
hasCustomClientId: !!process.env.GITHUB_CLIENT_ID,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}),
|
|
195
|
+
githubStart: protectedProcedure.mutation(async ({ ctx }) => {
|
|
196
|
+
const state = await githubSyncService.startDeviceAuth(ctx.db, (db, provider) => runSyncFromCtx(ctx, db, provider));
|
|
197
|
+
return state;
|
|
198
|
+
}),
|
|
199
|
+
githubPoll: protectedProcedure.query(async () => {
|
|
200
|
+
return githubSyncService.getDeviceState();
|
|
201
|
+
}),
|
|
202
|
+
githubLogout: protectedProcedure.mutation(async () => {
|
|
203
|
+
await githubSyncService.logout();
|
|
204
|
+
await updateEnvFile({ SYNC_PROVIDER: '' });
|
|
205
|
+
await initSyncProvider();
|
|
206
|
+
return { success: true };
|
|
207
|
+
}),
|
|
208
|
+
saveRclone: protectedProcedure.input(rcloneSaveInput()).mutation(async ({ ctx, input }) => {
|
|
209
|
+
const { remote } = input;
|
|
210
|
+
await updateEnvFile({
|
|
211
|
+
RCLONE_REMOTE: remote,
|
|
212
|
+
SYNC_PROVIDER: 'rclone',
|
|
213
|
+
});
|
|
214
|
+
await runSyncFromCtx(ctx, ctx.db, 'rclone');
|
|
215
|
+
return { success: true };
|
|
216
|
+
}),
|
|
217
|
+
googleAuthUrl: protectedProcedure.query(async () => {
|
|
218
|
+
const url = await googleDriveService.getAuthUrl();
|
|
219
|
+
return { url };
|
|
220
|
+
}),
|
|
221
|
+
googleLogin: protectedProcedure.mutation(async ({ ctx }) => {
|
|
222
|
+
if (googleDriveService.isAuthenticated()) {
|
|
223
|
+
const user = await googleDriveService.getUserProfile();
|
|
224
|
+
if (user) {
|
|
225
|
+
await updateEnvFile({ SYNC_PROVIDER: 'google' });
|
|
226
|
+
await runSyncFromCtx(ctx, ctx.db, 'google');
|
|
227
|
+
return { url: null, authenticated: true };
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
logger.warn('Google tokens found but invalid. Clearing and requesting new auth.');
|
|
231
|
+
await googleDriveService.logout();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const url = await googleDriveService.getAuthUrl();
|
|
235
|
+
return { url, authenticated: false };
|
|
236
|
+
}),
|
|
237
|
+
user: protectedProcedure.query(async () => {
|
|
238
|
+
const user = await googleDriveService.getUserProfile();
|
|
239
|
+
return user;
|
|
240
|
+
}),
|
|
241
|
+
logout: protectedProcedure.mutation(async () => {
|
|
242
|
+
await googleDriveService.logout();
|
|
243
|
+
await updateEnvFile({ SYNC_PROVIDER: '' });
|
|
244
|
+
await initSyncProvider();
|
|
245
|
+
return { success: true };
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { protectedProcedure, router } from '../index.js';
|
|
2
|
+
import { performWriteTransactionAsync } from '../../sync.js';
|
|
3
|
+
import { WatchedEpisodesRepository } from '../../repositories/watched-episodes.repository.js';
|
|
4
|
+
import { NotificationsRepository } from '../../repositories/notifications.repository.js';
|
|
5
|
+
import { getMigratedId } from '../../lib/migration.js';
|
|
6
|
+
import { getAdultNonWatchlistShowIds } from '../../hono/watchlist.js';
|
|
7
|
+
import { dbAll } from '../../utils/db-utils.js';
|
|
8
|
+
import { idsInput, showIdInput } from '../validation.js';
|
|
9
|
+
export const continueWatchingRouter = router({
|
|
10
|
+
remove: protectedProcedure.input(showIdInput()).mutation(async ({ ctx, input }) => {
|
|
11
|
+
const showId = await getMigratedId(ctx.db, input.showId);
|
|
12
|
+
await performWriteTransactionAsync(ctx.db, async (tx) => {
|
|
13
|
+
await WatchedEpisodesRepository.deleteByShow(tx, showId);
|
|
14
|
+
await NotificationsRepository.deleteByShow(tx, showId);
|
|
15
|
+
});
|
|
16
|
+
return { success: true };
|
|
17
|
+
}),
|
|
18
|
+
removeMany: protectedProcedure.input(idsInput()).mutation(async ({ ctx, input }) => {
|
|
19
|
+
const ids = await Promise.all(input.ids.map((id) => getMigratedId(ctx.db, id)));
|
|
20
|
+
await performWriteTransactionAsync(ctx.db, async (tx) => {
|
|
21
|
+
for (const id of ids) {
|
|
22
|
+
await WatchedEpisodesRepository.deleteByShow(tx, id);
|
|
23
|
+
await NotificationsRepository.deleteByShow(tx, id);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
ctx.db.scheduleSave();
|
|
27
|
+
return { success: true, removed: ids.length };
|
|
28
|
+
}),
|
|
29
|
+
adultCount: protectedProcedure.query(async ({ ctx }) => {
|
|
30
|
+
const ids = await getAdultNonWatchlistShowIds(ctx.db);
|
|
31
|
+
return { count: ids.length };
|
|
32
|
+
}),
|
|
33
|
+
purgeAdult: protectedProcedure.mutation(async ({ ctx }) => {
|
|
34
|
+
const ids = await getAdultNonWatchlistShowIds(ctx.db);
|
|
35
|
+
if (ids.length > 0) {
|
|
36
|
+
await performWriteTransactionAsync(ctx.db, async (tx) => {
|
|
37
|
+
for (const id of ids) {
|
|
38
|
+
await WatchedEpisodesRepository.deleteByShow(tx, id);
|
|
39
|
+
await NotificationsRepository.deleteByShow(tx, id);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
ctx.db.scheduleSave();
|
|
43
|
+
}
|
|
44
|
+
return { success: true, removed: ids.length };
|
|
45
|
+
}),
|
|
46
|
+
thisWeek: protectedProcedure.query(async ({ ctx }) => {
|
|
47
|
+
const rows = await dbAll(ctx.db, `SELECT
|
|
48
|
+
w.id, w.name, w.thumbnail, w.nativeName, w.englishName, w.type,
|
|
49
|
+
dn.episodeNumber, dn.discoveredAt
|
|
50
|
+
FROM discovered_notifications dn
|
|
51
|
+
JOIN watchlist w ON dn.showId = w.id
|
|
52
|
+
WHERE w.status = 'Watching'
|
|
53
|
+
AND EXISTS (
|
|
54
|
+
SELECT 1 FROM discovered_notifications dn_recent
|
|
55
|
+
WHERE dn_recent.showId = dn.showId
|
|
56
|
+
AND dn_recent.discoveredAt >= datetime('now', '-7 days')
|
|
57
|
+
)
|
|
58
|
+
AND CAST(dn.episodeNumber AS INTEGER) = (
|
|
59
|
+
SELECT MIN(CAST(dn2.episodeNumber AS INTEGER))
|
|
60
|
+
FROM discovered_notifications dn2
|
|
61
|
+
WHERE dn2.showId = dn.showId
|
|
62
|
+
AND NOT EXISTS (
|
|
63
|
+
SELECT 1 FROM watched_episodes we2
|
|
64
|
+
WHERE we2.showId = dn2.showId AND we2.episodeNumber = dn2.episodeNumber
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
AND NOT EXISTS (
|
|
68
|
+
SELECT 1 FROM watched_episodes we
|
|
69
|
+
WHERE we.showId = dn.showId AND we.episodeNumber = dn.episodeNumber
|
|
70
|
+
)
|
|
71
|
+
ORDER BY CAST(dn.episodeNumber AS INTEGER) ASC`);
|
|
72
|
+
return rows.map((row) => ({
|
|
73
|
+
_id: row.id,
|
|
74
|
+
id: row.id,
|
|
75
|
+
name: row.name,
|
|
76
|
+
thumbnail: row.thumbnail || '',
|
|
77
|
+
nativeName: row.nativeName,
|
|
78
|
+
englishName: row.englishName,
|
|
79
|
+
type: row.type,
|
|
80
|
+
episodeNumber: parseInt(row.episodeNumber) || row.episodeNumber,
|
|
81
|
+
}));
|
|
82
|
+
}),
|
|
83
|
+
});
|