@serifpersia/dango 3.6.8 → 3.7.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.
Files changed (154) hide show
  1. package/README.md +2 -2
  2. package/client/dist/assets/{AnimeCard-DraA_TF9.js → AnimeCard-VxBIpH-H.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-BJ7pzs-P.js → AnimeInfo-D_Yk-Yyk.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-B_esUr5C.js → AnimeInfoPage-DprI9g3s.js} +2 -2
  5. package/client/dist/assets/{Asmr-Ckuh2xfW.js → Asmr-CwgL7nn4.js} +2 -2
  6. package/client/dist/assets/{AsmrResetProgressModal-BS6UFJN4.js → AsmrResetProgressModal-Cw_1gqgF.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-DwHCd9yB.js → AvSyncCalibrator-Cf_hnFZR.js} +1 -1
  8. package/client/dist/assets/{Home-DuM1EuvN.js → Home-BaR_V71s.js} +1 -1
  9. package/client/dist/assets/{Insights-D3Pd9c9V.js → Insights-CUeDNk4y.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-Dm2yVn0-.js → LibraryListPage-AoI14-dx.js} +1 -1
  11. package/client/dist/assets/ListeningList-BZexqK7W.js +1 -0
  12. package/client/dist/assets/{Manga-JwprMKBk.js → Manga-CPOuaQjZ.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-DaeyJl2p.js → MangaInfoPage-Bo-FOngx.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-COg0fHI9.js → MangaReadPage-DuGPnGRF.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-kguXv0_M.js → MangaResetProgressModal-CUvP02Ve.js} +1 -1
  16. package/client/dist/assets/{Mature-IHDaasrs.js → Mature-B1ZOlHvr.js} +1 -1
  17. package/client/dist/assets/{MediaCard-CpwsrXo5.js → MediaCard-C1kVq8jh.js} +1 -1
  18. package/client/dist/assets/{OptionListMenu-11CcAsy8.js → OptionListMenu-vUGV_jq6.js} +1 -1
  19. package/client/dist/assets/{Player-DQoEQkHL.js → Player-xUmvl3aX.js} +2 -2
  20. package/client/dist/assets/{PlayerSettings-D4Ud4kv4.js → PlayerSettings-B63XtOiv.js} +1 -1
  21. package/client/dist/assets/{QueueOptionsButton-B8PlPKjC.js → QueueOptionsButton-BGz5pDT3.js} +1 -1
  22. package/client/dist/assets/{QueueRail-B4Dok7U3.js → QueueRail-CQzlXS9D.js} +1 -1
  23. package/client/dist/assets/{ReadingList-BCsxIz57.js → ReadingList-BVU8jeNE.js} +1 -1
  24. package/client/dist/assets/{ResetProgressModal-BXHxSfy-.js → ResetProgressModal-MfdZU4Jw.js} +1 -1
  25. package/client/dist/assets/{Search-PK1ds07O.js → Search-Dl_zHidX.js} +1 -1
  26. package/client/dist/assets/{Settings-Bxk_x55l.js → Settings-OXxsNnw3.js} +1 -1
  27. package/client/dist/assets/{SkeletonGrid-CpJCZSRN.js → SkeletonGrid-BaEViX2G.js} +1 -1
  28. package/client/dist/assets/{Trackers-RieCh_84.js → Trackers-CF2lf0FQ.js} +1 -1
  29. package/client/dist/assets/{Tv-DLytPJUj.js → Tv-C3lwYGvC.js} +1 -1
  30. package/client/dist/assets/{TvInfo-C8djKebR.js → TvInfo-CgduZDby.js} +1 -1
  31. package/client/dist/assets/{TvPopup-40JGGJDk.js → TvPopup-Cwcs0iMP.js} +1 -1
  32. package/client/dist/assets/{TvSearch-A3hbospf.js → TvSearch-BCYb067l.js} +1 -1
  33. package/client/dist/assets/{TvWatchlist-DuXjqmZM.js → TvWatchlist-Bk_L18MC.js} +1 -1
  34. package/client/dist/assets/{Watchlist-B9wGBh0u.js → Watchlist-B0bqJqc4.js} +1 -1
  35. package/client/dist/assets/{hls-CZ5X3Ya-.js → hls-CBz3B7bO.js} +1 -1
  36. package/client/dist/assets/{index-dRhc9-ol.js → index-DxRnpRhu.js} +19 -19
  37. package/client/dist/assets/{useAnimeInfoData-ZzDtLfvD.js → useAnimeInfoData-VGk7ptlM.js} +1 -1
  38. package/client/dist/assets/{useAsmr-CKOdHgol.js → useAsmr-CxGWhApO.js} +1 -1
  39. package/client/dist/assets/useAsmrPopup-HC0kPwIY.js +1 -0
  40. package/client/dist/assets/{useMangaLibrary-CKATe31P.js → useMangaLibrary-8FuAAh_p.js} +1 -1
  41. package/client/dist/assets/{useMangaPopup-C75QuYgY.js → useMangaPopup-0vWil7K4.js} +1 -1
  42. package/client/dist/assets/{useProviders-CMtvZbEi.js → useProviders-DHClk0cR.js} +1 -1
  43. package/client/dist/assets/{useTv-DUX4zYMr.js → useTv-SJHkxQtj.js} +1 -1
  44. package/client/dist/assets/{useTvLibrary-By4Gt-R_.js → useTvLibrary-CnPxLtnZ.js} +1 -1
  45. package/client/dist/index.html +1 -1
  46. package/package.json +3 -19
  47. package/server/dist/server.js +1089 -1
  48. package/server/package.json +9 -13
  49. package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
  50. package/client/dist/assets/ListeningList-D-zt6EXZ.js +0 -1
  51. package/client/dist/assets/Music-BGH_0voi.css +0 -1
  52. package/client/dist/assets/Music-Dks4tPeK.js +0 -1
  53. package/client/dist/assets/Radio-BHK6LSk4.js +0 -1
  54. package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
  55. package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
  56. package/client/dist/assets/useAsmrPopup-SFHPhpcl.js +0 -1
  57. package/server/dist/app-auth.js +0 -1
  58. package/server/dist/app-hono.js +0 -48
  59. package/server/dist/db/drizzle.js +0 -1
  60. package/server/dist/db/schema-anime.js +0 -1
  61. package/server/dist/db/schema-asmr.js +0 -1
  62. package/server/dist/db/schema-manga.js +0 -1
  63. package/server/dist/db/schema-tv.js +0 -1
  64. package/server/dist/db.js +0 -1
  65. package/server/dist/discord-gateway.js +0 -1
  66. package/server/dist/discord-rpc.js +0 -1
  67. package/server/dist/github-sync.js +0 -1
  68. package/server/dist/google.js +0 -1
  69. package/server/dist/hono/asmr.js +0 -1
  70. package/server/dist/hono/auth.js +0 -26
  71. package/server/dist/hono/music.js +0 -1
  72. package/server/dist/hono/proxy.js +0 -80
  73. package/server/dist/hono/tv.js +0 -5
  74. package/server/dist/hono/watchlist.js +0 -5
  75. package/server/dist/lib/anilist.js +0 -151
  76. package/server/dist/lib/anischedule.js +0 -1
  77. package/server/dist/lib/insights.js +0 -1
  78. package/server/dist/lib/ipc.js +0 -1
  79. package/server/dist/lib/kitsu.js +0 -1
  80. package/server/dist/lib/mal-import.js +0 -1
  81. package/server/dist/lib/mal.js +0 -1
  82. package/server/dist/lib/migration.js +0 -1
  83. package/server/dist/lib/offline-db.js +0 -15
  84. package/server/dist/lib/potoken.js +0 -1
  85. package/server/dist/lib/recommendations.service.js +0 -1
  86. package/server/dist/lib/temp-ids.js +0 -1
  87. package/server/dist/lib/tmdb.js +0 -1
  88. package/server/dist/lib/tracker/anilist-tracker.js +0 -71
  89. package/server/dist/lib/tracker/manga-sync.service.js +0 -1
  90. package/server/dist/lib/tracker/sync-state.js +0 -1
  91. package/server/dist/lib/tracker/sync.service.js +0 -1
  92. package/server/dist/lib/translate.js +0 -3
  93. package/server/dist/lib/ump.js +0 -1
  94. package/server/dist/lib/ytmusic.js +0 -1
  95. package/server/dist/logger.js +0 -1
  96. package/server/dist/providers/manga/manga.types.js +0 -0
  97. package/server/dist/providers/provider.interface.js +0 -0
  98. package/server/dist/providers/remote-loader.js +0 -2
  99. package/server/dist/providers/remote-types.js +0 -1
  100. package/server/dist/providers/title-matching.js +0 -1
  101. package/server/dist/providers/tv.types.js +0 -0
  102. package/server/dist/rclone.js +0 -2
  103. package/server/dist/repositories/asmr.repository.js +0 -42
  104. package/server/dist/repositories/insights.repository.js +0 -92
  105. package/server/dist/repositories/library.repository.js +0 -3
  106. package/server/dist/repositories/mal-cache.repository.js +0 -1
  107. package/server/dist/repositories/manga.repository.js +0 -51
  108. package/server/dist/repositories/notifications.repository.js +0 -14
  109. package/server/dist/repositories/queue.repository.js +0 -19
  110. package/server/dist/repositories/settings.repository.js +0 -3
  111. package/server/dist/repositories/shows-meta.repository.js +0 -5
  112. package/server/dist/repositories/temp-show-ids.repository.js +0 -3
  113. package/server/dist/repositories/tv.repository.js +0 -50
  114. package/server/dist/repositories/watched-episodes.repository.js +0 -33
  115. package/server/dist/repositories/watchlist.repository.js +0 -9
  116. package/server/dist/scripts/backfill-episode-duration.js +0 -1
  117. package/server/dist/sync-payload.js +0 -1
  118. package/server/dist/sync.js +0 -13
  119. package/server/dist/trpc/context.js +0 -1
  120. package/server/dist/trpc/errors.js +0 -1
  121. package/server/dist/trpc/index.js +0 -1
  122. package/server/dist/trpc/router.js +0 -1
  123. package/server/dist/trpc/routers/asmr-library.js +0 -1
  124. package/server/dist/trpc/routers/asmr-progress.js +0 -1
  125. package/server/dist/trpc/routers/asmr.js +0 -1
  126. package/server/dist/trpc/routers/auth.js +0 -1
  127. package/server/dist/trpc/routers/continue-watching.js +0 -25
  128. package/server/dist/trpc/routers/data.js +0 -1
  129. package/server/dist/trpc/routers/discord.js +0 -1
  130. package/server/dist/trpc/routers/discovery.js +0 -1
  131. package/server/dist/trpc/routers/insights.js +0 -1
  132. package/server/dist/trpc/routers/manga-library.js +0 -1
  133. package/server/dist/trpc/routers/manga-progress.js +0 -1
  134. package/server/dist/trpc/routers/manga.js +0 -1
  135. package/server/dist/trpc/routers/music.js +0 -1
  136. package/server/dist/trpc/routers/notifications.js +0 -1
  137. package/server/dist/trpc/routers/progress.js +0 -1
  138. package/server/dist/trpc/routers/queue.js +0 -1
  139. package/server/dist/trpc/routers/radio.js +0 -1
  140. package/server/dist/trpc/routers/settings.js +0 -1
  141. package/server/dist/trpc/routers/tracker.js +0 -1
  142. package/server/dist/trpc/routers/tv-library.js +0 -1
  143. package/server/dist/trpc/routers/tv-progress.js +0 -1
  144. package/server/dist/trpc/routers/tv.js +0 -1
  145. package/server/dist/trpc/routers/watchlist.js +0 -3
  146. package/server/dist/trpc/validation.js +0 -1
  147. package/server/dist/utils/cache.utils.js +0 -1
  148. package/server/dist/utils/cookie.utils.js +0 -1
  149. package/server/dist/utils/db-utils.js +0 -5
  150. package/server/dist/utils/env.utils.js +0 -5
  151. package/server/dist/utils/http.utils.js +0 -1
  152. package/server/dist/utils/request-context.js +0 -1
  153. package/server/dist/utils/security.utils.js +0 -1
  154. /package/client/dist/assets/{Asmr-D1KuQwQy.css → useAsmrPopup-D1KuQwQy.css} +0 -0
