@serifpersia/dango 3.6.5 → 3.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-DraA_TF9.js} +1 -1
- package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-BJ7pzs-P.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-B_esUr5C.js} +2 -2
- package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-Ckuh2xfW.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
- package/client/dist/assets/AvSyncCalibrator-DwHCd9yB.js +10 -0
- package/client/dist/assets/{Home-DxMBHFmc.js → Home-DuM1EuvN.js} +1 -1
- package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-D3Pd9c9V.js} +1 -1
- package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-Dm2yVn0-.js} +1 -1
- package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-D-zt6EXZ.js} +1 -1
- package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-JwprMKBk.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-DaeyJl2p.js} +1 -1
- package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-COg0fHI9.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
- package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-IHDaasrs.js} +1 -1
- package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard-CpwsrXo5.js} +1 -1
- package/client/dist/assets/{Music-DULtVsAM.js → Music-Dks4tPeK.js} +1 -1
- package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-11CcAsy8.js} +1 -1
- package/client/dist/assets/{Player-B8XeOmso.js → Player-DQoEQkHL.js} +6 -6
- package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-D4Ud4kv4.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
- package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-B4Dok7U3.js} +1 -1
- package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-BHK6LSk4.js} +1 -1
- package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-BCsxIz57.js} +1 -1
- package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-BXHxSfy-.js} +1 -1
- package/client/dist/assets/{Search-DvgVDlUR.js → Search-PK1ds07O.js} +1 -1
- package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-Bxk_x55l.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-CpJCZSRN.js} +1 -1
- package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-RieCh_84.js} +1 -1
- package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-DLytPJUj.js} +4 -4
- package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-C8djKebR.js} +1 -1
- package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-40JGGJDk.js} +1 -1
- package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-A3hbospf.js} +1 -1
- package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-DuXjqmZM.js} +1 -1
- package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-B9wGBh0u.js} +1 -1
- package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-CZ5X3Ya-.js} +1 -1
- package/client/dist/assets/{index-BTnhzofk.js → index-dRhc9-ol.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
- package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-CKOdHgol.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SFHPhpcl.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-CKATe31P.js} +1 -1
- package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-C75QuYgY.js} +1 -1
- package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-CMtvZbEi.js} +1 -1
- package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DUX4zYMr.js} +1 -1
- package/client/dist/assets/{useTvLibrary-a_Y0c2FS.js → useTvLibrary-By4Gt-R_.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/dist/app-auth.js +1 -157
- package/server/dist/app-hono.js +7 -557
- package/server/dist/config.js +1 -174
- package/server/dist/db/drizzle.js +1 -16
- package/server/dist/db/schema-anime.js +1 -61
- package/server/dist/db/schema-asmr.js +1 -13
- package/server/dist/db/schema-manga.js +1 -31
- package/server/dist/db/schema-tv.js +1 -18
- package/server/dist/db.js +1 -217
- package/server/dist/discord-gateway.js +1 -385
- package/server/dist/discord-rpc.js +1 -413
- package/server/dist/github-sync.js +1 -508
- package/server/dist/google.js +1 -741
- package/server/dist/hono/asmr.js +1 -10
- package/server/dist/hono/auth.js +3 -26
- package/server/dist/hono/music.js +1 -209
- package/server/dist/hono/proxy.js +36 -905
- package/server/dist/hono/tv.js +5 -169
- package/server/dist/hono/watchlist.js +2 -338
- package/server/dist/lib/anilist.js +26 -1458
- package/server/dist/lib/anischedule.js +1 -191
- package/server/dist/lib/insights.js +1 -185
- package/server/dist/lib/ipc.js +1 -23
- package/server/dist/lib/kitsu.js +1 -517
- package/server/dist/lib/mal-import.js +1 -291
- package/server/dist/lib/mal.js +1 -957
- package/server/dist/lib/migration.js +1 -310
- package/server/dist/lib/offline-db.js +4 -384
- package/server/dist/lib/potoken.js +1 -135
- package/server/dist/lib/recommendations.service.js +1 -216
- package/server/dist/lib/temp-ids.js +1 -3
- package/server/dist/lib/tmdb.js +1 -72
- package/server/dist/lib/tracker/anilist-tracker.js +8 -245
- package/server/dist/lib/tracker/manga-sync.service.js +1 -773
- package/server/dist/lib/tracker/sync-state.js +1 -21
- package/server/dist/lib/tracker/sync.service.js +1 -476
- package/server/dist/lib/translate.js +3 -79
- package/server/dist/lib/ump.js +1 -106
- package/server/dist/lib/ytmusic.js +1 -162
- package/server/dist/logger.js +1 -16
- package/server/dist/providers/manga/manga.types.js +0 -1
- package/server/dist/providers/provider.interface.js +0 -1
- package/server/dist/providers/remote-loader.js +2 -605
- package/server/dist/providers/remote-types.js +1 -124
- package/server/dist/providers/title-matching.js +1 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +2 -120
- package/server/dist/repositories/asmr.repository.js +11 -46
- package/server/dist/repositories/insights.repository.js +11 -25
- package/server/dist/repositories/library.repository.js +3 -94
- package/server/dist/repositories/mal-cache.repository.js +1 -52
- package/server/dist/repositories/manga.repository.js +14 -58
- package/server/dist/repositories/notifications.repository.js +6 -52
- package/server/dist/repositories/queue.repository.js +5 -58
- package/server/dist/repositories/settings.repository.js +2 -29
- package/server/dist/repositories/shows-meta.repository.js +3 -56
- package/server/dist/repositories/temp-show-ids.repository.js +3 -63
- package/server/dist/repositories/tv.repository.js +11 -54
- package/server/dist/repositories/watched-episodes.repository.js +6 -55
- package/server/dist/repositories/watchlist.repository.js +3 -54
- package/server/dist/scripts/backfill-episode-duration.js +1 -42
- package/server/dist/server.js +1 -307
- package/server/dist/shipped-defaults.js +1 -6
- package/server/dist/sync-payload.js +1 -91
- package/server/dist/sync.js +3 -647
- package/server/dist/trpc/context.js +1 -7
- package/server/dist/trpc/errors.js +1 -15
- package/server/dist/trpc/index.js +1 -18
- package/server/dist/trpc/router.js +1 -49
- package/server/dist/trpc/routers/asmr-library.js +1 -112
- package/server/dist/trpc/routers/asmr-progress.js +1 -121
- package/server/dist/trpc/routers/asmr.js +1 -87
- package/server/dist/trpc/routers/auth.js +1 -194
- package/server/dist/trpc/routers/continue-watching.js +2 -58
- package/server/dist/trpc/routers/data.js +1 -1026
- package/server/dist/trpc/routers/discord.js +1 -285
- package/server/dist/trpc/routers/discovery.js +1 -11
- package/server/dist/trpc/routers/insights.js +1 -14
- package/server/dist/trpc/routers/manga-library.js +1 -263
- package/server/dist/trpc/routers/manga-progress.js +1 -169
- package/server/dist/trpc/routers/manga.js +1 -201
- package/server/dist/trpc/routers/music.js +1 -655
- package/server/dist/trpc/routers/notifications.js +1 -80
- package/server/dist/trpc/routers/progress.js +1 -109
- package/server/dist/trpc/routers/queue.js +1 -93
- package/server/dist/trpc/routers/radio.js +1 -145
- package/server/dist/trpc/routers/settings.js +1 -96
- package/server/dist/trpc/routers/tracker.js +1 -213
- package/server/dist/trpc/routers/tv-library.js +1 -135
- package/server/dist/trpc/routers/tv-progress.js +1 -159
- package/server/dist/trpc/routers/tv.js +1 -551
- package/server/dist/trpc/routers/watchlist.js +2 -549
- package/server/dist/trpc/validation.js +1 -258
- package/server/dist/utils/cache.utils.js +1 -25
- package/server/dist/utils/cookie.utils.js +1 -13
- package/server/dist/utils/db-utils.js +5 -17
- package/server/dist/utils/env.utils.js +5 -77
- package/server/dist/utils/http.utils.js +1 -82
- package/server/dist/utils/request-context.js +1 -2
- package/server/dist/utils/security.utils.js +1 -76
- package/server/package.json +3 -2
- package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
- package/server/dist/utils/megaplay.utils.js +0 -40
|
@@ -1,124 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const SHA_RE = /^[0-9a-f]{64}$/i;
|
|
3
|
-
export function parseRegistry(raw) {
|
|
4
|
-
if (!raw || typeof raw !== 'object')
|
|
5
|
-
throw new Error('registry: not an object');
|
|
6
|
-
const r = raw;
|
|
7
|
-
if (typeof r.version !== 'number')
|
|
8
|
-
throw new Error('registry.version must be a number');
|
|
9
|
-
if (!Array.isArray(r.providers))
|
|
10
|
-
throw new Error('registry.providers must be an array');
|
|
11
|
-
const providers = r.providers.map((p, i) => parseEntry(p, i));
|
|
12
|
-
const seen = new Set();
|
|
13
|
-
for (const p of providers) {
|
|
14
|
-
const key = p.id.toLowerCase();
|
|
15
|
-
if (seen.has(key))
|
|
16
|
-
throw new Error(`registry: duplicate provider id "${p.id}"`);
|
|
17
|
-
seen.add(key);
|
|
18
|
-
}
|
|
19
|
-
return {
|
|
20
|
-
version: r.version,
|
|
21
|
-
updatedAt: typeof r.updatedAt === 'string' ? r.updatedAt : undefined,
|
|
22
|
-
providers,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function parseEntry(raw, index) {
|
|
26
|
-
if (!raw || typeof raw !== 'object')
|
|
27
|
-
throw new Error(`registry.providers[${index}]: not an object`);
|
|
28
|
-
const p = raw;
|
|
29
|
-
const id = p.id;
|
|
30
|
-
const entry = p.entry;
|
|
31
|
-
const label = p.label;
|
|
32
|
-
const version = p.version;
|
|
33
|
-
if (typeof id !== 'string' || !ID_RE.test(id.trim())) {
|
|
34
|
-
throw new Error(`registry.providers[${index}].id must match ${ID_RE}`);
|
|
35
|
-
}
|
|
36
|
-
if (typeof label !== 'string' || !label.trim()) {
|
|
37
|
-
throw new Error(`registry.providers[${index}].label must be a non-empty string`);
|
|
38
|
-
}
|
|
39
|
-
if (typeof version !== 'string' || !version.trim()) {
|
|
40
|
-
throw new Error(`registry.providers[${index}].version must be a non-empty string`);
|
|
41
|
-
}
|
|
42
|
-
const embedBase = p.embedBase;
|
|
43
|
-
if (embedBase !== undefined) {
|
|
44
|
-
if (typeof embedBase !== 'string' || !/^https:\/\/[^/]+/i.test(embedBase.trim())) {
|
|
45
|
-
throw new Error(`registry.providers[${index}].embedBase must be an https URL`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if ((typeof entry !== 'string' || !entry.trim()) && embedBase === undefined) {
|
|
49
|
-
throw new Error(`registry.providers[${index}].entry must be a non-empty string`);
|
|
50
|
-
}
|
|
51
|
-
if (p.sha256 !== undefined && (typeof p.sha256 !== 'string' || !SHA_RE.test(p.sha256.trim()))) {
|
|
52
|
-
throw new Error(`registry.providers[${index}].sha256 must be 64 hex chars`);
|
|
53
|
-
}
|
|
54
|
-
if (p.mature !== undefined && typeof p.mature !== 'boolean') {
|
|
55
|
-
throw new Error(`registry.providers[${index}].mature must be boolean`);
|
|
56
|
-
}
|
|
57
|
-
if (p.kind !== undefined &&
|
|
58
|
-
p.kind !== 'anime' &&
|
|
59
|
-
p.kind !== 'asmr' &&
|
|
60
|
-
p.kind !== 'manga' &&
|
|
61
|
-
p.kind !== 'tv') {
|
|
62
|
-
throw new Error(`registry.providers[${index}].kind must be anime|asmr|manga|tv`);
|
|
63
|
-
}
|
|
64
|
-
if (p.sub !== undefined && p.sub !== 'soft' && p.sub !== 'hard' && p.sub !== 'mixed') {
|
|
65
|
-
throw new Error(`registry.providers[${index}].sub must be soft|hard|mixed`);
|
|
66
|
-
}
|
|
67
|
-
if (p.tier !== undefined && p.tier !== 'direct' && p.tier !== 'embed' && p.tier !== 'cookie') {
|
|
68
|
-
throw new Error(`registry.providers[${index}].tier must be direct|embed|cookie`);
|
|
69
|
-
}
|
|
70
|
-
let modes;
|
|
71
|
-
if (p.modes !== undefined) {
|
|
72
|
-
if (!Array.isArray(p.modes))
|
|
73
|
-
throw new Error(`registry.providers[${index}].modes must be an array`);
|
|
74
|
-
modes = [];
|
|
75
|
-
for (const m of p.modes) {
|
|
76
|
-
if (m !== 'sub' && m !== 'dub') {
|
|
77
|
-
throw new Error(`registry.providers[${index}].modes must contain only sub|dub`);
|
|
78
|
-
}
|
|
79
|
-
if (!modes.includes(m))
|
|
80
|
-
modes.push(m);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
id: id.trim(),
|
|
85
|
-
label: label.trim(),
|
|
86
|
-
version: version.trim(),
|
|
87
|
-
entry: typeof entry === 'string' ? entry.trim() : undefined,
|
|
88
|
-
embedBase: typeof embedBase === 'string' ? embedBase.trim().replace(/\/+$/, '') : undefined,
|
|
89
|
-
sha256: typeof p.sha256 === 'string' ? p.sha256.trim().toLowerCase() : undefined,
|
|
90
|
-
minDango: typeof p.minDango === 'string' ? p.minDango : undefined,
|
|
91
|
-
mature: typeof p.mature === 'boolean' ? p.mature : false,
|
|
92
|
-
kind: p.kind === 'asmr' || p.kind === 'manga' || p.kind === 'tv' ? p.kind : 'anime',
|
|
93
|
-
sub: p.sub,
|
|
94
|
-
tier: p.tier,
|
|
95
|
-
modes,
|
|
96
|
-
browse: parseBrowseCaps(p.browse, index),
|
|
97
|
-
auth: p.auth === 'cookie' ? 'cookie' : undefined,
|
|
98
|
-
enabledByDefault: p.enabledByDefault === false ? false : true,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
function parseBrowseCaps(raw, index) {
|
|
102
|
-
if (raw === undefined)
|
|
103
|
-
return undefined;
|
|
104
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
105
|
-
throw new Error(`registry.providers[${index}].browse must be an object`);
|
|
106
|
-
}
|
|
107
|
-
const b = raw;
|
|
108
|
-
const caps = {};
|
|
109
|
-
for (const key of ['genre', 'order', 'studio', 'sort']) {
|
|
110
|
-
if (b[key] === undefined)
|
|
111
|
-
continue;
|
|
112
|
-
if (typeof b[key] !== 'boolean') {
|
|
113
|
-
throw new Error(`registry.providers[${index}].browse.${key} must be boolean`);
|
|
114
|
-
}
|
|
115
|
-
caps[key] = b[key];
|
|
116
|
-
}
|
|
117
|
-
if (b.pageSize !== undefined) {
|
|
118
|
-
if (typeof b.pageSize !== 'number' || !Number.isInteger(b.pageSize) || b.pageSize < 1) {
|
|
119
|
-
throw new Error(`registry.providers[${index}].browse.pageSize must be a positive integer`);
|
|
120
|
-
}
|
|
121
|
-
caps.pageSize = b.pageSize;
|
|
122
|
-
}
|
|
123
|
-
return caps;
|
|
124
|
-
}
|
|
1
|
+
const f=/^[a-z0-9][a-z0-9-]*$/i,m=/^[0-9a-f]{64}$/i;function g(t){if(!t||typeof t!="object")throw new Error("registry: not an object");const o=t;if(typeof o.version!="number")throw new Error("registry.version must be a number");if(!Array.isArray(o.providers))throw new Error("registry.providers must be an array");const e=o.providers.map((r,s)=>p(r,s)),n=new Set;for(const r of e){const s=r.id.toLowerCase();if(n.has(s))throw new Error(`registry: duplicate provider id "${r.id}"`);n.add(s)}return{version:o.version,updatedAt:typeof o.updatedAt=="string"?o.updatedAt:void 0,providers:e}}function p(t,o){if(!t||typeof t!="object")throw new Error(`registry.providers[${o}]: not an object`);const e=t,n=e.id,r=e.entry,s=e.label,u=e.version;if(typeof n!="string"||!f.test(n.trim()))throw new Error(`registry.providers[${o}].id must match ${f}`);if(typeof s!="string"||!s.trim())throw new Error(`registry.providers[${o}].label must be a non-empty string`);if(typeof u!="string"||!u.trim())throw new Error(`registry.providers[${o}].version must be a non-empty string`);const i=e.embedBase;if(i!==void 0&&(typeof i!="string"||!/^https:\/\/[^/]+/i.test(i.trim())))throw new Error(`registry.providers[${o}].embedBase must be an https URL`);if((typeof r!="string"||!r.trim())&&i===void 0)throw new Error(`registry.providers[${o}].entry must be a non-empty string`);if(e.sha256!==void 0&&(typeof e.sha256!="string"||!m.test(e.sha256.trim())))throw new Error(`registry.providers[${o}].sha256 must be 64 hex chars`);if(e.mature!==void 0&&typeof e.mature!="boolean")throw new Error(`registry.providers[${o}].mature must be boolean`);if(e.kind!==void 0&&e.kind!=="anime"&&e.kind!=="asmr"&&e.kind!=="manga"&&e.kind!=="tv")throw new Error(`registry.providers[${o}].kind must be anime|asmr|manga|tv`);if(e.sub!==void 0&&e.sub!=="soft"&&e.sub!=="hard"&&e.sub!=="mixed")throw new Error(`registry.providers[${o}].sub must be soft|hard|mixed`);if(e.tier!==void 0&&e.tier!=="direct"&&e.tier!=="embed"&&e.tier!=="cookie")throw new Error(`registry.providers[${o}].tier must be direct|embed|cookie`);let a;if(e.modes!==void 0){if(!Array.isArray(e.modes))throw new Error(`registry.providers[${o}].modes must be an array`);a=[];for(const d of e.modes){if(d!=="sub"&&d!=="dub")throw new Error(`registry.providers[${o}].modes must contain only sub|dub`);a.includes(d)||a.push(d)}}return{id:n.trim(),label:s.trim(),version:u.trim(),entry:typeof r=="string"?r.trim():void 0,embedBase:typeof i=="string"?i.trim().replace(/\/+$/,""):void 0,sha256:typeof e.sha256=="string"?e.sha256.trim().toLowerCase():void 0,minDango:typeof e.minDango=="string"?e.minDango:void 0,mature:typeof e.mature=="boolean"?e.mature:!1,kind:e.kind==="asmr"||e.kind==="manga"||e.kind==="tv"?e.kind:"anime",sub:e.sub,tier:e.tier,modes:a,browse:y(e.browse,o),proxy:b(e.proxy,o),auth:e.auth==="cookie"?"cookie":void 0,enabledByDefault:e.enabledByDefault!==!1}}function b(t,o){if(t===void 0)return;if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`registry.providers[${o}].proxy must be an object`);const e=t,n={};for(const r of["hosts","suffixes","mirrors"])if(e[r]!==void 0){if(!Array.isArray(e[r])||!e[r].every(s=>typeof s=="string"&&s))throw new Error(`registry.providers[${o}].proxy.${r} must be a non-empty string array`);n[r]=e[r].map(s=>s.trim().toLowerCase())}for(const r of["referer","origin"])if(e[r]!==void 0){if(typeof e[r]!="string"||!/^https:\/\//i.test(e[r].trim()))throw new Error(`registry.providers[${o}].proxy.${r} must be an https URL`);n[r]=e[r].trim()}for(const r of["originFromReferer","scrape","cfCookie","embedPatch"])if(e[r]!==void 0){if(typeof e[r]!="boolean")throw new Error(`registry.providers[${o}].proxy.${r} must be boolean`);n[r]=e[r]}return n}function y(t,o){if(t===void 0)return;if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`registry.providers[${o}].browse must be an object`);const e=t,n={};for(const r of["genre","order","studio","sort"])if(e[r]!==void 0){if(typeof e[r]!="boolean")throw new Error(`registry.providers[${o}].browse.${r} must be boolean`);n[r]=e[r]}if(e.pageSize!==void 0){if(typeof e.pageSize!="number"||!Number.isInteger(e.pageSize)||e.pageSize<1)throw new Error(`registry.providers[${o}].browse.pageSize must be a positive integer`);n.pageSize=e.pageSize}return n}export{g as parseRegistry};
|
|
@@ -1,226 +1 @@
|
|
|
1
|
-
function
|
|
2
|
-
return s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
3
|
-
}
|
|
4
|
-
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
|
-
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
|
-
const trailingNum = t.match(/\b(\d+)\s*(season)?\s*$/);
|
|
128
|
-
if (trailingNum)
|
|
129
|
-
return trailingNum[1];
|
|
130
|
-
return '';
|
|
131
|
-
}
|
|
132
|
-
function distinctiveTokens(s) {
|
|
133
|
-
return [...new Set(tokenize(s).filter((w) => !STOPWORDS.has(w)))];
|
|
134
|
-
}
|
|
135
|
-
function titleSimilarity(query, candidate) {
|
|
136
|
-
const q = normalizeCompact(query);
|
|
137
|
-
const c = normalizeCompact(candidate);
|
|
138
|
-
if (!q || !c)
|
|
139
|
-
return 0;
|
|
140
|
-
if (q === c)
|
|
141
|
-
return 1;
|
|
142
|
-
let score = bigramDice(q, c);
|
|
143
|
-
if (q.startsWith(c) || c.startsWith(q)) {
|
|
144
|
-
const prefixScore = 0.5 + 0.5 * (Math.min(q.length, c.length) / Math.max(q.length, c.length));
|
|
145
|
-
if (prefixScore > score)
|
|
146
|
-
score = prefixScore;
|
|
147
|
-
}
|
|
148
|
-
const qD = distinctiveTokensExcludingSeason(query);
|
|
149
|
-
const cD = distinctiveTokensExcludingSeason(candidate);
|
|
150
|
-
if (qD.length > 0 && cD.length >= 2) {
|
|
151
|
-
const cSet = new Set(cD);
|
|
152
|
-
let hits = 0;
|
|
153
|
-
for (const t of qD) {
|
|
154
|
-
if (cSet.has(t))
|
|
155
|
-
hits++;
|
|
156
|
-
}
|
|
157
|
-
const coverage = hits / Math.min(qD.length, cD.length);
|
|
158
|
-
if (coverage > score)
|
|
159
|
-
score = coverage;
|
|
160
|
-
}
|
|
161
|
-
const qMarker = detectSeasonMarker(query);
|
|
162
|
-
if (qMarker) {
|
|
163
|
-
const cMarker = detectSeasonMarker(candidate);
|
|
164
|
-
if (cMarker === qMarker) {
|
|
165
|
-
score += 0.25;
|
|
166
|
-
}
|
|
167
|
-
else if (!cMarker && score >= 0.5) {
|
|
168
|
-
score -= 0.25;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return score >= 1 ? 0.99 : score;
|
|
172
|
-
}
|
|
173
|
-
export function pickBestMatch(candidates, targets, minScore = 0.6) {
|
|
174
|
-
if (!candidates.length)
|
|
175
|
-
return null;
|
|
176
|
-
const cleanTargets = targets.filter((t) => !!t && t.trim().length > 0);
|
|
177
|
-
if (!cleanTargets.length)
|
|
178
|
-
return null;
|
|
179
|
-
let best = null;
|
|
180
|
-
for (const item of candidates) {
|
|
181
|
-
if (!item.title)
|
|
182
|
-
continue;
|
|
183
|
-
let score = 0;
|
|
184
|
-
for (const target of cleanTargets) {
|
|
185
|
-
const s = titleSimilarity(target, item.title);
|
|
186
|
-
if (s > score)
|
|
187
|
-
score = s;
|
|
188
|
-
}
|
|
189
|
-
if (!best || score > best.score)
|
|
190
|
-
best = { item, score };
|
|
191
|
-
}
|
|
192
|
-
return best && best.score >= minScore ? best : null;
|
|
193
|
-
}
|
|
194
|
-
export function buildQueryVariants(title, romaji) {
|
|
195
|
-
const seen = new Set();
|
|
196
|
-
const out = [];
|
|
197
|
-
const push = (q) => {
|
|
198
|
-
const v = q.replace(/\s+/g, ' ').trim();
|
|
199
|
-
if (v && !seen.has(v.toLowerCase())) {
|
|
200
|
-
seen.add(v.toLowerCase());
|
|
201
|
-
out.push(v);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
if (romaji && romaji.trim())
|
|
205
|
-
push(romaji);
|
|
206
|
-
push(title);
|
|
207
|
-
if (/^re\s*zero$/i.test(title.trim()))
|
|
208
|
-
push('Re Zero');
|
|
209
|
-
const stripped = (s) => s.replace(/[^\p{L}\p{N}\s]/gu, ' ');
|
|
210
|
-
push(stripped(title));
|
|
211
|
-
if (romaji)
|
|
212
|
-
push(stripped(romaji));
|
|
213
|
-
const words = stripped(title).split(/\s+/).filter(Boolean);
|
|
214
|
-
if (words.length > 3)
|
|
215
|
-
push(words.slice(0, 3).join(' '));
|
|
216
|
-
if (words.length > 4)
|
|
217
|
-
push(words.slice(0, 4).join(' '));
|
|
218
|
-
const tokens = distinctiveTokens(title)
|
|
219
|
-
.concat(romaji ? distinctiveTokens(romaji) : [])
|
|
220
|
-
.filter((w) => w.length >= 4);
|
|
221
|
-
const unique = [...new Set(tokens)];
|
|
222
|
-
unique.sort((a, b) => b.length - a.length);
|
|
223
|
-
for (const t of unique.slice(0, 4))
|
|
224
|
-
push(t);
|
|
225
|
-
return out;
|
|
226
|
-
}
|
|
1
|
+
function h(e){return e.toLowerCase().replace(/[^a-z0-9]/g,"")}function u(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(n=>n.length>=2)}function x(e,n){if(e.length<2||n.length<2)return e===n?1:0;const c=i=>{const g=new Map;for(let l=0;l<i.length-1;l++){const f=i.slice(l,l+2);g.set(f,(g.get(f)||0)+1)}return g},o=c(e),t=c(n);let s=0,r=0,a=0;return o.forEach(i=>s+=i),t.forEach(i=>r+=i),o.forEach((i,g)=>{const l=t.get(g);l&&(a+=Math.min(i,l))}),2*a/(s+r)}const v=new Set(["the","animation","no","wa","ga","wo","ni","de","to","e","o","x","ii","yo","ova","oad","hd","ep","episode","season","part","hen","a"]),S=new Set(["season","2nd","3rd","4th","5th","ii","iii","iv","v","vi","vii","viii","ix","x","two","three","four","five","six","seven","eight","nine","ten"]);function d(e){return[...new Set(u(e).filter(n=>!v.has(n)&&!S.has(n)))]}const m={ii:2,iii:3,iv:4,v:5,vi:6,vii:7,viii:8,ix:9,x:10},b={two:2,three:3,four:4,five:5,six:6,seven:7,eight:8,nine:9,ten:10};function p(e){const n=e.toLowerCase(),c=n.match(/\bseason\s*(\d+|ii|iii|iv|v|vi|vii|viii|ix|x|two|three|four|five|six|seven|eight|nine|ten)\b/);if(c){const r=c[1],a=m[r],i=b[r];return String(a??i??r)}const o=n.match(/\b(\d+)(st|nd|rd|th)\b/);if(o)return o[1];const t=n.match(/\b(ii|iii|iv|v|vi|vii|viii|ix|x)\b(?!\s*(st|nd|rd|th|season))/);if(t)return String(m[t[1]]);const s=n.match(/\b(\d+)\s*(season)?\s*$/);return s?s[1]:""}function w(e){return[...new Set(u(e).filter(n=>!v.has(n)))]}function M(e,n){const c=h(e),o=h(n);if(!c||!o)return 0;if(c===o)return 1;let t=x(c,o);if(c.startsWith(o)||o.startsWith(c)){const i=.5+.5*(Math.min(c.length,o.length)/Math.max(c.length,o.length));i>t&&(t=i)}const s=d(e),r=d(n);if(s.length>0&&r.length>=2){const i=new Set(r);let g=0;for(const f of s)i.has(f)&&g++;const l=g/Math.min(s.length,r.length);l>t&&(t=l)}const a=p(e);if(a){const i=p(n);i===a?t+=.25:!i&&t>=.5&&(t-=.25)}return t>=1?.99:t}function T(e,n,c=.6){if(!e.length)return null;const o=n.filter(s=>!!s&&s.trim().length>0);if(!o.length)return null;let t=null;for(const s of e){if(!s.title)continue;let r=0;for(const a of o){const i=M(a,s.title);i>r&&(r=i)}(!t||r>t.score)&&(t={item:s,score:r})}return t&&t.score>=c?t:null}function k(e,n){const c=new Set,o=[],t=g=>{const l=g.replace(/\s+/g," ").trim();l&&!c.has(l.toLowerCase())&&(c.add(l.toLowerCase()),o.push(l))};n&&n.trim()&&t(n),t(e),/^re\s*zero$/i.test(e.trim())&&t("Re Zero");const s=g=>g.replace(/[^\p{L}\p{N}\s]/gu," ");t(s(e)),n&&t(s(n));const r=s(e).split(/\s+/).filter(Boolean);r.length>3&&t(r.slice(0,3).join(" ")),r.length>4&&t(r.slice(0,4).join(" "));const a=w(e).concat(n?w(n):[]).filter(g=>g.length>=4),i=[...new Set(a)];i.sort((g,l)=>l.length-g.length);for(const g of i.slice(0,4))t(g);return o}export{k as buildQueryVariants,T as pickBestMatch};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/server/dist/rclone.js
CHANGED
|
@@ -1,120 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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();
|
|
1
|
+
import{spawn as l,execFile as m}from"child_process";import a from"./logger.js";import{CONFIG as c}from"./config.js";class u{activeRemote=null;executeCommand(t){return new Promise((e,r)=>{m("rclone",t,(i,o,n)=>{if(i)return n&&a.warn({stderr:n},"Rclone command warning"),r(new Error(n||i.message));e(o.trim())})})}executeRcloneArgs(t){return new Promise((e,r)=>{const i=l("rclone",t,{stdio:"ignore"});i.on("close",o=>{o===0?e():r(new Error(`Rclone exited with code ${o}`))}),i.on("error",o=>r(o))})}async listRemotes(){try{return(await this.executeCommand(["listremotes"])).split(`
|
|
2
|
+
`).map(e=>e.trim()).filter(e=>e!=="").map(e=>e.replace(/:$/,""))}catch{return[]}}async init(){try{await this.executeCommand(["version"]);const t=await this.listRemotes();if(c.RCLONE_REMOTE){const e=t.find(r=>r.toLowerCase()===c.RCLONE_REMOTE?.toLowerCase());if(e)return this.activeRemote=e,a.info(`Rclone initialized with manual remote: ${this.activeRemote}`),!0;a.warn(`Configured RCLONE_REMOTE '${c.RCLONE_REMOTE}' not found in rclone listremotes.`)}return t.length>0&&!c.RCLONE_REMOTE&&a.info({remotes:t},"Rclone available but no manual remote is configured in settings."),!1}catch(t){return a.warn({err:t},"Rclone initialization failed"),!1}}isActive(){return this.activeRemote!==null}getRemoteName(){return this.activeRemote||"unknown"}async downloadFile(t,e,r){if(!this.activeRemote)throw new Error("Rclone not active");const i=`${this.activeRemote}:${t}/${e}`;await this.executeRcloneArgs(["copyto",i,r])}async uploadFile(t,e,r){if(!this.activeRemote)throw new Error("Rclone not active");const i=`${this.activeRemote}:${e}/${r}`;await this.executeRcloneArgs(["copyto",t,i])}executeRcloneArgsWithOutput(t){return new Promise((e,r)=>{const i=l("rclone",t,{stdio:"pipe"});let o="",n="";i.stdout?.on("data",s=>o+=s),i.stderr?.on("data",s=>n+=s),i.on("close",s=>{s===0?e(o.trim()):(n&&a.warn({stderr:n},"Rclone command warning"),r(new Error(n||`Rclone exited with code ${s}`)))}),i.on("error",s=>r(s))})}async fileExists(t,e){if(!this.activeRemote)return!1;try{const r=`${this.activeRemote}:${t}/${e}`,i=await this.executeRcloneArgsWithOutput(["lsjson",r]),o=JSON.parse(i);return o&&o.length>0}catch{return!1}}}const w=new u;export{w as rcloneService};
|
|
@@ -1,42 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getDrizzle } from '../db/drizzle.js';
|
|
3
|
-
import { asmrProgress } from '../db/schema-asmr.js';
|
|
4
|
-
import { makeLibraryRepo } from './library.repository.js';
|
|
5
|
-
export const ASMR_STATUSES = ['Listening', 'Completed', 'On-Hold', 'Dropped', 'Planned'];
|
|
6
|
-
export function buildAsmrId(rjCode) {
|
|
7
|
-
return String(rjCode).trim().toUpperCase();
|
|
8
|
-
}
|
|
9
|
-
export const AsmrLibraryRepository = {
|
|
10
|
-
...makeLibraryRepo('asmr_library'),
|
|
11
|
-
upsert: (db, data) => getDrizzle(db).run(sql `
|
|
1
|
+
import{eq as i,inArray as l,sql as a}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";import{asmrProgress as n}from"../db/schema-asmr.js";import{makeLibraryRepo as u}from"./library.repository.js";const C=["Listening","Completed","On-Hold","Dropped","Planned"];function A(t){return String(t).trim().toUpperCase()}const g={...u("asmr_library"),upsert:(t,r)=>s(t).run(a`
|
|
12
2
|
INSERT INTO asmr_library (id, rjCode, title, thumbnail, status, isAdult, updatedAt)
|
|
13
|
-
VALUES (${
|
|
3
|
+
VALUES (${r.id}, ${r.rjCode}, ${r.title}, ${r.thumbnail}, ${r.status}, ${r.isAdult?1:0}, strftime('%s', 'now'))
|
|
14
4
|
ON CONFLICT(id) DO UPDATE SET
|
|
15
5
|
rjCode = COALESCE(NULLIF(EXCLUDED.rjCode, ''), asmr_library.rjCode),
|
|
16
6
|
title = COALESCE(NULLIF(EXCLUDED.title, ''), asmr_library.title),
|
|
17
7
|
thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), asmr_library.thumbnail),
|
|
18
8
|
status = COALESCE(NULLIF(EXCLUDED.status, ''), asmr_library.status),
|
|
19
9
|
isAdult = COALESCE(EXCLUDED.isAdult, asmr_library.isAdult),
|
|
20
|
-
updatedAt = strftime('%s', 'now')`),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
getByWork: (db, workId) => getDrizzle(db).all(sql `
|
|
26
|
-
SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${workId} ORDER BY updatedAt DESC`),
|
|
27
|
-
getTrack: async (db, workId, trackIndex) => {
|
|
28
|
-
const rows = await getDrizzle(db).all(sql `
|
|
29
|
-
SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${workId} AND trackIndex = ${trackIndex}`);
|
|
30
|
-
return rows[0];
|
|
31
|
-
},
|
|
32
|
-
getLatest: async (db, workId) => {
|
|
33
|
-
const rows = await getDrizzle(db).all(sql `
|
|
34
|
-
SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${workId} ORDER BY updatedAt DESC LIMIT 1`);
|
|
35
|
-
return rows[0];
|
|
36
|
-
},
|
|
37
|
-
upsert: (db, data) => getDrizzle(db).run(sql `
|
|
10
|
+
updatedAt = strftime('%s', 'now')`),touchProgress:(t,r,e)=>s(t).run(a`
|
|
11
|
+
UPDATE asmr_library SET lastTrackIndex = ${e.trackIndex}, lastTrackLabel = ${e.trackLabel}, lastPosition = ${e.position}, updatedAt = strftime('%s', 'now') WHERE id = ${r}`)},c={getByWork:(t,r)=>s(t).all(a`
|
|
12
|
+
SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${r} ORDER BY updatedAt DESC`),getTrack:async(t,r,e)=>(await s(t).all(a`
|
|
13
|
+
SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${r} AND trackIndex = ${e}`))[0],getLatest:async(t,r)=>(await s(t).all(a`
|
|
14
|
+
SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${r} ORDER BY updatedAt DESC LIMIT 1`))[0],upsert:(t,r)=>s(t).run(a`
|
|
38
15
|
INSERT INTO asmr_progress (workId, trackIndex, trackLabel, currentTime, duration, title, thumbnail, rjCode, isAdult, updatedAt)
|
|
39
|
-
VALUES (${
|
|
16
|
+
VALUES (${r.workId}, ${r.trackIndex}, ${r.trackLabel}, ${r.currentTime}, ${r.duration}, ${r.title??null}, ${r.thumbnail??null}, ${r.rjCode??null}, ${r.isAdult??null}, strftime('%s', 'now'))
|
|
40
17
|
ON CONFLICT(workId, trackIndex) DO UPDATE SET
|
|
41
18
|
trackLabel = COALESCE(NULLIF(EXCLUDED.trackLabel, ''), asmr_progress.trackLabel),
|
|
42
19
|
currentTime = EXCLUDED.currentTime,
|
|
@@ -45,18 +22,8 @@ export const AsmrProgressRepository = {
|
|
|
45
22
|
thumbnail = COALESCE(EXCLUDED.thumbnail, asmr_progress.thumbnail),
|
|
46
23
|
rjCode = COALESCE(EXCLUDED.rjCode, asmr_progress.rjCode),
|
|
47
24
|
isAdult = COALESCE(EXCLUDED.isAdult, asmr_progress.isAdult),
|
|
48
|
-
updatedAt = strftime('%s', 'now')`),
|
|
49
|
-
|
|
50
|
-
deleteMany: (db, ids) => {
|
|
51
|
-
if (ids.length === 0)
|
|
52
|
-
return Promise.resolve();
|
|
53
|
-
return getDrizzle(db).delete(asmrProgress).where(inArray(asmrProgress.workId, ids));
|
|
54
|
-
},
|
|
55
|
-
deleteTrack: (db, workId, trackIndex) => getDrizzle(db).run(sql `
|
|
56
|
-
DELETE FROM asmr_progress WHERE workId = ${workId} AND trackIndex = ${trackIndex}`),
|
|
57
|
-
getContinueListening: (db, limit) => {
|
|
58
|
-
const limitClause = typeof limit === 'number' ? `LIMIT ${limit}` : '';
|
|
59
|
-
return getDrizzle(db).all(sql `
|
|
25
|
+
updatedAt = strftime('%s', 'now')`),deleteByWork:(t,r)=>s(t).delete(n).where(i(n.workId,r)),deleteMany:(t,r)=>r.length===0?Promise.resolve():s(t).delete(n).where(l(n.workId,r)),deleteTrack:(t,r,e)=>s(t).run(a`
|
|
26
|
+
DELETE FROM asmr_progress WHERE workId = ${r} AND trackIndex = ${e}`),getContinueListening:(t,r)=>{const e=typeof r=="number"?`LIMIT ${r}`:"";return s(t).all(a`
|
|
60
27
|
SELECT p.workId as id,
|
|
61
28
|
COALESCE(l.rjCode, p.rjCode, p.workId) as rjCode,
|
|
62
29
|
COALESCE(l.title, p.title) as title,
|
|
@@ -72,6 +39,4 @@ export const AsmrProgressRepository = {
|
|
|
72
39
|
WHERE p.rn = 1
|
|
73
40
|
AND (l.status IS NULL OR l.status = 'Listening')
|
|
74
41
|
ORDER BY p.updatedAt DESC
|
|
75
|
-
${
|
|
76
|
-
},
|
|
77
|
-
};
|
|
42
|
+
${a.raw(e)}`)}};export{C as ASMR_STATUSES,g as AsmrLibraryRepository,c as AsmrProgressRepository,A as buildAsmrId};
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getDrizzle } from '../db/drizzle.js';
|
|
3
|
-
export const InsightsRepository = {
|
|
4
|
-
getCoreStats: async (db) => (await getDrizzle(db).all(sql `
|
|
1
|
+
import{sql as t}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";const w={getCoreStats:async e=>(await s(e).all(t`
|
|
5
2
|
SELECT
|
|
6
3
|
(SELECT SUM(
|
|
7
4
|
CASE WHEN we.currentTime > 0 THEN we.currentTime
|
|
@@ -18,12 +15,9 @@ export const InsightsRepository = {
|
|
|
18
15
|
(SELECT COUNT(*) FROM watched_episodes) as totalEpisodes,
|
|
19
16
|
(SELECT COUNT(DISTINCT showId) FROM watched_episodes) as totalAnime,
|
|
20
17
|
(SELECT COUNT(*) FROM watchlist WHERE status = 'Completed') as completedCount,
|
|
21
|
-
(SELECT COUNT(*) FROM watchlist) as totalWatchlist`))[0],
|
|
22
|
-
|
|
23
|
-
SELECT
|
|
24
|
-
getHourlyDist: (db) => getDrizzle(db).all(sql `
|
|
25
|
-
SELECT strftime('%H', watchedAt) as hour, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY hour`),
|
|
26
|
-
getSeasonality: (db) => getDrizzle(db).all(sql `
|
|
18
|
+
(SELECT COUNT(*) FROM watchlist) as totalWatchlist`))[0],getActivityGrid:e=>s(e).all(t`
|
|
19
|
+
SELECT date(watchedAt) as day, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY day`),getHourlyDist:e=>s(e).all(t`
|
|
20
|
+
SELECT strftime('%H', watchedAt) as hour, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY hour`),getSeasonality:e=>s(e).all(t`
|
|
27
21
|
SELECT strftime('%m', we.watchedAt) as month, SUM(
|
|
28
22
|
CASE WHEN we.currentTime > 0 THEN we.currentTime
|
|
29
23
|
WHEN we.currentTime = 0 AND we.duration = 0 THEN
|
|
@@ -36,8 +30,7 @@ export const InsightsRepository = {
|
|
|
36
30
|
ELSE 0 END) as seconds
|
|
37
31
|
FROM watched_episodes we
|
|
38
32
|
LEFT JOIN shows_meta sm ON sm.id = we.showId
|
|
39
|
-
GROUP BY month`),
|
|
40
|
-
getAllWatches: (db) => getDrizzle(db).all(sql `
|
|
33
|
+
GROUP BY month`),getAllWatches:e=>s(e).all(t`
|
|
41
34
|
SELECT we.watchedAt, we.currentTime,
|
|
42
35
|
CASE WHEN we.currentTime > 0 THEN we.currentTime
|
|
43
36
|
WHEN we.currentTime = 0 AND we.duration = 0 THEN
|
|
@@ -50,27 +43,23 @@ export const InsightsRepository = {
|
|
|
50
43
|
ELSE 0 END as effectiveSeconds
|
|
51
44
|
FROM watched_episodes we
|
|
52
45
|
LEFT JOIN shows_meta sm ON sm.id = we.showId
|
|
53
|
-
ORDER BY we.watchedAt ASC`),
|
|
54
|
-
getWatchedShowsMeta: (db) => getDrizzle(db).all(sql `
|
|
46
|
+
ORDER BY we.watchedAt ASC`),getWatchedShowsMeta:e=>s(e).all(t`
|
|
55
47
|
SELECT DISTINCT sm.id, sm.genres, sm.popularityScore
|
|
56
48
|
FROM shows_meta sm
|
|
57
|
-
JOIN watched_episodes we ON sm.id = we.showId`),
|
|
58
|
-
getDroppedShows: (db) => getDrizzle(db).all(sql `
|
|
49
|
+
JOIN watched_episodes we ON sm.id = we.showId`),getDroppedShows:e=>s(e).all(t`
|
|
59
50
|
SELECT w.id, w.name, MAX(we.watchedAt) as lastActivity
|
|
60
51
|
FROM watchlist w
|
|
61
52
|
JOIN watched_episodes we ON w.id = we.showId
|
|
62
53
|
WHERE w.status = 'Watching' AND NOT (we.currentTime = 0 AND we.duration = 0)
|
|
63
54
|
GROUP BY w.id
|
|
64
|
-
HAVING lastActivity < date('now', '-90 days')`),
|
|
65
|
-
getCompletionVelocities: (db) => getDrizzle(db).all(sql `
|
|
55
|
+
HAVING lastActivity < date('now', '-90 days')`),getCompletionVelocities:e=>s(e).all(t`
|
|
66
56
|
SELECT
|
|
67
57
|
(julianday(MAX(we.watchedAt)) - julianday(MIN(we.watchedAt))) as daysToFinish
|
|
68
58
|
FROM watchlist w
|
|
69
59
|
JOIN watched_episodes we ON w.id = we.showId
|
|
70
60
|
WHERE w.status = 'Completed'
|
|
71
61
|
GROUP BY w.id
|
|
72
|
-
HAVING SUM(we.currentTime) > 0`),
|
|
73
|
-
getWatchedEpisodesWithMeta: (db) => getDrizzle(db).all(sql `
|
|
62
|
+
HAVING SUM(we.currentTime) > 0`),getWatchedEpisodesWithMeta:e=>s(e).all(t`
|
|
74
63
|
SELECT
|
|
75
64
|
we.showId,
|
|
76
65
|
we.currentTime,
|
|
@@ -91,8 +80,7 @@ export const InsightsRepository = {
|
|
|
91
80
|
sm.englishName,
|
|
92
81
|
sm.thumbnail
|
|
93
82
|
FROM watched_episodes we
|
|
94
|
-
JOIN shows_meta sm ON we.showId = sm.id`),
|
|
95
|
-
getLibraryShowsWithGenres: (db) => getDrizzle(db).all(sql `
|
|
83
|
+
JOIN shows_meta sm ON we.showId = sm.id`),getLibraryShowsWithGenres:e=>s(e).all(t`
|
|
96
84
|
SELECT
|
|
97
85
|
w.id,
|
|
98
86
|
w.status,
|
|
@@ -101,6 +89,4 @@ export const InsightsRepository = {
|
|
|
101
89
|
(SELECT COUNT(*) FROM watched_episodes we WHERE we.showId = w.id) as episodesWatched,
|
|
102
90
|
COALESCE(sm.popularityScore, 0) as popularityScore
|
|
103
91
|
FROM watchlist w
|
|
104
|
-
LEFT JOIN shows_meta sm ON sm.id = w.id`),
|
|
105
|
-
getAllAnilistIds: (db) => getDrizzle(db).all(sql `SELECT anilistId FROM shows_meta WHERE anilistId IS NOT NULL`),
|
|
106
|
-
};
|
|
92
|
+
LEFT JOIN shows_meta sm ON sm.id = w.id`),getAllAnilistIds:e=>s(e).all(t`SELECT anilistId FROM shows_meta WHERE anilistId IS NOT NULL`)};export{w as InsightsRepository};
|