@serifpersia/dango 3.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/client/dist/assets/AnimeCard-B5A20jAI.js +1 -0
- package/client/dist/assets/AnimeInfo-0huPAq0O.js +1 -0
- package/client/dist/assets/AnimeInfo-BFWtHlPC.css +1 -0
- package/client/dist/assets/AnimeInfoPage-Dqr5QH6R.js +2 -0
- package/client/dist/assets/AnimeMetaDetails-BPvv5xIl.js +1 -0
- package/client/dist/assets/AnimeMetaDetails-BwNQnRCd.css +1 -0
- package/client/dist/assets/Asmr-B6ZiqkmU.js +2 -0
- package/client/dist/assets/Asmr-Cq2ua6Zk.css +1 -0
- package/client/dist/assets/Asmr.module-ChUXDZKx.js +1 -0
- package/client/dist/assets/AsmrResetProgressModal-Cqsoo3XC.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-C1_iBSyL.css +1 -0
- package/client/dist/assets/AvSyncCalibrator-C8ZoIwMY.js +10 -0
- package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
- package/client/dist/assets/ErrorMessage-z4f4WGgP.js +1 -0
- package/client/dist/assets/Home-DtRtTuI9.css +1 -0
- package/client/dist/assets/Home-thJhPfs1.js +1 -0
- package/client/dist/assets/Insights-DKtoVe6L.css +1 -0
- package/client/dist/assets/Insights-ZlgPrzGD.js +1 -0
- package/client/dist/assets/LibraryListPage-CCiee-Ot.js +1 -0
- package/client/dist/assets/ListeningList-ctAz_6Zi.js +1 -0
- package/client/dist/assets/Manga-BI9WMTx5.js +1 -0
- package/client/dist/assets/Manga-Dp_-l1Pw.css +1 -0
- package/client/dist/assets/Manga.module-BcfOdsNP.js +1 -0
- package/client/dist/assets/MangaInfoPage-DcrEr7Sk.js +1 -0
- package/client/dist/assets/MangaReadPage-SYg63m48.js +1 -0
- package/client/dist/assets/MangaResetProgressModal-DX-1PS4h.js +1 -0
- package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
- package/client/dist/assets/Map-Dj51g5zL.js +1 -0
- package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
- package/client/dist/assets/Mature-Dqh_9SA3.js +1 -0
- package/client/dist/assets/MediaCard-Be1ST9oj.js +1 -0
- package/client/dist/assets/MediaCard-DB6Yjgky.css +1 -0
- package/client/dist/assets/Music-BGH_0voi.css +1 -0
- package/client/dist/assets/Music-zsPwT8_5.js +1 -0
- package/client/dist/assets/OptionListMenu-Drc_vybW.js +1 -0
- package/client/dist/assets/Player-B036Ic3m.css +1 -0
- package/client/dist/assets/Player-BEtAYps2.js +56 -0
- package/client/dist/assets/PlayerRedirect-D70t8VUW.js +1 -0
- package/client/dist/assets/PlayerSettings-Bj6SsmQ2.css +1 -0
- package/client/dist/assets/PlayerSettings-Cwoji-P7.js +1 -0
- package/client/dist/assets/QueueOptionsButton-B6BMq66e.js +1 -0
- package/client/dist/assets/QueueOptionsButton-CM15uQn8.css +1 -0
- package/client/dist/assets/QueueRail-BUYEjdaq.css +1 -0
- package/client/dist/assets/QueueRail-VxpUDBSm.js +1 -0
- package/client/dist/assets/Radio-B2Q_Egj7.js +1 -0
- package/client/dist/assets/Radio-CCHNFN7R.css +1 -0
- package/client/dist/assets/Radio.module-DGSZLpKM.js +1 -0
- package/client/dist/assets/ReadingList-O0QPwXu7.js +1 -0
- package/client/dist/assets/ResetProgressModal-CR_ovLAu.js +1 -0
- package/client/dist/assets/Search-C8e2nlDD.js +1 -0
- package/client/dist/assets/Search-Cs45oAwI.css +1 -0
- package/client/dist/assets/Search.module-CMEai-B-.js +1 -0
- package/client/dist/assets/Settings-AuFxQcJF.css +1 -0
- package/client/dist/assets/Settings-BgPWBl2V.js +21 -0
- package/client/dist/assets/SkeletonGrid-CtHKKMLj.js +1 -0
- package/client/dist/assets/SkeletonGrid-DhPYefXr.css +1 -0
- package/client/dist/assets/SynopsisText-BcpwT5iw.js +1 -0
- package/client/dist/assets/SynopsisText-CYv2W3yR.css +1 -0
- package/client/dist/assets/Trackers-Bfoiyuzy.js +2 -0
- package/client/dist/assets/Trackers-D5XBOCDp.css +1 -0
- package/client/dist/assets/Tv-Ca0fCdko.js +3 -0
- package/client/dist/assets/Tv-CuEl3Iol.css +1 -0
- package/client/dist/assets/TvInfo-Bu36xJyO.css +1 -0
- package/client/dist/assets/TvInfo-D_scgw_h.js +1 -0
- package/client/dist/assets/TvPopup-YTI7dEbJ.js +1 -0
- package/client/dist/assets/TvSearch-CxU33UOf.js +1 -0
- package/client/dist/assets/TvWatchlist-B4X1fWEH.js +1 -0
- package/client/dist/assets/VirtualKeyboard-6ZNTym4R.js +1 -0
- package/client/dist/assets/VirtualKeyboard-KIPc5ZGH.css +1 -0
- package/client/dist/assets/Watchlist-BiDEU0iV.css +1 -0
- package/client/dist/assets/Watchlist-P6YMZUdF.js +1 -0
- package/client/dist/assets/Watchlist.module-XGtVQr1d.js +1 -0
- package/client/dist/assets/hls-BOLXTnjL.js +42 -0
- package/client/dist/assets/hls-C4cG9wQw.js +1 -0
- package/client/dist/assets/index-BOKlZEHq.js +30354 -0
- package/client/dist/assets/index-CUm1meqr.css +1 -0
- package/client/dist/assets/index-IZgh1beL.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DeuwDSnI.js +1 -0
- package/client/dist/assets/useAsmr-CbMVRjmV.js +1 -0
- package/client/dist/assets/useAsmrPopup-D91Q3CBg.js +1 -0
- package/client/dist/assets/useIsMobile-Dm_b6Ob3.js +1 -0
- package/client/dist/assets/useMangaLibrary-BPvFb1lU.js +1 -0
- package/client/dist/assets/useMangaPopup-BOL2mQJs.js +1 -0
- package/client/dist/assets/useMatureConsent-CT7-KfzG.js +1 -0
- package/client/dist/assets/useProviders-BsRGONMA.js +1 -0
- package/client/dist/assets/useTv-D37MfTHA.js +1 -0
- package/client/dist/assets/useTvLibrary-Cwxgy_Bg.js +1 -0
- package/client/dist/assets/vendor-core-8R2fFbyN.js +1 -0
- package/client/dist/assets/vendor-icons-CDMuNDeL.js +1 -0
- package/client/dist/assets/vendor-query-BXRECSI6.js +1 -0
- package/client/dist/assets/vendor-ui-CUDCvRS_.js +1 -0
- package/client/dist/av-sync-test.mp4 +0 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +46 -0
- package/client/dist/logo-mochi.png +0 -0
- package/client/dist/logo-sakura.png +0 -0
- package/client/dist/logo.png +0 -0
- package/client/dist/placeholder.svg +4 -0
- package/client/dist/robots.txt +3 -0
- package/orchestrator.js +534 -0
- package/package.json +85 -0
- package/server/dist/app-auth.js +177 -0
- package/server/dist/config.js +175 -0
- package/server/dist/controllers/asmr-library.controller.js +288 -0
- package/server/dist/controllers/auth.controller.js +225 -0
- package/server/dist/controllers/data.controller.js +766 -0
- package/server/dist/controllers/insights.controller.js +198 -0
- package/server/dist/controllers/manga-library.controller.js +296 -0
- package/server/dist/controllers/music.controller.js +698 -0
- package/server/dist/controllers/proxy.controller.js +820 -0
- package/server/dist/controllers/settings.controller.js +609 -0
- package/server/dist/controllers/tv-library.controller.js +310 -0
- package/server/dist/controllers/watchlist.controller.js +1015 -0
- package/server/dist/db.js +145 -0
- package/server/dist/discord-gateway.js +391 -0
- package/server/dist/discord-rpc.js +427 -0
- package/server/dist/github-sync.js +512 -0
- package/server/dist/google.js +748 -0
- package/server/dist/lib/anilist.js +1549 -0
- package/server/dist/lib/anischedule.js +194 -0
- package/server/dist/lib/discord-roles-sync.service.js +130 -0
- package/server/dist/lib/ipc.js +24 -0
- package/server/dist/lib/kitsu.js +544 -0
- package/server/dist/lib/mal.js +934 -0
- package/server/dist/lib/migration.js +329 -0
- package/server/dist/lib/offline-db.js +401 -0
- package/server/dist/lib/temp-ids.js +3 -0
- package/server/dist/lib/tmdb.js +72 -0
- package/server/dist/lib/tracker/anilist-tracker.js +245 -0
- package/server/dist/lib/tracker/sync.service.js +496 -0
- package/server/dist/lib/ytmusic.js +125 -0
- package/server/dist/logger.js +16 -0
- package/server/dist/providers/manga/manga.types.js +7 -0
- package/server/dist/providers/provider.interface.js +1 -0
- package/server/dist/providers/remote-loader.js +574 -0
- package/server/dist/providers/remote-types.js +117 -0
- package/server/dist/providers/title-matching.js +225 -0
- package/server/dist/providers/tv.types.js +1 -0
- package/server/dist/rclone.js +120 -0
- package/server/dist/repositories/asmr.repository.js +123 -0
- package/server/dist/repositories/insights.repository.js +85 -0
- package/server/dist/repositories/library.repository.js +108 -0
- package/server/dist/repositories/mal-cache.repository.js +55 -0
- package/server/dist/repositories/manga.repository.js +138 -0
- package/server/dist/repositories/notifications.repository.js +41 -0
- package/server/dist/repositories/queue.repository.js +61 -0
- package/server/dist/repositories/settings.repository.js +12 -0
- package/server/dist/repositories/shows-meta.repository.js +42 -0
- package/server/dist/repositories/temp-show-ids.repository.js +51 -0
- package/server/dist/repositories/tv.repository.js +151 -0
- package/server/dist/repositories/watched-episodes.repository.js +52 -0
- package/server/dist/repositories/watchlist.repository.js +71 -0
- package/server/dist/request-logger.js +16 -0
- package/server/dist/routes/asmr-library.routes.js +23 -0
- package/server/dist/routes/asmr.routes.js +81 -0
- package/server/dist/routes/auth.routes.js +25 -0
- package/server/dist/routes/data.routes.js +47 -0
- package/server/dist/routes/discord-gateway.routes.js +51 -0
- package/server/dist/routes/insights.routes.js +34 -0
- package/server/dist/routes/lan-auth.routes.js +46 -0
- package/server/dist/routes/manga-library.routes.js +23 -0
- package/server/dist/routes/manga.routes.js +141 -0
- package/server/dist/routes/music.routes.js +20 -0
- package/server/dist/routes/providers.routes.js +17 -0
- package/server/dist/routes/proxy.routes.js +12 -0
- package/server/dist/routes/radio.routes.js +138 -0
- package/server/dist/routes/settings.routes.js +24 -0
- package/server/dist/routes/tracker.routes.js +164 -0
- package/server/dist/routes/translate.routes.js +88 -0
- package/server/dist/routes/tv-library.routes.js +23 -0
- package/server/dist/routes/tv.routes.js +629 -0
- package/server/dist/routes/watchlist.routes.js +341 -0
- package/server/dist/scripts/backfill-episode-duration.js +42 -0
- package/server/dist/server.js +529 -0
- package/server/dist/shipped-defaults.js +7 -0
- package/server/dist/sync-config.js +23 -0
- package/server/dist/sync-payload.js +91 -0
- package/server/dist/sync.js +1128 -0
- package/server/dist/utils/cache.utils.js +25 -0
- package/server/dist/utils/cookie.utils.js +13 -0
- package/server/dist/utils/db-utils.js +9 -0
- package/server/dist/utils/env.utils.js +77 -0
- package/server/dist/utils/http.utils.js +70 -0
- package/server/dist/utils/machine-id.js +40 -0
- package/server/dist/utils/megaplay.utils.js +41 -0
- package/server/dist/utils/request-context.js +2 -0
- package/server/dist/utils/security.utils.js +94 -0
- package/server/package.json +44 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
export function normalizeCompact(s) {
|
|
2
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
3
|
+
}
|
|
4
|
+
export function tokenize(s) {
|
|
5
|
+
return s
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
|
|
8
|
+
.split(/\s+/)
|
|
9
|
+
.filter((w) => w.length >= 2);
|
|
10
|
+
}
|
|
11
|
+
export function bigramDice(a, b) {
|
|
12
|
+
if (a.length < 2 || b.length < 2)
|
|
13
|
+
return a === b ? 1 : 0;
|
|
14
|
+
const grams = (s) => {
|
|
15
|
+
const map = new Map();
|
|
16
|
+
for (let i = 0; i < s.length - 1; i++) {
|
|
17
|
+
const g = s.slice(i, i + 2);
|
|
18
|
+
map.set(g, (map.get(g) || 0) + 1);
|
|
19
|
+
}
|
|
20
|
+
return map;
|
|
21
|
+
};
|
|
22
|
+
const A = grams(a);
|
|
23
|
+
const B = grams(b);
|
|
24
|
+
let totalA = 0;
|
|
25
|
+
let totalB = 0;
|
|
26
|
+
let shared = 0;
|
|
27
|
+
A.forEach((c) => (totalA += c));
|
|
28
|
+
B.forEach((c) => (totalB += c));
|
|
29
|
+
A.forEach((c, g) => {
|
|
30
|
+
const other = B.get(g);
|
|
31
|
+
if (other)
|
|
32
|
+
shared += Math.min(c, other);
|
|
33
|
+
});
|
|
34
|
+
return (2 * shared) / (totalA + totalB);
|
|
35
|
+
}
|
|
36
|
+
const STOPWORDS = new Set([
|
|
37
|
+
'the',
|
|
38
|
+
'animation',
|
|
39
|
+
'no',
|
|
40
|
+
'wa',
|
|
41
|
+
'ga',
|
|
42
|
+
'wo',
|
|
43
|
+
'ni',
|
|
44
|
+
'de',
|
|
45
|
+
'to',
|
|
46
|
+
'e',
|
|
47
|
+
'o',
|
|
48
|
+
'x',
|
|
49
|
+
'ii',
|
|
50
|
+
'yo',
|
|
51
|
+
'ova',
|
|
52
|
+
'oad',
|
|
53
|
+
'hd',
|
|
54
|
+
'ep',
|
|
55
|
+
'episode',
|
|
56
|
+
'season',
|
|
57
|
+
'part',
|
|
58
|
+
'hen',
|
|
59
|
+
'a',
|
|
60
|
+
]);
|
|
61
|
+
const SEASON_TOKENS = new Set([
|
|
62
|
+
'season',
|
|
63
|
+
'2nd',
|
|
64
|
+
'3rd',
|
|
65
|
+
'4th',
|
|
66
|
+
'5th',
|
|
67
|
+
'ii',
|
|
68
|
+
'iii',
|
|
69
|
+
'iv',
|
|
70
|
+
'v',
|
|
71
|
+
'vi',
|
|
72
|
+
'vii',
|
|
73
|
+
'viii',
|
|
74
|
+
'ix',
|
|
75
|
+
'x',
|
|
76
|
+
'two',
|
|
77
|
+
'three',
|
|
78
|
+
'four',
|
|
79
|
+
'five',
|
|
80
|
+
'six',
|
|
81
|
+
'seven',
|
|
82
|
+
'eight',
|
|
83
|
+
'nine',
|
|
84
|
+
'ten',
|
|
85
|
+
]);
|
|
86
|
+
function distinctiveTokensExcludingSeason(s) {
|
|
87
|
+
return [...new Set(tokenize(s).filter((w) => !STOPWORDS.has(w) && !SEASON_TOKENS.has(w)))];
|
|
88
|
+
}
|
|
89
|
+
const ROMAN_TO_NUM = {
|
|
90
|
+
ii: 2,
|
|
91
|
+
iii: 3,
|
|
92
|
+
iv: 4,
|
|
93
|
+
v: 5,
|
|
94
|
+
vi: 6,
|
|
95
|
+
vii: 7,
|
|
96
|
+
viii: 8,
|
|
97
|
+
ix: 9,
|
|
98
|
+
x: 10,
|
|
99
|
+
};
|
|
100
|
+
const WORD_TO_NUM = {
|
|
101
|
+
two: 2,
|
|
102
|
+
three: 3,
|
|
103
|
+
four: 4,
|
|
104
|
+
five: 5,
|
|
105
|
+
six: 6,
|
|
106
|
+
seven: 7,
|
|
107
|
+
eight: 8,
|
|
108
|
+
nine: 9,
|
|
109
|
+
ten: 10,
|
|
110
|
+
};
|
|
111
|
+
function detectSeasonMarker(s) {
|
|
112
|
+
const t = s.toLowerCase();
|
|
113
|
+
// "season 2", "season ii", "season 2nd", "season three"
|
|
114
|
+
const seasonNum = t.match(/\bseason\s*(\d+|ii|iii|iv|v|vi|vii|viii|ix|x|two|three|four|five|six|seven|eight|nine|ten)\b/);
|
|
115
|
+
if (seasonNum) {
|
|
116
|
+
const token = seasonNum[1];
|
|
117
|
+
const n = ROMAN_TO_NUM[token];
|
|
118
|
+
const w = WORD_TO_NUM[token];
|
|
119
|
+
return String(n ?? w ?? token);
|
|
120
|
+
}
|
|
121
|
+
const ord = t.match(/\b(\d+)(st|nd|rd|th)\b/);
|
|
122
|
+
if (ord)
|
|
123
|
+
return ord[1];
|
|
124
|
+
const trailing = t.match(/\b(ii|iii|iv|v|vi|vii|viii|ix|x)\b(?!\s*(st|nd|rd|th|season))/);
|
|
125
|
+
if (trailing)
|
|
126
|
+
return String(ROMAN_TO_NUM[trailing[1]]);
|
|
127
|
+
// bare trailing number, e.g. "...Another World 2"
|
|
128
|
+
const trailingNum = t.match(/\b(\d+)\s*(season)?\s*$/);
|
|
129
|
+
if (trailingNum)
|
|
130
|
+
return trailingNum[1];
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
export function distinctiveTokens(s) {
|
|
134
|
+
return [...new Set(tokenize(s).filter((w) => !STOPWORDS.has(w)))];
|
|
135
|
+
}
|
|
136
|
+
export function titleSimilarity(query, candidate) {
|
|
137
|
+
const q = normalizeCompact(query);
|
|
138
|
+
const c = normalizeCompact(candidate);
|
|
139
|
+
if (!q || !c)
|
|
140
|
+
return 0;
|
|
141
|
+
if (q === c)
|
|
142
|
+
return 1;
|
|
143
|
+
let score = bigramDice(q, c);
|
|
144
|
+
if (q.startsWith(c) || c.startsWith(q)) {
|
|
145
|
+
const prefixScore = 0.5 + 0.5 * (Math.min(q.length, c.length) / Math.max(q.length, c.length));
|
|
146
|
+
if (prefixScore > score)
|
|
147
|
+
score = prefixScore;
|
|
148
|
+
}
|
|
149
|
+
const qD = distinctiveTokensExcludingSeason(query);
|
|
150
|
+
const cD = distinctiveTokensExcludingSeason(candidate);
|
|
151
|
+
if (qD.length > 0 && cD.length >= 2) {
|
|
152
|
+
const cSet = new Set(cD);
|
|
153
|
+
let hits = 0;
|
|
154
|
+
for (const t of qD) {
|
|
155
|
+
if (cSet.has(t))
|
|
156
|
+
hits++;
|
|
157
|
+
}
|
|
158
|
+
const coverage = hits / Math.min(qD.length, cD.length);
|
|
159
|
+
if (coverage > score)
|
|
160
|
+
score = coverage;
|
|
161
|
+
}
|
|
162
|
+
const qMarker = detectSeasonMarker(query);
|
|
163
|
+
if (qMarker) {
|
|
164
|
+
const cMarker = detectSeasonMarker(candidate);
|
|
165
|
+
if (cMarker === qMarker) {
|
|
166
|
+
score += 0.25;
|
|
167
|
+
}
|
|
168
|
+
else if (!cMarker && score >= 0.5) {
|
|
169
|
+
score -= 0.25;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return score >= 1 ? 0.99 : score;
|
|
173
|
+
}
|
|
174
|
+
export function pickBestMatch(candidates, targets, minScore = 0.6) {
|
|
175
|
+
if (!candidates.length)
|
|
176
|
+
return null;
|
|
177
|
+
const cleanTargets = targets.filter((t) => !!t && t.trim().length > 0);
|
|
178
|
+
if (!cleanTargets.length)
|
|
179
|
+
return null;
|
|
180
|
+
let best = null;
|
|
181
|
+
for (const item of candidates) {
|
|
182
|
+
if (!item.title)
|
|
183
|
+
continue;
|
|
184
|
+
let score = 0;
|
|
185
|
+
for (const target of cleanTargets) {
|
|
186
|
+
const s = titleSimilarity(target, item.title);
|
|
187
|
+
if (s > score)
|
|
188
|
+
score = s;
|
|
189
|
+
}
|
|
190
|
+
if (!best || score > best.score)
|
|
191
|
+
best = { item, score };
|
|
192
|
+
}
|
|
193
|
+
return best && best.score >= minScore ? best : null;
|
|
194
|
+
}
|
|
195
|
+
export function buildQueryVariants(title, romaji) {
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
const out = [];
|
|
198
|
+
const push = (q) => {
|
|
199
|
+
const v = q.replace(/\s+/g, ' ').trim();
|
|
200
|
+
if (v && !seen.has(v.toLowerCase())) {
|
|
201
|
+
seen.add(v.toLowerCase());
|
|
202
|
+
out.push(v);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
if (romaji && romaji.trim())
|
|
206
|
+
push(romaji);
|
|
207
|
+
push(title);
|
|
208
|
+
const stripped = (s) => s.replace(/[^\p{L}\p{N}\s]/gu, ' ');
|
|
209
|
+
push(stripped(title));
|
|
210
|
+
if (romaji)
|
|
211
|
+
push(stripped(romaji));
|
|
212
|
+
const words = stripped(title).split(/\s+/).filter(Boolean);
|
|
213
|
+
if (words.length > 3)
|
|
214
|
+
push(words.slice(0, 3).join(' '));
|
|
215
|
+
if (words.length > 4)
|
|
216
|
+
push(words.slice(0, 4).join(' '));
|
|
217
|
+
const tokens = distinctiveTokens(title)
|
|
218
|
+
.concat(romaji ? distinctiveTokens(romaji) : [])
|
|
219
|
+
.filter((w) => w.length >= 4);
|
|
220
|
+
const unique = [...new Set(tokens)];
|
|
221
|
+
unique.sort((a, b) => b.length - a.length);
|
|
222
|
+
for (const t of unique.slice(0, 4))
|
|
223
|
+
push(t);
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { spawn, execFile } from 'child_process';
|
|
2
|
+
import logger from './logger.js';
|
|
3
|
+
import { CONFIG } from './config.js';
|
|
4
|
+
class RcloneService {
|
|
5
|
+
activeRemote = null;
|
|
6
|
+
executeCommand(args) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
execFile('rclone', args, (err, stdout, stderr) => {
|
|
9
|
+
if (err) {
|
|
10
|
+
if (stderr)
|
|
11
|
+
logger.warn({ stderr }, 'Rclone command warning');
|
|
12
|
+
return reject(new Error(stderr || err.message));
|
|
13
|
+
}
|
|
14
|
+
resolve(stdout.trim());
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
executeRcloneArgs(args) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const process = spawn('rclone', args, { stdio: 'ignore' });
|
|
21
|
+
process.on('close', (code) => {
|
|
22
|
+
if (code === 0)
|
|
23
|
+
resolve();
|
|
24
|
+
else
|
|
25
|
+
reject(new Error(`Rclone exited with code ${code}`));
|
|
26
|
+
});
|
|
27
|
+
process.on('error', (err) => reject(err));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async listRemotes() {
|
|
31
|
+
try {
|
|
32
|
+
const remotesStr = await this.executeCommand(['listremotes']);
|
|
33
|
+
return remotesStr
|
|
34
|
+
.split('\n')
|
|
35
|
+
.map((r) => r.trim())
|
|
36
|
+
.filter((r) => r !== '')
|
|
37
|
+
.map((r) => r.replace(/:$/, ''));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async init() {
|
|
44
|
+
try {
|
|
45
|
+
await this.executeCommand(['version']);
|
|
46
|
+
const remotes = await this.listRemotes();
|
|
47
|
+
if (CONFIG.RCLONE_REMOTE) {
|
|
48
|
+
const found = remotes.find((r) => r.toLowerCase() === CONFIG.RCLONE_REMOTE?.toLowerCase());
|
|
49
|
+
if (found) {
|
|
50
|
+
this.activeRemote = found;
|
|
51
|
+
logger.info(`Rclone initialized with manual remote: ${this.activeRemote}`);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
logger.warn(`Configured RCLONE_REMOTE '${CONFIG.RCLONE_REMOTE}' not found in rclone listremotes.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (remotes.length > 0 && !CONFIG.RCLONE_REMOTE) {
|
|
59
|
+
logger.info({ remotes }, 'Rclone available but no manual remote is configured in settings.');
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
logger.warn({ err: error }, 'Rclone initialization failed');
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
isActive() {
|
|
70
|
+
return this.activeRemote !== null;
|
|
71
|
+
}
|
|
72
|
+
getRemoteName() {
|
|
73
|
+
return this.activeRemote || 'unknown';
|
|
74
|
+
}
|
|
75
|
+
async downloadFile(remoteFolder, fileName, localPath) {
|
|
76
|
+
if (!this.activeRemote)
|
|
77
|
+
throw new Error('Rclone not active');
|
|
78
|
+
const remotePath = `${this.activeRemote}:${remoteFolder}/${fileName}`;
|
|
79
|
+
await this.executeRcloneArgs(['copyto', remotePath, localPath]);
|
|
80
|
+
}
|
|
81
|
+
async uploadFile(localPath, remoteFolder, fileName) {
|
|
82
|
+
if (!this.activeRemote)
|
|
83
|
+
throw new Error('Rclone not active');
|
|
84
|
+
const remotePath = `${this.activeRemote}:${remoteFolder}/${fileName}`;
|
|
85
|
+
await this.executeRcloneArgs(['copyto', localPath, remotePath]);
|
|
86
|
+
}
|
|
87
|
+
executeRcloneArgsWithOutput(args) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const process = spawn('rclone', args, { stdio: 'pipe' });
|
|
90
|
+
let stdout = '';
|
|
91
|
+
let stderr = '';
|
|
92
|
+
process.stdout?.on('data', (data) => (stdout += data));
|
|
93
|
+
process.stderr?.on('data', (data) => (stderr += data));
|
|
94
|
+
process.on('close', (code) => {
|
|
95
|
+
if (code === 0)
|
|
96
|
+
resolve(stdout.trim());
|
|
97
|
+
else {
|
|
98
|
+
if (stderr)
|
|
99
|
+
logger.warn({ stderr }, 'Rclone command warning');
|
|
100
|
+
reject(new Error(stderr || `Rclone exited with code ${code}`));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
process.on('error', (err) => reject(err));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async fileExists(remoteFolder, fileName) {
|
|
107
|
+
if (!this.activeRemote)
|
|
108
|
+
return false;
|
|
109
|
+
try {
|
|
110
|
+
const remotePath = `${this.activeRemote}:${remoteFolder}/${fileName}`;
|
|
111
|
+
const output = await this.executeRcloneArgsWithOutput(['lsjson', remotePath]);
|
|
112
|
+
const json = JSON.parse(output);
|
|
113
|
+
return json && json.length > 0;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export const rcloneService = new RcloneService();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { dbAll, dbGet, dbRun } from '../utils/db-utils.js';
|
|
2
|
+
export const ASMR_STATUSES = [
|
|
3
|
+
'Listening',
|
|
4
|
+
'Completed',
|
|
5
|
+
'On-Hold',
|
|
6
|
+
'Dropped',
|
|
7
|
+
'Planned',
|
|
8
|
+
];
|
|
9
|
+
export function buildAsmrId(rjCode) {
|
|
10
|
+
return String(rjCode).trim().toUpperCase();
|
|
11
|
+
}
|
|
12
|
+
export const AsmrLibraryRepository = {
|
|
13
|
+
getById: (db, id) => dbGet(db, 'SELECT * FROM asmr_library WHERE id = ?', [id]),
|
|
14
|
+
exists: (db, id) => {
|
|
15
|
+
const row = dbGet(db, 'SELECT EXISTS(SELECT 1 FROM asmr_library WHERE id = ?) as inLibrary', [id]);
|
|
16
|
+
return !!(row && row.inLibrary);
|
|
17
|
+
},
|
|
18
|
+
getAll: (db, status, limit, offset) => {
|
|
19
|
+
let query = 'SELECT * FROM asmr_library';
|
|
20
|
+
const params = [];
|
|
21
|
+
if (status && status !== 'All') {
|
|
22
|
+
query += ' WHERE status = ?';
|
|
23
|
+
params.push(status);
|
|
24
|
+
}
|
|
25
|
+
query += ' ORDER BY updatedAt DESC';
|
|
26
|
+
if (limit !== undefined && offset !== undefined) {
|
|
27
|
+
query += ' LIMIT ? OFFSET ?';
|
|
28
|
+
params.push(limit, offset);
|
|
29
|
+
}
|
|
30
|
+
return dbAll(db, query, params);
|
|
31
|
+
},
|
|
32
|
+
getCount: (db, status) => {
|
|
33
|
+
let query = 'SELECT COUNT(*) as total FROM asmr_library';
|
|
34
|
+
const params = [];
|
|
35
|
+
if (status && status !== 'All') {
|
|
36
|
+
query += ' WHERE status = ?';
|
|
37
|
+
params.push(status);
|
|
38
|
+
}
|
|
39
|
+
const row = dbGet(db, query, params);
|
|
40
|
+
return row?.total || 0;
|
|
41
|
+
},
|
|
42
|
+
getIds: (db) => dbAll(db, 'SELECT id FROM asmr_library').map((r) => r.id),
|
|
43
|
+
upsert: (db, data) => dbRun(db, `INSERT INTO asmr_library (id, rjCode, title, thumbnail, status, isAdult, updatedAt)
|
|
44
|
+
VALUES (?, ?, ?, ?, ?, ?, strftime('%s', 'now'))
|
|
45
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
46
|
+
rjCode = COALESCE(NULLIF(EXCLUDED.rjCode, ''), asmr_library.rjCode),
|
|
47
|
+
title = COALESCE(NULLIF(EXCLUDED.title, ''), asmr_library.title),
|
|
48
|
+
thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), asmr_library.thumbnail),
|
|
49
|
+
status = COALESCE(NULLIF(EXCLUDED.status, ''), asmr_library.status),
|
|
50
|
+
isAdult = COALESCE(EXCLUDED.isAdult, asmr_library.isAdult),
|
|
51
|
+
updatedAt = strftime('%s', 'now')`, [data.id, data.rjCode, data.title, data.thumbnail, data.status, data.isAdult ? 1 : 0]),
|
|
52
|
+
updateStatus: (db, id, status) => dbRun(db, "UPDATE asmr_library SET status = ?, updatedAt = strftime('%s', 'now') WHERE id = ?", [status, id]),
|
|
53
|
+
updateStatusMany: (db, ids, status) => {
|
|
54
|
+
if (ids.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
57
|
+
dbRun(db, `UPDATE asmr_library SET status = ?, updatedAt = strftime('%s', 'now') WHERE id IN (${placeholders})`, [status, ...ids]);
|
|
58
|
+
},
|
|
59
|
+
touchProgress: (db, id, progress) => dbRun(db, `UPDATE asmr_library SET lastTrackIndex = ?, lastTrackLabel = ?, lastPosition = ?, updatedAt = strftime('%s', 'now') WHERE id = ?`, [progress.trackIndex, progress.trackLabel, progress.position, id]),
|
|
60
|
+
delete: (db, id) => dbRun(db, 'DELETE FROM asmr_library WHERE id = ?', [id]),
|
|
61
|
+
deleteMany: (db, ids) => {
|
|
62
|
+
if (ids.length === 0)
|
|
63
|
+
return;
|
|
64
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
65
|
+
dbRun(db, `DELETE FROM asmr_library WHERE id IN (${placeholders})`, ids);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
export const AsmrProgressRepository = {
|
|
69
|
+
getByWork: (db, workId) => dbAll(db, 'SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ? ORDER BY updatedAt DESC', [workId]),
|
|
70
|
+
getTrack: (db, workId, trackIndex) => dbGet(db, 'SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ? AND trackIndex = ?', [workId, trackIndex]),
|
|
71
|
+
getLatest: (db, workId) => dbGet(db, 'SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ? ORDER BY updatedAt DESC LIMIT 1', [workId]),
|
|
72
|
+
upsert: (db, data) => dbRun(db, `INSERT INTO asmr_progress (workId, trackIndex, trackLabel, currentTime, duration, title, thumbnail, rjCode, isAdult, updatedAt)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'))
|
|
74
|
+
ON CONFLICT(workId, trackIndex) DO UPDATE SET
|
|
75
|
+
trackLabel = COALESCE(NULLIF(EXCLUDED.trackLabel, ''), asmr_progress.trackLabel),
|
|
76
|
+
currentTime = EXCLUDED.currentTime,
|
|
77
|
+
duration = EXCLUDED.duration,
|
|
78
|
+
title = COALESCE(EXCLUDED.title, asmr_progress.title),
|
|
79
|
+
thumbnail = COALESCE(EXCLUDED.thumbnail, asmr_progress.thumbnail),
|
|
80
|
+
rjCode = COALESCE(EXCLUDED.rjCode, asmr_progress.rjCode),
|
|
81
|
+
isAdult = COALESCE(EXCLUDED.isAdult, asmr_progress.isAdult),
|
|
82
|
+
updatedAt = strftime('%s', 'now')`, [
|
|
83
|
+
data.workId,
|
|
84
|
+
data.trackIndex,
|
|
85
|
+
data.trackLabel,
|
|
86
|
+
data.currentTime,
|
|
87
|
+
data.duration,
|
|
88
|
+
data.title ?? null,
|
|
89
|
+
data.thumbnail ?? null,
|
|
90
|
+
data.rjCode ?? null,
|
|
91
|
+
data.isAdult ?? null,
|
|
92
|
+
]),
|
|
93
|
+
deleteByWork: (db, workId) => dbRun(db, 'DELETE FROM asmr_progress WHERE workId = ?', [workId]),
|
|
94
|
+
deleteMany: (db, ids) => {
|
|
95
|
+
if (ids.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
98
|
+
dbRun(db, `DELETE FROM asmr_progress WHERE workId IN (${placeholders})`, ids);
|
|
99
|
+
},
|
|
100
|
+
deleteTrack: (db, workId, trackIndex) => dbRun(db, 'DELETE FROM asmr_progress WHERE workId = ? AND trackIndex = ?', [
|
|
101
|
+
workId,
|
|
102
|
+
trackIndex,
|
|
103
|
+
]),
|
|
104
|
+
getContinueListening: (db, limit) => {
|
|
105
|
+
const limitClause = typeof limit === 'number' ? `LIMIT ${limit}` : '';
|
|
106
|
+
return dbAll(db, `SELECT p.workId as id,
|
|
107
|
+
COALESCE(l.rjCode, p.rjCode, p.workId) as rjCode,
|
|
108
|
+
COALESCE(l.title, p.title) as title,
|
|
109
|
+
COALESCE(l.thumbnail, p.thumbnail) as thumbnail,
|
|
110
|
+
COALESCE(l.isAdult, p.isAdult) as isAdult,
|
|
111
|
+
l.status as watchlistStatus,
|
|
112
|
+
p.trackIndex, p.trackLabel, p.currentTime, p.duration, p.updatedAt as progressAt
|
|
113
|
+
FROM (
|
|
114
|
+
SELECT *, ROW_NUMBER() OVER (PARTITION BY workId ORDER BY updatedAt DESC) as rn
|
|
115
|
+
FROM asmr_progress
|
|
116
|
+
) p
|
|
117
|
+
LEFT JOIN asmr_library l ON p.workId = l.id
|
|
118
|
+
WHERE p.rn = 1
|
|
119
|
+
AND (l.status IS NULL OR l.status = 'Listening')
|
|
120
|
+
ORDER BY p.updatedAt DESC
|
|
121
|
+
${limitClause}`);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { dbAll, dbGet } from '../utils/db-utils.js';
|
|
2
|
+
export const InsightsRepository = {
|
|
3
|
+
getCoreStats: (db) => dbGet(db, `SELECT
|
|
4
|
+
(SELECT SUM(
|
|
5
|
+
CASE WHEN we.currentTime > 0 THEN we.currentTime
|
|
6
|
+
WHEN we.currentTime = 0 AND we.duration = 0 THEN
|
|
7
|
+
COALESCE(sm.episodeDuration * 60,
|
|
8
|
+
CASE UPPER(COALESCE(sm.type, ''))
|
|
9
|
+
WHEN 'MOVIE' THEN 6000
|
|
10
|
+
WHEN 'TV_SHORT' THEN 720
|
|
11
|
+
WHEN 'MUSIC' THEN 300
|
|
12
|
+
ELSE 1440 END)
|
|
13
|
+
ELSE 0 END)
|
|
14
|
+
FROM watched_episodes we
|
|
15
|
+
LEFT JOIN shows_meta sm ON sm.id = we.showId) as totalSeconds,
|
|
16
|
+
(SELECT COUNT(*) FROM watched_episodes) as totalEpisodes,
|
|
17
|
+
(SELECT COUNT(DISTINCT showId) FROM watched_episodes) as totalAnime,
|
|
18
|
+
(SELECT COUNT(*) FROM watchlist WHERE status = 'Completed') as completedCount,
|
|
19
|
+
(SELECT COUNT(*) FROM watchlist) as totalWatchlist`),
|
|
20
|
+
getActivityGrid: (db) => dbAll(db, `SELECT date(watchedAt) as day, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY day`),
|
|
21
|
+
getHourlyDist: (db) => dbAll(db, `SELECT strftime('%H', watchedAt) as hour, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY hour`),
|
|
22
|
+
getSeasonality: (db) => dbAll(db, `SELECT strftime('%m', we.watchedAt) as month, SUM(
|
|
23
|
+
CASE WHEN we.currentTime > 0 THEN we.currentTime
|
|
24
|
+
WHEN we.currentTime = 0 AND we.duration = 0 THEN
|
|
25
|
+
COALESCE(sm.episodeDuration * 60,
|
|
26
|
+
CASE UPPER(COALESCE(sm.type, ''))
|
|
27
|
+
WHEN 'MOVIE' THEN 6000
|
|
28
|
+
WHEN 'TV_SHORT' THEN 720
|
|
29
|
+
WHEN 'MUSIC' THEN 300
|
|
30
|
+
ELSE 1440 END)
|
|
31
|
+
ELSE 0 END) as seconds
|
|
32
|
+
FROM watched_episodes we
|
|
33
|
+
LEFT JOIN shows_meta sm ON sm.id = we.showId
|
|
34
|
+
GROUP BY month`),
|
|
35
|
+
getAllWatches: (db) => dbAll(db, `SELECT we.watchedAt, we.currentTime,
|
|
36
|
+
CASE WHEN we.currentTime > 0 THEN we.currentTime
|
|
37
|
+
WHEN we.currentTime = 0 AND we.duration = 0 THEN
|
|
38
|
+
COALESCE(sm.episodeDuration * 60,
|
|
39
|
+
CASE UPPER(COALESCE(sm.type, ''))
|
|
40
|
+
WHEN 'MOVIE' THEN 6000
|
|
41
|
+
WHEN 'TV_SHORT' THEN 720
|
|
42
|
+
WHEN 'MUSIC' THEN 300
|
|
43
|
+
ELSE 1440 END)
|
|
44
|
+
ELSE 0 END as effectiveSeconds
|
|
45
|
+
FROM watched_episodes we
|
|
46
|
+
LEFT JOIN shows_meta sm ON sm.id = we.showId
|
|
47
|
+
ORDER BY we.watchedAt ASC`),
|
|
48
|
+
getWatchedShowsMeta: (db) => dbAll(db, `SELECT DISTINCT sm.id, sm.genres, sm.popularityScore
|
|
49
|
+
FROM shows_meta sm
|
|
50
|
+
JOIN watched_episodes we ON sm.id = we.showId`),
|
|
51
|
+
getDroppedShows: (db) => dbAll(db, `SELECT w.id, w.name, MAX(we.watchedAt) as lastActivity
|
|
52
|
+
FROM watchlist w
|
|
53
|
+
JOIN watched_episodes we ON w.id = we.showId
|
|
54
|
+
WHERE w.status = 'Watching' AND NOT (we.currentTime = 0 AND we.duration = 0)
|
|
55
|
+
GROUP BY w.id
|
|
56
|
+
HAVING lastActivity < date('now', '-90 days')`),
|
|
57
|
+
getCompletionVelocities: (db) => dbAll(db, `SELECT
|
|
58
|
+
(julianday(MAX(we.watchedAt)) - julianday(MIN(we.watchedAt))) as daysToFinish
|
|
59
|
+
FROM watchlist w
|
|
60
|
+
JOIN watched_episodes we ON w.id = we.showId
|
|
61
|
+
WHERE w.status = 'Completed'
|
|
62
|
+
GROUP BY w.id
|
|
63
|
+
HAVING SUM(we.currentTime) > 0`),
|
|
64
|
+
getWatchedEpisodesWithMeta: (db) => dbAll(db, `SELECT
|
|
65
|
+
we.showId,
|
|
66
|
+
we.currentTime,
|
|
67
|
+
we.duration,
|
|
68
|
+
CASE WHEN we.currentTime > 0 THEN we.currentTime
|
|
69
|
+
WHEN we.currentTime = 0 AND we.duration = 0 THEN
|
|
70
|
+
COALESCE(sm.episodeDuration * 60,
|
|
71
|
+
CASE UPPER(COALESCE(sm.type, ''))
|
|
72
|
+
WHEN 'MOVIE' THEN 6000
|
|
73
|
+
WHEN 'TV_SHORT' THEN 720
|
|
74
|
+
WHEN 'MUSIC' THEN 300
|
|
75
|
+
ELSE 1440 END)
|
|
76
|
+
ELSE 0 END as effectiveSeconds,
|
|
77
|
+
sm.genres,
|
|
78
|
+
sm.popularityScore,
|
|
79
|
+
sm.name,
|
|
80
|
+
sm.nativeName,
|
|
81
|
+
sm.englishName,
|
|
82
|
+
sm.thumbnail
|
|
83
|
+
FROM watched_episodes we
|
|
84
|
+
JOIN shows_meta sm ON we.showId = sm.id`),
|
|
85
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { dbGet, dbRun } from '../utils/db-utils.js';
|
|
2
|
+
const LIBRARY_TABLES = [
|
|
3
|
+
'watchlist',
|
|
4
|
+
'watched_episodes',
|
|
5
|
+
'queue',
|
|
6
|
+
'shows_meta',
|
|
7
|
+
'dismissed_notifications',
|
|
8
|
+
'discovered_notifications',
|
|
9
|
+
'legacy_id_mapping',
|
|
10
|
+
'temp_show_ids',
|
|
11
|
+
];
|
|
12
|
+
const MANGA_LIBRARY_TABLES = ['manga_library', 'manga_progress'];
|
|
13
|
+
const TV_LIBRARY_TABLES = ['tv_library', 'tv_progress'];
|
|
14
|
+
const ASMR_LIBRARY_TABLES = ['asmr_library', 'asmr_progress'];
|
|
15
|
+
export const LibraryRepository = {
|
|
16
|
+
countAll: (db) => {
|
|
17
|
+
const counts = {};
|
|
18
|
+
for (const table of LIBRARY_TABLES) {
|
|
19
|
+
try {
|
|
20
|
+
counts[table] =
|
|
21
|
+
dbGet(db, `SELECT COUNT(*) AS rows FROM "${table}"`)?.rows ?? 0;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
counts[table] = 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return counts;
|
|
28
|
+
},
|
|
29
|
+
clearAll: (db) => {
|
|
30
|
+
for (const table of LIBRARY_TABLES) {
|
|
31
|
+
try {
|
|
32
|
+
dbRun(db, `DELETE FROM "${table}"`);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// ignore
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
countManga: (db) => {
|
|
40
|
+
const counts = {};
|
|
41
|
+
for (const table of MANGA_LIBRARY_TABLES) {
|
|
42
|
+
try {
|
|
43
|
+
counts[table] =
|
|
44
|
+
dbGet(db, `SELECT COUNT(*) AS rows FROM "${table}"`)?.rows ?? 0;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
counts[table] = 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return counts;
|
|
51
|
+
},
|
|
52
|
+
clearManga: (db) => {
|
|
53
|
+
for (const table of MANGA_LIBRARY_TABLES) {
|
|
54
|
+
try {
|
|
55
|
+
dbRun(db, `DELETE FROM "${table}"`);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// ignore
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
countTv: (db) => {
|
|
63
|
+
const counts = {};
|
|
64
|
+
for (const table of TV_LIBRARY_TABLES) {
|
|
65
|
+
try {
|
|
66
|
+
counts[table] =
|
|
67
|
+
dbGet(db, `SELECT COUNT(*) AS rows FROM "${table}"`)?.rows ?? 0;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
counts[table] = 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return counts;
|
|
74
|
+
},
|
|
75
|
+
clearTv: (db) => {
|
|
76
|
+
for (const table of TV_LIBRARY_TABLES) {
|
|
77
|
+
try {
|
|
78
|
+
dbRun(db, `DELETE FROM "${table}"`);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
countAsmr: (db) => {
|
|
86
|
+
const counts = {};
|
|
87
|
+
for (const table of ASMR_LIBRARY_TABLES) {
|
|
88
|
+
try {
|
|
89
|
+
counts[table] =
|
|
90
|
+
dbGet(db, `SELECT COUNT(*) AS rows FROM "${table}"`)?.rows ?? 0;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
counts[table] = 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return counts;
|
|
97
|
+
},
|
|
98
|
+
clearAsmr: (db) => {
|
|
99
|
+
for (const table of ASMR_LIBRARY_TABLES) {
|
|
100
|
+
try {
|
|
101
|
+
dbRun(db, `DELETE FROM "${table}"`);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// ignore
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|