@@ -1,33 +0,0 @@
1
- import{eq as o,sql as s}from"drizzle-orm";import{getDrizzle as r}from"../db/drizzle.js";import{watchedEpisodes as i}from"../db/schema-anime.js";const p={getByShowAndEpisode:async(t,e,a)=>(await r(t).all(s`
2
- SELECT currentTime, duration FROM watched_episodes WHERE showId = ${e} AND episodeNumber = ${a}`))[0],getWatchedEpisodeNumbers:async(t,e)=>(await r(t).all(s`SELECT episodeNumber FROM watched_episodes WHERE showId = ${e}`)).map(n=>n.episodeNumber),getByShow:(t,e)=>r(t).all(s`
3
- SELECT showId, episodeNumber, currentTime, duration, watchedAt FROM watched_episodes WHERE showId = ${e} ORDER BY CAST(episodeNumber AS REAL) ASC`),getLatestResumeProgress:async(t,e)=>(await r(t).all(s`
4
- SELECT showId, episodeNumber, currentTime, duration, watchedAt
5
- FROM watched_episodes
6
- WHERE showId = ${e} AND currentTime > 5 AND (duration <= 0 OR currentTime < duration * 0.8)
7
- ORDER BY watchedAt DESC
8
- LIMIT 1`))[0],upsert:(t,e)=>r(t).insert(i).values({showId:e.showId,episodeNumber:e.episodeNumber,watchedAt:s`CURRENT_TIMESTAMP`,currentTime:e.currentTime,duration:e.duration}).onConflictDoUpdate({target:[i.showId,i.episodeNumber],set:{watchedAt:s`CURRENT_TIMESTAMP`,currentTime:e.currentTime,duration:e.duration}}),insertIfMissing:(t,e)=>r(t).insert(i).values({showId:e.showId,episodeNumber:e.episodeNumber,watchedAt:e.watchedAt?s`${e.watchedAt}`:s`CURRENT_TIMESTAMP`,currentTime:0,duration:0}).onConflictDoNothing(),deleteByShow:(t,e)=>r(t).delete(i).where(o(i.showId,e)),getContinueWatching:(t,e)=>{const a=typeof e=="number"?`LIMIT ${e}`:"";return r(t).all(s`
9
- SELECT
10
- we.showId as _id,
11
- we.showId as id,
12
- COALESCE(NULLIF(w.name, ''), sm.name) as name,
13
- COALESCE(NULLIF(w.thumbnail, ''), NULLIF(sm.thumbnail, ''), '') as thumbnail,
14
- COALESCE(NULLIF(w.nativeName, ''), sm.nativeName) as nativeName,
15
- COALESCE(NULLIF(w.englishName, ''), sm.englishName) as englishName,
16
- COALESCE(NULLIF(w.type, ''), sm.type) as type,
17
- sm.episodeCount,
18
- sm.type as smType,
19
- sm.isAdult as isAdult,
20
- w.status as watchlistStatus,
21
- (SELECT COUNT(DISTINCT episodeNumber) FROM watched_episodes WHERE showId = we.showId) as watchedCount,
22
- we.episodeNumber, we.currentTime, we.duration, we.watchedAt
23
- FROM (
24
- SELECT *, ROW_NUMBER() OVER(PARTITION BY showId ORDER BY (currentTime > 0 OR duration > 0) DESC, watchedAt DESC) as rn
25
- FROM watched_episodes
26
- ) we
27
- LEFT JOIN watchlist w ON we.showId = w.id
28
- LEFT JOIN shows_meta sm ON we.showId = sm.id
29
- WHERE we.rn = 1
30
- AND (w.status IS NULL OR w.status = 'Watching')
31
- AND (w.id IS NOT NULL OR sm.id IS NOT NULL)
32
- ORDER BY we.watchedAt DESC
33
- ${s.raw(a)}`)}};export{p as WatchedEpisodesRepository};
@@ -1,9 +0,0 @@
1
- import{eq as E,inArray as m,sql as n}from"drizzle-orm";import{getDrizzle as i}from"../db/drizzle.js";import{watchlist as s}from"../db/schema-anime.js";const L={getById:async(e,t)=>(await i(e).all(n`SELECT * FROM watchlist WHERE id = ${t}`))[0],exists:async(e,t)=>{const a=await i(e).all(n`SELECT EXISTS(SELECT 1 FROM watchlist WHERE id = ${t}) as inWatchlist`);return!!(a[0]&&a[0].inWatchlist)},getAll:(e,t,a,l)=>{const r=n`SELECT * FROM watchlist`;return t&&t!=="All"&&r.append(n` WHERE status = ${t}`),r.append(n` ORDER BY rowid DESC`),a!==void 0&&l!==void 0&&r.append(n` LIMIT ${a} OFFSET ${l}`),i(e).all(r)},getCount:async(e,t)=>{const a=n`SELECT COUNT(*) as total FROM watchlist`;return t&&t!=="All"&&a.append(n` WHERE status = ${t}`),(await i(e).all(a))[0]?.total||0},upsert:(e,t)=>i(e).run(n`
2
- INSERT INTO watchlist (id, name, thumbnail, status, nativeName, englishName, type) VALUES (${t.id}, ${t.name}, ${t.thumbnail}, ${t.status}, ${t.nativeName}, ${t.englishName}, ${t.type})
3
- ON CONFLICT(id) DO UPDATE SET
4
- name = COALESCE(NULLIF(EXCLUDED.name, ''), watchlist.name),
5
- thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), watchlist.thumbnail),
6
- status = COALESCE(NULLIF(EXCLUDED.status, ''), watchlist.status),
7
- nativeName = COALESCE(NULLIF(EXCLUDED.nativeName, ''), watchlist.nativeName),
8
- englishName = COALESCE(NULLIF(EXCLUDED.englishName, ''), watchlist.englishName),
9
- type = COALESCE(NULLIF(EXCLUDED.type, ''), watchlist.type)`),updateStatus:(e,t,a)=>i(e).update(s).set({status:a}).where(E(s.id,t)),updateThumbnail:(e,t,a)=>!a||a.trim()===""?Promise.resolve():i(e).update(s).set({thumbnail:a}).where(E(s.id,t)),delete:(e,t)=>i(e).delete(s).where(E(s.id,t)),deleteMany:(e,t)=>t.length===0?Promise.resolve():i(e).delete(s).where(m(s.id,t)),updateStatusMany:(e,t,a)=>t.length===0?Promise.resolve():i(e).update(s).set({status:a}).where(m(s.id,t)),getWatchingShows:e=>i(e).all(n`SELECT id, name, thumbnail, nativeName, englishName FROM watchlist WHERE status = 'Watching'`),getMissingThumbnails:e=>i(e).all(n`SELECT id, name FROM watchlist WHERE thumbnail IS NULL OR TRIM(thumbnail) = ''`)};export{L as WatchlistRepository};
@@ -1 +0,0 @@
1
- import g from"node:path";import{CONFIG as s}from"../config.js";import{DatabaseWrapper as p}from"../db.js";import{anilistRequest as f}from"../lib/anilist.js";import{dbAll as l,dbRun as E}from"../utils/db-utils.js";const a=50;async function h(){const n=s.IS_DEV?s.DB_NAME_DEV:s.DB_NAME_PROD,o=await p.create(g.join(s.ROOT,n));o.configure("busyTimeout",1e4),l(o,"PRAGMA table_info(shows_meta)").some(i=>i.name==="episodeDuration")||o.run("ALTER TABLE shows_meta ADD COLUMN episodeDuration INTEGER");const e=l(o,"SELECT id, anilistId FROM shows_meta WHERE anilistId IS NOT NULL AND episodeDuration IS NULL");if(e.length===0){console.log("Nothing to backfill."),o.close();return}let r=0;for(let i=0;i<e.length;i+=a){const d=e.slice(i,i+a),c=await f(`query ($ids: [Int]) { Page(perPage: ${a}) { media(id_in: $ids, type: ANIME) { id duration } } }`,{ids:d.map(t=>t.anilistId)});for(const t of c?.data?.Page?.media??[]){if(!t.duration||t.duration<=0)continue;const m=d.find(u=>u.anilistId===t.id);m&&(E(o,"UPDATE shows_meta SET episodeDuration = ? WHERE id = ?",[t.duration,m.id]),r++)}console.log(`Progress: ${Math.min(i+a,e.length)}/${e.length} (updated ${r})`)}console.log(`Done. Updated ${r} of ${e.length} shows.`),o.close()}h().catch(n=>{console.error(n?.message||n),process.exit(1)});
@@ -1 +0,0 @@
1
- import S from"path";import f from"./logger.js";import{CONFIG as u}from"./config.js";import{dbAll as w,dbGet as _}from"./utils/db-utils.js";import{isTempSyncRow as d}from"./lib/temp-ids.js";const R=f.child({module:"SyncPayload"}),$=["watchlist","watched_episodes","queue","settings","shows_meta","sync_metadata","dismissed_notifications","discovered_notifications"],L=["manga_library","manga_progress","sync_metadata"],P=["tv_library","tv_progress","sync_metadata"],k=["asmr_library","asmr_progress","sync_metadata"];function x(e){const n=e.tables.sync_metadata?.find(t=>t.key==="db_version")?.value;return typeof n=="number"?n:Number(n||e.version||0)}function C(e,o,a){if(!e||typeof e!="object"||!("tables"in e))throw new Error(`Invalid ${a} sync payload.`);const n=e;for(const t of o)if(!Array.isArray(n.tables?.[t]))throw new Error(`Invalid ${a} sync payload: missing ${String(t)}.`);return n}function O(e,o){const a={};for(const n of o){const t=w(e,`SELECT * FROM "${String(n).replace(/"/g,'""')}"`);a[n]=t.filter(y=>!d(y))}return{version:x({version:0,exportedAt:"",tables:a}),exportedAt:new Date().toISOString(),tables:a}}function M(e,o,a,n){const t=a.tables,y=n.libraryTables.reduce((r,s)=>r+(t[s]?.length??0),0),p=n.libraryTables.length===0?1:_(e,`SELECT ${n.libraryTables.map(r=>`(SELECT COUNT(*) FROM "${String(r).replace(/"/g,'""')}")`).join(" + ")} AS n`)?.n??0;if(y===0&&p>0)throw new Error("Sync down refused: remote library is empty while local library has data");try{e.backup(S.join(u.ROOT,n.backupName))}catch(r){R.warn({err:r},"Pre-sync backup failed, continuing without backup")}const b=new Map;for(const r of o){const s=e.all(`PRAGMA table_info("${String(r)}")`);b.set(String(r),new Set(s.map(i=>i.name)))}e.serialize(()=>{for(const r of o)e.run(`DELETE FROM "${String(r).replace(/"/g,'""')}"`);for(const r of o){const s=b.get(String(r));for(const i of t[String(r)]||[]){if(d(i))continue;const c=Object.keys(i).filter(l=>s?.has(l));if(c.length===0)continue;const T=c.map(l=>`"${l.replace(/"/g,'""')}"`).join(", "),m=c.map(()=>"?").join(", "),g=c.map(l=>i[l]);e.run(`INSERT INTO "${String(r).replace(/"/g,'""')}" (${T}) VALUES (${m})`,g)}}})}export{$ as ANIME_SYNC_TABLES,k as ASMR_SYNC_TABLES,L as MANGA_SYNC_TABLES,P as TV_SYNC_TABLES,O as exportTables,M as importTables,C as normalizePayload,x as readPayloadVersion};
@@ -1,13 +0,0 @@
1
- import*as u from"fs/promises";import{existsSync as _}from"fs";import b from"path";import m from"./logger.js";import{googleDriveService as E}from"./google.js";import{rcloneService as l}from"./rclone.js";import{githubSyncService as T}from"./github-sync.js";import{CONFIG as d}from"./config.js";import{DatabaseWrapper as L}from"./db.js";import{runTx as F}from"./db/drizzle.js";import{dbGet as S}from"./utils/db-utils.js";import{TempShowIdsRepository as G}from"./repositories/temp-show-ids.repository.js";import{malCachePruneExpired as W}from"./repositories/mal-cache.repository.js";import{notifySyncStart as I,notifySyncEnd as N}from"./lib/ipc.js";import{ANIME_SYNC_TABLES as x,exportTables as k,importTables as Y,MANGA_SYNC_TABLES as D,TV_SYNC_TABLES as O,ASMR_SYNC_TABLES as w,normalizePayload as B,readPayloadVersion as v}from"./sync-payload.js";const o=m.child({module:"Sync"}),V=x;function y(a){return a.length===0?a:a[0].toUpperCase()+a.slice(1)}const A={anime:{mid:"",Head:"",rcloneLabel:"rclone",tables:V,libraryTables:["watchlist","watched_episodes"],backupName:"pre-sync-backup.db",manifestPath:d.LOCAL_MANIFEST_PATH,rcloneFile:d.RCLONE_SYNC_FILENAME,githubDown:a=>T.syncDown(a),githubUp:a=>T.syncUp(a),googleDown:a=>E.syncDown(a),githubVersion:()=>T.getRemoteVersion(),googleVersion:()=>E.getRemoteVersion(),googleUp:a=>E.syncUp(a)},manga:{mid:"manga ",Head:"Manga ",rcloneLabel:"rclone manga",tables:D,libraryTables:["manga_library","manga_progress"],backupName:"pre-sync-manga-backup.db",manifestPath:d.MANGA_LOCAL_MANIFEST_PATH,rcloneFile:d.MANGA_RCLONE_SYNC_FILENAME,githubDown:a=>T.syncMangaDown(a),githubUp:a=>T.syncMangaUp(a),googleDown:a=>E.syncMangaDown(a),githubVersion:()=>T.getMangaRemoteVersion(),googleVersion:()=>E.getMangaRemoteVersion(),googleUp:a=>E.syncMangaUp(a)},tv:{mid:"TV ",Head:"TV ",rcloneLabel:"rclone tv",tables:O,libraryTables:["tv_library","tv_progress"],backupName:"pre-sync-tv-backup.db",manifestPath:d.TV_LOCAL_MANIFEST_PATH,rcloneFile:d.TV_RCLONE_SYNC_FILENAME,githubDown:a=>T.syncTvDown(a),githubUp:a=>T.syncTvUp(a),googleDown:a=>E.syncTvDown(a),githubVersion:()=>T.getTvRemoteVersion(),googleVersion:()=>E.getTvRemoteVersion(),googleUp:a=>E.syncTvUp(a)},asmr:{mid:"ASMR ",Head:"ASMR ",rcloneLabel:"rclone asmr",tables:w,libraryTables:["asmr_library","asmr_progress"],backupName:"pre-sync-asmr-backup.db",manifestPath:d.ASMR_LOCAL_MANIFEST_PATH,rcloneFile:d.ASMR_RCLONE_SYNC_FILENAME,githubDown:a=>T.syncAsmrDown(a),githubUp:a=>T.syncAsmrUp(a),googleDown:a=>E.syncAsmrDown(a),githubVersion:()=>T.getAsmrRemoteVersion(),googleVersion:()=>E.getAsmrRemoteVersion(),googleUp:a=>E.syncAsmrUp(a)}};async function $(a,e=d.RCLONE_SYNC_FILENAME){if(!await l.fileExists(a,e))return 0;const r=b.join(d.ROOT,`temp_${Date.now()}_rclone_sync.json`);try{await l.downloadFile(a,e,r);const n=await u.readFile(r,"utf-8"),i=JSON.parse(n);return v(i)}catch{return 0}finally{_(r)&&await u.unlink(r)}}async function H(a,e,t){const r=A[e],n=k(a,r.tables),i=b.join(d.ROOT,`temp_${Date.now()}_rclone_${e}_up.json`);try{await u.writeFile(i,JSON.stringify(n,null,2)),await l.uploadFile(i,t,r.rcloneFile)}finally{_(i)&&await u.unlink(i).catch(()=>{})}}async function K(a,e,t){const r=A[e],n=b.join(d.ROOT,`temp_${Date.now()}_rclone_${e}_down.json`);try{await l.downloadFile(t,r.rcloneFile,n);const i=await u.readFile(n,"utf-8"),c=B(JSON.parse(i),r.tables,r.rcloneLabel);return Y(a,r.tables,c,{libraryTables:r.libraryTables,backupName:r.backupName}),v(c)}finally{_(n)&&await u.unlink(n).catch(()=>{})}}class j{_locked=!1;_waiting=[];async lock(){return new Promise(e=>{this._locked?this._waiting.push(e):(this._locked=!0,e())})}unlock(){const e=this._waiting.shift();e?e():this._locked=!1}}const p=new j;let R=!1,s="none";async function me(){await p.lock(),p.unlock()}function z(){return s}async function q(a){const e=a||process.env.SYNC_PROVIDER;if(e==="github"&&T.isAuthenticated()){s="github",o.info("Sync Provider: GitHub (Selected)");return}if(e==="google"&&E.isAuthenticated()){s="google",o.info("Sync Provider: Google Drive API (Selected)");return}if(e==="rclone"&&await l.init()){s="rclone",o.info(`Sync Provider: Rclone (${l.getRemoteName()}) (Selected)`);return}if(e==="none"){s="none",o.info("Sync Provider: None (Forced)");return}if(T.isAuthenticated()){s="github",o.info("Sync Provider: GitHub (Fallback)");return}if(E.isAuthenticated()){s="google",o.info("Sync Provider: Google Drive API (Fallback)");return}if(await l.init()){s="rclone",o.info(`Sync Provider: Rclone (${l.getRemoteName()}) (Fallback)`);return}s="none",o.info("No sync provider available.")}async function J(a=d.LOCAL_MANIFEST_PATH){if(_(a))try{const e=await u.readFile(a,"utf-8");return JSON.parse(e).version||0}catch{return 0}return 0}async function h(a,e=d.LOCAL_MANIFEST_PATH){await u.writeFile(e,JSON.stringify({version:a}))}function M(a){return J(A[a].manifestPath)}function g(a,e){return h(e,A[a].manifestPath)}async function X(a,e="anime"){const t=A[e],r=e==="anime"?"manifest":`${t.mid.trim()} manifest`;try{if(s==="github")return T.isAuthenticated()?{version:await t.githubVersion()}:{version:0};if(s==="google")return E.isAuthenticated()?{version:await t.googleVersion()}:{version:0};if(s==="rclone")return{version:await $(a,t.rcloneFile)}}catch(n){o.warn({err:n},`Could not read remote ${r}.`)}return{version:0}}async function P(a,e,t){const r=A[e];let n=await M(e);if(n===0&&a&&(n=S(a,"SELECT value FROM sync_metadata WHERE key = 'db_version'")?.value??0,n>0&&await g(e,n)),s!=="none"){if(await p.lock(),R){p.unlock();return}R=!0;try{I(`Initial ${r.mid}sync check (${s})`);const{version:i}=await X(t,e);if(N(),o.info(`${r.Head}Sync Check: Local v${n} vs Remote v${i}`),i>n){if(s==="github"){if(!T.isAuthenticated())return;I(`Importing GitHub ${r.mid}sync data (Remote v${i})`);const c=await r.githubDown(a);await g(e,c||i),N(),o.info(`GitHub ${r.mid}sync down complete.`);return}if(s==="google"){if(!E.isAuthenticated())return;I(`Importing Google ${r.mid}sync data (Remote v${i})`);const c=await r.googleDown(a);await g(e,c||i),N(),o.info(`Google ${r.mid}sync down complete.`);return}if(s==="rclone"){I(`Importing Rclone ${r.mid}sync data (Remote v${i})`);const c=await K(a,e,t);await g(e,c||i),N(),o.info(`Rclone ${r.mid}sync down complete.`);return}}else o.info(`Local ${r.mid}DB is up to date.`)}catch(i){N(),o.error({err:i},`${y(`${r.mid}sync`)} boot error.`)}finally{R=!1,p.unlock()}}}async function C(a,e,t){const r=A[e];if(s!=="none"){if(await p.lock(),R){p.unlock();return}R=!0;try{const n=await M(e),{version:i}=await X(t,e);if(n>i){if(I(`Syncing ${r.mid}up (Local v${n})`),s==="github"){if(!T.isAuthenticated())return;await r.githubUp(a)}else if(s==="google"){if(!E.isAuthenticated())return;await r.googleUp(a)}else s==="rclone"&&await H(a,e,t);N(),o.info(`${y(`${r.mid}sync`)} up complete.`)}else o.info(`No ${r.mid}changes to sync up or remote is newer.`)}catch(n){N(),o.error({err:n},`${y(`${r.mid}sync`)} up failed.`)}finally{R=!1,p.unlock()}}}async function ue(a,e){if(await q(e.preferredProvider),z()==="github"&&T.isAuthenticated())try{await T.migrateFromAniWebSync()}catch(r){m.error({err:r},"GitHub sync migration from ani-web failed")}await P(a.db,"anime",e.remoteFolder);try{await C(a.db,"anime",e.remoteFolder)}catch(r){m.error({err:r},"Sync up on boot failed")}const t=[{kind:"manga",db:a.mangaDb},{kind:"tv",db:a.tvDb},{kind:"asmr",db:a.asmrDb}];for(const{kind:r,db:n}of t){const i=A[r];try{await P(n,r,e.remoteFolder)}catch(c){m.error({err:c},`${i.Head}sync down on boot failed`)}try{await C(n,r,e.remoteFolder)}catch(c){m.error({err:c},`${i.Head}sync up on boot failed`)}}}async function f(a,e,t=r=>h(r)){await F(a,async i=>{await e(i),i.run("UPDATE sync_metadata SET value = value + 1 WHERE key = 'db_version'")});const n=S(a,"SELECT value FROM sync_metadata WHERE key = 'db_version'")?.value??1;await t(n)}async function le(a,e){await f(a,e,t=>g("manga",t))}async function Ae(a,e){await f(a,e,t=>g("tv",t))}async function pe(a,e){await f(a,e,t=>g("asmr",t))}async function Ne(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS manga_library (id TEXT PRIMARY KEY, provider TEXT NOT NULL, mangaId TEXT NOT NULL, title TEXT, cover TEXT, status TEXT DEFAULT 'Reading', author TEXT, contentRating TEXT, lastChapterId TEXT, lastChapterNumber TEXT, lastPage INTEGER, updatedAt INTEGER, altTitle TEXT, anilistId INTEGER, anilistIdSource TEXT)"),e.run("CREATE TABLE IF NOT EXISTS manga_progress (mangaId TEXT NOT NULL, chapterId TEXT NOT NULL, chapterNumber TEXT, page INTEGER DEFAULT 0, pageCount INTEGER DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (mangaId, chapterId))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_manga_library_provider_manga ON manga_library(provider, mangaId)"),e.run("CREATE INDEX IF NOT EXISTS idx_manga_library_status ON manga_library(status)"),e.run("CREATE INDEX IF NOT EXISTS idx_manga_progress_manga ON manga_progress(mangaId, updatedAt)");const t=e.all("PRAGMA table_info(manga_library)");t.some(n=>n.name==="altTitle")||e.run("ALTER TABLE manga_library ADD COLUMN altTitle TEXT"),t.some(n=>n.name==="anilistId")||e.run("ALTER TABLE manga_library ADD COLUMN anilistId INTEGER"),t.some(n=>n.name==="anilistIdSource")||e.run("ALTER TABLE manga_library ADD COLUMN anilistIdSource TEXT"),e.run("CREATE INDEX IF NOT EXISTS idx_manga_library_anilist ON manga_library(anilistId)");const r=e.all("PRAGMA table_info(manga_progress)");return r.some(n=>n.name==="title")||e.run("ALTER TABLE manga_progress ADD COLUMN title TEXT"),r.some(n=>n.name==="cover")||e.run("ALTER TABLE manga_progress ADD COLUMN cover TEXT"),r.some(n=>n.name==="provider")||e.run("ALTER TABLE manga_progress ADD COLUMN provider TEXT"),r.some(n=>n.name==="altTitle")||e.run("ALTER TABLE manga_progress ADD COLUMN altTitle TEXT"),r.some(n=>n.name==="contentRating")||e.run("ALTER TABLE manga_progress ADD COLUMN contentRating TEXT"),e}catch(e){throw o.error({err:e},"Manga database opening error"),e}}async function ge(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS tv_library (id TEXT PRIMARY KEY, tmdbId INTEGER NOT NULL, mediaType TEXT NOT NULL, title TEXT, poster TEXT, backdrop TEXT, year TEXT, overview TEXT, status TEXT DEFAULT 'Watching', adult INTEGER DEFAULT 0, lastSeason INTEGER, lastEpisode INTEGER, updatedAt INTEGER)"),e.run("CREATE TABLE IF NOT EXISTS tv_progress (mediaId TEXT NOT NULL, season INTEGER NOT NULL, episode INTEGER NOT NULL, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, completed INTEGER NOT NULL DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (mediaId, season, episode))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE INDEX IF NOT EXISTS idx_tv_library_status ON tv_library(status)"),e.run("CREATE INDEX IF NOT EXISTS idx_tv_library_tmdb ON tv_library(tmdbId, mediaType)"),e.run("CREATE INDEX IF NOT EXISTS idx_tv_progress_media ON tv_progress(mediaId, updatedAt)");const t=e.all("PRAGMA table_info(tv_progress)");return t.some(r=>r.name==="title")||e.run("ALTER TABLE tv_progress ADD COLUMN title TEXT"),t.some(r=>r.name==="poster")||e.run("ALTER TABLE tv_progress ADD COLUMN poster TEXT"),t.some(r=>r.name==="backdrop")||e.run("ALTER TABLE tv_progress ADD COLUMN backdrop TEXT"),t.some(r=>r.name==="year")||e.run("ALTER TABLE tv_progress ADD COLUMN year TEXT"),t.some(r=>r.name==="overview")||e.run("ALTER TABLE tv_progress ADD COLUMN overview TEXT"),t.some(r=>r.name==="tmdbId")||e.run("ALTER TABLE tv_progress ADD COLUMN tmdbId INTEGER"),t.some(r=>r.name==="mediaType")||e.run("ALTER TABLE tv_progress ADD COLUMN mediaType TEXT"),t.some(r=>r.name==="adult")||e.run("ALTER TABLE tv_progress ADD COLUMN adult INTEGER"),t.some(r=>r.name==="completed")||e.run("ALTER TABLE tv_progress ADD COLUMN completed INTEGER NOT NULL DEFAULT 0"),e.run("UPDATE tv_progress SET completed = 1 WHERE completed = 0 AND duration > 0 AND currentTime >= duration * 0.8"),e.run(`CREATE TRIGGER IF NOT EXISTS trg_tv_progress_completed_insert
2
- AFTER INSERT ON tv_progress
3
- WHEN NEW.duration > 0 AND NEW.currentTime >= NEW.duration * 0.8
4
- BEGIN
5
- UPDATE tv_progress SET completed = 1
6
- WHERE mediaId = NEW.mediaId AND season = NEW.season AND episode = NEW.episode;
7
- END`),e.run("DROP TRIGGER IF EXISTS trg_tv_progress_completed_update"),e.run(`CREATE TRIGGER IF NOT EXISTS trg_tv_progress_completed_update
8
- AFTER UPDATE OF currentTime, duration ON tv_progress
9
- WHEN NEW.duration > 0 AND NEW.currentTime >= NEW.duration * 0.8
10
- BEGIN
11
- UPDATE tv_progress SET completed = 1
12
- WHERE mediaId = NEW.mediaId AND season = NEW.season AND episode = NEW.episode;
13
- END`),e.run("UPDATE tv_progress SET completed = 0 WHERE completed = 1 AND NOT (duration > 0 AND currentTime >= duration * 0.8)"),e.run("UPDATE OR IGNORE tv_progress SET mediaId = REPLACE(mediaId, '-', ':') WHERE mediaId LIKE 'tv-%' OR mediaId LIKE 'movie-%'"),e.run("DELETE FROM tv_progress WHERE mediaId LIKE 'tv-%' OR mediaId LIKE 'movie-%'"),e}catch(e){throw o.error({err:e},"TV database opening error"),e}}async function Re(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS asmr_library (id TEXT PRIMARY KEY, rjCode TEXT NOT NULL, title TEXT, thumbnail TEXT, status TEXT DEFAULT 'Listening', isAdult INTEGER DEFAULT 0, lastTrackIndex INTEGER, lastTrackLabel TEXT, lastPosition REAL, updatedAt INTEGER)"),e.run("CREATE TABLE IF NOT EXISTS asmr_progress (workId TEXT NOT NULL, trackIndex INTEGER NOT NULL, trackLabel TEXT, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (workId, trackIndex))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE INDEX IF NOT EXISTS idx_asmr_library_status ON asmr_library(status)"),e.run("CREATE INDEX IF NOT EXISTS idx_asmr_library_rjcode ON asmr_library(rjCode)"),e.run("CREATE INDEX IF NOT EXISTS idx_asmr_progress_work ON asmr_progress(workId, updatedAt)");const t=e.all("PRAGMA table_info(asmr_progress)");return t.some(r=>r.name==="title")||e.run("ALTER TABLE asmr_progress ADD COLUMN title TEXT"),t.some(r=>r.name==="thumbnail")||e.run("ALTER TABLE asmr_progress ADD COLUMN thumbnail TEXT"),t.some(r=>r.name==="rjCode")||e.run("ALTER TABLE asmr_progress ADD COLUMN rjCode TEXT"),t.some(r=>r.name==="isAdult")||e.run("ALTER TABLE asmr_progress ADD COLUMN isAdult INTEGER"),e}catch(e){throw o.error({err:e},"ASMR database opening error"),e}}async function Ie(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS watchlist (id TEXT NOT NULL, name TEXT, thumbnail TEXT, status TEXT, nativeName TEXT, englishName TEXT, type TEXT, PRIMARY KEY (id))"),e.run("CREATE TABLE IF NOT EXISTS watched_episodes (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, watchedAt DATETIME DEFAULT CURRENT_TIMESTAMP, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, PRIMARY KEY (showId, episodeNumber))"),e.run("CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY, showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, queue_order INTEGER NOT NULL)"),e.run("CREATE TABLE IF NOT EXISTS settings (key TEXT NOT NULL, value TEXT, PRIMARY KEY (key))"),e.run("CREATE TABLE IF NOT EXISTS shows_meta (id TEXT PRIMARY KEY, name TEXT, thumbnail TEXT, nativeName TEXT, englishName TEXT, episodeCount INTEGER, status TEXT, genres TEXT, popularityScore INTEGER, type TEXT)"),e.run("CREATE TABLE IF NOT EXISTS dismissed_notifications (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, dismissedAt DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (showId, episodeNumber))"),e.run("CREATE TABLE IF NOT EXISTS discovered_notifications (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, discoveredAt DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (showId, episodeNumber))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE TABLE IF NOT EXISTS legacy_id_mapping (legacyId TEXT PRIMARY KEY, numericId TEXT)"),e.run("CREATE TABLE IF NOT EXISTS temp_show_ids (id TEXT PRIMARY KEY, provider TEXT NOT NULL, nativeId TEXT NOT NULL, title TEXT NOT NULL, thumbnail TEXT, createdAt INTEGER NOT NULL)"),e.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_temp_show_ids_provider_native ON temp_show_ids(provider, nativeId)");try{const r=await G.purge(e);r>0&&m.info({purged:r},"Purged stale temp show ids on boot")}catch(r){m.warn({err:r},"Temp show purge on boot failed")}try{const r=W();r>0&&m.info({pruned:r},"Pruned expired mal cache rows on boot")}catch(r){m.warn({err:r},"Mal cache prune on boot failed")}e.run("CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId ON watched_episodes(showId)"),e.run("CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId_episodeNumber ON watched_episodes(showId, episodeNumber)"),e.run("CREATE INDEX IF NOT EXISTS idx_watched_episodes_watchedAt ON watched_episodes(watchedAt)"),e.run("CREATE INDEX IF NOT EXISTS idx_watchlist_status ON watchlist(status)"),e.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_queue_show_episode ON queue(showId, episodeNumber)"),e.run("CREATE INDEX IF NOT EXISTS idx_queue_order ON queue(queue_order)"),e.run("DELETE FROM dismissed_notifications WHERE showId NOT IN (SELECT id FROM watchlist)"),e.run("DELETE FROM discovered_notifications WHERE showId NOT IN (SELECT id FROM watchlist)"),e.run("DELETE FROM legacy_id_mapping WHERE legacyId NOT IN (SELECT id FROM watchlist) AND numericId NOT IN (SELECT id FROM watchlist)"),e.run("DELETE FROM dismissed_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = dismissed_notifications.showId AND we.episodeNumber = dismissed_notifications.episodeNumber)"),e.run("DELETE FROM discovered_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = discovered_notifications.showId AND we.episodeNumber = discovered_notifications.episodeNumber)"),e.run("UPDATE discovered_notifications SET discoveredAt = datetime('now') WHERE discoveredAt IS NULL");const t=(r,n,i)=>{e.all(`PRAGMA table_info(${r})`).some(U=>U.name===n)||e.run(`ALTER TABLE ${r} ADD COLUMN ${n} ${i}`)};return t("watchlist","nativeName","TEXT"),t("watchlist","englishName","TEXT"),t("shows_meta","nativeName","TEXT"),t("shows_meta","englishName","TEXT"),t("shows_meta","episodeCount","INTEGER"),t("shows_meta","status","TEXT"),t("shows_meta","genres","TEXT"),t("shows_meta","popularityScore","INTEGER"),t("watchlist","type","TEXT"),t("shows_meta","type","TEXT"),t("shows_meta","anilistId","INTEGER"),t("shows_meta","isAdult","INTEGER"),t("shows_meta","episodeDuration","INTEGER"),e}catch(e){throw o.error({err:e},"Database opening error"),e}}export{w as ASMR_SYNC_TABLES,A as KIND_SYNC,D as MANGA_SYNC_TABLES,V as SYNC_TABLES,O as TV_SYNC_TABLES,z as getActiveProvider,M as getKindManifestVersion,J as getLocalManifestVersion,q as initSyncProvider,Re as initializeAsmrDatabase,Ie as initializeDatabase,Ne as initializeMangaDatabase,ge as initializeTvDatabase,pe as performAsmrWriteTransactionAsync,le as performMangaWriteTransactionAsync,Ae as performTvWriteTransactionAsync,f as performWriteTransactionAsync,ue as runFullSyncSequence,g as setKindManifestVersion,h as setLocalManifestVersion,P as syncDownOnBoot,C as syncUp,me as waitForSync};
@@ -1 +0,0 @@
1
- import{getTokenFromHeaders as t,hasAppPassword as o,validateLanSession as p}from"../app-auth.js";function n(e,r,a){return{...e,lanAuthed:!o()||p(t(r,a))}}export{n as buildTrpcContext};
@@ -1 +0,0 @@
1
- import{TRPCError as r}from"@trpc/server";function E(e){return new r({code:"FORBIDDEN",message:"AUTH_REQUIRED",cause:{code:"AUTH_REQUIRED",provider:e}})}function R(){return new r({code:"UNAUTHORIZED",message:"LAN_AUTH_REQUIRED",cause:{code:"LAN_AUTH_REQUIRED"}})}export{E as authRequiredError,R as lanAuthRequiredError};
@@ -1 +0,0 @@
1
- import{initTRPC as o}from"@trpc/server";import{lanAuthRequiredError as n}from"./errors.js";const e=o.context().create({errorFormatter:({shape:r,error:t})=>({...r,data:{...r.data,cause:t.cause??null}})}),d=e.router,c=e.middleware(({ctx:r,next:t})=>{if(!r.lanAuthed)throw n();return t({ctx:r})}),p=e.procedure.use(c);export{p as protectedProcedure,d as router};
@@ -1 +0,0 @@
1
- import{router as r}from"./index.js";import{settingsRouter as o}from"./routers/settings.js";import{queueRouter as t}from"./routers/queue.js";import{watchlistRouter as m}from"./routers/watchlist.js";import{progressRouter as i}from"./routers/progress.js";import{notificationsRouter as e}from"./routers/notifications.js";import{continueWatchingRouter as a}from"./routers/continue-watching.js";import{discoveryRouter as s}from"./routers/discovery.js";import{mangaLibraryRouter as u}from"./routers/manga-library.js";import{mangaProgressRouter as p}from"./routers/manga-progress.js";import{asmrLibraryRouter as f}from"./routers/asmr-library.js";import{asmrProgressRouter as R}from"./routers/asmr-progress.js";import{tvLibraryRouter as n}from"./routers/tv-library.js";import{tvProgressRouter as g}from"./routers/tv-progress.js";import{tvRouter as c}from"./routers/tv.js";import{mangaRouter as d}from"./routers/manga.js";import{asmrRouter as y}from"./routers/asmr.js";import{musicRouter as h}from"./routers/music.js";import{radioRouter as v}from"./routers/radio.js";import{trackerRouter as b}from"./routers/tracker.js";import{insightsRouter as L}from"./routers/insights.js";import{authRouter as P}from"./routers/auth.js";import{dataRouter as k}from"./routers/data.js";import{discordRouter as l}from"./routers/discord.js";const V=r({settings:o,queue:t,watchlist:m,progress:i,notifications:e,continueWatching:a,discovery:s,mangaLibrary:u,mangaProgress:p,asmrLibrary:f,asmrProgress:R,tvLibrary:n,tvProgress:g,tv:c,manga:d,asmr:y,music:h,radio:v,tracker:b,insights:L,auth:P,data:k,discord:l});export{V as appRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as b}from"@trpc/server";import{protectedProcedure as e,router as L}from"../index.js";import{performAsmrWriteTransactionAsync as o}from"../../sync.js";import{buildAsmrId as p,ASMR_STATUSES as A,AsmrLibraryRepository as i,AsmrProgressRepository as g}from"../../repositories/asmr.repository.js";import{defineSchema as n,reqObj as u,reqStr as m,optStr as d,reqStrArray as l,readPageLimit as w}from"../validation.js";const h=()=>n(r=>{const t=u(r),s={...w(t)},a=d(t,"status");return a!==void 0&&(s.status=a),s}),I=()=>n(r=>({id:m(u(r),"id")})),f=()=>n(r=>{const t=u(r);return{rjCode:d(t,"rjCode"),id:d(t,"id"),title:d(t,"title"),thumbnail:d(t,"thumbnail"),status:d(t,"status"),isAdult:t.isAdult}}),D=()=>n(r=>{const t=u(r);return{id:m(t,"id"),status:m(t,"status")}}),j=()=>n(r=>{const t=u(r);return{ids:l(t,"ids",!0),status:m(t,"status")}}),q=()=>n(r=>({ids:l(u(r),"ids",!0)})),T=L({list:e.input(h()).query(async({ctx:r,input:t})=>{const s=Math.max(parseInt(String(t.page??1))||1,1),a=Math.min(Math.max(parseInt(String(t.limit??24))||24,1),100),c=(s-1)*a,y=await i.getAll(r.asmrDb,t.status,a,c),S=await i.getCount(r.asmrDb,t.status);return{data:y,total:S,page:s,limit:a}}),ids:e.query(async({ctx:r})=>({ids:await i.getIds(r.asmrDb)})),check:e.input(I()).query(async({ctx:r,input:t})=>{const s=await i.getById(r.asmrDb,t.id);return{inLibrary:!!s,status:s?.status??null}}),add:e.input(f()).mutation(async({ctx:r,input:t})=>{const s=String(t.rjCode||t.id||"").trim();if(!s)throw new b({code:"BAD_REQUEST",message:"rjCode is required"});const a=t.id&&t.id.trim()?p(t.id):p(s),c=typeof t.status=="string"&&A.includes(t.status)?t.status:"Listening";return await o(r.asmrDb,async y=>{await i.upsert(y,{id:a,rjCode:p(s),title:String(t.title||""),thumbnail:String(t.thumbnail||""),status:c,isAdult:t.isAdult===!0||t.isAdult===1})}),{success:!0,id:a}}),remove:e.input(I()).mutation(async({ctx:r,input:t})=>(await o(r.asmrDb,async s=>{await i.delete(s,String(t.id)),await g.deleteByWork(s,String(t.id))}),{success:!0})),setStatus:e.input(D()).mutation(async({ctx:r,input:t})=>{if(!t.id||!A.includes(t.status))throw new b({code:"BAD_REQUEST",message:"id and a valid status are required"});return await o(r.asmrDb,async s=>{await i.updateStatus(s,String(t.id),String(t.status))}),{success:!0}}),batchStatus:e.input(j()).mutation(async({ctx:r,input:t})=>{if(!t.status||!A.includes(t.status))throw new b({code:"BAD_REQUEST",message:"a valid status is required"});const s=t.ids.map(a=>String(a));return await o(r.asmrDb,async a=>{await i.updateStatusMany(a,s,String(t.status))}),{success:!0,updated:s.length}}),removeMany:e.input(q()).mutation(async({ctx:r,input:t})=>{const s=t.ids.map(a=>String(a));return await o(r.asmrDb,async a=>{await i.deleteMany(a,s),await g.deleteMany(a,s)}),{success:!0,removed:s.length}})});export{T as asmrLibraryRouter};
@@ -1 +0,0 @@
1
- import{protectedProcedure as s,router as p}from"../index.js";import{performAsmrWriteTransactionAsync as m}from"../../sync.js";import{adultNonListIds as w}from"../../utils/db-utils.js";import{SettingsRepository as b}from"../../repositories/settings.repository.js";import{AsmrLibraryRepository as y,AsmrProgressRepository as o}from"../../repositories/asmr.repository.js";import{defineSchema as i,reqObj as u,reqStr as l,reqStrArray as A,strOrNull as c}from"../validation.js";const I=t=>w(t,"asmr_progress","asmr_library","workId","COALESCE(l.isAdult, p.isAdult) = 1"),f=()=>i(t=>({ids:A(u(t),"ids",!0)})),k=()=>i(t=>({workId:l(u(t),"workId")})),h=()=>i(t=>{const r=u(t);return{workId:l(r,"workId"),trackIndex:r.trackIndex,trackLabel:r.trackLabel,currentTime:r.currentTime,duration:r.duration,title:c(r,"title"),thumbnail:c(r,"thumbnail"),rjCode:c(r,"rjCode"),isAdult:r.isAdult??null}}),L=()=>i(t=>{const r=u(t);return{workId:l(r,"workId"),trackIndex:r.trackIndex??void 0}}),P=()=>i(t=>{const r=u(t),e={};if(r.limit!==void 0&&r.limit!==null&&r.limit!==""){const n=Number(r.limit);Number.isFinite(n)&&(e.limit=n)}return e}),D=p({getByWork:s.input(k()).query(async({ctx:t,input:r})=>({progress:await o.getByWork(t.asmrDb,r.workId)})),latest:s.input(k()).query(async({ctx:t,input:r})=>await o.getLatest(t.asmrDb,r.workId)||{currentTime:0,duration:0}),save:s.input(h()).mutation(async({ctx:t,input:r})=>{const e=Math.max(Math.round(Number(r.trackIndex))||0,0),n=Math.max(Number(r.currentTime)||0,0),d=Math.max(Number(r.duration)||0,0);return await m(t.asmrDb,async a=>{await o.upsert(a,{workId:String(r.workId),trackIndex:e,trackLabel:String(r.trackLabel||""),currentTime:n,duration:d,title:r.title??null,thumbnail:r.thumbnail??null,rjCode:r.rjCode??null,isAdult:r.isAdult!=null?Number(r.isAdult):null}),await y.touchProgress(a,String(r.workId),{trackIndex:e,trackLabel:String(r.trackLabel||""),position:n})}),{success:!0}}),remove:s.input(L()).mutation(async({ctx:t,input:r})=>(await m(t.asmrDb,async e=>{r.trackIndex!==void 0&&r.trackIndex!==null?await o.deleteTrack(e,String(r.workId),Number(r.trackIndex)):await o.deleteByWork(e,String(r.workId))}),{success:!0})),removeMany:s.input(f()).mutation(async({ctx:t,input:r})=>{const e=r.ids.map(n=>String(n));return await m(t.asmrDb,async n=>{await o.deleteMany(n,e)}),{success:!0,removed:e.length}}),continueListening:s.input(P()).query(async({ctx:t,input:r})=>{const e=Math.min(Math.max(parseInt(String(r.limit??24))||24,1),100),{ignoreAdult:n,listOnly:d}=await b.readContinueFlags(t.db,"asmrIgnoreAdultContent","asmrCwWatchlistOnly"),a=(await o.getContinueListening(t.asmrDb,e)).filter(g=>!(n&&g.isAdult===1||d&&g.watchlistStatus!=="Listening"));return{data:a,total:a.length}}),adultCount:s.query(async({ctx:t})=>({count:(await I(t.asmrDb)).length})),purgeAdult:s.mutation(async({ctx:t})=>{const r=await I(t.asmrDb);return r.length>0&&await m(t.asmrDb,async e=>{for(const n of r)await o.deleteByWork(e,n)}),{success:!0,removed:r.length}})});export{D as asmrProgressRouter};
@@ -1 +0,0 @@
1
- import{protectedProcedure as c,router as h}from"../index.js";import{defineSchema as p,optStr as i,reqObj as m,reqStr as j}from"../validation.js";import{authRequiredError as u}from"../errors.js";import d from"../../logger.js";async function g(s,e,r,t){const o=s.apiCache.get(e);if(o)return o;const n=await t();return r!==void 0?s.apiCache.set(e,n,r):s.apiCache.set(e,n),n}const I=()=>p(s=>{if(s==null)return{};const e=m(s),r={},t=i(e,"q");t!==void 0&&(r.q=t),e.page!==void 0&&(r.page=parseInt(String(e.page),10)||1);const o=i(e,"sort");o!==void 0&&(r.sort=o);const n=i(e,"rating");return n!==void 0&&(r.rating=n),r}),q=()=>p(s=>({rj:j(m(s),"rj")})),k=h({browse:c.input(I()).query(async({ctx:s,input:e})=>{const r=e.q||"",t=e.page||1,o=e.sort||"",n=e.rating||"";return g(s,`route-asmr-browse-${r}-${t}-${o}-${n}-${o==="random"?Date.now():""}`,300,async()=>{try{const a=s.getJasmr();return a?await a.browse({query:r,page:t,sort:o,rating:n}):{shows:[],hasNext:!1}}catch(a){if(a.message==="AUTH_REQUIRED")throw u("jasmr");return d.error({err:a},"[Asmr] browse failed"),{shows:[],hasNext:!1}}})}),work:c.input(q()).query(async({ctx:s,input:e})=>{const r=e.rj;return g(s,`route-asmr-work-${r}`,1800,async()=>{try{const t=s.getJasmr();if(!t)return{rjCode:r,description:"",tracks:[],images:[],chapters:[]};const o=String(r).trim().toUpperCase(),n=await t.getEpisodes(o),[a,f,w]=await Promise.all([t.getStreamUrls(o,"1"),t.getImages(o),t.getChapters(o)]);return{rjCode:o,description:n?.description||"",tracks:a?.[0]?.links||[],images:f,chapters:w}}catch(t){if(t.message==="AUTH_REQUIRED")throw u("jasmr");return d.error({err:t,rj:r},"[Asmr] work fetch failed"),{rjCode:r,description:"",tracks:[],images:[],chapters:[]}}})})});export{k as asmrRouter};
@@ -1 +0,0 @@
1
- import p from"../../logger.js";import{TRPCError as _}from"@trpc/server";import{googleDriveService as s}from"../../google.js";import{githubSyncService as u}from"../../github-sync.js";import{initSyncProvider as E,getActiveProvider as v,runFullSyncSequence as I}from"../../sync.js";import{CONFIG as o}from"../../config.js";import{rcloneService as d}from"../../rclone.js";import{updateEnvFile as a}from"../../utils/env.utils.js";import{protectedProcedure as r,router as O}from"../index.js";import{defineSchema as c,reqObj as l}from"../validation.js";async function m(e,t,n){const i=o.IS_DEV?o.REMOTE_FOLDER_DEV:o.REMOTE_FOLDER_PROD;await I({db:t,mangaDb:e.mangaDb,tvDb:e.tvDb,asmrDb:e.asmrDb},{remoteFolder:i,preferredProvider:n})}const R=()=>c(e=>{const t=l(e);return{clientId:t.clientId,clientSecret:t.clientSecret,workerUrl:t.workerUrl}}),y=()=>c(e=>{const t=l(e);if(typeof t.clientId!="string")throw new Error("clientId required");return{clientId:t.clientId}}),w=()=>c(e=>({provider:l(e).provider})),f=()=>c(e=>({remote:l(e).remote})),C=()=>c(e=>({port:l(e).port})),A=O({configStatus:r.query(async()=>{const e=!!o.GOOGLE_AUTH_WORKER_URL,t=!!process.env.GOOGLE_CLIENT_ID&&!!process.env.GOOGLE_CLIENT_SECRET;return{hasConfig:e||t,useWorker:e}}),googleAuth:r.query(async()=>({useWorker:!!o.GOOGLE_AUTH_WORKER_URL,hasCustomWorkerUrl:!!process.env.GOOGLE_AUTH_WORKER_URL,hasCustomClientId:!!process.env.GOOGLE_CLIENT_ID,hasClientSecret:!!process.env.GOOGLE_CLIENT_SECRET})),saveGoogleAuth:r.input(R()).mutation(async({input:e})=>{const{clientId:t,clientSecret:n,workerUrl:i}=e,g={};return typeof t=="string"&&(g.GOOGLE_CLIENT_ID=t),typeof n=="string"&&(g.GOOGLE_CLIENT_SECRET=n),typeof i=="string"&&(g.GOOGLE_AUTH_WORKER_URL=i),await a(g),typeof i=="string"&&(o.GOOGLE_AUTH_WORKER_URL=i),typeof t=="string"&&(o.GOOGLE_CLIENT_ID=t||void 0),typeof n=="string"&&(o.GOOGLE_CLIENT_SECRET=n||void 0),await E(),{success:!0}}),githubAuth:r.query(async()=>({hasCustomClientId:!!process.env.GITHUB_CLIENT_ID})),saveGithubAuth:r.input(y()).mutation(async({input:e})=>(await a({GITHUB_CLIENT_ID:e.clientId}),{success:!0})),rcloneSettings:r.query(async()=>{const e=await d.listRemotes();return{remote:o.RCLONE_REMOTE||"",availableRemotes:e,activeRemote:d.isActive()?d.getRemoteName():null}}),syncSettings:r.query(async()=>({activeProvider:process.env.SYNC_PROVIDER||"default",actualActiveProvider:v(),authenticatedProviders:{github:u.isAuthenticated(),google:s.isAuthenticated(),rclone:d.isActive()}})),saveSyncProvider:r.input(w()).mutation(async({input:e})=>{const{provider:t}=e;return await a({SYNC_PROVIDER:t==="default"?"":t}),await E(),{success:!0,activeProvider:process.env.SYNC_PROVIDER||"default"}}),githubStatus:r.query(async()=>{try{const e=await u.getUserProfile();return{authenticated:!!e,user:e,device:u.getDeviceState(),hasCustomClientId:!!process.env.GITHUB_CLIENT_ID}}catch(e){return p.error({err:e},"Failed to fetch GitHub auth status"),{authenticated:!1,user:null,device:u.getDeviceState(),hasCustomClientId:!!process.env.GITHUB_CLIENT_ID}}}),githubStart:r.mutation(async({ctx:e})=>await u.startDeviceAuth(e.db,(n,i)=>m(e,n,i))),githubPoll:r.query(async()=>u.getDeviceState()),githubLogout:r.mutation(async()=>(await u.logout(),await a({SYNC_PROVIDER:""}),await E(),{success:!0})),saveRclone:r.input(f()).mutation(async({ctx:e,input:t})=>{const{remote:n}=t;return await a({RCLONE_REMOTE:n,SYNC_PROVIDER:"rclone"}),await m(e,e.db,"rclone"),{success:!0}}),setServerPort:r.input(C()).mutation(async({input:e})=>{const t=Number(e.port);if(!Number.isInteger(t)||t<1||t>65535)throw new _({code:"BAD_REQUEST",message:"Port must be an integer between 1 and 65535"});return await a({PORT:String(t)}),p.info({port:t},"Server port saved. Restart required to apply."),{success:!0,port:t}}),googleAuthUrl:r.query(async()=>({url:await s.getAuthUrl()})),googleLogin:r.mutation(async({ctx:e})=>{if(s.isAuthenticated()){if(await s.getUserProfile())return await a({SYNC_PROVIDER:"google"}),await m(e,e.db,"google"),{url:null,authenticated:!0};p.warn("Google tokens found but invalid. Clearing and requesting new auth."),await s.logout()}return{url:await s.getAuthUrl(),authenticated:!1}}),user:r.query(async()=>await s.getUserProfile()),logout:r.mutation(async()=>(await s.logout(),await a({SYNC_PROVIDER:""}),await E(),{success:!0}))});export{A as authRouter};
@@ -1,25 +0,0 @@
1
- import{protectedProcedure as s,router as w}from"../index.js";import{performWriteTransactionAsync as n}from"../../sync.js";import{WatchedEpisodesRepository as o}from"../../repositories/watched-episodes.repository.js";import{NotificationsRepository as a}from"../../repositories/notifications.repository.js";import{getMigratedId as m}from"../../lib/migration.js";import{getAdultNonWatchlistShowIds as u}from"../../hono/watchlist.js";import{dbAll as c}from"../../utils/db-utils.js";import{idsInput as p,showIdInput as h}from"../validation.js";const A=w({remove:s.input(h()).mutation(async({ctx:t,input:d})=>{const e=await m(t.db,d.showId);return await n(t.db,async i=>{await o.deleteByShow(i,e),await a.deleteByShow(i,e)}),{success:!0}}),removeMany:s.input(p()).mutation(async({ctx:t,input:d})=>{const e=await Promise.all(d.ids.map(i=>m(t.db,i)));return await n(t.db,async i=>{for(const r of e)await o.deleteByShow(i,r),await a.deleteByShow(i,r)}),{success:!0,removed:e.length}}),adultCount:s.query(async({ctx:t})=>({count:(await u(t.db)).length})),purgeAdult:s.mutation(async({ctx:t})=>{const d=await u(t.db);return d.length>0&&await n(t.db,async e=>{for(const i of d)await o.deleteByShow(e,i),await a.deleteByShow(e,i)}),{success:!0,removed:d.length}}),thisWeek:s.query(async({ctx:t})=>(await c(t.db,`SELECT
2
- w.id, w.name, w.thumbnail, w.nativeName, w.englishName, w.type,
3
- dn.episodeNumber, dn.discoveredAt
4
- FROM discovered_notifications dn
5
- JOIN watchlist w ON dn.showId = w.id
6
- WHERE w.status = 'Watching'
7
- AND EXISTS (
8
- SELECT 1 FROM discovered_notifications dn_recent
9
- WHERE dn_recent.showId = dn.showId
10
- AND dn_recent.discoveredAt >= datetime('now', '-7 days')
11
- )
12
- AND CAST(dn.episodeNumber AS INTEGER) = (
13
- SELECT MIN(CAST(dn2.episodeNumber AS INTEGER))
14
- FROM discovered_notifications dn2
15
- WHERE dn2.showId = dn.showId
16
- AND NOT EXISTS (
17
- SELECT 1 FROM watched_episodes we2
18
- WHERE we2.showId = dn2.showId AND we2.episodeNumber = dn2.episodeNumber
19
- )
20
- )
21
- AND NOT EXISTS (
22
- SELECT 1 FROM watched_episodes we
23
- WHERE we.showId = dn.showId AND we.episodeNumber = dn.episodeNumber
24
- )
25
- ORDER BY CAST(dn.episodeNumber AS INTEGER) ASC`)).map(e=>({_id:e.id,id:e.id,name:e.name,thumbnail:e.thumbnail||"",nativeName:e.nativeName,englishName:e.englishName,type:e.type,episodeNumber:parseInt(e.episodeNumber)||e.episodeNumber})))});export{A as continueWatchingRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as $}from"@trpc/server";import{protectedProcedure as h,router as re}from"../index.js";import{badRequest as L,defineSchema as b,failed as P,optNum as M,optStr as l,readCount as N,reqObj as S,reqStr as V}from"../validation.js";import{parseJsonBody as se}from"../../utils/http.utils.js";import{getTrending as H,getLatestReleases as ne,getSeasonal as ae,getSchedule as ie,searchAnilist as Q,searchAnilistByTitle as oe,getSpotlightBanners as de,getBatchedHomeData as ue,getGenreTagLists as ce,anilistUnavailable as J,wasAnilistDownAtBoot as F,checkAnilistStatus as le,getShowMetaById as U,getAnilistEpisodes as fe,parseMalId as j,getShowMetaByMalId as Y,fromAnilistMedia as me}from"../../lib/anilist.js";import{malAnimeDetail as ge,malSearchMedia as pe,malSearchTitle as he}from"../../lib/mal.js";import{malCacheStore as O}from"../../repositories/mal-cache.repository.js";import{getMigratedId as K}from"../../lib/migration.js";import{isTempShowId as z}from"../../lib/temp-ids.js";import{TempShowIdsRepository as k}from"../../repositories/temp-show-ids.repository.js";import{pickBestMatch as ye}from"../../providers/title-matching.js";import{authRequiredError as we}from"../errors.js";import{ShowsMetaRepository as B}from"../../repositories/shows-meta.repository.js";import{WatchlistRepository as W}from"../../repositories/watchlist.repository.js";import{dbRun as Ie}from"../../utils/db-utils.js";import g from"../../logger.js";async function D(r,e,t,s,n){const i=r.apiCache.get(e);if(i)return i;const a=await n();return s(a)&&(t!==void 0?r.apiCache.set(e,a,t):r.apiCache.set(e,a)),a}const E=r=>Array.isArray(r)&&r.length>0,ve=()=>b(r=>{const e=S(r),t={date:V(e,"date")},s=l(e,"format");return s!==void 0&&(t.format=s),t}),be=()=>b(r=>{const e=S(r),t={},s=l(e,"format");return s!==void 0&&(t.format=s),e.page!==void 0&&(t.page=N(e,"page",1)),e.size!==void 0&&(t.size=N(e,"size",12)),t}),Se=()=>b(r=>{const e=S(r),t={},s=l(e,"query");s!==void 0&&(t.query=s),e.page!==void 0&&(t.page=N(e,"page",1)),e.limit!==void 0&&(t.limit=N(e,"limit",14));const n=l(e,"sortBy");n!==void 0&&(t.sortBy=n);const i=l(e,"type");i!==void 0&&(t.type=i);const a=l(e,"status");a!==void 0&&(t.status=a);const o=l(e,"season");o!==void 0&&(t.season=o);const f=M(e,"year");f!==void 0&&(t.year=f);const p=l(e,"country");p!==void 0&&(t.country=p);const y=l(e,"genres");y!==void 0&&(t.genres=y);const d=l(e,"excludeGenres");d!==void 0&&(t.excludeGenres=d);const v=l(e,"excludeTags");v!==void 0&&(t.excludeTags=v);const m=M(e,"minScore");m!==void 0&&(t.minScore=m);const u=M(e,"minEpisodes");u!==void 0&&(t.minEpisodes=u);const c=l(e,"adult");return c!==void 0&&(t.adult=c),t}),De=()=>b(r=>{const e=S(r),t={},s=l(e,"title");return s!==void 0&&(t.title=s),t}),Ne=()=>b(r=>{const e=S(r),t={},s=l(e,"showId");s!==void 0&&(t.showId=s);const n=l(e,"episodeNumber");return n!==void 0&&(t.episodeNumber=n),t}),Ee=()=>b(r=>{const e=S(r),t={};e.page!==void 0&&(t.page=N(e,"page",1)),e.size!==void 0&&(t.size=N(e,"size",14));const s=l(e,"format");return s!==void 0&&(t.format=s),t}),Te=()=>b(r=>{const e=S(r),t={},s=l(e,"sort");return s!==void 0&&(t.sort=s),e.page!==void 0&&(t.page=N(e,"page",1)),e.size!==void 0&&(t.size=N(e,"size",20)),t}),Re=()=>b(r=>{if(r==null)return{};const e=S(r),t={},s=l(e,"format");return s!==void 0&&(t.format=s),t}),Ae=()=>b(r=>{if(r==null)return{};const e=S(r),t={},s=l(e,"showId");s!==void 0&&(t.showId=s);const n=l(e,"mode");(n==="sub"||n==="dub")&&(t.mode=n);const i=l(e,"provider");return i!==void 0&&(t.provider=i),t}),Me=()=>b(r=>({id:V(S(r),"id")})),qe=()=>b(r=>{if(r==null)return{};const e=S(r),t={},s=l(e,"showId");s!==void 0&&(t.showId=s);const n=l(e,"episodeNumber");n!==void 0&&(t.episodeNumber=n);const i=l(e,"mode");(i==="sub"||i==="dub")&&(t.mode=i);const a=l(e,"provider");return a!==void 0&&(t.provider=a),t}),Ce=()=>b(r=>{const e=S(r),t={};for(const a of["provider","query","type","status","season","country","genres","genre","excludeGenres","sortBy","order","studio","blacklist"]){const o=l(e,a);o!==void 0&&(t[a]=o)}e.page!==void 0&&(t.page=N(e,"page",1)),e.limit!==void 0&&(t.limit=N(e,"limit",14));const s=M(e,"year");s!==void 0&&(t.year=s);const n=M(e,"minScore");n!==void 0&&(t.minScore=n);const i=M(e,"minEpisodes");return i!==void 0&&(t.minEpisodes=i),t}),Le=()=>b(r=>{const e=S(r),t={},s=l(e,"provider");s!==void 0&&(t.provider=s);const n=l(e,"nativeId");n!==void 0&&(t.nativeId=n);const i=l(e,"title");i!==void 0&&(t.title=i);const a=l(e,"thumbnail");return a!==void 0&&(t.thumbnail=a),t});function Pe(r,e){const t=e?.toLowerCase();return t&&r.getProviders()[t]||null}function q(r,e){const t=(e||"").toLowerCase(),s=r.getProviders();if(!t||!s[t]||t==="mal"||t==="anilist")return!1;const n=Z(r);return n?n.has(t):!0}function Z(r){const e=r.getCatalog();return!e||e.length===0?null:new Set(e.filter(t=>t.mature&&(t.kind??"anime")==="anime"&&t.loaded).map(t=>t.id.toLowerCase()))}async function X(r,e,t){const s=r.getProviders(),n=Z(r),i=n?[...n]:Object.keys(s),a=[t,...i].filter(f=>!!f&&f!=="mal"&&f!=="anilist"&&!!s[f]),o=new Set;for(const f of a)if(!o.has(f)){o.add(f);try{const p=await s[f].resolveShowId?.(e);if(p)return{provider:s[f],nativeId:p}}catch{}}return null}function _(r){if(typeof r!="string"||!r.trim())return;const e=r.split(",").map(t=>t.trim()).filter(Boolean);return e.length>0?e:void 0}const Ke=re({schedule:h.input(ve()).query(async({ctx:r,input:e})=>{const t=e.date,s=e.format||"TV";return D(r,`schedule-${t}-${s}`,1800,E,async()=>{try{const n=new Date(t+"T00:00:00.000Z"),i=s==="ADULT";return await ie(n,i?void 0:s,i)}catch(n){return g.error({err:n,date:t},"Schedule fetch failed"),[]}})}),latestReleases:h.input(be()).query(async({ctx:r,input:e})=>{const t=e.format||"TV",s=e.page||1,n=e.size||12;return D(r,`latest-releases-${t}-${s}-${n}`,300,E,async()=>{try{return await ne(t,s,n)}catch(i){return g.error({err:i},"Latest releases fetch failed"),[]}})}),search:h.input(Se()).query(async({ctx:r,input:e})=>{const t={};return e.query!==void 0&&(t.query=e.query),e.page!==void 0&&(t.page=String(e.page)),e.limit!==void 0&&(t.limit=String(e.limit)),e.sortBy!==void 0&&(t.sortBy=e.sortBy),e.type!==void 0&&(t.type=e.type),e.status!==void 0&&(t.status=e.status),e.season!==void 0&&(t.season=e.season),e.year!==void 0&&(t.year=String(e.year)),e.country!==void 0&&(t.country=e.country),e.genres!==void 0&&(t.genres=e.genres),e.excludeGenres!==void 0&&(t.excludeGenres=e.excludeGenres),e.excludeTags!==void 0&&(t.excludeTags=e.excludeTags),e.minScore!==void 0&&(t.minScore=String(e.minScore)),e.minEpisodes!==void 0&&(t.minEpisodes=String(e.minEpisodes)),e.adult!==void 0&&(t.adult=e.adult),D(r,`search-${JSON.stringify(t)}`,1800,E,async()=>{try{return await Q({query:e.query||"",page:e.page||1,perPage:e.limit||14,format:e.type,status:e.status,season:e.season,seasonYear:e.year,countryOfOrigin:e.country,genre:e.genres,genre_not_in:_(e.excludeGenres),tag_not_in:_(e.excludeTags),averageScore_greater:e.minScore,episodes_greater:e.minEpisodes,isAdult:e.adult==="true"?!0:e.adult==="false"?!1:void 0,sort:e.sortBy})}catch(s){return g.error({err:s},"search failed"),[]}})}),matureResolve:h.input(De()).query(async({ctx:r,input:e})=>D(r,`mature-resolve-${e.title||""}`,3600,E,async()=>{try{const t=(e.title||"").trim();if(!t)throw L("title is required");const s=await oe(t);if(s){const i=s.id;return{id:typeof i=="number"&&i<0?`mal-${Math.abs(i)}`:i}}const n=await he(O(),t);if(n)return{id:n,provider:"mal"};throw new $({code:"NOT_FOUND",message:"No match found"})}catch(t){throw t instanceof $?t:(g.error({err:t},"mature resolve failed"),P("Resolve failed"))}})),skipTimes:h.input(Ne()).query(async({input:r})=>{try{const e=r.showId||"",t=r.episodeNumber||"";if(/^\d+$/.test(e)){const s=await fetch(`https://api.aniskip.com/v1/skip-times/${e}/${t}?types=op&types=ed`);if(s.ok)return await se(s)}return{found:!1,results:[]}}catch{return{found:!1,results:[]}}}),seasonal:h.input(Ee()).query(async({ctx:r,input:e})=>{const t=e.page||1,s=e.size||14,n=e.format;return D(r,`seasonal-${n||"ALL"}-${t}-${s}`,300,E,async()=>{try{return await ae(t,s,n)}catch(i){return g.error({err:i},"Seasonal fetch failed"),[]}})}),popularList:h.input(Te()).query(async({ctx:r,input:e})=>{const t=e.sort==="POPULARITY_DESC"?"POPULARITY_DESC":"TRENDING_DESC",s=e.page||1,n=e.size||20;return D(r,`popular-list-${e.sort||"TRENDING_DESC"}-${s}-${n}`,300,E,async()=>{try{return await H(s,n,t)}catch(i){return g.error({err:i},"Popular list fetch failed"),[]}})}),trending:h.query(async({ctx:r})=>D(r,"trending",300,E,async()=>{try{return await H(1,20,"TRENDING_DESC","RELEASING")}catch(e){return g.error({err:e},"Trending fetch failed"),[]}})),spotlight:h.query(async({ctx:r})=>D(r,"spotlight",300,E,async()=>{try{return await de(1,20)}catch(e){return g.error({err:e},"Spotlight fetch failed"),[]}})),home:h.input(Re()).query(async({ctx:r,input:e})=>{const t=e.format;return D(r,`home-${t||"TV"}`,300,E,async()=>{try{return await ue(t)}catch(s){return g.error({err:s},"Batched home fetch failed"),{trending:[],seasonal:[],spotlight:[]}}})}),genresAndTags:h.query(async()=>{try{return await ce()}catch(r){throw g.error({err:r},"genres-and-tags failed"),P("Failed to load genres")}}),anilistStatus:h.query(async()=>{const r=!J();return r||le().catch(()=>{}),{available:r,wasDownAtBoot:F()}}),systemNotifications:h.query(async()=>{const r=[];return F()&&J()&&r.push({id:"system-anilist-down",type:"system",title:"AniList API",message:"AniList metadata API is currently down. dango is experiencing degraded performance. MAL + Kitsu is being used as a fallback in the meantime. Some features may be limited until service is restored.",icon:"warning",createdAt:Date.now()}),r}),episodes:h.input(Ae()).query(async({ctx:r,input:e})=>{const t=e.showId;return D(r,`episodes-${t||""}`,3600,E,async()=>{const s=t;if(!s)return{episodes:[]};const n=r.db,i=r.getProviders();let a=await K(n,s);const o=j(a);if(o)try{const d=await Y(o);d?.anilistId&&d.anilistId>0&&(a=String(d.anilistId))}catch{}if(z(a)){try{const d=await k.getById(n,a);if(!d||!q(r,d.provider))return{episodes:[]};const v=String(e.provider||"").toLowerCase(),m=i[d.provider];if(m&&(!v||v===d.provider)){const c=await m.getEpisodes(d.nativeId,e.mode);return c?.episodes?.length?c:{episodes:[]}}const u=await X(r,d.title,v);if(u){const c=await u.provider.getEpisodes(u.nativeId,e.mode);if(c?.episodes?.length)return c}}catch(d){if(d.message==="AUTH_REQUIRED")throw d}return{episodes:[]}}if(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(a))try{if(i.animepahe)return await i.animepahe.getEpisodes(a,e.mode)||{episodes:[]}}catch(d){if(d.message==="AUTH_REQUIRED")throw d;return{episodes:[]}}if(/^(mal-\d+|-?\d+)$/i.test(a)){let d=[];try{d=await fe(a)}catch(v){g.error({err:v,showId:a},"Episodes fetch failed")}if(d.length===0){const v=Math.abs(parseInt(a,10));if(!Number.isFinite(v))return{episodes:d};try{const u=(await ge(O(),v)).detail?.episodes;u&&u>0&&u<=500&&(d=Array.from({length:u},(c,I)=>(I+1).toString()))}catch{}}if(d.length===0){const v=e.provider?.toLowerCase(),m=v?i[v]:void 0;if(m?.isDirectId?.(a))try{const u=await m.getEpisodes(a,e.mode);if(u?.episodes?.length)return u}catch(u){if(u.message==="AUTH_REQUIRED")throw u}}return{episodes:d}}const p=e.provider?.toLowerCase(),y=p?i[p]:void 0;if(y)try{const d=await y.getEpisodes(a,e.mode);if(d?.episodes?.length)return d}catch(d){if(d.message==="AUTH_REQUIRED")throw d}return{episodes:[]}})}),showMeta:h.input(Me()).query(async({ctx:r,input:e})=>{const t=e.id;return D(r,`meta-${t}`,3600,s=>!!s,async()=>{const s=r.db,n=await K(s,t);if(z(n)){const a=await k.getById(s,n);return!a||!q(r,a.provider)?{}:{_id:a.id,id:a.id,name:a.title,englishName:a.title,thumbnail:a.thumbnail||"",type:"TV",isAdult:!0,status:"UNKNOWN",provider:a.provider,nativeId:a.nativeId}}if(/^(mal-\d+|-?\d+)$/i.test(n)){let a=null;try{a=await U(n)}catch(o){g.warn({err:o,id:n},"AniList show-meta fetch failed, trying local cache");const f=await B.getById(s,n);if(f){if(typeof f.genres=="string")try{f.genres=JSON.parse(f.genres)}catch{f.genres=[]}return f}}if(a){const o=a.thumbnail?.trim()?a.thumbnail:void 0,f=!!o;try{await B.upsert(s,{id:n,name:a.name,thumbnail:o,nativeName:a.nativeName,englishName:a.englishName,popularityScore:a.score??(a.averageScore!=null?a.averageScore/10:void 0),genres:a.genres?JSON.stringify(a.genres.map(p=>typeof p=="string"?p:p?.name).filter(Boolean)):void 0,status:a.status,episodeCount:a.episodeCount!=null?Number(a.episodeCount):void 0,type:a.type,anilistId:a.anilistId})}catch{}if(f&&o){const p=await W.getById(s,n);if(p&&p.thumbnail!==o)try{await W.updateThumbnail(s,n,o)}catch{}}}if(/^(mal-\d+|-\d+)$/i.test(n)&&a?.anilistId&&a.anilistId>0)try{Ie(s,"INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)",[n,String(a.anilistId)])}catch{}return a||{}}return{}})}),matureSearch:h.input(Ce()).query(async({ctx:r,input:e})=>{const t=(e.provider||"anilist").toLowerCase();if(t!=="anilist"&&t!=="mal"&&!q(r,t))throw L("Unknown mature provider");const s={};for(const[n,i]of Object.entries(e))i!==void 0&&(s[n]=String(i));return D(r,`mature-search-${JSON.stringify(s)}`,600,n=>!!n&&typeof n=="object"&&Array.isArray(n.data),async()=>{try{const n=e.query||"",i=e.page||1,a=e.limit||14;if(t==="anilist"){const o=await Q({query:n,page:i,perPage:a,format:e.type,status:e.status,season:e.season,seasonYear:e.year,countryOfOrigin:e.country,genre:e.genres,genre_not_in:_(e.excludeGenres),isAdult:!0,sort:e.sortBy});return{data:o,hasMore:o.length>=a}}if(q(r,t)){const o=r.getProviders()[t];if(o?.browse){const p=e.genre||e.genres||void 0,y=await o.browse({query:n,page:i,limit:a,pageSize:a,genre:p,genres:e.genres||p,order:e.order||e.sortBy||void 0,sort:e.sortBy||e.order||void 0,studio:e.studio||void 0,blacklist:e.blacklist||void 0});return{data:y.shows,hasMore:y.hasMore,...y.total!==void 0?{total:y.total}:{},...y.genres?{genres:y.genres}:{}}}return{data:(await o.search({query:n})).map(p=>({...p,isAdult:!0})),hasMore:!1}}if(t==="mal"){let o=[];try{o=await pe(O(),{query:n,page:i,perPage:50,format:e.type,status:e.status,genre:e.genres||void 0,genre_not_in:_(e.excludeGenres),isAdult:!0,sort:e.sortBy,averageScore_greater:e.minScore,episodes_greater:e.minEpisodes})}catch(y){g.error({err:y},"mal mature search failed")}const f=o.map(y=>({...me(y),isAdult:!0}));return{data:f.slice(0,a),hasMore:f.length>=a}}return{data:[],hasMore:!1}}catch(n){return g.error({err:n},"mature search failed"),{data:[],hasMore:!1}}})}),matureFilters:h.query(async({ctx:r})=>{try{const e=r.getCatalog()??[],t={};for(const s of e)!s.mature||(s.kind??"anime")!=="anime"||!s.loaded||(t[s.id]={label:s.label,...s.browse?{browse:s.browse}:{},...s.facets?.genres?.length?{genres:s.facets.genres}:{},...s.facets?.orders?.length?{orders:s.facets.orders}:{}});return{providers:t}}catch(e){throw g.error({err:e},"mature filters failed"),P("Failed to load filters")}}),matureAllocate:h.input(Le()).mutation(async({ctx:r,input:e})=>{try{const t=String(e.provider||"").toLowerCase(),s=String(e.nativeId||"").trim(),n=String(e.title||"").trim(),i=String(e.thumbnail||"");if(!t||!q(r,t))throw L("Unknown provider");if(!s||!n)throw L("nativeId and title are required");return{id:(await k.allocate(r.db,{provider:t,nativeId:s,title:n,thumbnail:i})).id}}catch(t){throw t instanceof $?t:(g.error({err:t},"temp show allocate failed"),P("Allocate failed"))}}),video:h.input(qe()).query(async({ctx:r,input:e})=>{const t=r.db,s=r.getProviders();try{let n=e.showId;const i=j(n);if(i)try{const m=await Y(i);m?.anilistId&&m.anilistId>0&&(n=String(m.anilistId))}catch{}if(z(n)){try{const m=await k.getById(t,n);if(!m||!q(r,m.provider))return[];const u=String(e.provider||"").toLowerCase(),c=s[m.provider];if(c&&(!u||u===m.provider))return await c.getStreamUrls(m.nativeId,e.episodeNumber,e.mode)||[];const I=await X(r,m.title,u);if(I)return await I.provider.getStreamUrls(I.nativeId,e.episodeNumber,e.mode)||[]}catch(m){g.error({err:m,showId:n},"Temp show video fetch failed")}return[]}const o=e.provider?.toLowerCase(),f=j(n)!==null,y=!!!(o&&s[o]?.isDirectId?.(n))&&(/^\d+$/.test(n)||f);if(o&&y){const m=await B.getById(t,n);let u=m?.englishName||m?.name,c=null;if(!u)try{c=await U(n),u=c?.englishName||c?.name,c&&u&&await B.upsert(t,{id:n,name:c.name,thumbnail:c.thumbnail,nativeName:c.nativeName,englishName:c.englishName,genres:c.genres?JSON.stringify(c.genres.map(I=>I.name)):void 0,status:c.status,episodeCount:c.episodeCount!=null?Number(c.episodeCount):void 0,type:c.type,anilistId:c.anilistId})}catch(I){g.warn({err:I,provider:o,showId:n},"[Video] AniList metadata lookup failed while resolving numeric showId")}if(u){let I=c?.names?.romaji,R=c?.names?.native,x=c?.names?.synonyms;if(!I||!R)try{const w=await U(n);I=I||w?.names?.romaji,R=R||w?.names?.native,x=x||w?.names?.synonyms}catch{}const ee=[u,I,R,...(x??[]).slice(0,2)].filter(w=>!!w&&w.trim().length>0),G=[...new Set(ee.map(w=>w.trim()))].filter((w,T,A)=>A.findIndex(te=>te.toLowerCase()===w.toLowerCase())===T);let C=null;for(const w of G){try{C=await s[o]?.resolveShowId?.(w,I,e.mode)??null}catch(T){if(T.message==="AUTH_REQUIRED")throw T;C=null}if(C)break}if(C)n=C;else{g.warn({provider:o,showId:n,title:u,romaji:I},"[Video] resolveShowId failed, attempting fallback provider search");try{const w=await s[o]?.search?.({query:u}),T=ye((w||[]).map(A=>({title:A.name||A.englishName||"",id:A.id||A._id||""})),G);if(T)n=T.item.id;else return g.warn({provider:o,showId:n,title:u},"[Video] fallback provider search returned no results"),[]}catch(w){if(w.message==="AUTH_REQUIRED")throw w;return g.error({err:w,provider:o,showId:n,title:u},"[Video] fallback provider search failed"),[]}}}else return g.warn({provider:o,showId:n},"[Video] numeric showId passed to provider but local meta missing title"),[]}const d=Pe(r,e.provider);return d?await d.getStreamUrls(n,e.episodeNumber,e.mode)||[]:[]}catch(n){if(n.message==="AUTH_REQUIRED")throw we(n.provider||e.provider||"animepahe");return g.error({err:n,provider:e.provider},"Provider video fetch failed"),[]}})});export{Ke as dataRouter};
@@ -1 +0,0 @@
1
- import{protectedProcedure as c,router as I}from"../index.js";import{badRequest as P,defineSchema as h,failed as y,optStr as v,reqObj as p}from"../validation.js";import{discordRPCService as r}from"../../discord-rpc.js";import{discordGatewayService as m}from"../../discord-gateway.js";import{getDlsitePoster as T,getMangadexCover as w}from"../../hono/watchlist.js";import{updateEnvFile as S}from"../../utils/env.utils.js";import E from"../../logger.js";function g(e){return e?e.slice(0,8)+"..."+e.slice(-4):"none"}const f=E.child({module:"DiscordGatewayRoutes"});function s(e,t){return e[t]}function k(e){let t=String(e||"");if(t.includes("/api/proxy")||t.includes("/api/image-proxy")){const i=t.match(/url=([^&]+)/);if(i)try{t=decodeURIComponent(i[1])}catch{return""}}return!t.startsWith("https://")||t.includes("localhost")||t.includes("127.0.0.1")?"":t}const u=()=>h(e=>e==null?{}:{...p(e)}),G=I({pageStatus:c.input(u()).mutation(async({input:e})=>{const t=s(e,"page");return typeof t!="string"||!r.isServiceEnabled?{success:!0}:(r.setIdleStatus(t),{success:!0})}),heartbeat:c.input(u()).mutation(async({input:e})=>{const t=s(e,"sessionId"),i=s(e,"bye");return typeof t!="string"||!r.isServiceEnabled?{success:!0}:(i?r.removeHeartbeat(t):r.heartbeat(t),{success:!0})}),clear:c.input(u()).mutation(async({input:e})=>{const t=s(e,"sessionId");return r.isServiceEnabled?(r.clearPresence(typeof t=="string"?t:void 0),{success:!0}):{success:!0}}),asmrPresence:c.input(u()).mutation(async({input:e})=>{if(!r.isServiceEnabled)return{success:!0};const t=s(e,"sessionId");typeof t=="string"&&r.heartbeat(t);const i=s(e,"thumbnail"),n=s(e,"thumbnails"),o=s(e,"rjCode");let a=String(i||"");const d=Array.isArray(n)?n.map(String).filter(Boolean):void 0;if((!a||a.includes("weeabo0.xyz")||a.includes("japaneseasmr.com"))&&o){const b=await T(String(o));b&&(a=b)}return(a.includes("/api/image-proxy")||a.includes("weeabo0.xyz"))&&(a=""),r.updatePresence({title:String(s(e,"title")||"ASMR").slice(0,128),episode:String(s(e,"trackLabel")||"").slice(0,64),totalEpisodes:"",currentTime:Number(s(e,"currentTime"))||0,duration:Number(s(e,"duration"))||0,thumbnail:a,thumbnails:d,isPlaying:!!s(e,"isPlaying"),providerName:"ASMR",sessionId:typeof t=="string"?t:void 0,isAdult:!!s(e,"isAdult")}),{success:!0}}),radioPresence:c.input(u()).mutation(async({input:e})=>{if(!r.isServiceEnabled)return{success:!0};const t=s(e,"sessionId");typeof t=="string"&&r.heartbeat(t);let i=String(s(e,"thumbnail")||"");if(i.includes("/api/image-proxy")){const o=i.match(/url=([^&]+)/);o&&(i=decodeURIComponent(o[1]))}i&&!i.startsWith("https://")&&(i=""),(i.includes("localhost")||i.includes("127.0.0.1"))&&(i="");const n=String(s(e,"stationLabel")||"");return r.updatePresence({title:String(s(e,"title")||"Radio").slice(0,128),episode:n.slice(0,64),totalEpisodes:"",stateLine:(n||"Radio").slice(0,64),currentTime:Number(s(e,"currentTime"))||0,duration:0,thumbnail:i,isPlaying:!!s(e,"isPlaying"),providerName:"Radio",sessionId:typeof t=="string"?t:void 0,isAdult:!1}),{success:!0}}),musicPresence:c.input(u()).mutation(async({input:e})=>{if(!r.isServiceEnabled)return{success:!0};const t=s(e,"sessionId");typeof t=="string"&&r.heartbeat(t);let i=String(s(e,"thumbnail")||"");if(i.includes("/api/image-proxy")){const o=i.match(/url=([^&]+)/);o&&(i=decodeURIComponent(o[1]))}i&&!i.startsWith("https://")&&(i=""),(i.includes("localhost")||i.includes("127.0.0.1"))&&(i="");const n=String(s(e,"artistLabel")||"");return r.updatePresence({title:String(s(e,"title")||"Music").slice(0,128),episode:n.slice(0,64),totalEpisodes:"",stateLine:(n||"Music").slice(0,64),currentTime:Number(s(e,"currentTime"))||0,duration:Number(s(e,"duration"))||0,thumbnail:i,isPlaying:!!s(e,"isPlaying"),providerName:"Music",sessionId:typeof t=="string"?t:void 0,isAdult:!1}),{success:!0}}),mangaPresence:c.input(u()).mutation(async({input:e})=>{if(!r.isServiceEnabled)return{success:!0};const t=s(e,"sessionId");typeof t=="string"&&r.heartbeat(t);const i=s(e,"thumbnails"),n=s(e,"title");let o=k(s(e,"thumbnail")),a=Array.isArray(i)?i.map(k).filter(Boolean):void 0;if(!o&&n){const l=await w(String(n));l&&(o=l,a=[l,...a||[]])}const d=String(s(e,"chapterLabel")||"");return r.updatePresence({title:String(n||"Manga").slice(0,128),episode:d.slice(0,64),totalEpisodes:"",stateLine:(d?`Reading ${d}`:"Reading").slice(0,64),currentTime:0,duration:0,thumbnail:o,thumbnails:a,isPlaying:s(e,"isPlaying")!==!1,providerName:"Manga",sessionId:typeof t=="string"?t:void 0,isAdult:s(e,"isAdult")===!0}),{success:!0}}),tvPresence:c.input(u()).mutation(async({input:e})=>{if(!r.isServiceEnabled)return{success:!0};const t=s(e,"sessionId");typeof t=="string"&&r.heartbeat(t);const i=s(e,"thumbnail");let n=String(i||"");if(n.includes("/api/image-proxy")){const a=n.match(/url=([^&]+)/);a&&(n=decodeURIComponent(a[1]))}(n.includes("localhost")||n.includes("127.0.0.1"))&&(n="");const o=String(s(e,"episodeLabel")||"");return r.updatePresence({title:String(s(e,"title")||"TV").slice(0,128),episode:o.slice(0,64),totalEpisodes:"",stateLine:(o||"Movie").slice(0,64),currentTime:Number(s(e,"currentTime"))||0,duration:Number(s(e,"duration"))||0,thumbnail:n,isPlaying:!!s(e,"isPlaying"),providerName:"TV",sessionId:typeof t=="string"?t:void 0,isAdult:s(e,"isAdult")===!0}),{success:!0}}),gatewayStatus:c.query(()=>{const e=m.hasToken();return{hasToken:e,masked:e?g(process.env.DISCORD_GATEWAY_TOKEN):null,enabled:m.serviceEnabled}}),gatewaySave:c.input(h(e=>{if(e==null)return{};const t=p(e),i={},n=v(t,"token");return n!==void 0&&(i.token=n),i})).mutation(async({input:e})=>{let t=(e.token||"").trim();if(t.startsWith('"')&&t.endsWith('"')&&(t=t.slice(1,-1)),t.length<30)throw P("Token too short");try{return await S({DISCORD_GATEWAY_TOKEN:t}),m.reloadToken(),f.info(`Discord Gateway token saved ${g(t)} (not logged)`),{ok:!0,masked:g(t)}}catch(i){throw f.error({err:i},"Failed to save Discord Gateway token"),y("Failed to save token")}}),gatewayRemove:c.mutation(async()=>{try{return await S({DISCORD_GATEWAY_TOKEN:""}),m.reloadToken(),f.info("Discord Gateway token removed"),{ok:!0}}catch(e){throw f.error({err:e},"Failed to remove Discord Gateway token"),y("Failed to remove token")}})});export{G as discordRouter};
@@ -1 +0,0 @@
1
- import{protectedProcedure as t,router as e}from"../index.js";import{getWatchlistDiscoveryStatus as r,triggerWatchlistDiscovery as s}from"../../hono/watchlist.js";const i=e({status:t.query(()=>r()),refresh:t.mutation(()=>({success:!0,started:s(),...r()}))});export{i as discoveryRouter};
@@ -1 +0,0 @@
1
- import{protectedProcedure as e,router as t}from"../index.js";import{getWatchInsights as n,getGenreCards as o}from"../../lib/insights.js";import{getCachedRecommendations as a}from"../../lib/recommendations.service.js";const c=t({summary:e.query(async({ctx:r})=>n(r.db)),genreCards:e.query(async({ctx:r})=>o(r.db)),recommendations:e.query(async({ctx:r})=>({candidates:await a(r.db)}))});export{c as insightsRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as d}from"@trpc/server";import{protectedProcedure as c,router as k}from"../index.js";import{performMangaWriteTransactionAsync as h}from"../../sync.js";import{buildMangaId as R,MANGA_STATUSES as I,MangaLibraryRepository as i,MangaProgressRepository as f}from"../../repositories/manga.repository.js";import{badRequest as g,defineSchema as m,failed as y,optStr as s,readCount as v,reqObj as w,reqStr as q}from"../validation.js";function u(r){const t=r.mangaDb;if(!t||t.isClosedCheck())throw new Error("Manga database is not ready");return t}function T(r,t,a){return a&&a.includes(":")?a:R(r,t||a||"")}const C=()=>m(r=>{const t=w(r),a=s(t,"status"),n=Math.max(v(t,"page",1),1),e=Math.min(Math.max(v(t,"limit",24),1),100);return{status:a,page:n,limit:e}}),L=()=>m(r=>({id:q(w(r),"id")})),N=()=>m(r=>{const t=w(r),a={provider:s(t,"provider"),mangaId:s(t,"mangaId"),id:s(t,"id"),title:s(t,"title"),cover:s(t,"cover"),status:s(t,"status"),author:s(t,"author"),altTitle:s(t,"altTitle"),contentRating:s(t,"contentRating")};return typeof t.silent=="boolean"&&(a.silent=t.silent),a}),F=()=>m(r=>{if(typeof r!="string")throw new Error("id must be a string");return r}),B=()=>m(r=>{const t=w(r);return{id:s(t,"id"),status:s(t,"status")}}),D=()=>m(r=>{const t=w(r);return{id:s(t,"id"),anilistId:t.anilistId}}),j=()=>m(r=>{const t=w(r),a=t.ids;return{ids:Array.isArray(a)?a.map(e=>String(e)):[],status:s(t,"status")}}),P=()=>m(r=>{if(!Array.isArray(r))throw new Error("ids must be a non-empty array");return r.map(t=>String(t))}),z=k({list:c.input(C()).query(async({ctx:r,input:t})=>{try{const a=(t.page-1)*t.limit,n=u(r),e=await i.getAll(n,t.status,t.limit,a),l=await i.getCount(n,t.status);return{data:e,total:l,page:t.page,limit:t.limit}}catch(a){throw a instanceof d?a:y("Failed to load manga library")}}),ids:c.query(async({ctx:r})=>{try{return{ids:await i.getIds(u(r))}}catch{return{ids:[]}}}),check:c.input(L()).query(async({ctx:r,input:t})=>{try{const a=await i.getById(u(r),t.id);return{inLibrary:!!a,status:a?.status??null}}catch{return{inLibrary:!1,status:null}}}),entry:c.input(L()).query(async({ctx:r,input:t})=>{try{const a=await i.getById(u(r),t.id);if(!a)throw new d({code:"NOT_FOUND",message:"Not in library"});return{item:a}}catch(a){throw a instanceof d?a:y("Failed to load entry")}}),add:c.input(N()).mutation(async({ctx:r,input:t})=>{const a=t.provider||"",n=t.mangaId||"",e=t.id||"";if(!a||!n&&!e)throw g("provider and mangaId are required");const l=T(String(a),String(n||""),e?String(e):void 0),p=typeof t.status=="string"&&I.includes(t.status)?t.status:"Reading";try{return await h(u(r),async o=>{await i.upsert(o,{id:l,provider:String(a).toLowerCase(),mangaId:String(n||e||""),title:String(t.title||""),cover:String(t.cover||""),status:p,author:t.author?String(t.author):void 0,altTitle:t.altTitle?String(t.altTitle):void 0,contentRating:t.contentRating?String(t.contentRating):void 0})}),{success:!0,id:l}}catch(o){throw o instanceof d?o:y("Failed to bookmark manga")}}),remove:c.input(F()).mutation(async({ctx:r,input:t})=>{if(!t)throw g("id is required");try{return await h(u(r),async a=>{await i.delete(a,String(t)),await f.deleteByManga(a,String(t))}),{success:!0}}catch(a){throw a instanceof d?a:y("Failed to remove bookmark")}}),setStatus:c.input(B()).mutation(async({ctx:r,input:t})=>{if(!t.id||!t.status||!I.includes(t.status))throw g("id and a valid status are required");try{return await h(u(r),async a=>{await i.updateStatus(a,String(t.id),String(t.status))}),{success:!0}}catch(a){throw a instanceof d?a:y("Failed to update status")}}),link:c.input(D()).mutation(async({ctx:r,input:t})=>{const a=typeof t.id=="string"?t.id:"",n=Number(t.anilistId);if(!a||!Number.isInteger(n)||n<=0)throw g("id and a numeric anilistId are required");try{const e=u(r),l=await i.getById(e,a);if(!l)throw new d({code:"NOT_FOUND",message:"Library entry not found"});if(String(l.provider).toLowerCase()==="anilist")throw g("Link a provider bookmark to the AniList entry, not the reverse");let p=!1;return await h(e,async o=>{await i.setAnilistId(o,a,n,"manual");const b=`anilist:${n}`;if(b!==a){const S=await i.getById(o,b),M=await i.getById(o,a),A=await f.getByManga(o,a);S?.lastChapterNumber&&!M?.lastChapterNumber&&A.length===0&&(await i.setProgressPointer(o,a,S.lastChapterNumber),await f.moveSyntheticChapters(o,b,a),p=!0),await i.delete(o,b),await f.deleteByManga(o,b)}}),{success:!0,id:a,progressMigrated:p}}catch(e){throw e instanceof d?e:y("Failed to link entry")}}),batchStatus:c.input(j()).mutation(async({ctx:r,input:t})=>{if(!Array.isArray(t.ids)||t.ids.length===0)throw g("ids must be a non-empty array");if(!t.status||!I.includes(t.status))throw g("a valid status is required");const a=t.ids.map(n=>String(n));try{return await h(u(r),async n=>{await i.updateStatusMany(n,a,String(t.status))}),{success:!0,updated:a.length}}catch(n){throw n instanceof d?n:y("Failed to update statuses")}}),removeMany:c.input(P()).mutation(async({ctx:r,input:t})=>{if(!Array.isArray(t)||t.length===0)throw g("ids must be a non-empty array");const a=t.map(n=>String(n));try{return await h(u(r),async n=>{await i.deleteMany(n,a),await f.deleteMany(n,a)}),{success:!0,removed:a.length}}catch(n){throw n instanceof d?n:y("Failed to remove bookmarks")}})});export{z as mangaLibraryRouter};
@@ -1 +0,0 @@
1
- import{protectedProcedure as r,router as b}from"../index.js";import{defineSchema as p,optStr as g,reqObj as d,reqStr as m,reqStrArray as f}from"../validation.js";import{performMangaWriteTransactionAsync as l}from"../../sync.js";import{adultNonListIds as M}from"../../utils/db-utils.js";import{SettingsRepository as N}from"../../repositories/settings.repository.js";import{MangaLibraryRepository as R,MangaProgressRepository as i}from"../../repositories/manga.repository.js";const v=["erotica","pornographic"],C=n=>!!n.contentRating&&v.includes(n.contentRating),h=n=>M(n,"manga_progress","manga_library","mangaId","COALESCE(l.contentRating, p.contentRating) IN ('erotica', 'pornographic')"),y=()=>p(n=>({mangaId:m(d(n),"mangaId")})),S=()=>p(n=>{const t=d(n),a={mangaId:m(t,"mangaId"),chapterId:m(t,"chapterId")};if(t.chapterNumber!==void 0&&t.chapterNumber!==null&&(a.chapterNumber=String(t.chapterNumber)),t.page!==void 0&&t.page!==null){const c=Number(t.page);Number.isNaN(c)||(a.page=c)}if(t.pageCount!==void 0&&t.pageCount!==null){const c=Number(t.pageCount);Number.isNaN(c)||(a.pageCount=c)}const e=g(t,"title");e!==void 0&&(a.title=e);const o=g(t,"cover");o!==void 0&&(a.cover=o);const s=g(t,"provider");s!==void 0&&(a.provider=s);const u=g(t,"altTitle");u!==void 0&&(a.altTitle=u);const I=g(t,"contentRating");return I!==void 0&&(a.contentRating=I),a}),w=()=>p(n=>{const t=d(n),a={mangaId:m(t,"mangaId")},e=g(t,"chapterId");return e!==void 0&&(a.chapterId=e),a}),P=()=>p(n=>({ids:f(d(n),"ids",!0)})),A=()=>p(n=>{const t=d(n),a={};if(t.limit!==void 0&&t.limit!==null){const e=Number(t.limit);Number.isNaN(e)||(a.limit=e)}return a}),O=b({byManga:r.input(y()).query(async({ctx:n,input:t})=>{try{return{progress:await i.getByManga(n.mangaDb,t.mangaId)}}catch{return{progress:[]}}}),latest:r.input(y()).query(async({ctx:n,input:t})=>{try{return await i.getLatest(n.mangaDb,t.mangaId)||{page:0,pageCount:0}}catch{return{page:0,pageCount:0}}}),save:r.input(S()).mutation(async({ctx:n,input:t})=>{const a=Math.max(Number(t.page)||0,0),e=Math.max(Number(t.pageCount)||0,0);return await l(n.mangaDb,async o=>{await i.upsert(o,{mangaId:String(t.mangaId),chapterId:String(t.chapterId),chapterNumber:String(t.chapterNumber||""),page:a,pageCount:e,title:t.title??null,cover:t.cover??null,provider:t.provider??null,altTitle:t.altTitle??null,contentRating:t.contentRating??null}),await R.touchProgress(o,String(t.mangaId),{chapterId:String(t.chapterId),chapterNumber:String(t.chapterNumber||""),page:a})}),{success:!0}}),remove:r.input(w()).mutation(async({ctx:n,input:t})=>(await l(n.mangaDb,async a=>{t.chapterId?await i.deleteChapter(a,String(t.mangaId),String(t.chapterId)):await i.deleteByManga(a,String(t.mangaId))}),{success:!0})),removeMany:r.input(P()).mutation(async({ctx:n,input:t})=>{const a=t.ids.map(e=>String(e));return await l(n.mangaDb,async e=>{await i.deleteMany(e,a)}),{success:!0,removed:a.length}}),continueReading:r.input(A()).query(async({ctx:n,input:t})=>{try{const a=Math.min(Math.max(parseInt(String(t.limit))||24,1),100),{ignoreAdult:e,listOnly:o}=await N.readContinueFlags(n.db,"mangaIgnoreAdultContent","mangaCwWatchlistOnly"),s=(await i.getContinueReading(n.mangaDb,a)).filter(u=>!(e&&C(u)||o&&u.watchlistStatus!=="Reading"));return{data:s,total:s.length}}catch{return{data:[],total:0}}}),adultCount:r.query(async({ctx:n})=>{try{return{count:(await h(n.mangaDb)).length}}catch{return{count:0}}}),purgeAdult:r.mutation(async({ctx:n})=>{const t=await h(n.mangaDb);return t.length>0&&await l(n.mangaDb,async a=>{await i.deleteMany(a,t)}),{success:!0,removed:t.length}})});export{O as mangaProgressRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as $}from"@trpc/server";import{protectedProcedure as g,router as w}from"../index.js";import{parseJsonBody as P}from"../../utils/http.utils.js";import{defineSchema as f,optStr as u,reqObj as y}from"../validation.js";import c from"../../logger.js";const v=["safe"];function l(a){const t=a.mature==="1",r=String(a.rating||"safe").split(",").map(e=>e.trim().toLowerCase()).filter(e=>["safe","suggestive","erotica","pornographic"].includes(e));if(!t)return v;const n=[...new Set(r)];return n.length>0?n:v}function d(a,t){return a.getMangaProvider(t.toLowerCase())}async function p(a,t,r,n){const e=a.apiCache.get(t);if(e)return e;const s=await n();if((s.status??200)>=200&&(s.status??200)<400&&(r!==void 0?a.apiCache.set(t,s.body,r):a.apiCache.set(t,s.body)),s.status!==void 0&&s.status!==200)throw new $({code:s.status===400?"BAD_REQUEST":s.status===404?"NOT_FOUND":"INTERNAL_SERVER_ERROR",message:s.body?.error||"Request failed"});return s.body}const C=()=>f(a=>{const t=y(a),r={};for(const n of["provider","q","sort","status","type","rating","mature"]){const e=u(t,n);e!==void 0&&(r[n]=e)}return t.page!==void 0&&(r.page=parseInt(String(t.page),10)||1),t.limit!==void 0&&(r.limit=Math.min(parseInt(String(t.limit),10)||24,50)),r}),M=()=>f(a=>{const t=y(a),r={};for(const n of["provider","id","rating","mature"]){const e=u(t,n);e!==void 0&&(r[n]=e)}return r}),N=()=>f(a=>{const t=y(a),r={},n=u(t,"provider");n!==void 0&&(r.provider=n);const e=u(t,"id");return e!==void 0&&(r.id=e),r}),E=w({trending:g.query(async({ctx:a})=>{const t="route-manga-trending",r=a.apiCache.get(t);if(r)return r;try{const e=await fetch("https://api.mangadex.org/manga?limit=6&order%5BfollowedCount%5D=desc&includes%5B%5D=cover_art&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive",{headers:{"User-Agent":"dango/3.1.9"}});if(!e.ok)return[];const i=((await P(e)).data||[]).map(o=>{const h=o.attributes?.title?.en||Object.values(o.attributes?.title||{})[0]||"",I=o.attributes?.description?.en||"",S=(o.attributes?.tags||[]).map(m=>m.attributes?.name?.en).filter(Boolean),b=o.relationships?.find(m=>m.type==="cover_art")?.attributes?.fileName,R=b?`https://uploads.mangadex.org/covers/${o.id}/${b}.512.jpg`:"";return{id:o.id,title:h,description:I,tags:S,year:o.attributes?.year||null,status:o.attributes?.status||null,cover:R}});return a.apiCache.set(t,i,600),i}catch{return[]}}),search:g.input(C()).query(async({ctx:a,input:t})=>{const r=t.provider||"",n=t.q||"",e=t.page||1;return p(a,`route-manga-search-${r}-${n}-${e}-${t.sort||""}-${t.status||""}-${t.type||""}-${t.rating||""}-${t.mature||""}`,300,async()=>{try{const s=l({rating:t.rating,mature:t.mature}),i=t.type||"",o=i==="doujinshi"&&t.mature!=="1"?"":i;return{body:await d(a,r)?.search({query:n,page:e,limit:t.limit||24,sort:t.sort||"popular",status:t.status||"",type:o,ratings:s})??{items:[],hasNext:!1}}}catch(s){return c.error({err:s},"[Manga] search failed"),{body:{items:[],hasNext:!1}}}})}),info:g.input(M()).query(async({ctx:a,input:t})=>{const r=String(t.provider||""),n=String(t.id||""),e=String(t.rating||""),s=String(t.mature||"");return p(a,`route-manga-info-${r}-${n}-${e}-${s}`,600,async()=>{try{if(!n)return{status:400,body:{error:"Missing id"}};const i=await d(a,r)?.getDetail(n,l({rating:e,mature:s}));return i?{body:i}:{status:404,body:{error:"Not found"}}}catch(i){return c.error({err:i},"[Manga] info failed"),{status:500,body:{error:"Failed to load manga"}}}})}),chapters:g.input(M()).query(async({ctx:a,input:t})=>{const r=String(t.provider||""),n=String(t.id||""),e=String(t.rating||""),s=String(t.mature||"");return p(a,`route-manga-chapters-${r}-${n}-${e}-${s}`,600,async()=>{try{return n?{body:{chapters:await d(a,r)?.getChapters(n,l({rating:e,mature:s}))??[]}}:{status:400,body:{error:"Missing id"}}}catch(i){return c.error({err:i},"[Manga] chapters failed"),{body:{chapters:[]}}}})}),pages:g.input(N()).query(async({ctx:a,input:t})=>{const r=String(t.provider||""),n=String(t.id||"");return p(a,`route-manga-pages-${r}-${n}`,300,async()=>{try{return n?{body:{pages:await d(a,r)?.getPages(n)??[]}}:{status:400,body:{error:"Missing id"}}}catch(e){return c.error({err:e},"[Manga] pages failed"),{body:{pages:[]}}}})})});export{E as mangaRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as N}from"@trpc/server";import{protectedProcedure as y,router as $}from"../index.js";import{badRequest as I,defineSchema as A,failed as x,optStr as R,reqObj as U}from"../validation.js";import u from"../../logger.js";import{getInnertube as M,getAuthedInnertube as m,refreshAuthedInnertube as T,saveMusicCookie as j,getMusicAuthStatus as F,signOutMusic as H,isParserVariantError as Y,verifyMusicSession as V}from"../../lib/ytmusic.js";const Q=[/\s*(- topic)$/i,/\s*vevo$/i,/\s*[(|[]official(.*?)[)|\]]/i,/\s*[(|[]((lyrics?|visualizer|audio)\s*(video)?)[)|\]]/i,/\s*[(|[](performance video)[)|\]]/i,/\s*[(|[](clip official)[)|\]]/i,/\s*[(|[](video version)[)|\]]/i,/\s*[(|[](HD|HQ)\s*?(?:audio)?[)|\]]$/i,/\s*[(|[](live)[)|\]]$/i,/\s*[(|[]4K\s*?(?:upgrade)?[)|\]]$/i];function h(i){if(!i)return i;let n=i;for(const t of Q)n=n.replace(t,"");return n.trim()}function O(i){const n=i,t=n.video_id||n.videoId||n.id||"";if(!t||typeof t!="string")return null;const r=n.title,a=typeof r=="string"?r:r?.text??"Unknown title",s=n.artists??n.authors??n.subtitle;let c="";Array.isArray(s)?c=s.map(d=>{const f=d;return f.name||f.text||f.title||""}).filter(Boolean).join(", "):typeof s=="string"?c=s:s&&typeof s=="object"&&(c=s.text??"");const e=n.thumbnails??n.thumbnail,o=Array.isArray(e)?e:Array.isArray(e?.thumbnails)?e.thumbnails:[],l=E(n.album),k=E(n.duration);return{id:t,title:h(String(a)),artists:h(c),album:l?h(l):void 0,duration:k||void 0,thumbnails:o}}function E(i){if(!i)return"";if(typeof i=="string")return i;const n=i;return typeof n.text=="string"?n.text:Array.isArray(n.runs)?n.runs.map(t=>t.text||"").join(""):""}function _(i){if(!i)return[];if(Array.isArray(i))return i.filter(t=>t&&typeof t.url=="string").map(t=>({url:t.url,width:t.width,height:t.height}));const n=i;return Array.isArray(n.thumbnails)?_(n.thumbnails):[]}function P(i){const n=i;return typeof n?.type=="string"?n.type:n?.constructor?.name??""}function K(i,n=!1){const t=P(i);if(t==="AutomixPreviewVideo")return null;const r=i,a=t==="PlaylistPanelVideoWrapper"?r.primary??null:r;if(!a||typeof a!="object")return null;const s=a.video_id;if(typeof s!="string"||!s)return null;const c=a.artists;let e="";Array.isArray(c)&&(e=c.map(f=>f?.name||"").filter(Boolean).join(", ")),!e&&typeof a.author=="string"&&(e=a.author);const o=a.album,l=a.duration,k=typeof l=="string"?l:l?.text??"";let d=null;if(n)for(const f of[a.menu,i.menu]){let w=null;for(const b of D(f)){if(P(b)!=="ToggleMenuServiceItem")continue;const g=b.icon_type;if(!(g!=="FAVORITE"&&g!=="UNFAVORITE")){w=g==="UNFAVORITE";break}}if(w!==null){d=w;break}}return{id:s,title:h(E(a.title)||"Unknown title"),artists:h(e||"Unknown artist"),album:o?.name?h(o.name):void 0,duration:k||void 0,thumbnails:_(a.thumbnail??a.thumbnails),...d!==null?{liked:d}:{}}}function B(i){return!!i?.payload&&"signInEndpoint"in i.payload}function C(i,n){const t=[],r=i,a=r.contents??[];for(const c of a){const e=K(c,n);if(e&&t.push(e),t.length>=50)break}const s=r.playlist_id??null;return{tracks:t,playlistId:s}}function D(i){if(!i||typeof i!="object")return[];const n=i.items;return Array.isArray(n)?n:[]}function X(i,n){const t={like:null,unlike:null},r=P(i),a=i,s=r==="PlaylistPanelVideoWrapper"?a.primary??null:a;if(!s||s.video_id!==n)return t;const c=[s.menu,a.menu];for(const e of c)for(const o of D(e)){if(P(o)!=="ToggleMenuServiceItem")continue;const l=o.icon_type;if(!(l!=="FAVORITE"&&l!=="UNFAVORITE")){for(const k of["default_endpoint","toggled_endpoint"]){const d=o[k];if(typeof d?.call!="function")continue;const f=d.payload?.status;f==="LIKE"&&!t.like?t.like=d:f==="INDIFFERENT"&&!t.unlike&&(t.unlike=d)}if(t.like&&t.unlike)return t}}return t}async function S(i,n,t=500){const r=[];let a=await i.music.getPlaylist(n);for(let s=0;s<6;s++){for(const c of(a.items??a.contents??[]).slice(0,t-r.length)){const e=O(c);if(e&&r.push(e),r.length>=t)break}if(r.length>=t||!a.has_continuation||typeof a.getContinuation!="function")break;try{a=await a.getContinuation()}catch(c){u.warn({err:c},"[music] playlist continuation failed");break}}return r}async function v(i){let n;try{n=await i.music.getLibrary()}catch(e){if(!Y(e))throw e;u.warn("[music] library landing hit a layout variant, falling back to Liked playlist");try{return{tracks:await S(i,"LM"),playlists:[]}}catch(o){return u.warn({err:o},"[music] liked playlist fallback failed"),{tracks:[],playlists:[]}}}const t=[];for(const e of n.contents??[]){const o=e.items??e.contents??[];t.push(...o)}const r=[],a=new Set;for(const e of t){const o=e,l=typeof o.playlist_id=="string"&&o.playlist_id||typeof o.playlistId=="string"&&o.playlistId||typeof o.id=="string"&&o.id||"",k=E(o.title),d=E(o.subtitle);!l||!k||a.has(l)||!/playlist|mix/i.test(d)&&k!=="Liked Music"||(a.add(l),r.push({id:l,title:k,subtitle:d||void 0,thumbnails:_(o.thumbnails??o.thumbnail)}))}const s=[],c=r.find(e=>e.title==="Liked Music");if(c)try{s.push(...await S(i,c.id))}catch(e){u.warn({err:e},"[music] liked playlist fetch failed")}return u.info(`[music] library parsed tracks=${s.length} playlists=${r.length} gridItems=${t.length}`),{tracks:s,playlists:r}}function p(i){return new N({code:"UNAUTHORIZED",message:i})}const z=()=>A(i=>{const n=U(i),t={},r=R(n,"q");return r!==void 0&&(t.q=r),t}),q=()=>A(i=>{const n=U(i),t={},r=R(n,"id");return r!==void 0&&(t.id=r),t}),J=()=>A(i=>{const n=U(i),t={},r=R(n,"cookie");return r!==void 0&&(t.cookie=r),t}),W=()=>A(i=>{const n=U(i),t={},r=R(n,"id");return r!==void 0&&(t.id=r),(typeof n.like=="boolean"||typeof n.like=="string")&&(t.like=n.like),t});function L(i){const n=String(i.id||"").trim();if(!n)throw I("id is required");return n}const et=$({authStatus:y.query(()=>F()),authStart:y.input(J()).mutation(async({input:i})=>{let n=String(i.cookie||"").trim();if(!n)throw I("Cookie is required");if(n=n.replace(/^cookie\s*:\s*/i,"").replace(/^["']+|["']+$/g,"").trim(),n.includes("\u2026")||n.includes("..."))throw I(`Cookie looks truncated (${n.length} chars). In DevTools, right-click the Cookie header \u2192 Copy value (do not copy from the wrapped preview).`);if(!n.includes("SID")&&!n.includes("SAPISID"))throw I('That does not look like a YouTube cookie at all. Copy the full Cookie request header from a "browse" call on music.youtube.com in DevTools \u2192 Network.');try{await j(n);const t=await m();if(t)try{const r=await v(t);return{success:!0,tracks:r.tracks.length,playlists:r.playlists.length}}catch{}return{success:!0,tracks:0,playlists:0}}catch(t){throw t instanceof N?t:(u.error({err:t},"[music] cookie sign-in failed"),p("Cookie rejected by YouTube Music. Copy a fresh Cookie header from music.youtube.com."))}}),signOut:y.mutation(async()=>{try{return await H(),{success:!0}}catch(i){throw u.error({err:i},"[music] sign-out failed"),x("Sign-out failed")}}),search:y.input(z()).query(async({ctx:i,input:n})=>{const t=String(n.q||"").trim();if(!t)return{tracks:[]};const r=`music-search-${t.toLowerCase()}`,a=i.apiCache.get(r);if(a)return{tracks:a};try{const c=await(await M()).music.search(t,{type:"song"}),e=[],o=c.contents??[];for(const l of o){const k=l.contents??[l];for(const d of k){const f=O(d);if(f&&e.push(f),e.length>=25)break}if(e.length>=25)break}return i.apiCache.set(r,e,300),{tracks:e}}catch(s){return u.error({err:s},"[music] search failed"),{tracks:[]}}}),track:y.input(q()).query(async({ctx:i,input:n})=>{const t=L(n),r=`music-track-${t.toLowerCase()}`,a=i.apiCache.get(r);if(a)return{track:a};try{const e=(await(await M()).music.getInfo(t)).basic_info;if(!e?.title)return{track:null};const o=typeof e.duration=="number"&&Number.isFinite(e.duration)?Math.max(Math.round(e.duration),0):0,l={id:t,title:h(String(e.title)),artists:h(String(e.author||"Unknown artist")),duration:o?`${Math.floor(o/60)}:${String(o%60).padStart(2,"0")}`:void 0,thumbnails:Array.isArray(e.thumbnail)?e.thumbnail:[]};return i.apiCache.set(r,l,300),{track:l}}catch(s){return u.error({err:s},"[music] track lookup failed"),{track:null}}}),playlist:y.input(q()).query(async({input:i})=>{const n=L(i),t=await m().catch(()=>null);if(!t)throw p("MUSIC_AUTH_REQUIRED");try{return{tracks:await S(t,n)}}catch(r){u.warn({err:r},"[music] playlist failed, retrying with a fresh saved-cookie session");const a=await T();if(!a)return{tracks:[]};try{return{tracks:await S(a,n)}}catch(s){return u.warn({err:s},"[music] playlist retry failed, returning empty"),{tracks:[]}}}}),library:y.query(async()=>{const i=await m().catch(()=>null);if(!i)throw p("MUSIC_AUTH_REQUIRED");try{const t=await v(i);if(t.tracks.length>0||t.playlists.length>0)return t;u.warn("[music] library came back empty, retrying with a fresh saved-cookie session")}catch(t){u.warn({err:t},"[music] library failed, retrying with a fresh saved-cookie session")}const n=await T();if(!n)return{tracks:[],playlists:[]};try{return await v(n)}catch(t){return u.warn({err:t},"[music] library retry failed, returning empty"),{tracks:[],playlists:[]}}}),home:y.query(async()=>{try{const n=await(await M()).music.getHomeFeed();return JSON.parse(JSON.stringify(n))}catch(i){return u.error({err:i},"[music] home feed failed"),{contents:[]}}}),upnext:y.input(q()).query(async({ctx:i,input:n})=>{const t=L(n),r=await m().catch(()=>null);if(!r){const s=`music-upnext-${t.toLowerCase()}`,c=i.apiCache.get(s);if(c)return c;try{const e=await M(),o=C(await e.music.getUpNext(t),!1);return i.apiCache.set(s,o,300),o}catch(e){return u.error({err:e},"[music] upnext failed"),{tracks:[],playlistId:null}}}try{return C(await r.music.getUpNext(t),!0)}catch(s){u.warn({err:s},"[music] authed upnext failed, retrying with a fresh saved-cookie session")}const a=await T();if(a)try{return C(await a.music.getUpNext(t),!0)}catch(s){u.warn({err:s},"[music] upnext retry failed, falling back to public")}try{const s=await M();return C(await s.music.getUpNext(t),!1)}catch(s){return u.error({err:s},"[music] upnext failed"),{tracks:[],playlistId:null}}}),likes:y.query(async({ctx:i})=>{const n=await m().catch(()=>null);if(!n)throw p("MUSIC_AUTH_REQUIRED");const t="music-liked-ids",r=i.apiCache.get(t);if(r)return{likedIds:r};const a=async s=>(await S(s,"LM",500)).map(e=>e.id);try{const s=await a(n);return i.apiCache.set(t,s,120),{likedIds:s}}catch(s){u.warn({err:s},"[music] liked ids failed, retrying with a fresh saved-cookie session");const c=await T();if(!c)return{likedIds:[]};try{const e=await a(c);return i.apiCache.set(t,e,120),{likedIds:e}}catch(e){return u.warn({err:e},"[music] liked ids retry failed, returning empty"),{likedIds:[]}}}}),like:y.input(W()).mutation(async({ctx:i,input:n})=>{const t=String(n.id||"").trim(),r=n.like===!0||n.like==="true";if(!t)throw I("id is required");const a=await m().catch(()=>null);if(!a||!await V())throw p("MUSIC_AUTH_REQUIRED");const s=async c=>{const e=c,o=e.actions??e.session?.actions;if(!o)throw new Error("Actions unavailable");const k=(await c.music.getUpNext(t)).contents??[];let d=null,f=!1;for(const w of k){const b=X(w,t),g=r?b.like:b.unlike;if(B(g)){f=!0;continue}if(g){d=g;break}}if(f&&!d)throw new Error("MUSIC_SESSION_EXPIRED");if(!d)throw new Error("Like toggle not found for track");await d.call(o)};try{await s(a)}catch(c){if(c.message==="MUSIC_SESSION_EXPIRED")throw u.warn("[music] like rejected, session expired"),p("MUSIC_AUTH_REQUIRED");u.warn({err:c},"[music] rate failed, retrying with a fresh saved-cookie session");const e=await T();if(!e)throw x("Like action failed");try{await s(e)}catch(o){throw o.message==="MUSIC_SESSION_EXPIRED"?p("MUSIC_AUTH_REQUIRED"):(u.error({err:o},"[music] rate retry failed"),x("Like action failed"))}}return i.apiCache.delete("music-liked-ids"),u.info(`[music] rate id=${t} liked=${r} via=toggle`),{success:!0,liked:r}})});export{et as musicRouter};
@@ -1 +0,0 @@
1
- import{protectedProcedure as a,router as l}from"../index.js";import h from"../../logger.js";import{performWriteTransactionAsync as d}from"../../sync.js";import{WatchlistRepository as f}from"../../repositories/watchlist.repository.js";import{WatchedEpisodesRepository as w}from"../../repositories/watched-episodes.repository.js";import{NotificationsRepository as n}from"../../repositories/notifications.repository.js";import{notificationDismissInput as g}from"../validation.js";function b(){return{"~standard":{version:1,vendor:"dango",types:void 0,validate(t){try{if(t==null)return{value:{}};if(typeof t!="object"||t===null)throw new Error("Expected an object");const e=t.showId;if(e==null)return{value:{}};if(typeof e!="string"||e.length===0)throw new Error("showId must be a string");return{value:{showId:e}}}catch(e){return{issues:[{message:e instanceof Error?e.message:"Invalid input"}]}}}}}}const C=l({list:a.query(async({ctx:t})=>{const e=await f.getWatchingShows(t.db),o=[];for(const i of e)try{const[r,u,m]=await Promise.all([w.getWatchedEpisodeNumbers(t.db,i.id),n.getDismissedByShow(t.db,i.id),n.getDiscoveredByShow(t.db,i.id)]),p=new Set(r.map(s=>s.toString())),c=new Set(u.map(s=>s.episodeNumber.toString()));for(const s of m)!p.has(s.episodeNumber)&&!c.has(s.episodeNumber)&&o.push({showId:i.id,name:i.name,nativeName:i.nativeName,englishName:i.englishName,thumbnail:i.thumbnail,episodeNumber:s.episodeNumber,id:`${i.id}-${s.episodeNumber}`})}catch(r){h.error({err:r,showId:i.id},"Failed to get notifications for show")}return o.sort((i,r)=>parseFloat(r.episodeNumber)-parseFloat(i.episodeNumber))}),dismiss:a.input(g()).mutation(async({ctx:t,input:e})=>(await d(t.db,async o=>{await n.addDismissed(o,e.showId,e.episodeNumber)}),{success:!0})),clearAll:a.input(b()).mutation(async({ctx:t,input:e})=>(await d(t.db,async o=>{await n.dismissFromDiscovered(o,e.showId)}),{success:!0}))});export{C as notificationsRouter};
@@ -1 +0,0 @@
1
- import{protectedProcedure as u,router as c}from"../index.js";import{WatchedEpisodesRepository as d}from"../../repositories/watched-episodes.repository.js";import{ShowsMetaRepository as h}from"../../repositories/shows-meta.repository.js";import{NotificationsRepository as b}from"../../repositories/notifications.repository.js";import{SettingsRepository as N}from"../../repositories/settings.repository.js";import{performWriteTransactionAsync as y}from"../../sync.js";import{getMigratedId as m}from"../../lib/migration.js";import{discordRPCService as I}from"../../discord-rpc.js";import{dbGet as S}from"../../utils/db-utils.js";import{episodeProgressInput as A,progressUpdateInput as C,showIdInput as v}from"../validation.js";async function P(n,e,s){const t=await h.getById(n,e);if(!t)return!0;const r=(i,a)=>i==null||i===""?!1:String(i)!==String(a??"");return r(s.name,t.name)||r(s.thumbnail,t.thumbnail)||r(s.nativeName,t.nativeName)||r(s.englishName,t.englishName)||r(s.genres,t.genres)||r(s.popularityScore,t.popularityScore)||r(s.status,t.status)||r(s.episodeCount,t.episodeCount)||r(s.type,t.type)||r(s.anilistId,t.anilistId)||r(s.isAdult,t.isAdult)||r(s.episodeDuration,t.episodeDuration)}const U=c({getEpisode:u.input(A()).query(async({ctx:n,input:e})=>{const s=await m(n.db,e.showId);return await d.getByShowAndEpisode(n.db,s,e.episodeNumber)||{currentTime:0,duration:0}}),getWatchedEpisodes:u.input(v()).query(async({ctx:n,input:e})=>{const s=await m(n.db,e.showId);return d.getWatchedEpisodeNumbers(n.db,s)}),updateProgress:u.input(C()).mutation(async({ctx:n,input:e})=>{const s=await m(n.db,e.showId),{episodeNumber:t}=e,r=e.currentTime||0,i=e.duration||0,a=await N.getByKey(n.db,"titlePreference"),g=a?a.value:"englishName";let l=e.showName;g==="englishName"&&e.englishName?l=e.englishName:g==="nativeName"&&e.nativeName&&(l=e.nativeName),I.updatePresence({title:l,episode:String(t),totalEpisodes:e.episodeCount?String(e.episodeCount):void 0,currentTime:r,duration:i,thumbnail:e.showThumbnail||"",isPlaying:e.isPlaying!==!1,sessionId:e.sessionId,isAdult:e.isAdult});const f=Array.isArray(e.genres)?JSON.stringify(e.genres):e.genres,w=/^\d+$/.test(s)?S(n.db,"SELECT anilistId FROM shows_meta WHERE id = ? AND anilistId IS NOT NULL",[s])?.anilistId??parseInt(s):void 0,p={name:e.showName,thumbnail:e.showThumbnail,nativeName:e.nativeName,englishName:e.englishName,genres:f,popularityScore:e.popularityScore,status:e.status,episodeCount:e.episodeCount,type:e.type,anilistId:w,isAdult:typeof e.isAdult=="boolean"?e.isAdult?1:0:null,episodeDuration:i>0?Math.round(i/60):void 0};return await P(n.db,s,p)&&await y(n.db,async o=>{await h.upsert(o,{id:s,...p})}),await y(n.db,async o=>{await d.upsert(o,{showId:s,episodeNumber:t,currentTime:r,duration:i}),await b.deleteSpecificDismissed(o,s,t),await b.deleteDiscovered(o,s,t)}),{success:!0}})});export{U as progressRouter};
@@ -1 +0,0 @@
1
- import{protectedProcedure as n,router as h}from"../index.js";import{QueueRepository as o}from"../../repositories/queue.repository.js";import{ShowsMetaRepository as m}from"../../repositories/shows-meta.repository.js";import{performWriteTransactionAsync as d}from"../../sync.js";import{getMigratedId as u}from"../../lib/migration.js";import{queueAddInput as y,queueBatchInput as b,queueRemoveInput as p,queueRemoveManyInput as l,queueReorderInput as N}from"../validation.js";function w(a){return a.showName||a.showThumbnail||a.nativeName||a.englishName||a.type}const R=h({list:n.query(async({ctx:a})=>(await o.getAll(a.db)).map(t=>({...t,_id:t.showId,id:t.id,thumbnail:t.thumbnail||""}))),add:n.input(y()).mutation(async({ctx:a,input:e})=>{const t=await u(a.db,e.showId),s=e.episodeNumber,r=await o.getByEpisode(a.db,t,s);return await d(a.db,async i=>{w(e)&&await m.upsert(i,{id:t,name:e.showName||"",thumbnail:e.showThumbnail||"",nativeName:e.nativeName,englishName:e.englishName,type:e.type}),r?await o.removeEpisode(i,t,s):await o.addToEnd(i,t,s)}),{success:!0,queued:!r}}),addBatch:n.input(b()).mutation(async({ctx:a,input:e})=>{const t=await u(a.db,e.showId),s=[...new Set(e.episodeNumbers)];return await d(a.db,async r=>{w(e)&&await m.upsert(r,{id:t,name:e.showName||"",thumbnail:e.showThumbnail||"",nativeName:e.nativeName,englishName:e.englishName,type:e.type}),await o.addManyToEnd(r,s.map(i=>({showId:t,episodeNumber:i})))}),{success:!0,added:s.length}}),remove:n.input(p()).mutation(async({ctx:a,input:e})=>{const t=await u(a.db,e.showId);return await d(a.db,async s=>{await o.removeEpisode(s,t,e.episodeNumber)}),{success:!0}}),removeMany:n.input(l()).mutation(async({ctx:a,input:e})=>{const t=await u(a.db,e.showId);let s=[];return await d(a.db,async r=>{s=(await o.getByShow(r,t)||[]).map(c=>c.episodeNumber);const i=Array.isArray(e.episodeNumbers)&&e.episodeNumbers.length?[...new Set(e.episodeNumbers)]:s;await o.removeMany(r,t,i)}),{success:!0,removed:s.length}}),clear:n.mutation(async({ctx:a})=>(await d(a.db,async e=>{await o.clear(e)}),{success:!0})),reorder:n.input(N()).mutation(async({ctx:a,input:e})=>(await d(a.db,async t=>{await o.reorder(t,e)}),{success:!0}))});export{R as queueRouter};
@@ -1 +0,0 @@
1
- import{protectedProcedure as i,router as d}from"../index.js";import{defineSchema as m,optStr as u,reqObj as g}from"../validation.js";import s from"../../logger.js";import{parseJsonBody as l}from"../../utils/http.utils.js";const c=[{id:"listenmoe-jpop",name:"LISTEN.moe JPOP",streamUrl:"https://listen.moe/fallback",homepage:"https://listen.moe/",tags:"anime,j-pop,jpop",codec:"MP3",source:"listen.moe",gateway:"wss://listen.moe/gateway_v2"},{id:"listenmoe-kpop",name:"LISTEN.moe KPOP",streamUrl:"https://listen.moe/kpop/fallback",homepage:"https://listen.moe/",tags:"k-pop,kpop",codec:"MP3",source:"listen.moe",gateway:"wss://listen.moe/kpop/gateway_v2"}],h=[{id:"stereo-anime",name:"stereo anime",streamUrl:"https://radio.stereoanime.com/listen/stereoanime/128",tags:"anime",codec:"MP3",bitrate:128,source:"radio-browser"},{id:"jpop-sakura",name:"J-Pop Sakura (asia DREAM radio)",streamUrl:"https://quincy.torontocast.com:2070/stream.mp3",tags:"j-pop,jpop",codec:"MP3",bitrate:128,source:"radio-browser"},{id:"n-kpop",name:"0 N - K-Pop on Radio",streamUrl:"https://0n-kpop.radionetz.de/0n-kpop.mp3",tags:"k-pop,kpop",codec:"MP3",bitrate:128,source:"radio-browser"},{id:"bigb-kpop",name:"Big B Radio - Kpop",streamUrl:"https://antares.dribbcast.com/proxy/kpop?mp=/s",tags:"k-pop,kpop",codec:"MP3",bitrate:192,source:"radio-browser"}],f=["https://de1.api.radio-browser.info","https://nl1.api.radio-browser.info"];async function b(t){let e=null;for(const o of f)try{const r=await fetch(`${o}${t}`,{headers:{"User-Agent":"dango/1.0"},signal:AbortSignal.timeout(1e4)});if(!r.ok)throw new Error(`radio-browser ${r.status}`);return l(r)}catch(r){e=r}throw e}function S(t){const e=t.url_resolved||"";return e.startsWith("https://")?{id:t.stationuuid||e,name:t.name||"Unknown station",streamUrl:e,homepage:t.homepage||void 0,favicon:t.favicon&&t.favicon.startsWith("https://")?t.favicon:void 0,tags:t.tags||void 0,codec:t.codec||void 0,bitrate:t.bitrate||void 0,source:"radio-browser"}:null}const w=()=>m(t=>{const e=g(t),o={},r=u(e,"q");return r!==void 0&&(o.q=r),o}),U=d({stations:i.query(async({ctx:t})=>{try{const e=t.apiCache.get("route-radio-stations");if(e)return{stations:e};const o=[...c,...h];return t.apiCache.set("route-radio-stations",o,3600),{stations:o}}catch(e){return s.error({err:e},"[Radio] stations failed"),{stations:c}}}),search:i.input(w()).query(async({ctx:t,input:e})=>{try{const o=String(e.q||"").trim();if(!o)return{stations:[]};const r=`route-radio-search-${o.toLowerCase()}`,a=t.apiCache.get(r);if(a)return{stations:a};const n=(await b(`/json/stations/search?name=${encodeURIComponent(o)}&hidebroken=true&order=clickcount&reverse=true&limit=25`)).map(S).filter(p=>p!==null).slice(0,25);return t.apiCache.set(r,n,300),{stations:n}}catch(o){return s.error({err:o},"[Radio] search failed"),{stations:[]}}})});export{U as radioRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as c}from"@trpc/server";import{protectedProcedure as n,router as m}from"../index.js";import{SettingsRepository as o}from"../../repositories/settings.repository.js";import{performWriteTransactionAsync as b}from"../../sync.js";import{discordRPCService as i}from"../../discord-rpc.js";import g from"../../logger.js";import{offlineDb as s}from"../../lib/offline-db.js";import{translateTexts as p}from"../../lib/translate.js";import{getMalImportStatus as y,requestMalImportCancel as I}from"../../lib/mal-import.js";import{defineSchema as l,reqObj as d}from"../validation.js";import{keyInput as w,setSettingInput as C}from"../validation.js";const S={discordRPCEnabled:"true",discordRPCHideMature:"true",dangoGaze:"true",ignoreAdultContent:"true",mangaIgnoreAdultContent:"true",tvIgnoreAdultContent:"true",asmrIgnoreAdultContent:"true"},h=()=>l(t=>{const e=d(t);if(typeof e.enabled!="boolean")throw new Error("enabled must be a boolean");return{enabled:e.enabled}}),A=()=>l(t=>{if(t==null)return{};const e=d(t);return{texts:e.texts,source:e.source,target:e.target}}),U=m({getByKey:n.input(w()).query(async({ctx:t,input:e})=>{const r=await o.getByKey(t.db,e.key);return{value:r?r.value:S[e.key]??null}}),set:n.input(C()).mutation(async({ctx:t,input:e})=>{const r=e.key,a=String(e.value??""),f=a===""&&r==="tracker_anilist_client_id";return await b(t.db,async u=>{f?await o.deleteByKey(u,r):await o.upsert(u,r,a)}),r==="discordRPCEnabled"&&i.setEnabled(e.value==="true"||e.value===!0),r==="discordRPCHideMature"&&i.setHideMature(e.value==="true"||e.value===!0),{success:!0}}),offlineDbInfo:n.query(async({ctx:t})=>await s.getOfflineDbInfo(t.db)),setOfflineDbAutoUpdate:n.input(h()).mutation(async({ctx:t,input:e})=>(await o.upsert(t.db,"offlineDbAutoUpdateEnabled",String(e.enabled)),{success:!0,enabled:e.enabled})),triggerOfflineDbUpdate:n.mutation(async({ctx:t})=>{if((await s.getOfflineDbInfo(t.db)).isRefreshing)throw new c({code:"CONFLICT",message:"Offline database refresh already in progress"});return s.refreshDatabase(t.db).catch(r=>{g.warn({err:r?.message},"Manual offline database update failed")}),{message:"Offline database refresh started"}}),translate:n.input(A()).mutation(async({input:t})=>{const e=t.texts,r=t.source??"ja",a=t.target??"en";return!Array.isArray(e)||e.length===0?{translations:{}}:{translations:await p(e,String(r),String(a))}}),malImportStatus:n.query(()=>y()),malImportCancel:n.mutation(()=>I())});export{S as SETTING_DEFAULTS,U as settingsRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as i}from"@trpc/server";import{protectedProcedure as s,router as g}from"../index.js";import{SettingsRepository as u}from"../../repositories/settings.repository.js";import{performWriteTransactionAsync as h}from"../../sync.js";import{AniListTracker as T}from"../../lib/tracker/anilist-tracker.js";import{syncAniList as I,importFromMalUsername as E,importFromUsername as M}from"../../lib/tracker/sync.service.js";import{syncAniListManga as U,importFromUsernameManga as S,importMangaFromMalUsername as R}from"../../lib/tracker/manga-sync.service.js";import{badRequest as m,defineSchema as p,failed as c,reqObj as A}from"../validation.js";const k="tracker_anilist_token",f="tracker_anilist_user";function y(r){return new i({code:"SERVICE_UNAVAILABLE",message:r})}function d(r){return r==null?{}:A(r)}const D=()=>p(r=>{const e=d(r);return{token:e.token,code:e.code,redirectUri:e.redirectUri}}),O=()=>p(r=>({provider:d(r).provider})),w=()=>p(r=>{const e=d(r);return{username:e.username,erase:e.erase}}),C=()=>p(r=>{const e=d(r);return{username:e.username,erase:e.erase,useOfflineDb:e.useOfflineDb,skipFallback:e.skipFallback}}),F=()=>p(r=>({direction:d(r).direction}));function b(r){return r.includes("private")||r.includes("not found")?new i({code:"NOT_FOUND",message:r}):r.includes("blocked")||r.includes("HTTP 429")?new i({code:"TOO_MANY_REQUESTS",message:r}):c(r)}const B=g({status:s.query(async({ctx:r})=>{try{const e=await u.getByKey(r.db,k),a=await u.getByKey(r.db,f);let n=null;if(a?.value)try{n=JSON.parse(a.value)}catch{n=null}return{anilist:{connected:!!e?.value,user:n}}}catch{throw c("Failed to read tracker status")}}),anilistAuth:s.input(D()).mutation(async({ctx:r,input:e})=>{const a=typeof e.token=="string"?e.token.trim():"";if(!a)throw m("Access token is required");try{const t=await new T(a).getViewer();return await h(r.db,async o=>{await u.upsert(o,k,a),await u.upsert(o,f,JSON.stringify(t))}),{success:!0,user:t}}catch(n){throw new i({code:"UNAUTHORIZED",message:n instanceof Error?n.message:"Authentication failed"})}}),anilistDisconnect:s.mutation(async({ctx:r})=>{try{return await h(r.db,async e=>{await u.upsert(e,k,""),await u.upsert(e,f,"")}),{success:!0}}catch{throw c("Failed to disconnect")}}),sync:s.input(O()).mutation(async({ctx:r,input:e})=>{const{provider:a="anilist"}=e;if(a!=="anilist")throw m(`Provider "${a}" is not supported yet`);try{return{success:!0,summary:await I(r.db)}}catch(n){throw c(n instanceof Error?n.message:"Sync failed")}}),anilistImport:s.input(w()).mutation(async({ctx:r,input:e})=>{const{username:a,erase:n}=e;if(!a||typeof a!="string")throw m("Username is required");try{return{success:!0,count:await M(r.db,a.trim(),n===!0)}}catch(t){throw c(t instanceof Error?t.message:"Import failed")}}),malImport:s.input(C()).mutation(async({ctx:r,input:e})=>{const{username:a,erase:n,useOfflineDb:t,skipFallback:o}=e;if(!a||typeof a!="string")throw m("MAL username is required");try{return{success:!0,count:await E(r.db,a.trim(),{erase:n===!0,useOfflineDb:t!==!1,skipFallback:o===!0})}}catch(l){throw b(l instanceof Error?l.message:"Import failed")}}),mangaSync:s.input(F()).mutation(async({ctx:r,input:e})=>{const{direction:a}=e;try{const n=r.mangaDb;if(!n||n.isClosedCheck())throw y("Manga database is not ready");const t=a==="pull-only"?"pull-only":"two-way",o=await U(r.db,n,{direction:t});return{success:!0,summary:o.summary,details:o.details,direction:t}}catch(n){throw n instanceof i?n:c(n instanceof Error?n.message:"Manga sync failed")}}),mangaImport:s.input(w()).mutation(async({ctx:r,input:e})=>{const{username:a,erase:n}=e;if(!a||typeof a!="string")throw m("Username is required");try{const t=r.mangaDb;if(!t||t.isClosedCheck())throw y("Manga database is not ready");return{success:!0,count:await S(r.db,t,a.trim(),n===!0)}}catch(t){throw t instanceof i?t:c(t instanceof Error?t.message:"Import failed")}}),mangaMalImport:s.input(w()).mutation(async({ctx:r,input:e})=>{const{username:a,erase:n}=e;if(!a||typeof a!="string")throw m("Username is required");try{const t=r.mangaDb;if(!t||t.isClosedCheck())throw y("Manga database is not ready");return{success:!0,...await R(r.db,t,a.trim(),n===!0)}}catch(t){throw t instanceof i?t:b(t instanceof Error?t.message:"Import failed")}})});export{B as trackerRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as b}from"@trpc/server";import{protectedProcedure as n,router as g}from"../index.js";import{performTvWriteTransactionAsync as o}from"../../sync.js";import{buildTvId as w,TV_STATUSES as v,TvLibraryRepository as s,TvProgressRepository as p}from"../../repositories/tv.repository.js";import{defineSchema as i,reqObj as u,reqStr as y,optStr as S,reqStrArray as I,readPageLimit as T}from"../validation.js";function L(r){return String(r).toLowerCase()==="movie"?"movie":"tv"}const l=()=>i(r=>{const t=u(r),a={...T(t)},e=S(t,"status");return e!==void 0&&(a.status=e),a}),f=()=>i(r=>({id:y(u(r),"id")})),h=()=>i(r=>{const t=u(r);return{tmdbId:t.tmdbId,mediaType:t.mediaType,id:t.id,title:t.title,poster:t.poster,backdrop:t.backdrop,year:t.year,overview:t.overview,status:t.status,adult:t.adult}}),k=()=>i(r=>{if(typeof r!="string"||r.length===0)throw new Error("id must be a string");return r}),A=()=>i(r=>{const t=u(r);return{id:y(t,"id"),status:y(t,"status")}}),D=()=>i(r=>{const t=u(r);return{ids:I(t,"ids",!0),status:y(t,"status")}}),E=()=>i(r=>{if(!Array.isArray(r)||r.length===0)throw new Error("ids must be a non-empty array");return r.map(t=>String(t))}),x=g({list:n.input(l()).query(async({ctx:r,input:t})=>{const a=Math.max(parseInt(String(t.page??1))||1,1),e=Math.min(Math.max(parseInt(String(t.limit??24))||24,1),100),d=(a-1)*e,c=await s.getAll(r.tvDb,t.status,e,d),m=await s.getCount(r.tvDb,t.status);return{data:c,total:m,page:a,limit:e}}),ids:n.query(async({ctx:r})=>({ids:await s.getIds(r.tvDb)})),check:n.input(f()).query(async({ctx:r,input:t})=>{const a=await s.getById(r.tvDb,t.id);return{inLibrary:!!a,status:a?.status??null}}),add:n.input(h()).mutation(async({ctx:r,input:t})=>{const a=Number(t.tmdbId);if(!a||!t.id&&!t.mediaType)throw new b({code:"BAD_REQUEST",message:"tmdbId and mediaType are required"});const e=L(t.mediaType||(String(t.id).startsWith("movie:")?"movie":"tv")),d=t.id&&String(t.id).includes(":")?String(t.id):w(e,a),c=typeof t.status=="string"&&v.includes(t.status)?t.status:"Watching";return await o(r.tvDb,async m=>{await s.upsert(m,{id:d,tmdbId:a,mediaType:e,title:String(t.title||""),poster:String(t.poster||""),backdrop:t.backdrop?String(t.backdrop):void 0,year:t.year?String(t.year):void 0,overview:t.overview?String(t.overview):void 0,status:c,adult:t.adult===!0})}),{success:!0,id:d}}),remove:n.input(k()).mutation(async({ctx:r,input:t})=>(await o(r.tvDb,async a=>{await s.delete(a,String(t)),await p.deleteByMedia(a,String(t))}),{success:!0})),setStatus:n.input(A()).mutation(async({ctx:r,input:t})=>{if(!t.id||!v.includes(t.status))throw new b({code:"BAD_REQUEST",message:"id and a valid status are required"});return await o(r.tvDb,async a=>{await s.updateStatus(a,String(t.id),String(t.status))}),{success:!0}}),batchStatus:n.input(D()).mutation(async({ctx:r,input:t})=>{if(!t.status||!v.includes(t.status))throw new b({code:"BAD_REQUEST",message:"a valid status is required"});const a=t.ids.map(e=>String(e));return await o(r.tvDb,async e=>{await s.updateStatusMany(e,a,String(t.status))}),{success:!0,updated:a.length}}),removeMany:n.input(E()).mutation(async({ctx:r,input:t})=>{const a=t.map(e=>String(e));return await o(r.tvDb,async e=>{await s.deleteMany(e,a),await p.deleteMany(e,a)}),{success:!0,removed:a.length}})});export{x as tvLibraryRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as b}from"@trpc/server";import{protectedProcedure as a,router as h}from"../index.js";import{performTvWriteTransactionAsync as p}from"../../sync.js";import{adultNonListIds as f}from"../../utils/db-utils.js";import{SettingsRepository as M}from"../../repositories/settings.repository.js";import{normalizeTvMediaId as l,TvLibraryRepository as P,TvProgressRepository as s}from"../../repositories/tv.repository.js";import{defineSchema as u,reqObj as c,reqStr as g,strOrNull as m}from"../validation.js";const N=t=>t.adult===!0||t.adult===1,w=t=>f(t,"tv_progress","tv_library","mediaId","COALESCE(l.adult, p.adult) = 1"),S=()=>u(t=>({mediaId:g(c(t),"mediaId")})),k=()=>u(t=>{const e=c(t);return{mediaId:g(e,"mediaId"),season:e.season,episode:e.episode}}),C=()=>u(t=>{const e=c(t);return{mediaId:g(e,"mediaId"),season:e.season,episode:e.episode,currentTime:e.currentTime,duration:e.duration,completed:e.completed,title:m(e,"title"),poster:m(e,"poster"),backdrop:m(e,"backdrop"),year:m(e,"year"),overview:m(e,"overview"),tmdbId:e.tmdbId??null,mediaType:m(e,"mediaType"),adult:e.adult??null}}),D=()=>u(t=>{const e=c(t);return{mediaId:g(e,"mediaId"),season:e.season,episode:e.episode}}),R=()=>u(t=>{if(!Array.isArray(t)||t.length===0)throw new Error("ids must be a non-empty array");return t.map(e=>String(e))}),W=()=>u(t=>{const e=c(t),n={};if(e.limit!==void 0&&e.limit!==null&&e.limit!==""){const r=Number(e.limit);Number.isFinite(r)&&(n.limit=r)}return n}),O=h({getByMedia:a.input(S()).query(async({ctx:t,input:e})=>({progress:await s.getByMedia(t.tvDb,l(e.mediaId))})),latest:a.input(k()).query(async({ctx:t,input:e})=>{const n=l(e.mediaId),r=parseInt(String(e.season),10),i=parseInt(String(e.episode),10);return Number.isFinite(r)&&Number.isFinite(i)?await s.getEpisode(t.tvDb,n,r,i)||{currentTime:0,duration:0,completed:0}:await s.getLatest(t.tvDb,n)||{currentTime:0,duration:0,completed:0}}),save:a.input(C()).mutation(async({ctx:t,input:e})=>{const n=l(e.mediaId);if(!n)throw new b({code:"BAD_REQUEST",message:"mediaId is required"});const r=Math.max(Number(e.season)||1,1),i=Math.max(Number(e.episode)||1,1),d=Math.max(Number(e.currentTime)||0,0),o=Math.max(Number(e.duration)||0,0),v=o>0?Math.min(d,o):d,I=o>0&&v>=o*.8?o:v,T=Number(e.completed)===1||o>0&&I>=o*.8?1:0;return await p(t.tvDb,async y=>{await s.upsert(y,{mediaId:String(n),season:Math.round(r),episode:Math.round(i),currentTime:I,duration:o,completed:T,title:e.title??null,poster:e.poster??null,backdrop:e.backdrop??null,year:e.year??null,overview:e.overview??null,tmdbId:e.tmdbId!=null?Number(e.tmdbId):null,mediaType:e.mediaType??null,adult:e.adult!=null?Number(e.adult):null}),await P.touchProgress(y,String(n),{season:Math.round(r),episode:Math.round(i)})}),{success:!0}}),remove:a.input(D()).mutation(async({ctx:t,input:e})=>{const n=l(e.mediaId);if(!n)throw new b({code:"BAD_REQUEST",message:"mediaId is required"});return await p(t.tvDb,async r=>{e.season!==void 0&&e.episode!==void 0?await s.deleteEpisode(r,String(n),Number(e.season),Number(e.episode)):await s.deleteByMedia(r,String(n))}),{success:!0}}),removeMany:a.input(R()).mutation(async({ctx:t,input:e})=>{const n=e.map(r=>l(r));return await p(t.tvDb,async r=>{await s.deleteMany(r,n)}),{success:!0,removed:n.length}}),continueWatching:a.input(W()).query(async({ctx:t,input:e})=>{const n=Math.min(Math.max(parseInt(String(e.limit??24))||24,1),100),{ignoreAdult:r,listOnly:i}=await M.readContinueFlags(t.db,"tvIgnoreAdultContent","tvCwWatchlistOnly"),d=(await s.getContinueWatching(t.tvDb,n)).filter(o=>!(r&&N(o)||i&&o.watchlistStatus!=="Watching"));return{data:d,total:d.length}}),adultCount:a.query(async({ctx:t})=>({count:(await w(t.tvDb)).length})),purgeAdult:a.mutation(async({ctx:t})=>{const e=await w(t.tvDb);return e.length>0&&await p(t.tvDb,async n=>{for(const r of e)await s.deleteByMedia(n,r)}),{success:!0,removed:e.length}})});export{O as tvProgressRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as $}from"@trpc/server";import{protectedProcedure as h,router as C}from"../index.js";import{defineSchema as I,failed as g,optStr as b,readCount as q,reqId as k,reqObj as T,reqStr as M}from"../validation.js";import{getTmdbKey as w,TMDB_BASE as f,TMDB_IMAGE as B}from"../../lib/tmdb.js";import{parseJsonBody as _}from"../../utils/http.utils.js";async function D(s,e,r){const n={...r};if(n.title&&n.imdbId&&n.year)return n;try{const t=await w();if(t){const a=await fetch(`${f}/${s}/${e}?api_key=${t}&append_to_response=external_ids`,{headers:{"User-Agent":"Mozilla/5.0"},signal:AbortSignal.timeout(4e3)});if(a.ok){const i=await _(a);n.title||(n.title=i.title||i.name||""),n.year||(n.year=(i.release_date||i.first_air_date||"").split("-")[0]||""),n.imdbId||(n.imdbId=i.external_ids?.imdb_id||i.imdb_id||""),s==="tv"&&i.number_of_seasons&&(n.totalSeasons=String(i.number_of_seasons))}}}catch{}return n}function P(s,e){return{id:s.id,title:s.title||s.name||"",year:(s.release_date||s.first_air_date||"").split("-")[0],type:e,image:s.poster_path?`${B}/w500${s.poster_path}`:"",backdrop:s.backdrop_path?`${B}/w780${s.backdrop_path}`:"",overview:s.overview||"",vote_average:s.vote_average||0,adult:s.adult===!0,genre_ids:s.genre_ids||[]}}const E=()=>I(s=>{const e=T(s),r={},n=b(e,"q");n!==void 0&&(r.q=n),e.page!==void 0&&(r.page=q(e,"page",1));const t=b(e,"type");t!==void 0&&(r.type=t);const a=b(e,"genre");a!==void 0&&(r.genre=a);const i=b(e,"year");i!==void 0&&(r.year=i);const o=b(e,"sortBy");return o!==void 0&&(r.sortBy=o),r}),R=()=>I(s=>{const e=T(s),r={},n=b(e,"mediaType");n!==void 0&&(r.mediaType=n);const t=b(e,"timeWindow");return t!==void 0&&(r.timeWindow=t),e.page!==void 0&&(r.page=q(e,"page",1)),r}),L=()=>I(s=>{const e=T(s);return{type:M(e,"type"),id:k(e,"id")}}),x=()=>I(s=>{const e=T(s);return{id:k(e,"id"),season:k(e,"season")}}),j=()=>I(s=>({imdbId:M(T(s),"imdbId")})),z=()=>I(s=>{const e=T(s),r={},n=b(e,"q");return n!==void 0&&(r.q=n),r}),W=()=>I(s=>{const e=T(s),r={type:M(e,"type"),tmdbId:k(e,"tmdbId")},n=b(e,"season");n!==void 0&&(r.season=n);const t=b(e,"episode");return t!==void 0&&(r.episode=t),r}),N=()=>I(s=>{const e=T(s),r={provider:M(e,"provider"),type:M(e,"type"),tmdbId:k(e,"tmdbId")};e.season!==void 0&&(r.season=q(e,"season",1)),e.episode!==void 0&&(r.episode=q(e,"episode",1));const n=b(e,"server");n!==void 0&&(r.server=n);const t=b(e,"title");t!==void 0&&(r.title=t);const a=b(e,"year");a!==void 0&&(r.year=a);const i=b(e,"imdbId");i!==void 0&&(r.imdbId=i);const o=b(e,"totalSeasons");return o!==void 0&&(r.totalSeasons=o),r}),O=C({search:h.input(E()).query(async({ctx:s,input:e})=>{const r=e.q||"",n=e.page||1,t=e.type||"multi",a=e.genre||"",i=e.year||"",o=e.sortBy||"popularity.desc",l=`tv-search-${r.toLowerCase()}-${n}-${t}-${a}-${i}-${o}`,d=s.apiCache.get(l);if(d)return d;const v=await w();if(!v)throw g("No TMDB API key available");try{let c;if(t==="tv"||t==="movie")if(r.trim()){const u=new URLSearchParams({api_key:v,query:encodeURIComponent(r.trim()),page:String(n),include_adult:"true"});c=`${f}/search/${t}?${u.toString()}`}else{const u=new URLSearchParams({api_key:v,page:String(n),sort_by:o});a&&u.set("with_genres",a),i&&(t==="tv"?u.set("first_air_date_year",i):u.set("primary_release_year",i)),c=`${f}/discover/${t}?${u.toString()}`}else if(r.trim()){const u=new URLSearchParams({api_key:v,query:encodeURIComponent(r),page:String(n),include_adult:"true"});c=`${f}/search/multi?${u.toString()}`}else{const u=new URLSearchParams({api_key:v,page:String(n),sort_by:o});a&&u.set("with_genres",a),i&&u.set("first_air_date_year",i),c=`${f}/discover/tv?${u.toString()}`}const y=await fetch(c,{headers:{"User-Agent":"Mozilla/5.0"}});if(!y.ok)throw g("TMDB search failed");const m=await _(y);let S;t==="tv"||t==="movie"?S=(m.results||[]).map(u=>P(u,t)):S=(m.results||[]).filter(u=>u.media_type!=="person").map(u=>P(u,u.media_type||"tv"));const p={results:S,total_results:m.total_results||0,total_pages:m.total_pages||0,page:m.page||n};return s.apiCache.set(l,p,1800),p}catch(c){throw c instanceof $?c:g(c.message)}}),trending:h.input(R()).query(async({ctx:s,input:e})=>{const r=e.mediaType||"all",n=e.timeWindow||"week",t=e.page||1,a=`tv-trending-${r}-${n}-${t}`,i=s.apiCache.get(a);if(i)return i;const o=await w();if(!o)throw g("No TMDB API key available");try{const l=await fetch(`${f}/trending/${r}/${n}?api_key=${o}&page=${t}`,{headers:{"User-Agent":"Mozilla/5.0"}});if(!l.ok)throw g("TMDB trending failed");const d=await _(l),c={results:(d.results||[]).filter(y=>y.media_type!=="person").map(y=>P(y,y.media_type||"tv")),total_results:d.total_results||0,total_pages:d.total_pages||0,page:d.page||t};return s.apiCache.set(a,c,3600),c}catch(l){throw l instanceof $?l:g(l.message)}}),genres:h.query(async({ctx:s})=>{const e="tv-genres-list",r=s.apiCache.get(e);if(r)return r;const n=await w();if(!n)throw g("No TMDB API key available");try{const[t,a]=await Promise.all([fetch(`${f}/genre/tv/list?api_key=${n}`,{headers:{"User-Agent":"Mozilla/5.0"}}),fetch(`${f}/genre/movie/list?api_key=${n}`,{headers:{"User-Agent":"Mozilla/5.0"}})]),i=t.ok?await _(t):{genres:[]},o=a.ok?await _(a):{genres:[]},l={tv:(i.genres||[]).map(d=>({id:d.id,name:d.name})),movie:(o.genres||[]).map(d=>({id:d.id,name:d.name}))};return s.apiCache.set(e,l,86400),l}catch(t){throw t instanceof $?t:g(t.message)}}),details:h.input(L()).query(async({input:s})=>{const e=s.type,r=s.id,n=await w();if(!n)throw g("No TMDB API key available");try{const t=await fetch(`${f}/${e}/${r}?api_key=${n}&append_to_response=external_ids`,{headers:{"User-Agent":"Mozilla/5.0"}});if(!t.ok)throw g("TMDB details failed");const a=await _(t),i={id:a.id,imdb_id:a.external_ids?.imdb_id||a.imdb_id||null,title:a.title||a.name,overview:a.overview,vote_average:a.vote_average,year:(a.release_date||a.first_air_date||"").split("-")[0],poster:a.poster_path?`${B}/w500${a.poster_path}`:"",backdrop:a.backdrop_path?`${B}/original${a.backdrop_path}`:"",adult:a.adult===!0};return e==="tv"&&(i.seasons=(a.seasons||[]).filter(o=>o.season_number>0).map(o=>({season_number:o.season_number,episode_count:o.episode_count})),i.number_of_seasons=a.number_of_seasons),i}catch(t){throw t instanceof $?t:g(t.message)}}),episodes:h.input(x()).query(async({input:s})=>{const e=s.id,r=s.season,n=await w();if(!n)throw g("No TMDB API key available");try{const t=await fetch(`${f}/tv/${e}/season/${r}?api_key=${n}`,{headers:{"User-Agent":"Mozilla/5.0"}});if(!t.ok)throw g("TMDB episodes failed");return{episodes:((await _(t)).episodes||[]).map(o=>({episode_number:o.episode_number,name:o.name,vote_average:o.vote_average,overview:o.overview,still_path:o.still_path}))}}catch(t){throw t instanceof $?t:g(t.message)}}),lookupImdb:h.input(j()).query(async({input:s})=>{const e=await w();if(!e)return{tmdbId:null,error:"No TMDB API key available"};try{const r=await fetch(`${f}/find/${s.imdbId}?api_key=${e}&external_source=imdb_id`,{headers:{"User-Agent":"Mozilla/5.0"}});if(!r.ok)return{tmdbId:null,error:`TMDB error ${r.status}`};const n=await _(r),t=n.movie_results?.[0],a=n.tv_results?.[0],i=t||a;return{tmdbId:i?.id||null,type:t?"movie":a?"tv":null,title:i?.title||i?.name||null,year:(i?.release_date||i?.first_air_date||"").substring(0,4)||null}}catch(r){return{tmdbId:null,error:r.message}}}),searchImdb:h.input(z()).query(async({ctx:s,input:e})=>{const r=e.q||"";if(!r)return[];const n=`tv-imdb-search-${r.toLowerCase()}`,t=s.apiCache.get(n);if(t)return t;try{const a=await fetch(`https://v3.sg.media-imdb.com/suggestion/x/${encodeURIComponent(r)}.json`,{headers:{"User-Agent":"Mozilla/5.0"}});if(!a.ok)throw g("IMDB search failed");const i=await _(a),o=new Set(["tvSeries","tvMiniSeries","movie"]),l=(i.d||[]).filter(d=>!!(d.id&&d.l)&&o.has(d.qid||"")).slice(0,10).map(d=>({id:d.id,title:d.l,year:d.y,type:d.qid,image:d.i?.imageUrl||""}));return s.apiCache.set(n,l,3600),l}catch(a){throw a instanceof $?a:g(a.message)}}),subtitles:h.input(W()).query(async({ctx:s,input:e})=>{const r=e.type,n=e.tmdbId,t=parseInt(n,10);if(!t)return{subtitles:[]};const a=r==="movie"?"movie":"tv",i=e.season||"1",o=e.episode||"1",l=a==="tv"?`tv-subs-${t}-${i}-${o}`:`tv-subs-${t}-movie`,d=s.apiCache.get(l);if(d)return d;try{const v=a==="tv"?`https://subtitles.vidy.st/search?id=${t}&season=${encodeURIComponent(i)}&episode=${encodeURIComponent(o)}`:`https://subtitles.vidy.st/search?id=${t}`,c=await fetch(v,{headers:{"User-Agent":"Mozilla/5.0"},signal:AbortSignal.timeout(1e4)});if(!c.ok)return{subtitles:[]};const y=await _(c),S={subtitles:(Array.isArray(y)?y:[]).filter(p=>typeof p.url=="string"&&p.url.startsWith("https://")).map(p=>({language:String(p.language||"en"),label:String(p.display||p.language||"Unknown"),url:String(p.url),trusted:p.isTrusted===!0,hi:p.isHearingImpaired===!0,downloads:Number(p.downloadCount)||0,media:String(p.media||p.release||"")})).sort((p,u)=>{const U=p.language.toLowerCase().startsWith("en")?0:1,A=u.language.toLowerCase().startsWith("en")?0:1;return U!==A?U-A:p.trusted!==u.trusted?p.trusted?-1:1:u.downloads-p.downloads}).slice(0,50)};return s.apiCache.set(l,S,3600),S}catch{return{subtitles:[]}}}),sources:h.input(N()).query(async({ctx:s,input:e})=>{const r=e.provider,n=e.type,t=e.tmdbId,a=n==="movie"?"movie":"tv",i=parseInt(t,10);if(!i)return{sources:[],audioTracks:[],error:"Bad tmdb id"};const o=s.getTvProvider(String(r).toLowerCase());if(!o||typeof o.getSources!="function")return{sources:[],audioTracks:[],error:`Unknown TV provider ${r}`};const l=e.season||1,d=e.episode||1,v=e.server,c=await D(a,i,{title:e.title||"",year:e.year||"",imdbId:e.imdbId||"",totalSeasons:e.totalSeasons||"1"}),y={tmdbId:i,type:a,season:l,episode:d,title:c.title,year:c.year,imdbId:c.imdbId,totalSeasons:parseInt(c.totalSeasons,10)||1};try{const m=await o.getSources(y,v);return!m||!Array.isArray(m.sources)||m.sources.length===0?{provider:o.name,server:m?.server||v,sources:[],audioTracks:[],valid:!1,error:"No sources found"}:{provider:o.name,server:m.server||v,servers:m.servers,embedUrl:m.embedUrl,sources:m.sources,audioTracks:m.audioTracks||[],subtitles:m.subtitles||[],referer:m.referer||"",valid:!0}}catch(m){return{provider:o.name,sources:[],audioTracks:[],valid:!1,error:m.message}}})});export{O as tvRouter};
@@ -1,3 +0,0 @@
1
- import{protectedProcedure as g,router as _}from"../index.js";import{WatchlistRepository as p}from"../../repositories/watchlist.repository.js";import{WatchedEpisodesRepository as N}from"../../repositories/watched-episodes.repository.js";import{ShowsMetaRepository as b}from"../../repositories/shows-meta.repository.js";import{NotificationsRepository as M}from"../../repositories/notifications.repository.js";import{QueueRepository as k}from"../../repositories/queue.repository.js";import{SettingsRepository as v}from"../../repositories/settings.repository.js";import{performWriteTransactionAsync as I}from"../../sync.js";import{getMigratedId as y}from"../../lib/migration.js";import{isAnilistRateLimited as $,searchAnilist as z,searchAnilistByTitle as H,getAnilistEpisodes as U,getShowMetaById as j}from"../../lib/anilist.js";import{kitsuSearchAnime as V}from"../../lib/kitsu.js";import{offlineDb as E}from"../../lib/offline-db.js";import{dbAll as A}from"../../utils/db-utils.js";import{defineSchema as B,idInput as Y,idsInput as K,optNum as W,optStr as h,reqObj as C,showIdInput as R,watchlistAddInput as J,watchlistBatchStatusInput as Q,watchlistStatusInput as X}from"../validation.js";const Z=()=>B(t=>{const e=C(t),s={...F(e)},n=h(e,"status");n!==void 0&&(s.status=n);const i=W(e,"page");i!==void 0&&(s.page=i);const r=W(e,"limit");r!==void 0&&(s.limit=r);const a=h(e,"showMature");return a!==void 0&&(s.showMature=a),s}),ee=()=>B(t=>{const e=C(t),s={...F(e)},n=W(e,"page");n!==void 0&&(s.page=n);const i=W(e,"limit");return i!==void 0&&(s.limit=i),s}),Ae=_({check:g.input(R()).query(async({ctx:t,input:e})=>{const s=await y(t.db,e.showId),n=await p.getById(t.db,s);return{inWatchlist:!!n,status:n?.status??null}}),add:g.input(J()).mutation(async({ctx:t,input:e})=>{const s=await y(t.db,e.id);if(await I(t.db,async n=>{await p.upsert(n,{id:s,name:e.name,thumbnail:e.thumbnail||"",status:e.status||"Watching",nativeName:e.nativeName||"",englishName:e.englishName||"",type:e.type||"TV"}),typeof e.isAdult=="boolean"&&await b.upsert(n,{id:s,isAdult:e.isAdult?1:0})}),e.name&&!/^\d+$/.test(s)){const n=t.db,i=e.name;(async()=>{if(!$()){const a=await H(i);if(a?.id){await b.upsert(n,{id:s,anilistId:a.id});return}}try{const a=await V({query:i,page:1,perPage:3});if(a.length>0){const l=Math.abs(a[0].id);await b.upsert(n,{id:s,anilistId:l})}}catch{}})().catch(()=>{})}return{success:!0}}),remove:g.input(Y()).mutation(async({ctx:t,input:e})=>{const s=await y(t.db,e.id);return await I(t.db,async n=>{await p.delete(n,s),await N.deleteByShow(n,s),await M.deleteByShow(n,s)}),{success:!0}}),removeMany:g.input(K()).mutation(async({ctx:t,input:e})=>{const s=await Promise.all(e.ids.map(n=>y(t.db,n)));return await I(t.db,async n=>{await p.deleteMany(n,s);for(const i of s)await N.deleteByShow(n,i),await M.deleteByShow(n,i)}),{success:!0,removed:s.length}}),setStatus:g.input(X()).mutation(async({ctx:t,input:e})=>{const s=await y(t.db,e.id);return await I(t.db,async n=>{await p.updateStatus(n,s,e.status)}),{success:!0}}),batchStatus:g.input(Q()).mutation(async({ctx:t,input:e})=>{const s=await Promise.all(e.ids.map(n=>y(t.db,n)));return await I(t.db,async n=>{await p.updateStatusMany(n,s,e.status)}),{success:!0,updated:s.length}}),list:g.input(Z()).query(async({ctx:t,input:e})=>{const s=t.db,n=Math.max(parseInt(String(e.page??1))||1,1),i=Math.min(Math.max(parseInt(String(e.limit??10))||10,1),100),r=e.showMature,a=(n-1)*i,l=q({query:e.query,type:e.type,season:e.season,year:e.year,genres:e.genres,excludeGenres:e.excludeGenres,sortBy:e.sortBy,titlePreference:e.titlePreference}),o=e.status,d=await p.getAll(s,o);let c=await O(d,l,s);if(r==="false"){const m=c.map(f=>f.id);if(m.length>0){const f=m.map(()=>"?").join(","),T=await A(s,`SELECT id FROM shows_meta WHERE id IN (${f}) AND isAdult = 1`,m),G=new Set(T.map(P=>P.id));c=c.filter(P=>!G.has(P.id))}}const u=c.slice(a,a+i);for(const m of u)if(!m.thumbnail||m.thumbnail.trim()==="")try{const f=await b.getById(s,m.id);f?.thumbnail&&f.thumbnail.trim()!==""&&(m.thumbnail=f.thumbnail)}catch{}await x(s,u);const w=u.map(m=>m.id);let L=new Set;if(w.length>0){const m=w.map(()=>"?").join(","),f=await A(s,`SELECT id FROM shows_meta WHERE id IN (${m}) AND isAdult = 1`,w);L=new Set(f.map(T=>T.id))}return{data:u.map(m=>({...m,_id:m.id,thumbnail:m.thumbnail||"",isAdult:L.has(m.id)})),total:c.length,page:n,limit:i}}),continueWatchingAll:g.input(ee()).query(async({ctx:t,input:e})=>{const s=t.db,n=Math.max(parseInt(String(e.page??1))||1,1),i=Math.min(Math.max(parseInt(String(e.limit??10))||10,1),100),r=(n-1)*i,a=q({query:e.query,type:e.type,season:e.season,year:e.year,genres:e.genres,excludeGenres:e.excludeGenres,sortBy:e.sortBy,titlePreference:e.titlePreference}),l=await O(await le(s),a,s);return{data:l.slice(r,r+i),total:l.length,page:n,limit:i}}),queueSuggested:g.input(R()).query(async({ctx:t,input:e})=>{const s=t.db,n=await y(s,e.showId),i=await N.getLatestResumeProgress(s,n);if(i)return{showId:n,episodeNumber:i.episodeNumber,resumeTime:i.currentTime||0};const[r,a]=await Promise.all([N.getByShow(s,n),D(n)]),l=new Set(r.map(u=>u.episodeNumber.toString())),o=r.filter(u=>u.duration>0&&u.currentTime>=u.duration*.8).map(u=>parseFloat(u.episodeNumber)).filter(u=>!Number.isNaN(u)),d=o.length>0?String(Math.max(...o)+1):void 0,c=(d&&a.includes(d)&&!l.has(d)?d:a.find(u=>!l.has(u)))||a[0]||"1";return{showId:n,episodeNumber:c,resumeTime:0}}),queueRemaining:g.input(R()).query(async({ctx:t,input:e})=>{const s=t.db,n=await y(s,e.showId),[i,r,a]=await Promise.all([N.getByShow(s,n),k.getByShow(s,n),D(n)]),l=new Set(i.map(c=>c.episodeNumber.toString())),o=new Set(r.map(c=>c.episodeNumber.toString())),d=a.filter(c=>!l.has(c)&&!o.has(c));return{showId:n,episodes:d}})});function F(t){const e={},s=h(t,"query");s!==void 0&&(e.query=s);const n=h(t,"type");n!==void 0&&(e.type=n);const i=h(t,"season");i!==void 0&&(e.season=i);const r=h(t,"year");r!==void 0&&(e.year=r);const a=h(t,"genres");a!==void 0&&(e.genres=a);const l=h(t,"excludeGenres");l!==void 0&&(e.excludeGenres=l);const o=h(t,"sortBy");o!==void 0&&(e.sortBy=o);const d=h(t,"titlePreference");return d!==void 0&&(e.titlePreference=d),e}function S(t){if(typeof t!="string")return;const e=t.trim();return e&&e!=="ALL"?e:void 0}function q(t){return{query:S(t.query),type:S(t.type),season:S(t.season),year:S(t.year),genres:S(t.genres),excludeGenres:S(t.excludeGenres),sortBy:S(t.sortBy),titlePreference:["name","nativeName","englishName"].includes(String(t.titlePreference))?t.titlePreference:"name"}}function te(t,e){if(e.query){const s=new Set(e.query.toLowerCase().split(/\s+/).filter(a=>a.length>=2));if((t.englishName||t.name||t.nativeName||"").toLowerCase().split(/\s+/).filter(a=>s.has(a)).length<s.size)return!1}return!0}function ne(t,e){const s=n=>(e.titlePreference?n[e.titlePreference]:void 0)||n.name||"";return e.sortBy==="name_asc"?[...t].sort((n,i)=>s(n).localeCompare(s(i))):e.sortBy==="name_desc"?[...t].sort((n,i)=>s(i).localeCompare(s(n))):t}async function se(t,e){const s=new Map;if(!e||e==="ALL")return s;const n=parseInt(e);if(Number.isNaN(n))return s;const i=50;let r=1;for(;;){const a={seasonYear:n,page:r,perPage:i};t&&t!=="ALL"&&(a.season=t.toUpperCase());const l=await z(a);for(const o of l)o.anilistId&&s.set(o.anilistId,{title:o.names||{}});if(l.length<i||(r++,r>10))break}return s}function ie(t,e){if(e.size===0||/^\d+$/.test(t.id)&&e.has(parseInt(t.id)))return!0;const s=(t.englishName||t.name||t.nativeName||"").toLowerCase();if(!s)return!1;const n=new Set(s.split(/\s+/).filter(i=>i.length>=2));if(n.size===0)return!1;for(const[,i]of e){const r=[i.title?.romaji,i.title?.english,i.title?.native].filter(Boolean);for(const a of r){const l=new Set(a.toLowerCase().split(/\s+/).filter(c=>c.length>=2)),o=[...n].filter(c=>l.has(c)).length,d=Math.min(n.size,l.size);if(d>=2&&o/d>=.7)return!0}}return!1}async function O(t,e,s){let n=t.filter(i=>te(i,e));if(e.type==="ADULT"&&s&&n.length>0){const i=n.map(o=>o.id),r=i.map(()=>"?").join(","),a=await A(s,`SELECT id FROM shows_meta WHERE id IN (${r}) AND isAdult = 1`,i),l=new Set(a.map(o=>o.id));n=n.filter(o=>l.has(o.id))}else e.type&&e.type!=="ALL"&&e.type!=="ADULT"&&(n=n.filter(i=>i.type===e.type));if((e.genres||e.excludeGenres)&&s){const i=n.map(c=>c.id),r=i.map(()=>"?").join(","),a=await A(s,`SELECT id, genres FROM shows_meta WHERE id IN (${r})`,i),l=new Map(a.map(c=>[c.id,c.genres?JSON.parse(c.genres):[]])),o=e.genres?.split(",").map(c=>c.trim()).filter(Boolean)||[],d=e.excludeGenres?.split(",").map(c=>c.trim()).filter(Boolean)||[];n=n.filter(c=>{const u=l.get(c.id)||[];return!(o.length&&!o.every(w=>u.includes(w))||d.length&&d.some(w=>u.includes(w)))})}if((e.season&&e.season!=="ALL"||e.year&&e.year!=="ALL")&&n.length>0){const i=await se(e.season,e.year);i.size>0&&(n=n.filter(r=>ie(r,i)))}return ne(n,e)}async function x(t,e){const s=e.filter(i=>!i.thumbnail||i.thumbnail.trim()==="");if(s.length===0)return;const n=s.slice(0,10);await Promise.allSettled(n.map(async i=>{try{const r=await j(i.id),a=r?.thumbnail?.trim();if(!a)return;i.thumbnail=a;const l=i;l._id!==void 0&&(l._id=i.id);try{await b.upsert(t,{id:i.id,thumbnail:a,popularityScore:r?.score??(r?.averageScore!=null?r.averageScore/10:void 0)})}catch{}try{await p.updateThumbnail(t,i.id,a)}catch{}}catch{}})),await ae(t)}async function ae(t){try{t.run(`UPDATE shows_meta SET name = (SELECT w.name FROM watchlist w WHERE w.id = shows_meta.id)
2
- WHERE (name IS NULL OR TRIM(name) = '')
3
- AND EXISTS (SELECT 1 FROM watchlist w WHERE w.id = shows_meta.id AND w.name IS NOT NULL AND TRIM(w.name) != '')`)}catch{}const[e,s]=await Promise.all([p.getMissingThumbnails(t),b.getShowIdsMissingMeta(t)]),n=[...new Set([...e.map(i=>i.id),...s.map(i=>i.id)])].slice(0,500);if(n.length!==0)for(const i of n){const r=re(i);r&&await oe(t,i,r)}}function re(t){if(/^\d+$/.test(t))return E.getByAnilistId(Number(t))?.thumbnail?.trim()||null;const e=/^mal-(\d+)$/i.exec(t)?.[1];return e&&E.getByMalId(Number(e))?.thumbnail?.trim()||null}async function oe(t,e,s){try{await b.upsert(t,{id:e,thumbnail:s})}catch{}try{await p.updateThumbnail(t,e,s)}catch{}}async function le(t,e){const s=await N.getContinueWatching(t,e),n=await v.getByKey(t,"ignoreAdultContent"),i=n?n.value!=="false":!0,r=await v.getByKey(t,"cwWatchlistOnly"),a=r?r.value==="true"||r.value==="1":!1,l=s.filter(o=>!(i&&o.isAdult||a&&o.watchlistStatus!=="Watching")).map(o=>({...o,episodeCount:o.episodeCount,type:o.type||o.smType,thumbnail:o.thumbnail??""}));return await x(t,l),l}async function D(t){const e=await U(t);return Array.isArray(e)&&e.length?[...e].sort((n,i)=>parseFloat(n)-parseFloat(i)):[]}export{Ae as watchlistRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as S}from"@trpc/server";function i(t){return{"~standard":{version:1,vendor:"dango",types:void 0,validate(e){try{return{value:t(e)}}catch(n){return{issues:[{message:n instanceof Error?n.message:"Invalid input"}]}}}}}}function v(t){if(typeof t!="object"||t===null)throw new Error("Expected an object");const e=t.key;if(typeof e!="string"||e.length===0)throw new Error("key must be a string");return e}function o(t,e){const n=t[e];if(typeof n!="string"||n.length===0)throw new Error(`${e} must be a string`);return n}function s(t,e){const n=t[e];if(n!=null)return String(n)}function a(t,e){const n=t[e];if(n==null||n==="")return;const r=Number(n);return Number.isFinite(r)?r:void 0}function k(t,e){const n=t[e];if(typeof n=="boolean")return n}function Q(t){return new S({code:"BAD_REQUEST",message:t})}function T(t){return new S({code:"INTERNAL_SERVER_ERROR",message:t})}function P(t,e){const n=t[e];if(typeof n!="string"&&typeof n!="number"||String(n).length===0)throw new Error(`${e} must be a string`);return String(n)}function j(t,e,n){const r=t[e];if(r==null||r==="")return n;const d=Math.floor(Number(r));return Number.isFinite(d)?d:n}function q(t,e){const n=t[e];return typeof n=="string"?n:null}function M(t){const e={},n=a(t,"page");n!==void 0&&(e.page=n);const r=a(t,"limit");return r!==void 0&&(e.limit=r),e}function f(t,e){const n=t[e];if(typeof n!="string"&&typeof n!="number"||String(n).length===0)throw new Error(`${e} must be a string`);return String(n)}function l(t,e,n){const r=t[e];if(!Array.isArray(r)||n&&r.length===0)throw new Error(`${e} must be ${n?"a non-empty ":"an "}array`);return r.map(d=>String(d))}function u(t){if(typeof t!="object"||t===null)throw new Error("Expected an object");return t}const O=()=>i(t=>({key:v(t)})),B=()=>i(t=>({id:o(u(t),"id")})),W=()=>i(t=>({showId:o(u(t),"showId")})),C=()=>i(t=>{const e=u(t);return{showId:o(e,"showId"),episodeNumber:f(e,"episodeNumber")}}),U=()=>i(t=>({ids:[...new Set(l(u(t),"ids",!0))]})),D=()=>i(t=>{const e=u(t),n={id:o(e,"id"),name:o(e,"name")},r=s(e,"status");r!==void 0&&(n.status=r);const d=s(e,"thumbnail");d!==void 0&&(n.thumbnail=d);const p=s(e,"nativeName");p!==void 0&&(n.nativeName=p);const g=s(e,"englishName");g!==void 0&&(n.englishName=g);const c=s(e,"type");return c!==void 0&&(n.type=c),typeof e.isAdult=="boolean"&&(n.isAdult=e.isAdult),n}),$=()=>i(t=>{const e=u(t);return{id:o(e,"id"),status:o(e,"status")}}),_=()=>i(t=>{const e=u(t);return{ids:[...new Set(l(e,"ids",!0))],status:o(e,"status")}});function A(t){return{showName:s(t,"showName"),showThumbnail:s(t,"showThumbnail"),nativeName:s(t,"nativeName"),englishName:s(t,"englishName"),type:s(t,"type")}}const F=()=>i(t=>{const e=u(t);return{...A(e),showId:o(e,"showId"),episodeNumber:f(e,"episodeNumber")}}),L=()=>i(t=>{const e=u(t);return{...A(e),showId:o(e,"showId"),episodeNumbers:l(e,"episodeNumbers",!0)}}),K=()=>i(t=>{const e=u(t);return{showId:o(e,"showId"),episodeNumber:f(e,"episodeNumber")}}),V=()=>i(t=>{const e=u(t),n={showId:o(e,"showId")};return e.episodeNumbers!==void 0&&(n.episodeNumbers=l(e,"episodeNumbers",!1)),n}),z=()=>i(t=>{if(!Array.isArray(t))throw new Error("items must be an array");return t.map(e=>{const n=u(e),r={};return typeof n.id=="number"&&(r.id=n.id),n.showId!==void 0&&(r.showId=String(n.showId)),n.episodeNumber!==void 0&&(r.episodeNumber=String(n.episodeNumber)),r})}),G=()=>i(t=>{if(typeof t!="object"||t===null)throw new Error("Expected an object");return{key:v(t),value:t.value}}),H=()=>i(t=>{const e=u(t),n={showId:o(e,"showId"),episodeNumber:f(e,"episodeNumber")},r=a(e,"currentTime");r!==void 0&&(n.currentTime=r);const d=a(e,"duration");d!==void 0&&(n.duration=d);const p=s(e,"showName");p!==void 0&&(n.showName=p);const g=s(e,"showThumbnail");g!==void 0&&(n.showThumbnail=g);const c=s(e,"nativeName");c!==void 0&&(n.nativeName=c);const h=s(e,"englishName");h!==void 0&&(n.englishName=h);const m=e.genres;(typeof m=="string"||Array.isArray(m))&&(n.genres=(Array.isArray(m)?m:[m]).map(String));const I=a(e,"popularityScore");I!==void 0&&(n.popularityScore=I);const w=s(e,"type");w!==void 0&&(n.type=w);const y=s(e,"status");y!==void 0&&(n.status=y);const b=a(e,"episodeCount");b!==void 0&&(n.episodeCount=b);const N=k(e,"isPlaying");N!==void 0&&(n.isPlaying=N);const x=s(e,"sessionId");x!==void 0&&(n.sessionId=x);const R=k(e,"isAdult");return R!==void 0&&(n.isAdult=R),n}),J=()=>i(t=>{const e=u(t);return{showId:o(e,"showId"),episodeNumber:f(e,"episodeNumber")}});export{Q as badRequest,i as defineSchema,C as episodeProgressInput,T as failed,B as idInput,U as idsInput,O as keyInput,J as notificationDismissInput,k as optBool,a as optNum,s as optStr,H as progressUpdateInput,F as queueAddInput,L as queueBatchInput,K as queueRemoveInput,V as queueRemoveManyInput,z as queueReorderInput,j as readCount,M as readPageLimit,P as reqId,u as reqObj,o as reqStr,l as reqStrArray,G as setSettingInput,W as showIdInput,q as strOrNull,D as watchlistAddInput,_ as watchlistBatchStatusInput,$ as watchlistStatusInput};
@@ -1 +0,0 @@
1
- import{LRUCache as s}from"lru-cache";class r{cache;constructor(e={}){this.cache=new s({max:e.maxKeys??5e3,ttl:(e.ttlSeconds??3600)*1e3})}get(e){return this.cache.get(e)}set(e,n,t){const c=n;t===void 0?this.cache.set(e,c):this.cache.set(e,c,{ttl:t*1e3})}delete(e){this.cache.delete(e)}}export{r as AppCache};
@@ -1 +0,0 @@
1
- function r(n){if(!n)return"";let e=String(n).trim();return e=e.replace(/^cf_clearance/i,""),e=e.replace(/^[:=]\s*/,""),e=e.replace(/["']/g,"").trim(),e}function i(n){const e=r(n);return e?`cf_clearance=${e}`:""}export{i as buildCfClearanceCookie,r as sanitizeCfClearance};
@@ -1,5 +0,0 @@
1
- const e=(r,n,a=[])=>r.all(n,a),u=(r,n,a=[])=>r.get(n,a),b=(r,n,a=[])=>{r.run(n,a)};async function d(r,n,a,t,s){return(await e(r,`SELECT DISTINCT p."${t}" as id
2
- FROM "${n}" p
3
- LEFT JOIN "${a}" l ON l.id = p."${t}"
4
- WHERE ${s}
5
- AND l.id IS NULL`)).map(i=>i.id)}export{d as adultNonListIds,e as dbAll,u as dbGet,b as dbRun};
@@ -1,5 +0,0 @@
1
- import t from"fs";import{CONFIG as u}from"../config.js";const f=`${u.ENV_PATH}.lock`;async function w(){let e,a=0;for(;!e;)try{e=await t.promises.open(f,"wx")}catch{if(++a>=100)throw t.promises.unlink(f).catch(()=>{}),new Error("Failed to acquire lock on .env file - another process may be holding it");await new Promise(i=>setTimeout(i,100))}return e}function k(e){e.close().catch(()=>{}),t.promises.unlink(f).catch(()=>{})}async function $(e){const a=await w();try{const i=u.ENV_PATH;let p="";t.existsSync(i)&&(p=t.readFileSync(i,"utf8"));const n=[...p.split(`
2
- `)];Object.entries(e).forEach(([r,c])=>{const s=r.trim();if(!/^[A-Z0-9_]+$/i.test(s))throw new Error(`Invalid environment variable key: ${s}`);const l=String(c??"").replace(/[\r\n]/g,"");let m=!1;for(let o=0;o<n.length;o++){const h=n[o];if(h&&h.startsWith(`${s}=`)){if(l===""){n.splice(o,1);continue}else n[o]=`${s}=${l}`;m=!0;break}}!m&&l!==""&&n.push(`${s}=${l}`)});const d=n.join(`
3
- `).replace(/\n{2,}/g,`
4
- `).trim()+`
5
- `;t.writeFileSync(i,d),Object.entries(e).forEach(([r,c])=>{c===""?delete process.env[r]:process.env[r]=c})}finally{k(a)}}export{$ as updateEnvFile};
@@ -1 +0,0 @@
1
- import{brotliDecompressSync as l,gunzipSync as m,inflateSync as p}from"node:zlib";class w extends Error{status;constructor(r,e){super(e||`Upstream request failed with status ${r}`),this.name="HttpError",this.status=r}}function b(t){return t.length>2&&t[0]===31&&t[1]===139?m(t):t}async function x(t){const r=Buffer.from(await t.arrayBuffer()),e=b(r).toString("utf8");try{return JSON.parse(e)}catch(n){const s=[()=>l(r),()=>p(r)];for(const i of s)try{return JSON.parse(i().toString("utf8"))}catch{}throw n}}function A(t,r){t.addEventListener("abort",()=>r.abort(),{once:!0})}function S(t,r){for(const e of["content-length","content-range","accept-ranges"]){const n=r(e),s=Array.isArray(n)?n[0]:n;s&&t.set(e,s)}t.set("Access-Control-Allow-Origin","*")}function g(t){if(!t||typeof t!="object")return!1;const r=t.name;return r==="AbortError"||r==="TimeoutError"}const y=t=>new Promise(r=>setTimeout(r,t));function h(t){return t===408||t===429||t>=500}async function E(t,r={},e={}){const{retries:n=0,baseDelayMs:s=100,timeoutMs:i=3e4,signal:c}=e;let a=0;for(;;){const u=AbortSignal.timeout(i),f=c?AbortSignal.any([c,u]):u;try{const o=await fetch(t,{...r,signal:f});if(!h(o.status)||a>=n)return o;try{await o.body?.cancel()}catch{}}catch(o){if(c?.aborted||a>=n||!g(o)&&!(o instanceof TypeError))throw o}a+=1,await y(Math.min(s*2**(a-1),3e3))}}export{w as HttpError,b as decodeMaybeGzip,E as fetchWithRetry,g as isAbortError,A as linkAbort,x as parseJsonBody,S as setProxyHeaders};
@@ -1 +0,0 @@
1
- import{AsyncLocalStorage as t}from"node:async_hooks";const r=new t;export{r as requestContext};