@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,71 +0,0 @@
1
- import f from"../../logger.js";import{waitForAnilistSlot as b,applyRateLimitHeaders as I}from"../anilist.js";import{parseJsonBody as A}from"../../utils/http.utils.js";const E="https://graphql.anilist.co",$={CURRENT:"Watching",COMPLETED:"Completed",PAUSED:"On-Hold",DROPPED:"Dropped",PLANNING:"Planned"},h={Watching:"CURRENT",Reading:"CURRENT",Completed:"COMPLETED","On-Hold":"PAUSED",Dropped:"DROPPED",Planned:"PLANNING"},L={CURRENT:"Reading",COMPLETED:"Completed",PAUSED:"On-Hold",DROPPED:"Dropped",PLANNING:"Planned"};class P{constructor(t){this.token=t}token;sleep(t){return new Promise(s=>setTimeout(s,t))}async request(t,s={},o={}){const i=o.retries??3;await b();const d={"Content-Type":"application/json",Accept:"application/json"};this.token&&(d.Authorization=`Bearer ${this.token}`);let r;try{r=await fetch(E,{method:"POST",headers:d,body:JSON.stringify({query:t,variables:s})})}catch(e){throw f.error({err:e},"[AniList Tracker] Network error"),new Error("Could not reach AniList. Please try again later.",{cause:e})}if(I(r.headers),r.status===429){if(i<=0)throw new Error("AniList rate limit reached, try again later");const e=r.headers.get("retry-after"),m=e&&Number.parseInt(e,10)||60;return f.warn({waitSeconds:m},"[AniList Tracker] Rate limited, waiting before retry"),await this.sleep(Math.min(m,60)*1e3),this.request(t,s,{retries:i-1})}if(!r.ok&&r.status===401)throw new Error("AniList token is invalid or expired. Please reconnect your account.");if(!r.ok)throw new Error(`AniList request failed with status ${r.status}`);const n=await A(r);if(n.errors&&!n.data)throw new Error(n.errors.map(e=>e.message).join(", "));return n.data}async getViewer(){const s=await this.request(`
2
- query {
3
- Viewer {
4
- id
5
- name
6
- avatar { medium }
7
- }
8
- }
9
- `);return{id:s.Viewer.id,name:s.Viewer.name,avatar:s.Viewer.avatar?.medium}}async fetchUserAnimeList(t){const s=`
10
- query ($userId: Int, $userName: String) {
11
- MediaListCollection(userId: $userId, userName: $userName, type: ANIME) {
12
- lists {
13
- entries {
14
- id
15
- mediaId
16
- status
17
- progress
18
- score(format: POINT_10)
19
- updatedAt
20
- media {
21
- id
22
- idMal
23
- episodes
24
- duration
25
- title { romaji english native }
26
- coverImage { large }
27
- }
28
- }
29
- }
30
- }
31
- }
32
- `,o=typeof t=="number"?{userId:t}:typeof t=="string"?{userName:t}:{},d=(await this.request(s,o)).MediaListCollection?.lists??[],r=[];for(const n of d)for(const e of n.entries??[])e.media&&e.status!=="REPEATING"&&r.push({mediaId:e.mediaId,entryId:e.id,idMal:e.media.idMal??void 0,status:$[e.status]??"Planned",progress:e.progress??0,score:e.score??void 0,updatedAt:e.updatedAt??0,title:e.media.title??{},coverImage:e.media.coverImage?.large,totalEpisodes:e.media.episodes??void 0,episodeDuration:e.media.duration??void 0});return r}async fetchUserMangaList(t){const s=`
33
- query ($userId: Int, $userName: String) {
34
- MediaListCollection(userId: $userId, userName: $userName, type: MANGA) {
35
- lists {
36
- entries {
37
- id
38
- mediaId
39
- status
40
- progress
41
- progressVolumes
42
- score(format: POINT_10)
43
- updatedAt
44
- media {
45
- id
46
- idMal
47
- chapters
48
- volumes
49
- title { romaji english native }
50
- coverImage { large }
51
- }
52
- }
53
- }
54
- }
55
- }
56
- `,o=typeof t=="number"?{userId:t}:typeof t=="string"?{userName:t}:{},d=(await this.request(s,o)).MediaListCollection?.lists??[],r=[];for(const n of d)for(const e of n.entries??[])e.media&&e.status!=="REPEATING"&&r.push({mediaId:e.mediaId,entryId:e.id,idMal:e.media.idMal??void 0,status:L[e.status]??"Planned",progress:e.progress??0,progressVolumes:e.progressVolumes??void 0,score:e.score??void 0,updatedAt:e.updatedAt??0,title:e.media.title??{},coverImage:e.media.coverImage?.large,totalChapters:e.media.chapters??void 0,totalVolumes:e.media.volumes??void 0});return r}async updateMediaEntry(t){const s=`
57
- mutation ($mediaId: Int, $status: MediaListStatus, $progress: Int, $score: Float) {
58
- SaveMediaListEntry(mediaId: $mediaId, status: $status, progress: $progress, score: $score) {
59
- id
60
- mediaId
61
- status
62
- progress
63
- }
64
- }
65
- `,o={mediaId:t.mediaId},i=t.status?h[t.status]:void 0;return i&&(o.status=i),t.progress!==void 0&&(o.progress=t.progress),t.score!==void 0&&(o.score=t.score),this.request(s,o)}async batchUpdateMediaEntries(t){if(t.length===0)return{};if(t.length===1)return this.updateMediaEntry(t[0]);const s=10,o={};for(let i=0;i<t.length;i+=s){const d=t.slice(i,i+s),r=[],n={},e=[];d.forEach((u,l)=>{const a=`a${i+l}`,g=[`mediaId: $${a}mediaId`];if(n[`${a}mediaId`]=u.mediaId,e.push(`$${a}mediaId: Int!`),u.status){const p=h[u.status];p&&(g.push(`status: $${a}status`),n[`${a}status`]=p,e.push(`$${a}status: MediaListStatus`))}u.progress!==void 0&&(g.push(`progress: $${a}progress`),n[`${a}progress`]=u.progress,e.push(`$${a}progress: Int`)),u.score!==void 0&&(g.push(`score: $${a}score`),n[`${a}score`]=u.score,e.push(`$${a}score: Float`)),r.push(`${a}: SaveMediaListEntry(${g.join(", ")}) { id mediaId status progress }`)});const m=`mutation (${e.join(", ")}) { ${r.join(`
66
- `)} }`,c=await this.request(m,n);c&&Object.assign(o,c)}return o}async deleteMediaEntry(t){return this.request(`
67
- mutation ($id: Int) {
68
- DeleteMediaListEntry(id: $id) { deleted }
69
- }
70
- `,{id:t})}async batchDeleteMediaEntries(t){if(t.length===0)return{};if(t.length===1)return this.deleteMediaEntry(t[0]);const s=10,o={};for(let i=0;i<t.length;i+=s){const d=t.slice(i,i+s),r=[],n={};d.forEach((u,l)=>{const a=`d${i+l}`;r.push(`${a}: DeleteMediaListEntry(id: $${a}id) { deleted }`),n[`${a}id`]=u});const m=`mutation (${d.map((u,l)=>`$d${i+l}id: Int`).join(", ")}) { ${r.join(`
71
- `)} }`,c=await this.request(m,n);c&&Object.assign(o,c)}return o}}export{$ as ANILIST_TO_DANGO_STATUS,L as ANILIST_TO_MANGA_STATUS,P as AniListTracker,h as DANGO_TO_ANILIST_STATUS};
@@ -1 +0,0 @@
1
- import{AniListTracker as et}from"./anilist-tracker.js";import{MangaLibraryRepository as A,MangaProgressRepository as C}from"../../repositories/manga.repository.js";import{SettingsRepository as K}from"../../repositories/settings.repository.js";import{performMangaWriteTransactionAsync as nt,performWriteTransactionAsync as at}from"../../sync.js";import B from"../../logger.js";import{searchAnilistMangaByTitle as rt,getMangaMetaById as ft,getMangaMetaByMalId as yt}from"../anilist.js";import{fetchMalMangaList as St,mapMalMangaStatusCode as It}from"../mal.js";import{readTrackerSyncState as it,resolveStatus as wt}from"./sync-state.js";const st="tracker_anilist_token",z="tracker_anilist_manga_sync_state",P="anilist",Mt=25;function ot(r){if(r==null)return 0;const i=String(r).match(/(\d+(?:\.\d+)?)/g);if(!i)return 0;let o=0;for(const g of i){const a=Number(g);Number.isFinite(a)&&(o=Math.max(o,Math.floor(a)))}return o}async function lt(r,i,o){let g=ot(o);try{const a=await C.getByManga(r,i);for(const d of a??[])g=Math.max(g,ot(d.chapterNumber))}catch{}return g}function O(r){return`${P}:${r}`}function X(r){return`${P}:ch:${r}`}function bt(r){const i=/^anilist:(\d+)$/i.exec(r.trim());if(!i)return null;const o=Number.parseInt(i[1],10);return Number.isFinite(o)?o:null}function ct(r){return["Reading","Completed","On-Hold","Dropped","Planned"].includes(r)?r:void 0}function ut(r){return String(r??"").normalize("NFD").replace(/[\u0300-\u036f]/g,"").toLowerCase().replace(/[^\p{L}\p{N}]+/gu," ").replace(/\s+/g," ").trim()}function dt(r,i){const o=ut(r);return o.length>0&&o===ut(i)}function pt(r){let i=String(r??"").trim(),o="";for(;o!==i;)o=i,i=i.replace(/\s*\([^()]*\)$/,"").trim();return i}function vt(r,i){const o=[r.title,r.altTitle].filter(a=>!!a).map(pt).filter(a=>a.length>0),g=[i.romaji,i.english,i.native].filter(a=>!!a).map(pt).filter(a=>a.length>0);return o.some(a=>g.some(d=>dt(a,d)))}function gt(r,i){const o=[r.title,r.altTitle],g=[i.romaji,i.english,i.native];return o.some(a=>!!a&&g.some(d=>!!d&&dt(a,d)))}async function jt(r,i,o={}){const a=(o.direction==="pull-only"?"pull-only":"two-way")==="pull-only",c=(await K.getByKey(r,st))?.value;if(!c)throw new Error("AniList is not connected. Please log in first.");const l=new et(c),p=await l.getViewer(),S=await l.fetchUserMangaList(p.id),b=await A.getAll(i),f=await it(r,z),y=new Map;for(const e of S)y.set(e.mediaId,e);const v=new Map,E=[],R=new Set,V=new Map;let q=0,T=0;const G=[],h=e=>{G.push(e)},J=[];for(const e of b){let t=typeof e.anilistId=="number"?e.anilistId:null;const n=t!=null,u=n?e.anilistIdSource??null:null;let k=n;if(t==null&&(t=bt(e.id),k=t!=null),t==null&&e.provider&&e.mangaId){const s=String(e.provider).toLowerCase()===P&&/^\d+$/.test(String(e.mangaId).trim())?Number.parseInt(String(e.mangaId).trim(),10):null;s!=null&&(t=s,k=!0)}if(t==null){if(q>=Mt||!e.title){T++,h({title:e.title||e.id,action:"unresolved",note:"No AniList link yet \u2014 skipped so nothing is written to the wrong series."});continue}q++;try{const s=await rt(e.title);if(s&&gt(e,s.title))t=s.id,k=!1,E.push({id:e.id,anilistId:s.id}),R.add(s.id);else{T++,h({title:e.title,action:"unresolved",note:s?`Closest AniList match ("${s.title.english||s.title.romaji||s.title.native}") is not an exact title match \u2014 skipped so nothing is written to the wrong series.`:"No AniList match found \u2014 skipped."});continue}}catch(s){B.warn({err:s,mangaId:e.id},"[AniList Manga Sync] Title lookup failed"),T++,h({title:e.title||e.id,action:"unresolved",note:"Title lookup failed \u2014 skipped."});continue}}v.has(t)||(v.set(t,e),k&&n&&!R.has(t)&&V.set(t,{title:e.title,altTitle:e.altTitle,source:u}))}const Z=new Set;for(const[e,t]of V){const n=y.get(e);if(!n||t.source==="manual"||t.source==="anilist"||t.source==="search"||gt(t,n.title)||vt(t,n.title))continue;Z.add(e);const u=n.title.english||n.title.romaji||n.title.native||`Manga #${e}`;J.push(`"${t.title}" is linked to AniList #${e} ("${u}") by an older automatic match and the titles do not match \u2014 skipped in both directions to protect your AniList list.`),h({mediaId:e,title:t.title,action:"suspicious",remoteStatus:n.status,remoteProgress:n.progress,note:`Linked AniList title is "${u}".`})}const ht=new Set([...v.keys(),...y.keys(),...Object.keys(f).map(e=>Number.parseInt(e,10)).filter(e=>Number.isFinite(e))]),m={pushed:0,pulled:0,merged:0,unchanged:0,skipped:0,errors:[],warnings:J},D=[],U=[],_=[],x=[],I={},$=[],j=[],N=Math.floor(Date.now()/1e3);for(const e of ht){if(Z.has(e))continue;const t=v.get(e),n=y.get(e),u=String(e),k=t?.id??O(e);try{if(t&&!n){if(f[u]&&S.length>0){if(a){m.skipped++,h({mediaId:e,title:t.title,action:"skipped",localStatus:t.status,note:"Pull-only mode \u2014 local entry kept, nothing deleted."});continue}x.push(t.id),I[u]=void 0,m.pulled++,h({mediaId:e,title:t.title,action:"delete-local",localStatus:t.status});continue}const s=await lt(i,t.id,t.lastChapterNumber);if(a){m.skipped++,h({mediaId:e,title:t.title,action:"skipped",localStatus:t.status,localChapters:s,note:"Pull-only mode \u2014 not pushed to AniList."});continue}const L=ct(t.status);if(!L||L==="Planned"&&s===0){m.skipped++,h({mediaId:e,title:t.title,action:"skipped",localStatus:t.status,localChapters:s,note:"Bookmark with no chapters read \u2014 not pushed to AniList."});continue}$.push({mediaId:e,status:L,progress:s}),I[u]={lastSyncedAt:N,remoteUpdatedAt:N},m.pushed++,h({mediaId:e,title:t.title,action:"push",localStatus:t.status,localChapters:s});continue}if(!t&&n){if(f[u]&&!a){if(n.entryId){j.push({showId:u,entryId:n.entryId}),I[u]=void 0,m.pushed++,h({mediaId:e,title:n.title.english||n.title.romaji||`Manga #${e}`,action:"delete-remote",remoteStatus:n.status,remoteProgress:n.progress,note:"Removed locally after a previous sync \u2014 delete propagated to AniList."});continue}I[u]=void 0,m.pushed++,h({mediaId:e,title:`Manga #${e}`,action:"delete-remote"});continue}B.debug({mediaId:e,remoteStatus:n.status},"[AniList Manga Sync] Remote only -> pulling to local");const s=n.title.english||n.title.romaji||`Manga #${e}`;D.push({id:O(e),provider:P,mangaId:String(e),title:s,cover:n.coverImage??"",status:n.status,anilistId:e,anilistIdSource:"anilist"}),n.progress>0&&(U.push({id:O(e),chapterNumber:String(n.progress)}),_.push({mangaId:O(e),chapterId:X(n.progress),chapterNumber:String(n.progress),title:s,cover:n.coverImage??"",provider:P})),I[u]={lastSyncedAt:N,remoteUpdatedAt:n.updatedAt},m.pulled++,h({mediaId:e,title:s,action:"pull",remoteStatus:n.status,remoteProgress:n.progress});continue}if(!t&&!n&&f[u]){I[u]=void 0;continue}if(t&&n){const s=await lt(i,t.id,t.lastChapterNumber),L=f[u]?.lastSyncedAt??0,Y=n.updatedAt||0,w=Math.max(s,n.progress),Q=s>n.progress,W=n.progress>s,{status:M,pull:F}=wt(t.status,n.status,Y,L),tt=n.status!==t.status;a?F||W?(F&&M!==t.status&&D.push({id:t.id,provider:t.provider,mangaId:t.mangaId,title:t.title,cover:t.cover??"",status:M,anilistId:e}),W&&(U.push({id:k,chapterNumber:String(w)}),_.push({mangaId:k,chapterId:X(w),chapterNumber:String(w),title:t.title,cover:t.cover??"",provider:t.provider})),I[u]={lastSyncedAt:N,remoteUpdatedAt:Y},m.merged++,h({mediaId:e,title:t.title,action:"merge-pull",localStatus:t.status,remoteStatus:M,localChapters:s,remoteProgress:w})):Q||tt?(m.skipped++,h({mediaId:e,title:t.title,action:"skipped",localStatus:t.status,remoteStatus:n.status,localChapters:s,remoteProgress:n.progress,note:"Local is ahead \u2014 not pushed in pull-only mode."})):m.unchanged++:Q||!F&&tt?($.push({mediaId:e,status:M,progress:w}),M!==t.status&&D.push({id:t.id,provider:t.provider,mangaId:t.mangaId,title:t.title,cover:t.cover??"",status:M,anilistId:e}),I[u]={lastSyncedAt:N,remoteUpdatedAt:N},m.merged++,h({mediaId:e,title:t.title,action:"merge-push",localStatus:M,remoteStatus:n.status,localChapters:w,remoteProgress:n.progress})):F||W?(M!==t.status&&D.push({id:t.id,provider:t.provider,mangaId:t.mangaId,title:t.title,cover:t.cover??"",status:M,anilistId:e}),W&&(U.push({id:k,chapterNumber:String(w)}),_.push({mangaId:k,chapterId:X(w),chapterNumber:String(w),title:t.title,cover:t.cover??"",provider:t.provider})),I[u]={lastSyncedAt:N,remoteUpdatedAt:Y},m.merged++,h({mediaId:e,title:t.title,action:"merge-pull",localStatus:t.status,remoteStatus:M,localChapters:s,remoteProgress:w})):m.unchanged++}}catch(s){const L=s instanceof Error?s.message:String(s);B.warn({err:s,mediaId:e},"[AniList Manga Sync] Entry failed"),m.errors.push(`${t?.title??`Manga ${e}`}: ${L}`),h({mediaId:e,title:t?.title??`Manga #${e}`,action:"error",note:L})}}if($.length>0||j.length>0||x.length>0){const e=Math.max(5,Math.ceil(b.length*.1)),t=x.length+j.length;if(t>e)throw new Error(`AniList manga sync would delete ${t} titles but only ${S.length} were fetched. Aborting to protect your library \u2014 if you really removed that many manga on AniList, delete them locally first and sync again.`)}if($.length>0)for(let t=0;t<$.length;t+=10){const n=$.slice(t,t+10);await l.batchUpdateMediaEntries(n)}if(j.length>0){const e=j.map(n=>n.entryId),t=10;for(let n=0;n<e.length;n+=t){const u=e.slice(n,n+t);await l.batchDeleteMediaEntries(u)}}if((D.length>0||U.length>0||_.length>0||x.length>0||E.length>0)&&await nt(i,async e=>{for(const t of D)await A.upsert(e,t);for(const t of E)await A.setAnilistId(e,t.id,t.anilistId,"search");for(const t of U)await A.setProgressPointer(e,t.id,t.chapterNumber);for(const t of _)await C.deleteSyntheticChapters(e,t.mangaId,t.chapterId),await C.upsert(e,{mangaId:t.mangaId,chapterId:t.chapterId,chapterNumber:t.chapterNumber,page:0,pageCount:0,title:t.title,cover:t.cover,provider:t.provider});for(const t of x)await A.delete(e,t),await C.deleteByManga(e,t)}),Object.keys(I).length>0)try{const e={...f};for(const[n,u]of Object.entries(I))u===void 0?delete e[n]:e[n]=u;const t=JSON.stringify(e);await at(r,async n=>{await K.upsert(n,z,t)})}catch(e){const t=e instanceof Error?e.message:String(e);B.warn({err:e},"[AniList Manga Sync] Failed to persist sync state"),m.errors.push(`Sync state was not saved, next sync will re-compare: ${t}`)}return T>0&&m.errors.push(`${T} local title${T===1?" was":"s were"} not matched to AniList (no link yet) \u2014 they were skipped and will be retried on the next sync.`),{summary:m,details:G}}function kt(r){return ct(r)??"Planned"}async function H(r,i,o,g){if(o.length===0)return 0;const a=Math.floor(Date.now()/1e3),d=await it(r,z);await nt(i,async c=>{if(g)for(const l of await A.getAll(c))String(l.provider).toLowerCase()===P&&(await A.delete(c,l.id),await C.deleteByManga(c,l.id));for(const l of o){const p=O(l.mediaId),S=kt(l.status);if(await A.upsert(c,{id:p,provider:P,mangaId:String(l.mediaId),title:l.title,cover:l.cover??"",status:S,anilistId:l.mediaId,anilistIdSource:"anilist"}),l.progress>0){await A.setProgressPointer(c,p,String(l.progress));const b=X(l.progress);await C.deleteSyntheticChapters(c,p,b),await C.upsert(c,{mangaId:p,chapterId:b,chapterNumber:String(l.progress),page:0,pageCount:0,title:l.title,cover:l.cover??"",provider:P})}d[String(l.mediaId)]={lastSyncedAt:a,remoteUpdatedAt:l.remoteUpdatedAt??a}}});try{const c=JSON.stringify(d);await at(r,async l=>{await K.upsert(l,z,c)})}catch(c){B.warn({err:c},"[AniList Manga Sync] Failed to persist sync state after import")}return o.length}async function Bt(r,i,o,g=!1){const d=(await K.getByKey(r,st))?.value,c=new et(d);let l;if(d){const p=await c.getViewer();l=await c.fetchUserMangaList(p.id)}else l=await c.fetchUserMangaList(o);return l.length===0?0:H(r,i,l.map(p=>({mediaId:p.mediaId,title:p.title.english||p.title.romaji||`Manga #${p.mediaId}`,cover:p.coverImage,status:p.status,progress:p.progress,remoteUpdatedAt:p.updatedAt})),g)}async function mt(r,i,o){if(r)try{const a=await yt(r);if(a)return{id:a.id,title:a.title.english||a.title.romaji||i,cover:a.cover}}catch{}const g=o||i;if(!g)return null;try{const a=await rt(g);if(!a)return null;const d=await ft(a.id);return{id:a.id,title:d?.title.english||d?.title.romaji||a.title.english||a.title.romaji||g,cover:d?.cover}}catch{return null}}async function Ot(r,i,o,g=!1){const a=await St(o);if(a.length===0)return{imported:0,skipped:0};const d=[];let c=0;const l=5;for(let S=0;S<a.length;S+=l){const b=a.slice(S,S+l);(await Promise.allSettled(b.map(y=>mt(y.malId,y.title,y.titleEnglish)))).forEach((y,v)=>{const E=b[v],R=It(E.status);if(y.status!=="fulfilled"||!y.value||!R){c++;return}d.push({mediaId:y.value.id,title:y.value.title,cover:y.value.cover,status:R,progress:Math.max(E.chaptersRead,0)})})}return{imported:await H(r,i,d,g),skipped:c}}async function Wt(r,i,o,g=!1){if(o.length===0)return{imported:0,skipped:0};const a=[];let d=0;const c=5;for(let p=0;p<o.length;p+=c){const S=o.slice(p,p+c);(await Promise.allSettled(S.map(f=>mt(f.malId,f.title,null)))).forEach((f,y)=>{const v=S[y];if(f.status!=="fulfilled"||!f.value){d++;return}a.push({mediaId:f.value.id,title:f.value.title,cover:f.value.cover,status:v.status,progress:Math.max(v.chapters,0)})})}return{imported:await H(r,i,a,g),skipped:d}}export{Bt as importFromUsernameManga,Ot as importMangaFromMalUsername,Wt as importMangaFromMalXmlItems,H as insertMangaImportEntries,jt as syncAniListManga};
@@ -1 +0,0 @@
1
- import{SettingsRepository as a}from"../../repositories/settings.repository.js";async function p(r,e){const t=await a.getByKey(r,e);if(!t?.value)return{};try{return JSON.parse(t.value)}catch{return{}}}function i(r,e,t,n){return t>n&&e!==r?{status:e,pull:!0}:e!==r?{status:r,pull:!1}:{status:r,pull:!1}}export{p as readTrackerSyncState,i as resolveStatus};
@@ -1 +0,0 @@
1
- import{AniListTracker as _}from"./anilist-tracker.js";import{WatchlistRepository as v}from"../../repositories/watchlist.repository.js";import{WatchedEpisodesRepository as U}from"../../repositories/watched-episodes.repository.js";import{ShowsMetaRepository as D}from"../../repositories/shows-meta.repository.js";import{SettingsRepository as M}from"../../repositories/settings.repository.js";import{NotificationsRepository as Y}from"../../repositories/notifications.repository.js";import{performWriteTransactionAsync as B}from"../../sync.js";import{dbGet as Z}from"../../utils/db-utils.js";import P from"../../logger.js";import{fetchMalUserList as q}from"../mal.js";import{offlineDb as G}from"../offline-db.js";import{getShowMetaById as J,getShowMetaByMalId as Q,isAnilistRateLimited as X,searchAnilistByTitle as x}from"../anilist.js";import{kitsuSearchAnime as tt}from"../kitsu.js";import{readTrackerSyncState as F,resolveStatus as et}from"./sync-state.js";const $="tracker_anilist_token",L="tracker_anilist_sync_state";async function j(r,y){return(await Z(r,"SELECT COUNT(DISTINCT episodeNumber) as total FROM watched_episodes WHERE showId = ?",[y]))?.total??0}async function Nt(r){const g=(await M.getByKey(r,$))?.value;if(!g)throw new Error("AniList is not connected. Please log in first.");const N=new _(g),E=await N.getViewer(),I=await N.fetchUserAnimeList(E.id),f=await v.getAll(r),p=await F(r,L),w=new Map;for(const e of I)w.set(e.mediaId,e);const d=new Map;for(const e of f){const t=(await D.getById(r,e.id))?.anilistId;t==null||!Number.isFinite(t)||String(t)!==String(e.id)||d.has(t)||d.set(t,e)}const l=new Set([...d.keys(),...w.keys(),...Object.keys(p).map(e=>Number.parseInt(e,10)).filter(e=>Number.isFinite(e))]),i={pushed:0,pulled:0,merged:0,unchanged:0,errors:[]},s=[],o=[],b=[],c={},S=[],u=[],m=[],A=Math.floor(Date.now()/1e3);for(const e of l){const n=d.get(e),t=w.get(e),a=String(e);try{if(n&&!t){if(p[a]&&I.length>0){S.push(a),c[a]=void 0,i.pulled++;continue}const h=await j(r,a);u.push({mediaId:e,status:nt(n.status),progress:h}),c[a]={lastSyncedAt:A,remoteUpdatedAt:A},i.pushed++;continue}if(!n&&t){if(p[a]){if(t.entryId){m.push({showId:a,entryId:t.entryId}),c[a]=void 0,i.pushed++;continue}c[a]=void 0,i.pushed++;continue}P.debug({mediaId:e,remoteStatus:t.status},"[AniList Sync] Remote only -> pulling to local");const h=t.title.english||t.title.romaji||`Anime #${e}`;s.push({id:a,name:h,thumbnail:t.coverImage??"",status:t.status,nativeName:t.title.native??"",englishName:t.title.english??"",type:"TV"}),o.push({id:a,name:h,thumbnail:t.coverImage,nativeName:t.title.native,englishName:t.title.english,episodeCount:t.totalEpisodes,anilistId:e,episodeDuration:t.episodeDuration}),t.progress>0&&b.push({showId:a,from:1,to:t.progress,updatedAt:t.updatedAt}),c[a]={lastSyncedAt:A,remoteUpdatedAt:t.updatedAt},i.pulled++;continue}if(!n&&!t&&p[a]){c[a]=void 0;continue}if(n&&t){const h=await j(r,a),R=p[a]?.lastSyncedAt??0,T=t.updatedAt||0,C=Math.max(h,t.progress),V=h>t.progress,O=t.progress>h,{status:k,pull:W}=et(n.status,t.status,T,R),z=t.status!==n.status;V||!W&&z?(u.push({mediaId:e,status:k,progress:C}),k!==n.status&&s.push({id:a,name:n.name,thumbnail:n.thumbnail??"",status:k,nativeName:n.nativeName??"",englishName:n.englishName??"",type:n.type??""}),O&&b.push({showId:a,from:h+1,to:C,updatedAt:T}),c[a]={lastSyncedAt:A,remoteUpdatedAt:A},i.merged++):W||O?(k!==n.status&&s.push({id:a,name:n.name,thumbnail:n.thumbnail??"",status:k,nativeName:n.nativeName??"",englishName:n.englishName??"",type:n.type??""}),O&&b.push({showId:a,from:h+1,to:C,updatedAt:T}),c[a]={lastSyncedAt:A,remoteUpdatedAt:T},i.merged++):i.unchanged++}}catch(h){const R=h instanceof Error?h.message:String(h);P.warn({err:h,mediaId:e},"[AniList Sync] Entry failed"),i.errors.push(`${n?.name??`Media ${e}`}: ${R}`)}}if(u.length>0||m.length>0||S.length>0){const e=Math.max(5,Math.ceil(f.length*.1)),n=S.length+m.length;if(n>e)throw new Error(`AniList sync would delete ${n} shows but only ${I.length} were fetched. Aborting to protect your library \u2014 if you really removed that many shows on AniList, delete them locally first and sync again.`)}if(u.length>0)for(let n=0;n<u.length;n+=10){const t=u.slice(n,n+10);await N.batchUpdateMediaEntries(t)}if(m.length>0){const e=m.map(t=>t.entryId),n=10;for(let t=0;t<e.length;t+=n){const a=e.slice(t,t+n);await N.batchDeleteMediaEntries(a)}}return(s.length>0||o.length>0||b.length>0||S.length>0||Object.keys(c).length>0)&&await B(r,async e=>{for(const t of s)await v.upsert(e,t);for(const t of o)await D.upsert(e,t);for(const t of b)for(let a=t.from;a<=t.to;a++)await U.insertIfMissing(e,{showId:t.showId,episodeNumber:String(a),watchedAt:H(t.updatedAt)});for(const t of S)await v.delete(e,t),await U.deleteByShow(e,t),await Y.deleteByShow(e,t);const n={...p};for(const[t,a]of Object.entries(c))a===void 0?delete n[t]:n[t]=a;await M.upsert(e,L,JSON.stringify(n))}),i}function H(r){if(!(!r||r<=0))return new Date(r*1e3).toISOString().slice(0,19).replace("T"," ")}function nt(r){return["Watching","Completed","On-Hold","Dropped","Planned"].includes(r)?r:void 0}async function Et(r,y,g=!1){const E=(await M.getByKey(r,$))?.value,I=new _(E);let f;if(E){const d=await I.getViewer();f=await I.fetchUserAnimeList(d.id)}else f=await I.fetchUserAnimeList(y);if(f.length===0)return 0;const p=Math.floor(Date.now()/1e3),w=await F(r,L);return await B(r,async d=>{g&&await M.clearWatchlist(d);for(const l of f){const i=String(l.mediaId),s=l.title.english||l.title.romaji||`Anime #${l.mediaId}`;if(await v.upsert(d,{id:i,name:s,thumbnail:l.coverImage??"",status:l.status,nativeName:l.title.native??"",englishName:l.title.english??"",type:"TV"}),await D.upsert(d,{id:i,name:s,thumbnail:l.coverImage,nativeName:l.title.native,englishName:l.title.english,episodeCount:l.totalEpisodes,anilistId:l.mediaId}),l.progress>0)for(let o=1;o<=l.progress;o++)await U.insertIfMissing(d,{showId:i,episodeNumber:String(o),watchedAt:H(l.updatedAt)});w[i]={lastSyncedAt:p,remoteUpdatedAt:l.updatedAt}}await M.upsert(d,L,JSON.stringify(w))}),f.length}function K(r){switch(r){case 1:return"Watching";case 2:return"Completed";case 3:return"On-Hold";case 4:return"Dropped";case 6:return"Planned";default:return null}}async function st(r){const y=await x(r);if(y)return y.id;if(X())try{const g=await tt({query:r,page:1,perPage:5});if(g.length>0)return g[0].id>0?g[0].id:null}catch{}return null}async function Mt(r,y,g={}){const{erase:N=!1,useOfflineDb:E=!0,skipFallback:I=!1}=g,f=await q(y);if(f.length===0)return 0;const p=[],w=[],d=[],l=[];for(const i of f){const s=K(i.status);if(!s)continue;const o=E?G.getByMalId(i.malId):null;if(o){const b=String(o.anilistId),c=o.title||i.titleEnglish||i.title||`MAL #${i.malId}`;p.push({id:b,name:c,thumbnail:o.thumbnail??i.imageUrl??void 0,status:s,englishName:i.titleEnglish??void 0,type:o.type??i.type??void 0}),(o.thumbnail||o.type||o.genres)&&w.push({id:b,thumbnail:o.thumbnail,type:o.type,genres:o.genres}),i.watchedEpisodes>0&&d.push({showId:b,progress:i.watchedEpisodes})}else l.push(i)}if(l.length>0&&!I)for(let s=0;s<l.length;s+=5){const o=l.slice(s,s+5);(await Promise.allSettled(o.map(async c=>{const S=await Q(c.malId).catch(()=>null);if(S?.anilistId)return S;const u=c.titleEnglish||c.title;if(!u)return null;const m=await st(u);return m?J(String(m)).catch(()=>null):null}))).forEach((c,S)=>{if(c.status!=="fulfilled"||!c.value)return;const u=c.value,m=o[S],A=K(m.status);if(!A)return;const e=u.anilistId?String(u.anilistId):u._id,n=u.names?.english||u.names?.romaji||m.titleEnglish||m.title||`MAL #${m.malId}`;p.push({id:e,name:n,thumbnail:u.thumbnail||m.imageUrl||void 0,status:A,englishName:m.titleEnglish??void 0,type:u.type??m.type??"TV"}),(u.thumbnail||u.type)&&w.push({id:e,thumbnail:u.thumbnail,type:u.type}),m.watchedEpisodes>0&&d.push({showId:e,progress:m.watchedEpisodes})})}return(p.length>0||N)&&await B(r,async i=>{N&&await M.clearWatchlist(i);for(const s of p)await v.upsert(i,{id:s.id,name:s.name,thumbnail:s.thumbnail??"",status:s.status,nativeName:"",englishName:s.englishName??"",type:s.type??"TV"});for(const s of w)(s.thumbnail||s.type||s.genres)&&await D.upsert(i,{id:s.id,thumbnail:s.thumbnail,type:s.type,genres:s.genres});for(const s of d)for(let o=1;o<=s.progress;o++)await U.insertIfMissing(i,{showId:s.showId,episodeNumber:String(o)})}),p.length}export{Mt as importFromMalUsername,Et as importFromUsername,Nt as syncAniList};
@@ -1,3 +0,0 @@
1
- import{AppCache as A}from"../utils/cache.utils.js";import{parseJsonBody as $}from"../utils/http.utils.js";const h=new A({ttlSeconds:10080*60,maxKeys:5e3});function k(e){return e.split(" ").map((t,i)=>i!==0&&["x","and","or","the","a","an","of","in","on","with"].includes(t.toLowerCase())?t.toLowerCase():t.charAt(0).toUpperCase()+t.slice(1)).join(" ").replace(/\bX\b/g,"x")}function C(e){const t=e.trim();return t&&k(t)}async function U(e,t,i){const d=e.map(r=>String(r).trim()).filter(Boolean),a={},c=[];for(const r of d){const n=`${t}:${i}:${r}`,l=h.get(n);l?a[r]=l:c.push(r)}if(c.length>0){const r=[];for(let n=0;n<c.length;n+=15)r.push(c.slice(n,n+15));for(const n of r){const l=n.join(`
2
- `),w=`https://clients5.google.com/translate_a/t?client=dict-chrome-ex&sl=${encodeURIComponent(t)}&tl=${encodeURIComponent(i)}&dt=t&q=${encodeURIComponent(l)}`;try{const o=await fetch(w,{headers:{"User-Agent":"Mozilla/5.0"},signal:AbortSignal.timeout(5e3)});if(!o.ok)throw new Error(`translate ${o.status}`);const s=await $(o);let g="";Array.isArray(s)&&typeof s[0]=="string"?g=s[0]:Array.isArray(s)&&Array.isArray(s[0])&&(g=s[0]?.map(u=>u[0]).join("")||"");const y=g.split(`
3
- `);n.forEach((f,u)=>{const m=(y[u]??"").trim(),p=m?C(m):f;a[f]=p,h.set(`${t}:${i}:${f}`,p)})}catch{for(const o of n)a[o]||(a[o]=o)}await new Promise(o=>setTimeout(o,80))}}return a}export{U as translateTexts};
@@ -1 +0,0 @@
1
- const y=20,g=21,p=58;function f(e,t){if(t>=e.length)return null;const l=e[t];let a=0;for(let r=1;r<=5;r++)if((l&128>>r-1)===0){a=r;break}if(a<1||t+a>e.length)return null;if(a===1)return[l,t+1];if(a===5)return[e[t+1]+e[t+2]*256+e[t+3]*65536+e[t+4]*16777216>>>0,t+5];const i=8-a;let n=l&(1<<i)-1;for(let r=1;r<a;r++)n+=e[t+r]*2**(i+8*(r-1));return[n,t+a]}function h(e){let t=0;for(;t<e.length;){const l=e[t++],a=l>>3;if((l&7)!==0)break;const n=f(e,t);if(!n)break;if(a===1)return n[0];t=n[1]}return 1}function k(e,t){const l=[];let a=1,i=t,n=0;if(i){const r=f(e,n),u=r&&f(e,r[1]);r&&u&&r[0]===20&&(n=u[1]+u[0]);const s=f(e,n),c=s&&f(e,s[1]);if(s&&c&&s[0]===i.pendingType){n=c[1];const o=Math.min(c[0],e.length-n);if(i.chunks.push(e.subarray(n,n+o)),i.remaining=c[0]-o,n+=o,i.remaining<=0){const m=Buffer.concat(i.chunks);i=null,s[0]===21&&m.length>1&&l.push(m.subarray(1))}}else i=null}for(;n<e.length;){const r=f(e,n);if(!r)break;const u=f(e,r[1]);if(!u)break;const[s,c]=[r[0],u[0]];n=u[1];const o=e.length-n;if(c>o){i={pendingType:s,remaining:c-o,chunks:[e.subarray(n)]};break}const m=e.subarray(n,n+c);n+=c,s===21?m.length>1&&l.push(m.subarray(1)):s===58&&(a=h(m))}return{media:Buffer.concat(l),status:a,carry:i}}export{k as parseUmpResponse};
@@ -1 +0,0 @@
1
- import o from"fs";import v from"path";import{CONFIG as d}from"../config.js";import l from"../logger.js";const a=v.join(d.ROOT,"ytmusic_cookie.txt");let f=null,u=null,h=0;const S=60*1e3;let n=null,m=null,g=!1;async function s(){return m||(m=await import("youtubei.js")),m}async function y(){if(g)return;const{Platform:t,Log:e}=await s();t.shim.eval=(async r=>new Function(r.output)()),e.setLevel(e.Level.ERROR),g=!0}function p(){try{return o.existsSync(a)&&o.readFileSync(a,"utf-8").trim()||null}catch{return null}}function T(){return p()}function I(t){if(!t||typeof t!="object")return!1;if(t.constructor?.name==="ParsingError")return!0;const r=t instanceof Error?t.message:String(t);return/Expected node of any type|Cannot cast \S+ to one of|got ItemSection|is not valid JSON|Incorrect JSPB formatting/i.test(r)}function C(){return f?Promise.resolve(f):Date.now()-h<S?Promise.reject(new Error("YouTube session unavailable (cooling down)")):(u||(u=(async()=>{const{Innertube:t}=await s();await y();try{const e=await t.create({generate_session_locally:!1});return f=e,e}catch(e){throw u=null,h=Date.now(),e}})()),u)}async function P(t=!1){const e=p();if(!e)return n=null,null;if(n&&!t)return n;await y();const{Innertube:r}=await s();return n=await r.create({cookie:e}),n}function k(){n=null}async function E(){k();try{return await P()}catch(t){return l.warn({err:t},"[ytmusic] saved-cookie session refresh failed"),null}}const x=300*1e3;let c=0,i=!1;async function _(){if(!p())return!1;const t=Date.now();if(t-c<x)return i;c=t;try{const e=await P();if(!e)return!1;await e.account.getInfo(),i=!0}catch{i=!1}return i}async function L(){return{authenticated:await _()}}async function D(t){const e=t.trim();if(!e)throw new Error("Cookie is empty");await y();const{Innertube:r}=await s(),b=await r.create({cookie:e});try{await b.music.getLibrary()}catch(w){if(!I(w))throw w;l.warn("[ytmusic] sign-in validation hit a layout variant, accepting cookie anyway")}try{o.mkdirSync(d.ROOT,{recursive:!0})}catch{}o.writeFileSync(a,e,"utf-8"),n=b,c=Date.now(),i=!0,l.info("[ytmusic] cookie sign-in validated and saved")}async function F(){try{o.existsSync(a)&&o.unlinkSync(a)}catch{}n=null,c=0,i=!1}export{P as getAuthedInnertube,C as getInnertube,L as getMusicAuthStatus,T as getMusicCookie,k as invalidateAuthedSession,I as isParserVariantError,E as refreshAuthedInnertube,D as saveMusicCookie,F as signOutMusic,_ as verifyMusicSession};
@@ -1 +0,0 @@
1
- import e from"pino";const o=process.argv.includes("--dev")||process.env.NODE_ENV==="development",t=o?e({level:"debug",transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):e({level:process.env.LOG_LEVEL||"info"});var n=t;export{n as default};
File without changes
File without changes
@@ -1,2 +0,0 @@
1
- import m from"fs";import y from"path";import E from"crypto";import{execFileSync as K}from"node:child_process";import{pathToFileURL as Q}from"url";import*as X from"cheerio";import{gotScraping as Y}from"got-scraping";import g from"../logger.js";import{CONFIG as M}from"../config.js";import{requestContext as Z}from"../utils/request-context.js";import{sanitizeCfClearance as ee,buildCfClearanceCookie as re}from"../utils/cookie.utils.js";import{decodeMaybeGzip as U,parseJsonBody as j}from"../utils/http.utils.js";import{buildQueryVariants as O,pickBestMatch as D}from"./title-matching.js";import{anilistRequest as te,parseMalId as ne,searchAnilistByTitle as oe}from"../lib/anilist.js";import{kitsuMetaByAnilistId as se}from"../lib/kitsu.js";import{getTmdbKey as ie,TMDB_BASE as N,TMDB_IMAGE as ae}from"../lib/tmdb.js";import{parseRegistry as H}from"./remote-types.js";const x=15e3,b="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";function T(r,n,e){let t;const o=new Promise((a,c)=>{t=setTimeout(()=>c(new Error(`${e} timed out after ${n}ms`)),n)});return Promise.race([r,o]).finally(()=>clearTimeout(t))}function $(r){return/^https?:\/\//i.test(r)}function S(r){return r.startsWith("file://")?new URL(r).pathname:r}function ce(r){return $(r)||r.startsWith("file://")?!1:r.includes("/")||r.includes("\\")||r.endsWith(".json")}async function de(r){if($(r)){const e=await T(fetch(r,{headers:{"User-Agent":b,Accept:"application/json"}}),x,"registry fetch");if(!e.ok)throw new Error(`registry HTTP ${e.status}: ${r}`);return U(Buffer.from(await e.arrayBuffer()))}const n=y.resolve(S(r));return await m.promises.readFile(n)}async function ue(r){if($(r)){const e=await T(fetch(r,{headers:{"User-Agent":b,Accept:"text/javascript,*/*"}}),x,"provider fetch");if(!e.ok)throw new Error(`provider HTTP ${e.status}: ${r}`);return U(Buffer.from(await e.arrayBuffer()))}const n=y.resolve(S(r));return await m.promises.readFile(n)}function le(r,n){const e=(r??"").trim();if($(e)||e.startsWith("file://"))return e;if($(n))return new URL(e,n).href;const t=y.dirname(y.resolve(S(n)));return y.resolve(t,e)}function I(r){return E.createHash("sha256").update(r).digest("hex")}function fe(r,n,e){let t;try{t=Buffer.from(r,"base64url")}catch{return null}if(!t.length||t.length%16!==0)return null;try{const o=Buffer.alloc(32);Buffer.from(n).copy(o);const a=E.createDecipheriv("aes-256-cbc",o,Buffer.from(e)),c=Buffer.concat([a.update(t),a.final()]);return JSON.parse(c.toString("utf8"))}catch{return null}}function W(r,n){const e=["-sSL","-A",b,...Object.entries(n??{}).flatMap(([c,i])=>["-H",`${c}: ${i}`]),"--max-time","15","-w",`
2
- __HTTP_STATUS__%{http_code}`,"--",r];let t;try{t=K("curl",e,{encoding:"utf-8",maxBuffer:10*1024*1024})}catch{return null}const o=t.match(/__HTTP_STATUS__(\d+)\s*$/);return{status:o?parseInt(o[1],10):0,body:o&&o.index!==void 0?t.slice(0,o.index):t}}function pe(r){const n=async(e,t,o=x)=>T(fetch(e,{...t,headers:{"User-Agent":b,...t?.headers}}),o,`fetch ${e}`);return{cache:{get:e=>r.get(e),set:(e,t,o)=>{r.set(e,t,o)}},logger:{info:(e,t)=>{g.info(e,t)},warn:(e,t)=>{g.warn(e,t)},error:(e,t)=>{g.error(e,t)},debug:(e,t)=>{g.debug(e,t)}},fetchText:async(e,t)=>{try{const o=await n(e,t);return o.ok?await o.text():null}catch{return null}},fetchJson:async(e,t)=>{const o=await n(e,t);if(!o.ok)throw new Error(`HTTP ${o.status}: ${e}`);return j(o)},proxyUrl:(e,t)=>`/api/proxy?url=${encodeURIComponent(e)}&referer=${encodeURIComponent(t)}`,userAgent:b,titleMatch:{buildQueryVariants:O,pickBestMatch:D},resolveBestShowId:async(e,t,o)=>{const a=[e,t].filter(c=>typeof c=="string"&&c.trim().length>0);if(a.length===0)return null;for(const c of O(e,t)){let i;try{i=await o(c)}catch(u){if(u.message==="AUTH_REQUIRED")throw u;continue}if(!i||i.length===0)continue;const l=D(i.filter(u=>u&&typeof u.id=="string"&&u.id.length>0),a);if(l)return l.item.id}return null},anilist:{request:(e,t)=>te(e,t),parseMalId:ne,searchByTitle:oe},kitsu:{metaByAnilistId:se},tmdb:{base:N,image:ae,get:async e=>{try{const t=await ie(),o=e.includes("?")?"&":"?",a=await n(`${N}${e}${o}api_key=${t}`);return a.ok?j(a):null}catch{return null}}},request:{get:e=>Z.getStore()?.get(e)},cookies:{sanitizeCfClearance:ee,buildCfClearanceCookie:re},crypto:{aes256CbcDecryptJson:fe,hmacSha256Base64Url:(e,t)=>E.createHmac("sha256",e).update(t).digest("base64url")},scraping:{fetch:async(e,t)=>{const o=typeof e=="string"?{url:e,...t??{}}:e,{url:a,...c}=o,i=await Y({url:a,method:"GET",responseType:"text",throwHttpErrors:!1,...c});return{statusCode:i.statusCode,body:String(i.body??""),headers:i.headers}}},curl:{getJson:async(e,t)=>{try{const o=W(e,t);return!o||o.status!==200?null:JSON.parse(o.body)}catch{return null}},getText:async(e,t)=>{const o=W(e,t);return!o||o.status!==200?null:o.body}},cheerio:{load:e=>X.load(e)}}}function A(r){if(!r||typeof r!="object")return null;const n=r;return typeof n.name=="string"?n:null}function G(r,n){return n.every(e=>typeof r[e]=="function")}function F(r){const n=A(r);return!!n&&G(n,["search","getEpisodes","getStreamUrls"])}function L(r){const n=A(r);return!!n&&G(n,["search","getDetail","getChapters","getPages"])}function _(r){const n=A(r);return!!n&&(typeof n.getSources=="function"||typeof n.getEmbedUrl=="function")}function ge(r,n){const e=n.trim().replace(/\/+$/,"");return{name:r,getEmbedUrl:async t=>{const o=Number(t.tmdbId);return o?t.type==="movie"?`${e}/movie/${o}`:`${e}/tv/${o}/${t.season||1}/${t.episode||1}`:null}}}async function me(r,n,e,t,o){if(r.embedBase){const f=ge(r.id,r.embedBase);return C.set(`${r.kind??"anime"}:${r.id}-${r.version}-embed`,{provider:f}),{provider:f}}const a=le(r.entry,n);await m.promises.mkdir(e,{recursive:!0});const c=`${r.id}-${r.version}.mjs`,i=y.join(e,c),l=`${r.kind??"anime"}:${r.id}-${r.version}-${(r.sha256??"nosha").slice(0,12)}`;let u=null;try{const f=await m.promises.readFile(i);(r.sha256&&I(f)===r.sha256||!r.sha256&&o)&&(u=f)}catch{}if(u){const f=C.get(l);if(f)return g.debug({provider:r.id,version:r.version},"remote provider reused cached instance"),{provider:f.provider,facets:f.facets}}else{try{u=await ue(a)}catch(f){return{provider:null,error:`download failed: ${f.message}`}}if(r.sha256){if(I(u)!==r.sha256)return{provider:null,error:`sha256 mismatch for ${r.id}`}}else if(!o)return{provider:null,error:`missing sha256 for ${r.id}`};await m.promises.writeFile(i,u)}const B=`${r.version}-${(r.sha256??"nosha").slice(0,12)}`,w=await J(i,B,t,r);return w.provider&&!w.error&&C.set(l,{provider:w.provider,facets:w.facets}),w}const C=new Map;function ve(r){let n,e;for(const[t,o]of Object.entries(r)){if(t==="default"||!Array.isArray(o)||o.length===0||!o.every(i=>typeof i=="string"&&i.length>0))continue;const a=o.filter(i=>i.length<=64).slice(0,500);if(a.length===0)continue;const c=t.toUpperCase();c.includes("GENRE")||c.includes("TAG")?n??=a:(c.includes("ORDER")||c.includes("SORT"))&&(e??=a)}if(!(!n&&!e))return{...n?{genres:n}:{},...e?{orders:e}:{}}}async function J(r,n,e,t){const o=t.id;let a;try{const l=`${Q(r).href}?v=${encodeURIComponent(n)}`;a=await T(import(l),x,`import ${o}`)}catch(l){return{provider:null,error:`import failed: ${l.message}`}}const c=a.default;if(typeof c!="function")return{provider:null,error:"missing default factory export"};let i;try{i=await T(Promise.resolve(c(e)),1e4,`factory ${o}`)}catch(l){return{provider:null,error:`factory failed: ${l.message}`}}return(t.kind==="manga"?!L(i):t.kind==="tv"?!_(i):!F(i))?{provider:null,error:"invalid provider shape"}:{provider:i,facets:ve(a)}}async function he(r,n,e,t){if(!n.sha256)return{provider:null,error:"no verified fallback"};const o=y.join(e,`${n.id}-${n.version}.mjs`);let a;try{a=await m.promises.readFile(o)}catch{return{provider:null,error:"no cached fallback file"}}if(I(a)!==n.sha256)return{provider:null,error:"cached fallback failed verification"};const c=`${n.version}-${n.sha256.slice(0,12)}`;return J(o,c,t,{...r,version:n.version})}async function Me(r){const n=(r.registryUrl??M.PROVIDER_REPO_URL??"").trim(),e={providers:{},mangaProviders:{},tvProviders:{},catalog:[],proxyRules:[]};if(!n)return e;const t=y.join(M.ROOT,"remote-providers"),o=y.join(t,"registry.json");let a=null;try{a=H(JSON.parse((await m.promises.readFile(o)).toString("utf8")))}catch{}const c=new Map((a?.providers??[]).map(s=>[s.id.toLowerCase(),s]));let i;try{const s=await de(n);i=JSON.parse(s.toString("utf8")),await m.promises.mkdir(t,{recursive:!0}),await m.promises.writeFile(o,JSON.stringify(i))}catch(s){g.error({err:s,registryUrl:n},"remote providers: manifest fetch failed, trying disk cache");try{i=JSON.parse((await m.promises.readFile(o)).toString("utf8"))}catch{return e}}let l;try{l=H(i)}catch(s){return g.error({err:s,registryUrl:n},"remote providers: manifest parse failed"),e}const u=new Set((r.enabledProviders??[]).map(s=>s.toLowerCase())),B=u.size>0,w=pe(r.cache),f=r.allowUnsigned??(process.env.DANGO_ALLOW_UNSIGNED_PROVIDERS==="1"||ce(n));for(const s of l.providers){const v=s.id.toLowerCase();if(s.enabledByDefault===!1&&(!B||!u.has(v))){e.catalog.push(P(s,!1,"disabled by default"));continue}if(B&&!u.has(v)){e.catalog.push(P(s,!1,"not in enabledProviders"));continue}const{provider:h,facets:d,error:k}=await me(s,n,t,w,f);if(h&&!k){if(s.kind==="manga"&&L(h))e.mangaProviders[v]=h;else if(s.kind==="tv"&&_(h))e.tvProviders[v]=h;else if(s.kind!=="manga"&&s.kind!=="tv"&&F(h))e.providers[v]=h;else{e.catalog.push(P(s,!1,"invalid provider shape for kind")),g.warn({provider:s.id},"remote provider kind mismatch, skipped");continue}e.catalog.push(V(d,q(s,h,P(s,!0)))),s.proxy&&e.proxyRules.push(s.proxy),g.info({provider:s.id,version:s.version},"remote provider loaded")}else{const R=c.get(v);if(R&&R.version!==s.version){const p=await he(s,R,t,w);if(p.provider&&!p.error&&(s.kind==="manga"&&L(p.provider)?e.mangaProviders[v]=p.provider:s.kind==="tv"&&_(p.provider)?e.tvProviders[v]=p.provider:s.kind!=="manga"&&s.kind!=="tv"&&F(p.provider)?e.providers[v]=p.provider:p.error="cached fallback failed validation"),p.provider&&!p.error){e.catalog.push({...V(p.facets,q(s,p.provider,P({...s,version:R.version},!0))),error:`serving cached ${R.version}: ${k}`}),s.proxy&&e.proxyRules.push(s.proxy),g.warn({provider:s.id,error:k,fallback:R.version},"remote provider update failed, serving cached version");continue}}e.catalog.push(P(s,!1,k)),g.warn({provider:s.id,error:k},"remote provider skipped")}}const z=new Set(e.catalog.filter(s=>!s.loaded).map(s=>`${s.id}-`));try{const s=new Set(l.providers.map(d=>`${d.id}-${d.version}.mjs`.toLowerCase())),v=new Set(l.providers.map(d=>`${d.kind??"anime"}:${d.id}-${d.version}-${(d.sha256??"nosha").slice(0,12)}`.toLowerCase()));for(const d of C.keys())v.has(d.toLowerCase())||C.delete(d);const h=await m.promises.readdir(t);for(const d of h)if(d.toLowerCase().endsWith(".mjs")&&!s.has(d.toLowerCase())){if([...z].some(k=>d.toLowerCase().startsWith(k)))continue;await m.promises.unlink(y.join(t,d)),g.info({file:d},"remote providers: removed superseded module")}}catch(s){g.warn({err:s},"remote providers: superseded module cleanup failed")}return e}function q(r,n,e){if(r.kind!=="tv"||!n||typeof n!="object")return e;const t=n.servers;return Array.isArray(t)&&t.every(o=>typeof o=="string")&&t.length>0?{...e,servers:[...t]}:e}function V(r,n){const e=(r?.genres??[]).length>0?r?.genres:void 0,t=(r?.orders??[]).length>0?r?.orders:void 0;return!e&&!t?n:{...n,facets:{...e?{genres:e}:{},...t?{orders:t}:{}}}}function P(r,n,e){return{id:r.id.toLowerCase(),label:r.label,version:r.version,mature:r.mature,kind:r.kind??"anime",sub:r.sub,tier:r.tier,modes:r.modes??(r.kind==="tv"?[]:["sub","dub"]),browse:r.browse,enabledByDefault:r.enabledByDefault!==!1,loaded:n,remote:!0,error:e}}export{Me as loadRemoteProviders};
@@ -1 +0,0 @@
1
- const f=/^[a-z0-9][a-z0-9-]*$/i,m=/^[0-9a-f]{64}$/i;function g(t){if(!t||typeof t!="object")throw new Error("registry: not an object");const o=t;if(typeof o.version!="number")throw new Error("registry.version must be a number");if(!Array.isArray(o.providers))throw new Error("registry.providers must be an array");const e=o.providers.map((r,s)=>p(r,s)),n=new Set;for(const r of e){const s=r.id.toLowerCase();if(n.has(s))throw new Error(`registry: duplicate provider id "${r.id}"`);n.add(s)}return{version:o.version,updatedAt:typeof o.updatedAt=="string"?o.updatedAt:void 0,providers:e}}function p(t,o){if(!t||typeof t!="object")throw new Error(`registry.providers[${o}]: not an object`);const e=t,n=e.id,r=e.entry,s=e.label,u=e.version;if(typeof n!="string"||!f.test(n.trim()))throw new Error(`registry.providers[${o}].id must match ${f}`);if(typeof s!="string"||!s.trim())throw new Error(`registry.providers[${o}].label must be a non-empty string`);if(typeof u!="string"||!u.trim())throw new Error(`registry.providers[${o}].version must be a non-empty string`);const i=e.embedBase;if(i!==void 0&&(typeof i!="string"||!/^https:\/\/[^/]+/i.test(i.trim())))throw new Error(`registry.providers[${o}].embedBase must be an https URL`);if((typeof r!="string"||!r.trim())&&i===void 0)throw new Error(`registry.providers[${o}].entry must be a non-empty string`);if(e.sha256!==void 0&&(typeof e.sha256!="string"||!m.test(e.sha256.trim())))throw new Error(`registry.providers[${o}].sha256 must be 64 hex chars`);if(e.mature!==void 0&&typeof e.mature!="boolean")throw new Error(`registry.providers[${o}].mature must be boolean`);if(e.kind!==void 0&&e.kind!=="anime"&&e.kind!=="asmr"&&e.kind!=="manga"&&e.kind!=="tv")throw new Error(`registry.providers[${o}].kind must be anime|asmr|manga|tv`);if(e.sub!==void 0&&e.sub!=="soft"&&e.sub!=="hard"&&e.sub!=="mixed")throw new Error(`registry.providers[${o}].sub must be soft|hard|mixed`);if(e.tier!==void 0&&e.tier!=="direct"&&e.tier!=="embed"&&e.tier!=="cookie")throw new Error(`registry.providers[${o}].tier must be direct|embed|cookie`);let a;if(e.modes!==void 0){if(!Array.isArray(e.modes))throw new Error(`registry.providers[${o}].modes must be an array`);a=[];for(const d of e.modes){if(d!=="sub"&&d!=="dub")throw new Error(`registry.providers[${o}].modes must contain only sub|dub`);a.includes(d)||a.push(d)}}return{id:n.trim(),label:s.trim(),version:u.trim(),entry:typeof r=="string"?r.trim():void 0,embedBase:typeof i=="string"?i.trim().replace(/\/+$/,""):void 0,sha256:typeof e.sha256=="string"?e.sha256.trim().toLowerCase():void 0,minDango:typeof e.minDango=="string"?e.minDango:void 0,mature:typeof e.mature=="boolean"?e.mature:!1,kind:e.kind==="asmr"||e.kind==="manga"||e.kind==="tv"?e.kind:"anime",sub:e.sub,tier:e.tier,modes:a,browse:y(e.browse,o),proxy:b(e.proxy,o),auth:e.auth==="cookie"?"cookie":void 0,enabledByDefault:e.enabledByDefault!==!1}}function b(t,o){if(t===void 0)return;if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`registry.providers[${o}].proxy must be an object`);const e=t,n={};for(const r of["hosts","suffixes","mirrors"])if(e[r]!==void 0){if(!Array.isArray(e[r])||!e[r].every(s=>typeof s=="string"&&s))throw new Error(`registry.providers[${o}].proxy.${r} must be a non-empty string array`);n[r]=e[r].map(s=>s.trim().toLowerCase())}for(const r of["referer","origin"])if(e[r]!==void 0){if(typeof e[r]!="string"||!/^https:\/\//i.test(e[r].trim()))throw new Error(`registry.providers[${o}].proxy.${r} must be an https URL`);n[r]=e[r].trim()}for(const r of["originFromReferer","scrape","cfCookie","embedPatch"])if(e[r]!==void 0){if(typeof e[r]!="boolean")throw new Error(`registry.providers[${o}].proxy.${r} must be boolean`);n[r]=e[r]}return n}function y(t,o){if(t===void 0)return;if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`registry.providers[${o}].browse must be an object`);const e=t,n={};for(const r of["genre","order","studio","sort"])if(e[r]!==void 0){if(typeof e[r]!="boolean")throw new Error(`registry.providers[${o}].browse.${r} must be boolean`);n[r]=e[r]}if(e.pageSize!==void 0){if(typeof e.pageSize!="number"||!Number.isInteger(e.pageSize)||e.pageSize<1)throw new Error(`registry.providers[${o}].browse.pageSize must be a positive integer`);n.pageSize=e.pageSize}return n}export{g as parseRegistry};
@@ -1 +0,0 @@
1
- function h(e){return e.toLowerCase().replace(/[^a-z0-9]/g,"")}function u(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(n=>n.length>=2)}function x(e,n){if(e.length<2||n.length<2)return e===n?1:0;const c=i=>{const g=new Map;for(let l=0;l<i.length-1;l++){const f=i.slice(l,l+2);g.set(f,(g.get(f)||0)+1)}return g},o=c(e),t=c(n);let s=0,r=0,a=0;return o.forEach(i=>s+=i),t.forEach(i=>r+=i),o.forEach((i,g)=>{const l=t.get(g);l&&(a+=Math.min(i,l))}),2*a/(s+r)}const v=new Set(["the","animation","no","wa","ga","wo","ni","de","to","e","o","x","ii","yo","ova","oad","hd","ep","episode","season","part","hen","a"]),S=new Set(["season","2nd","3rd","4th","5th","ii","iii","iv","v","vi","vii","viii","ix","x","two","three","four","five","six","seven","eight","nine","ten"]);function d(e){return[...new Set(u(e).filter(n=>!v.has(n)&&!S.has(n)))]}const m={ii:2,iii:3,iv:4,v:5,vi:6,vii:7,viii:8,ix:9,x:10},b={two:2,three:3,four:4,five:5,six:6,seven:7,eight:8,nine:9,ten:10};function p(e){const n=e.toLowerCase(),c=n.match(/\bseason\s*(\d+|ii|iii|iv|v|vi|vii|viii|ix|x|two|three|four|five|six|seven|eight|nine|ten)\b/);if(c){const r=c[1],a=m[r],i=b[r];return String(a??i??r)}const o=n.match(/\b(\d+)(st|nd|rd|th)\b/);if(o)return o[1];const t=n.match(/\b(ii|iii|iv|v|vi|vii|viii|ix|x)\b(?!\s*(st|nd|rd|th|season))/);if(t)return String(m[t[1]]);const s=n.match(/\b(\d+)\s*(season)?\s*$/);return s?s[1]:""}function w(e){return[...new Set(u(e).filter(n=>!v.has(n)))]}function M(e,n){const c=h(e),o=h(n);if(!c||!o)return 0;if(c===o)return 1;let t=x(c,o);if(c.startsWith(o)||o.startsWith(c)){const i=.5+.5*(Math.min(c.length,o.length)/Math.max(c.length,o.length));i>t&&(t=i)}const s=d(e),r=d(n);if(s.length>0&&r.length>=2){const i=new Set(r);let g=0;for(const f of s)i.has(f)&&g++;const l=g/Math.min(s.length,r.length);l>t&&(t=l)}const a=p(e);if(a){const i=p(n);i===a?t+=.25:!i&&t>=.5&&(t-=.25)}return t>=1?.99:t}function T(e,n,c=.6){if(!e.length)return null;const o=n.filter(s=>!!s&&s.trim().length>0);if(!o.length)return null;let t=null;for(const s of e){if(!s.title)continue;let r=0;for(const a of o){const i=M(a,s.title);i>r&&(r=i)}(!t||r>t.score)&&(t={item:s,score:r})}return t&&t.score>=c?t:null}function k(e,n){const c=new Set,o=[],t=g=>{const l=g.replace(/\s+/g," ").trim();l&&!c.has(l.toLowerCase())&&(c.add(l.toLowerCase()),o.push(l))};n&&n.trim()&&t(n),t(e),/^re\s*zero$/i.test(e.trim())&&t("Re Zero");const s=g=>g.replace(/[^\p{L}\p{N}\s]/gu," ");t(s(e)),n&&t(s(n));const r=s(e).split(/\s+/).filter(Boolean);r.length>3&&t(r.slice(0,3).join(" ")),r.length>4&&t(r.slice(0,4).join(" "));const a=w(e).concat(n?w(n):[]).filter(g=>g.length>=4),i=[...new Set(a)];i.sort((g,l)=>l.length-g.length);for(const g of i.slice(0,4))t(g);return o}export{k as buildQueryVariants,T as pickBestMatch};
File without changes
@@ -1,2 +0,0 @@
1
- import{spawn as l,execFile as m}from"child_process";import a from"./logger.js";import{CONFIG as c}from"./config.js";class u{activeRemote=null;executeCommand(t){return new Promise((e,r)=>{m("rclone",t,(i,o,n)=>{if(i)return n&&a.warn({stderr:n},"Rclone command warning"),r(new Error(n||i.message));e(o.trim())})})}executeRcloneArgs(t){return new Promise((e,r)=>{const i=l("rclone",t,{stdio:"ignore"});i.on("close",o=>{o===0?e():r(new Error(`Rclone exited with code ${o}`))}),i.on("error",o=>r(o))})}async listRemotes(){try{return(await this.executeCommand(["listremotes"])).split(`
2
- `).map(e=>e.trim()).filter(e=>e!=="").map(e=>e.replace(/:$/,""))}catch{return[]}}async init(){try{await this.executeCommand(["version"]);const t=await this.listRemotes();if(c.RCLONE_REMOTE){const e=t.find(r=>r.toLowerCase()===c.RCLONE_REMOTE?.toLowerCase());if(e)return this.activeRemote=e,a.info(`Rclone initialized with manual remote: ${this.activeRemote}`),!0;a.warn(`Configured RCLONE_REMOTE '${c.RCLONE_REMOTE}' not found in rclone listremotes.`)}return t.length>0&&!c.RCLONE_REMOTE&&a.info({remotes:t},"Rclone available but no manual remote is configured in settings."),!1}catch(t){return a.warn({err:t},"Rclone initialization failed"),!1}}isActive(){return this.activeRemote!==null}getRemoteName(){return this.activeRemote||"unknown"}async downloadFile(t,e,r){if(!this.activeRemote)throw new Error("Rclone not active");const i=`${this.activeRemote}:${t}/${e}`;await this.executeRcloneArgs(["copyto",i,r])}async uploadFile(t,e,r){if(!this.activeRemote)throw new Error("Rclone not active");const i=`${this.activeRemote}:${e}/${r}`;await this.executeRcloneArgs(["copyto",t,i])}executeRcloneArgsWithOutput(t){return new Promise((e,r)=>{const i=l("rclone",t,{stdio:"pipe"});let o="",n="";i.stdout?.on("data",s=>o+=s),i.stderr?.on("data",s=>n+=s),i.on("close",s=>{s===0?e(o.trim()):(n&&a.warn({stderr:n},"Rclone command warning"),r(new Error(n||`Rclone exited with code ${s}`)))}),i.on("error",s=>r(s))})}async fileExists(t,e){if(!this.activeRemote)return!1;try{const r=`${this.activeRemote}:${t}/${e}`,i=await this.executeRcloneArgsWithOutput(["lsjson",r]),o=JSON.parse(i);return o&&o.length>0}catch{return!1}}}const w=new u;export{w as rcloneService};
@@ -1,42 +0,0 @@
1
- import{eq as i,inArray as l,sql as a}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";import{asmrProgress as n}from"../db/schema-asmr.js";import{makeLibraryRepo as u}from"./library.repository.js";const C=["Listening","Completed","On-Hold","Dropped","Planned"];function A(t){return String(t).trim().toUpperCase()}const g={...u("asmr_library"),upsert:(t,r)=>s(t).run(a`
2
- INSERT INTO asmr_library (id, rjCode, title, thumbnail, status, isAdult, updatedAt)
3
- VALUES (${r.id}, ${r.rjCode}, ${r.title}, ${r.thumbnail}, ${r.status}, ${r.isAdult?1:0}, strftime('%s', 'now'))
4
- ON CONFLICT(id) DO UPDATE SET
5
- rjCode = COALESCE(NULLIF(EXCLUDED.rjCode, ''), asmr_library.rjCode),
6
- title = COALESCE(NULLIF(EXCLUDED.title, ''), asmr_library.title),
7
- thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), asmr_library.thumbnail),
8
- status = COALESCE(NULLIF(EXCLUDED.status, ''), asmr_library.status),
9
- isAdult = COALESCE(EXCLUDED.isAdult, asmr_library.isAdult),
10
- updatedAt = strftime('%s', 'now')`),touchProgress:(t,r,e)=>s(t).run(a`
11
- UPDATE asmr_library SET lastTrackIndex = ${e.trackIndex}, lastTrackLabel = ${e.trackLabel}, lastPosition = ${e.position}, updatedAt = strftime('%s', 'now') WHERE id = ${r}`)},c={getByWork:(t,r)=>s(t).all(a`
12
- SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${r} ORDER BY updatedAt DESC`),getTrack:async(t,r,e)=>(await s(t).all(a`
13
- SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${r} AND trackIndex = ${e}`))[0],getLatest:async(t,r)=>(await s(t).all(a`
14
- SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${r} ORDER BY updatedAt DESC LIMIT 1`))[0],upsert:(t,r)=>s(t).run(a`
15
- INSERT INTO asmr_progress (workId, trackIndex, trackLabel, currentTime, duration, title, thumbnail, rjCode, isAdult, updatedAt)
16
- VALUES (${r.workId}, ${r.trackIndex}, ${r.trackLabel}, ${r.currentTime}, ${r.duration}, ${r.title??null}, ${r.thumbnail??null}, ${r.rjCode??null}, ${r.isAdult??null}, strftime('%s', 'now'))
17
- ON CONFLICT(workId, trackIndex) DO UPDATE SET
18
- trackLabel = COALESCE(NULLIF(EXCLUDED.trackLabel, ''), asmr_progress.trackLabel),
19
- currentTime = EXCLUDED.currentTime,
20
- duration = EXCLUDED.duration,
21
- title = COALESCE(EXCLUDED.title, asmr_progress.title),
22
- thumbnail = COALESCE(EXCLUDED.thumbnail, asmr_progress.thumbnail),
23
- rjCode = COALESCE(EXCLUDED.rjCode, asmr_progress.rjCode),
24
- isAdult = COALESCE(EXCLUDED.isAdult, asmr_progress.isAdult),
25
- updatedAt = strftime('%s', 'now')`),deleteByWork:(t,r)=>s(t).delete(n).where(i(n.workId,r)),deleteMany:(t,r)=>r.length===0?Promise.resolve():s(t).delete(n).where(l(n.workId,r)),deleteTrack:(t,r,e)=>s(t).run(a`
26
- DELETE FROM asmr_progress WHERE workId = ${r} AND trackIndex = ${e}`),getContinueListening:(t,r)=>{const e=typeof r=="number"?`LIMIT ${r}`:"";return s(t).all(a`
27
- SELECT p.workId as id,
28
- COALESCE(l.rjCode, p.rjCode, p.workId) as rjCode,
29
- COALESCE(l.title, p.title) as title,
30
- COALESCE(l.thumbnail, p.thumbnail) as thumbnail,
31
- COALESCE(l.isAdult, p.isAdult) as isAdult,
32
- l.status as watchlistStatus,
33
- p.trackIndex, p.trackLabel, p.currentTime, p.duration, p.updatedAt as progressAt
34
- FROM (
35
- SELECT *, ROW_NUMBER() OVER (PARTITION BY workId ORDER BY updatedAt DESC) as rn
36
- FROM asmr_progress
37
- ) p
38
- LEFT JOIN asmr_library l ON p.workId = l.id
39
- WHERE p.rn = 1
40
- AND (l.status IS NULL OR l.status = 'Listening')
41
- ORDER BY p.updatedAt DESC
42
- ${a.raw(e)}`)}};export{C as ASMR_STATUSES,g as AsmrLibraryRepository,c as AsmrProgressRepository,A as buildAsmrId};
@@ -1,92 +0,0 @@
1
- import{sql as t}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";const w={getCoreStats:async e=>(await s(e).all(t`
2
- SELECT
3
- (SELECT SUM(
4
- CASE WHEN we.currentTime > 0 THEN we.currentTime
5
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
6
- COALESCE(sm.episodeDuration * 60,
7
- CASE UPPER(COALESCE(sm.type, ''))
8
- WHEN 'MOVIE' THEN 6000
9
- WHEN 'TV_SHORT' THEN 720
10
- WHEN 'MUSIC' THEN 300
11
- ELSE 1440 END)
12
- ELSE 0 END)
13
- FROM watched_episodes we
14
- LEFT JOIN shows_meta sm ON sm.id = we.showId) as totalSeconds,
15
- (SELECT COUNT(*) FROM watched_episodes) as totalEpisodes,
16
- (SELECT COUNT(DISTINCT showId) FROM watched_episodes) as totalAnime,
17
- (SELECT COUNT(*) FROM watchlist WHERE status = 'Completed') as completedCount,
18
- (SELECT COUNT(*) FROM watchlist) as totalWatchlist`))[0],getActivityGrid:e=>s(e).all(t`
19
- SELECT date(watchedAt) as day, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY day`),getHourlyDist:e=>s(e).all(t`
20
- SELECT strftime('%H', watchedAt) as hour, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY hour`),getSeasonality:e=>s(e).all(t`
21
- SELECT strftime('%m', we.watchedAt) as month, SUM(
22
- CASE WHEN we.currentTime > 0 THEN we.currentTime
23
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
24
- COALESCE(sm.episodeDuration * 60,
25
- CASE UPPER(COALESCE(sm.type, ''))
26
- WHEN 'MOVIE' THEN 6000
27
- WHEN 'TV_SHORT' THEN 720
28
- WHEN 'MUSIC' THEN 300
29
- ELSE 1440 END)
30
- ELSE 0 END) as seconds
31
- FROM watched_episodes we
32
- LEFT JOIN shows_meta sm ON sm.id = we.showId
33
- GROUP BY month`),getAllWatches:e=>s(e).all(t`
34
- SELECT we.watchedAt, we.currentTime,
35
- CASE WHEN we.currentTime > 0 THEN we.currentTime
36
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
37
- COALESCE(sm.episodeDuration * 60,
38
- CASE UPPER(COALESCE(sm.type, ''))
39
- WHEN 'MOVIE' THEN 6000
40
- WHEN 'TV_SHORT' THEN 720
41
- WHEN 'MUSIC' THEN 300
42
- ELSE 1440 END)
43
- ELSE 0 END as effectiveSeconds
44
- FROM watched_episodes we
45
- LEFT JOIN shows_meta sm ON sm.id = we.showId
46
- ORDER BY we.watchedAt ASC`),getWatchedShowsMeta:e=>s(e).all(t`
47
- SELECT DISTINCT sm.id, sm.genres, sm.popularityScore
48
- FROM shows_meta sm
49
- JOIN watched_episodes we ON sm.id = we.showId`),getDroppedShows:e=>s(e).all(t`
50
- SELECT w.id, w.name, MAX(we.watchedAt) as lastActivity
51
- FROM watchlist w
52
- JOIN watched_episodes we ON w.id = we.showId
53
- WHERE w.status = 'Watching' AND NOT (we.currentTime = 0 AND we.duration = 0)
54
- GROUP BY w.id
55
- HAVING lastActivity < date('now', '-90 days')`),getCompletionVelocities:e=>s(e).all(t`
56
- SELECT
57
- (julianday(MAX(we.watchedAt)) - julianday(MIN(we.watchedAt))) as daysToFinish
58
- FROM watchlist w
59
- JOIN watched_episodes we ON w.id = we.showId
60
- WHERE w.status = 'Completed'
61
- GROUP BY w.id
62
- HAVING SUM(we.currentTime) > 0`),getWatchedEpisodesWithMeta:e=>s(e).all(t`
63
- SELECT
64
- we.showId,
65
- we.currentTime,
66
- we.duration,
67
- CASE WHEN we.currentTime > 0 THEN we.currentTime
68
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
69
- COALESCE(sm.episodeDuration * 60,
70
- CASE UPPER(COALESCE(sm.type, ''))
71
- WHEN 'MOVIE' THEN 6000
72
- WHEN 'TV_SHORT' THEN 720
73
- WHEN 'MUSIC' THEN 300
74
- ELSE 1440 END)
75
- ELSE 0 END as effectiveSeconds,
76
- sm.genres,
77
- sm.popularityScore,
78
- sm.name,
79
- sm.nativeName,
80
- sm.englishName,
81
- sm.thumbnail
82
- FROM watched_episodes we
83
- JOIN shows_meta sm ON we.showId = sm.id`),getLibraryShowsWithGenres:e=>s(e).all(t`
84
- SELECT
85
- w.id,
86
- w.status,
87
- COALESCE(NULLIF(sm.englishName, ''), sm.name, w.name) as title,
88
- sm.genres,
89
- (SELECT COUNT(*) FROM watched_episodes we WHERE we.showId = w.id) as episodesWatched,
90
- COALESCE(sm.popularityScore, 0) as popularityScore
91
- FROM watchlist w
92
- LEFT JOIN shows_meta sm ON sm.id = w.id`),getAllAnilistIds:e=>s(e).all(t`SELECT anilistId FROM shows_meta WHERE anilistId IS NOT NULL`)};export{w as InsightsRepository};
@@ -1,3 +0,0 @@
1
- import{sql as a}from"drizzle-orm";import{getDrizzle as n}from"../db/drizzle.js";const l=["watchlist","watched_episodes","queue","shows_meta","dismissed_notifications","discovered_notifications","legacy_id_mapping","temp_show_ids"],b=["manga_library","manga_progress"],m=["tv_library","tv_progress"],E=["asmr_library","asmr_progress"];async function u(e,o){const t={};for(const r of o)try{const s=await n(e).all(a`SELECT COUNT(*) AS rows FROM ${a.raw(`"${r}"`)}`);t[r]=s[0]?.rows??0}catch{t[r]=0}return t}async function c(e,o){for(const t of o)try{await n(e).run(a`DELETE FROM ${a.raw(`"${t}"`)}`)}catch{}}function A(e){const o=a.raw(`"${e}"`);return{getById:async(t,r)=>(await n(t).all(a`SELECT * FROM ${o} WHERE id = ${r}`))[0],getAll:(t,r,s,i)=>{const p=a`SELECT * FROM ${o}`;return r&&r!=="All"&&p.append(a` WHERE status = ${r}`),p.append(a` ORDER BY updatedAt DESC`),s!==void 0&&i!==void 0&&p.append(a` LIMIT ${s} OFFSET ${i}`),n(t).all(p)},getCount:async(t,r)=>r&&r!=="All"?(await n(t).all(a`SELECT COUNT(*) as total FROM ${o} WHERE status = ${r}`))[0]?.total||0:(await n(t).all(a`SELECT COUNT(*) as total FROM ${o}`))[0]?.total||0,getIds:async t=>(await n(t).all(a`SELECT id FROM ${o}`)).map(s=>s.id),updateStatus:(t,r,s)=>n(t).run(a`
2
- UPDATE ${o} SET status = ${s}, updatedAt = strftime('%s', 'now') WHERE id = ${r}`),updateStatusMany:(t,r,s)=>r.length===0?Promise.resolve():n(t).run(a`
3
- UPDATE ${o} SET status = ${s}, updatedAt = strftime('%s', 'now') WHERE id IN (${a.join(r.map(i=>a`${i}`),a`, `)})`),delete:(t,r)=>n(t).run(a`DELETE FROM ${o} WHERE id = ${r}`),deleteMany:(t,r)=>r.length===0?Promise.resolve():n(t).run(a`DELETE FROM ${o} WHERE id IN (${a.join(r.map(s=>a`${s}`),a`, `)})`)}}const L={countAll:e=>u(e,l),clearAll:e=>c(e,l),countManga:e=>u(e,b),clearManga:e=>c(e,b),countTv:e=>u(e,m),clearTv:e=>c(e,m),countAsmr:e=>u(e,E),clearAsmr:e=>c(e,E)};export{L as LibraryRepository,A as makeLibraryRepo};
@@ -1 +0,0 @@
1
- import{DatabaseSync as p}from"node:sqlite";import T from"fs";import m from"path";import{CONFIG as l}from"../config.js";import{DatabaseWrapper as u}from"../db.js";import{dbGet as a,dbRun as n}from"../utils/db-utils.js";const s=5e3,S=6*3600;function h(){return m.join(l.ROOT,l.IS_DEV?"mal-cache.dev.db":"mal-cache.db")}let o=null;function c(){if(!o){const e=h();T.mkdirSync(m.dirname(e),{recursive:!0}),o=new u(e,new p(e)),n(o,"CREATE TABLE IF NOT EXISTS mal_cache (key TEXT PRIMARY KEY, payload TEXT NOT NULL, fetched_at TEXT NOT NULL, expires_at TEXT NOT NULL)")}return o}function f(e){const t=a(c(),"SELECT payload, fetched_at AS fetchedAt, expires_at AS expiresAt FROM mal_cache WHERE key = ?",[e]);return t?{payload:t.payload,fresh:Date.now()<Date.parse(t.expiresAt)}:null}function O(e,t,i=S){const r=c(),E=new Date;n(r,"INSERT OR REPLACE INTO mal_cache (key, payload, fetched_at, expires_at) VALUES (?, ?, ?, ?)",[e,t,E.toISOString(),new Date(E.getTime()+i*1e3).toISOString()]),!((a(r,"SELECT COUNT(*) AS rows FROM mal_cache")?.rows??0)<s||(d(),(a(r,"SELECT COUNT(*) AS rows FROM mal_cache")?.rows??0)<s))&&n(r,`DELETE FROM mal_cache WHERE key NOT IN (SELECT key FROM mal_cache ORDER BY fetched_at DESC LIMIT ${s})`)}function d(){const e=new Date().toISOString(),t=a(c(),"SELECT COUNT(*) AS rows FROM mal_cache WHERE expires_at < ?",[e]);return n(c(),"DELETE FROM mal_cache WHERE expires_at < ?",[e]),t?.rows??0}function N(){return{get:e=>f(e),put:(e,t,i)=>O(e,t,i)}}export{s as MAL_CACHE_MAX_ROWS,f as malCacheGet,d as malCachePruneExpired,O as malCachePut,N as malCacheStore};
@@ -1,51 +0,0 @@
1
- import{eq as l,inArray as p,sql as n}from"drizzle-orm";import{getDrizzle as e}from"../db/drizzle.js";import{mangaLibrary as i,mangaProgress as s}from"../db/schema-manga.js";import{makeLibraryRepo as o}from"./library.repository.js";const I=["Reading","Completed","On-Hold","Dropped","Planned"];function L(a,t){return`${a.toLowerCase()}:${t}`}const D={...o("manga_library"),upsert:(a,t)=>e(a).run(n`
2
- INSERT INTO manga_library (id, provider, mangaId, title, cover, status, author, altTitle, contentRating, anilistId, anilistIdSource, updatedAt)
3
- VALUES (${t.id}, ${t.provider}, ${t.mangaId}, ${t.title}, ${t.cover}, ${t.status}, ${t.author||null}, ${t.altTitle||null}, ${t.contentRating||null}, ${t.anilistId??null}, ${t.anilistIdSource??null}, strftime('%s', 'now'))
4
- ON CONFLICT(id) DO UPDATE SET
5
- provider = COALESCE(NULLIF(EXCLUDED.provider, ''), manga_library.provider),
6
- title = COALESCE(NULLIF(EXCLUDED.title, ''), manga_library.title),
7
- cover = COALESCE(NULLIF(EXCLUDED.cover, ''), manga_library.cover),
8
- status = COALESCE(NULLIF(EXCLUDED.status, ''), manga_library.status),
9
- author = COALESCE(NULLIF(EXCLUDED.author, ''), manga_library.author),
10
- altTitle = COALESCE(NULLIF(EXCLUDED.altTitle, ''), manga_library.altTitle),
11
- contentRating = COALESCE(EXCLUDED.contentRating, manga_library.contentRating),
12
- anilistId = COALESCE(EXCLUDED.anilistId, manga_library.anilistId),
13
- anilistIdSource = COALESCE(EXCLUDED.anilistIdSource, manga_library.anilistIdSource),
14
- updatedAt = strftime('%s', 'now')`),setAnilistId:(a,t,r,g)=>g?e(a).update(i).set({anilistId:r,anilistIdSource:g}).where(l(i.id,t)):e(a).update(i).set({anilistId:r}).where(l(i.id,t)),touchProgress:(a,t,r)=>e(a).run(n`
15
- UPDATE manga_library SET lastChapterId = ${r.chapterId}, lastChapterNumber = ${r.chapterNumber}, lastPage = ${r.page}, updatedAt = strftime('%s', 'now') WHERE id = ${t}`),setProgressPointer:(a,t,r)=>e(a).run(n`
16
- UPDATE manga_library SET lastChapterNumber = ${r}, updatedAt = strftime('%s', 'now') WHERE id = ${t}`)},b={getByManga:(a,t)=>e(a).all(n`
17
- SELECT mangaId, chapterId, chapterNumber, page, pageCount, updatedAt FROM manga_progress WHERE mangaId = ${t} ORDER BY updatedAt DESC`),getChapter:async(a,t,r)=>(await e(a).all(n`
18
- SELECT mangaId, chapterId, chapterNumber, page, pageCount, updatedAt FROM manga_progress WHERE mangaId = ${t} AND chapterId = ${r}`))[0],getLatest:async(a,t)=>(await e(a).all(n`
19
- SELECT mangaId, chapterId, chapterNumber, page, pageCount, updatedAt FROM manga_progress WHERE mangaId = ${t} ORDER BY updatedAt DESC LIMIT 1`))[0],upsert:(a,t)=>e(a).run(n`
20
- INSERT INTO manga_progress (mangaId, chapterId, chapterNumber, page, pageCount, title, cover, provider, altTitle, contentRating, updatedAt)
21
- VALUES (${t.mangaId}, ${t.chapterId}, ${t.chapterNumber}, ${t.page}, ${t.pageCount}, ${t.title??null}, ${t.cover??null}, ${t.provider??null}, ${t.altTitle??null}, ${t.contentRating??null}, strftime('%s', 'now'))
22
- ON CONFLICT(mangaId, chapterId) DO UPDATE SET
23
- chapterNumber = COALESCE(NULLIF(EXCLUDED.chapterNumber, ''), manga_progress.chapterNumber),
24
- page = EXCLUDED.page,
25
- pageCount = EXCLUDED.pageCount,
26
- title = COALESCE(EXCLUDED.title, manga_progress.title),
27
- cover = COALESCE(EXCLUDED.cover, manga_progress.cover),
28
- provider = COALESCE(EXCLUDED.provider, manga_progress.provider),
29
- altTitle = COALESCE(EXCLUDED.altTitle, manga_progress.altTitle),
30
- contentRating = COALESCE(EXCLUDED.contentRating, manga_progress.contentRating),
31
- updatedAt = strftime('%s', 'now')`),deleteByManga:(a,t)=>e(a).delete(s).where(l(s.mangaId,t)),deleteMany:(a,t)=>t.length===0?Promise.resolve():e(a).delete(s).where(p(s.mangaId,t)),deleteChapter:(a,t,r)=>e(a).run(n`
32
- DELETE FROM manga_progress WHERE mangaId = ${t} AND chapterId = ${r}`),deleteSyntheticChapters:(a,t,r)=>e(a).run(n`
33
- DELETE FROM manga_progress WHERE mangaId = ${t} AND chapterId LIKE 'anilist:ch:%' AND chapterId != ${r}`),moveSyntheticChapters:(a,t,r)=>e(a).run(n`
34
- UPDATE manga_progress SET mangaId = ${r}, updatedAt = strftime('%s', 'now') WHERE mangaId = ${t} AND chapterId LIKE 'anilist:ch:%'`),getContinueReading:(a,t)=>{const r=typeof t=="number"?`LIMIT ${t}`:"";return e(a).all(n`
35
- SELECT p.mangaId as id,
36
- COALESCE(l.mangaId, SUBSTR(p.mangaId, INSTR(p.mangaId, ':') + 1)) as mangaId,
37
- COALESCE(l.provider, p.provider) as provider,
38
- COALESCE(l.title, p.title) as title, COALESCE(l.cover, p.cover) as cover,
39
- COALESCE(l.author, '') as author, COALESCE(l.altTitle, p.altTitle) as altTitle,
40
- COALESCE(l.contentRating, p.contentRating) as contentRating,
41
- l.status as watchlistStatus,
42
- p.chapterId, p.chapterNumber, p.page, p.pageCount, p.updatedAt as progressAt
43
- FROM (
44
- SELECT *, ROW_NUMBER() OVER (PARTITION BY mangaId ORDER BY updatedAt DESC) as rn
45
- FROM manga_progress
46
- ) p
47
- LEFT JOIN manga_library l ON p.mangaId = l.id
48
- WHERE p.rn = 1
49
- AND (l.status IS NULL OR l.status = 'Reading')
50
- ORDER BY p.updatedAt DESC
51
- ${n.raw(r)}`)}};export{I as MANGA_STATUSES,D as MangaLibraryRepository,b as MangaProgressRepository,L as buildMangaId};
@@ -1,14 +0,0 @@
1
- import{and as n,eq as t,sql as o}from"drizzle-orm";import{getDrizzle as i}from"../db/drizzle.js";import{discoveredNotifications as d,dismissedNotifications as a}from"../db/schema-anime.js";const N={getDismissedByShow:(e,s)=>i(e).all(o`SELECT episodeNumber FROM dismissed_notifications WHERE showId = ${s}`),getDiscoveredByShow:(e,s)=>i(e).all(o`SELECT episodeNumber FROM discovered_notifications WHERE showId = ${s}`),addDiscovered:(e,s,r)=>i(e).insert(d).values({showId:s,episodeNumber:r}).onConflictDoNothing(),addDismissed:(e,s,r)=>i(e).insert(a).values({showId:s,episodeNumber:r}).onConflictDoNothing(),dismissFromDiscovered:(e,s)=>s?i(e).run(o`
2
- INSERT OR IGNORE INTO dismissed_notifications (showId, episodeNumber) SELECT showId, episodeNumber FROM discovered_notifications WHERE showId = ${s}`):i(e).run(o`
3
- INSERT OR IGNORE INTO dismissed_notifications (showId, episodeNumber) SELECT showId, episodeNumber FROM discovered_notifications`),deleteByShow:(e,s)=>Promise.all([i(e).delete(a).where(t(a.showId,s)),i(e).delete(d).where(t(d.showId,s))]),deleteSpecificDismissed:(e,s,r)=>i(e).delete(a).where(n(t(a.showId,s),t(a.episodeNumber,r))),deleteDiscovered:(e,s,r)=>i(e).delete(d).where(n(t(d.showId,s),t(d.episodeNumber,r))),cleanupWatchedNotifications:e=>Promise.all([i(e).run(o`
4
- DELETE FROM dismissed_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = dismissed_notifications.showId AND we.episodeNumber = dismissed_notifications.episodeNumber)`),i(e).run(o`
5
- DELETE FROM discovered_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = discovered_notifications.showId AND we.episodeNumber = discovered_notifications.episodeNumber)`)]),hasAnyDiscovered:async e=>((await i(e).all(o`SELECT COUNT(*) as count FROM discovered_notifications`))[0]?.count||0)>0,hasActiveNotifications:async e=>((await i(e).all(o`
6
- SELECT COUNT(*) as count FROM discovered_notifications dn
7
- WHERE NOT EXISTS (
8
- SELECT 1 FROM dismissed_notifications dn2
9
- WHERE dn2.showId = dn.showId AND dn2.episodeNumber = dn.episodeNumber
10
- )
11
- AND NOT EXISTS (
12
- SELECT 1 FROM watched_episodes we
13
- WHERE we.showId = dn.showId AND we.episodeNumber = dn.episodeNumber
14
- )`))[0]?.count||0)>0};export{N as NotificationsRepository};
@@ -1,19 +0,0 @@
1
- import{and as n,eq as o,sql as u}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";import{queue as i}from"../db/schema-anime.js";const d={getAll:e=>s(e).all(u`
2
- SELECT
3
- q.id,
4
- q.showId,
5
- q.episodeNumber,
6
- q.queue_order,
7
- COALESCE(NULLIF(sm.name, ''), w.name) as name,
8
- COALESCE(NULLIF(sm.thumbnail, ''), NULLIF(w.thumbnail, ''), '') as thumbnail,
9
- COALESCE(NULLIF(sm.nativeName, ''), w.nativeName) as nativeName,
10
- COALESCE(NULLIF(sm.englishName, ''), w.englishName) as englishName,
11
- COALESCE(NULLIF(sm.type, ''), w.type) as type
12
- FROM queue q
13
- LEFT JOIN shows_meta sm ON q.showId = sm.id
14
- LEFT JOIN watchlist w ON q.showId = w.id
15
- ORDER BY q.queue_order ASC, q.id ASC`),getByEpisode:async(e,a,r)=>(await s(e).all(u`SELECT * FROM queue WHERE showId = ${a} AND episodeNumber = ${r}`))[0],getByShow:(e,a)=>s(e).all(u`SELECT * FROM queue WHERE showId = ${a} ORDER BY queue_order ASC`),getMaxOrder:async e=>(await s(e).all(u`SELECT COALESCE(MAX(queue_order), -1) as maxOrder FROM queue`))[0]?.maxOrder??-1,addToEnd:(e,a,r)=>s(e).run(u`
16
- INSERT INTO queue (showId, episodeNumber, queue_order) VALUES
17
- (${a}, ${r}, (SELECT COALESCE(MAX(queue_order), -1) + 1 FROM queue))`),removeEpisode:(e,a,r)=>s(e).delete(i).where(n(o(i.showId,a),o(i.episodeNumber,r))),addManyToEnd:async(e,a)=>{if(a.length===0)return;let r=await d.getMaxOrder(e)+1;for(const t of a)await d.getByEpisode(e,t.showId,t.episodeNumber)||(await s(e).insert(i).values({showId:t.showId,episodeNumber:t.episodeNumber,queueOrder:r}),r+=1)},removeMany:(e,a,r)=>r.length===0?Promise.resolve():s(e).run(u`
18
- DELETE FROM queue WHERE showId = ${a} AND episodeNumber IN (${u.join(r.map(t=>u`${t}`),u`, `)})`),clear:e=>s(e).delete(i),reorder:(e,a)=>Promise.all(a.map((r,t)=>r.id!==void 0?s(e).update(i).set({queueOrder:t}).where(o(i.id,r.id)):s(e).update(i).set({queueOrder:t}).where(n(o(i.showId,r.showId??""),o(i.episodeNumber,r.episodeNumber??""))))),cleanupOrphanedShowsMeta:e=>s(e).run(u`
19
- DELETE FROM shows_meta WHERE id NOT IN (SELECT id FROM watchlist) AND id NOT IN (SELECT showId FROM queue)`)};export{d as QueueRepository};
@@ -1,3 +0,0 @@
1
- import{eq as g,sql as r}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";import{settings as n,watchlist as p}from"../db/schema-anime.js";const o={getByKey:async(t,e)=>(await s(t).all(r`SELECT value FROM settings WHERE key = ${e}`))[0],upsert:(t,e,a)=>s(t).insert(n).values({key:e,value:a}).onConflictDoUpdate({target:n.key,set:{value:a}}),deleteByKey:(t,e)=>s(t).delete(n).where(g(n.key,e)),readContinueFlags:async(t,e,a)=>{const l=await o.getByKey(t,e),i=await o.getByKey(t,a);return{ignoreAdult:l?l.value!=="false":!0,listOnly:i?i.value==="true"||i.value==="1":!1}},clearWatchlist:t=>s(t).delete(p),upsertWatchlistBatch:(t,e)=>e.length===0?Promise.resolve():s(t).run(r`
2
- INSERT OR REPLACE INTO watchlist (id, name, thumbnail, status) VALUES
3
- ${r.join(e.map(a=>r`(${a.id}, ${a.name}, ${a.thumbnail??null}, ${a.status})`),r`, `)}`)};export{o as SettingsRepository};
@@ -1,5 +0,0 @@
1
- import{eq as r,sql as i}from"drizzle-orm";import{getDrizzle as a}from"../db/drizzle.js";import{showsMeta as s}from"../db/schema-anime.js";const E={getById:async(n,e)=>(await a(n).all(i`SELECT * FROM shows_meta WHERE id = ${e}`))[0],getStatus:async(n,e)=>(await a(n).all(i`SELECT status FROM shows_meta WHERE id = ${e}`))[0]?.status,upsert:(n,e)=>{const t={id:e.id,name:e.name??null,thumbnail:e.thumbnail??null,nativeName:e.nativeName??null,englishName:e.englishName??null,genres:e.genres??null,popularityScore:e.popularityScore??null,status:e.status??null,episodeCount:e.episodeCount??null,type:e.type??null,anilistId:e.anilistId??null,isAdult:e.isAdult??null,episodeDuration:e.episodeDuration??null};return a(n).insert(s).values(t).onConflictDoUpdate({target:s.id,set:{name:i`COALESCE(NULLIF(${t.name}, ''), ${s.name})`,thumbnail:i`COALESCE(NULLIF(${t.thumbnail}, ''), ${s.thumbnail})`,nativeName:i`COALESCE(NULLIF(${t.nativeName}, ''), ${s.nativeName})`,englishName:i`COALESCE(NULLIF(${t.englishName}, ''), ${s.englishName})`,genres:i`COALESCE(NULLIF(${t.genres}, ''), ${s.genres})`,popularityScore:i`COALESCE(${t.popularityScore}, ${s.popularityScore})`,status:i`COALESCE(NULLIF(${t.status}, ''), ${s.status})`,episodeCount:i`COALESCE(${t.episodeCount}, ${s.episodeCount})`,type:i`COALESCE(NULLIF(${t.type}, ''), ${s.type})`,anilistId:i`COALESCE(${t.anilistId}, ${s.anilistId})`,isAdult:i`COALESCE(${t.isAdult}, ${s.isAdult})`,episodeDuration:i`COALESCE(NULLIF(${t.episodeDuration}, 0), ${s.episodeDuration})`}})},updateEpisodeCount:(n,e,t)=>a(n).update(s).set({episodeCount:t}).where(r(s.id,e)),updateStatus:(n,e,t)=>a(n).update(s).set({status:t}).where(r(s.id,e)),cleanupOrphanedMeta:n=>a(n).run(i`
2
- DELETE FROM shows_meta WHERE id NOT IN (SELECT id FROM watchlist) AND id NOT IN (SELECT showId FROM queue)`),getShowIdsMissingMeta:n=>a(n).all(i`
3
- SELECT DISTINCT we.showId as id FROM watched_episodes we
4
- LEFT JOIN shows_meta sm ON sm.id = we.showId
5
- WHERE sm.thumbnail IS NULL OR TRIM(sm.thumbnail) = ''`)};export{E as ShowsMetaRepository};
@@ -1,3 +0,0 @@
1
- import{sql as a}from"drizzle-orm";import{getDrizzle as i}from"../db/drizzle.js";import{tempShowIds as p}from"../db/schema-anime.js";import{TEMP_SHOW_ID_PREFIX as o}from"../lib/temp-ids.js";const n={getById:async(t,e)=>(await i(t).all(a`SELECT * FROM temp_show_ids WHERE id = ${e}`))[0],getByProviderNative:async(t,e,r)=>(await i(t).all(a`SELECT * FROM temp_show_ids WHERE provider = ${e} AND nativeId = ${r}`))[0],count:async t=>(await i(t).all(a`SELECT COUNT(*) as total FROM temp_show_ids`))[0]?.total||0,allocate:async(t,e)=>{const r=await n.getByProviderNative(t,e.provider,e.nativeId);if(r)return r;const s=o.length+1,m=((await i(t).all(a`
2
- SELECT MAX(CAST(SUBSTR(id, ${s}) AS INTEGER)) as maxId FROM temp_show_ids WHERE id LIKE ${`${o}%`}`))[0]?.maxId||0)+1,E=`${o}${m}`;try{await i(t).insert(p).values({id:E,provider:e.provider,nativeId:e.nativeId,title:e.title,thumbnail:e.thumbnail||"",createdAt:Date.now()})}catch{const w=await n.getByProviderNative(t,e.provider,e.nativeId);if(w)return w;throw new Error("temp id allocation failed")}return await n.getById(t,E)},purge:async t=>{const e=await n.count(t);if(e===0)return 0;const r=`${o}%`;return await i(t).run(a`DELETE FROM watchlist WHERE id LIKE ${r}`),await i(t).run(a`DELETE FROM watched_episodes WHERE showId LIKE ${r}`),await i(t).run(a`DELETE FROM queue WHERE showId LIKE ${r}`),await i(t).run(a`DELETE FROM shows_meta WHERE id LIKE ${r}`),await i(t).run(a`DELETE FROM dismissed_notifications WHERE showId LIKE ${r}`),await i(t).run(a`DELETE FROM discovered_notifications WHERE showId LIKE ${r}`),await i(t).run(a`
3
- DELETE FROM legacy_id_mapping WHERE legacyId LIKE ${r} OR numericId LIKE ${r}`),await i(t).delete(p),e}};export{n as TempShowIdsRepository};
@@ -1,50 +0,0 @@
1
- import{eq as n,inArray as d,sql as i}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";import{tvProgress as a}from"../db/schema-tv.js";import{makeLibraryRepo as p}from"./library.repository.js";const g=["Watching","Completed","On-Hold","Dropped","Planned"];function v(r,e){return`${String(r).toLowerCase()==="movie"?"movie":"tv"}:${e}`}function D(r){const e=String(r??"").trim(),t=/^(movie|tv)[:-](\d+)$/i.exec(e);return t?`${t[1].toLowerCase()}:${t[2]}`:e}const T={...p("tv_library"),upsert:(r,e)=>s(r).run(i`
2
- INSERT INTO tv_library (id, tmdbId, mediaType, title, poster, backdrop, year, overview, status, adult, updatedAt)
3
- VALUES (${e.id}, ${e.tmdbId}, ${e.mediaType}, ${e.title}, ${e.poster}, ${e.backdrop||null}, ${e.year||null}, ${e.overview||null}, ${e.status}, ${e.adult?1:0}, strftime('%s', 'now'))
4
- ON CONFLICT(id) DO UPDATE SET
5
- tmdbId = EXCLUDED.tmdbId,
6
- mediaType = EXCLUDED.mediaType,
7
- title = COALESCE(NULLIF(EXCLUDED.title, ''), tv_library.title),
8
- poster = COALESCE(NULLIF(EXCLUDED.poster, ''), tv_library.poster),
9
- backdrop = COALESCE(NULLIF(EXCLUDED.backdrop, ''), tv_library.backdrop),
10
- year = COALESCE(NULLIF(EXCLUDED.year, ''), tv_library.year),
11
- overview = COALESCE(NULLIF(EXCLUDED.overview, ''), tv_library.overview),
12
- status = COALESCE(NULLIF(EXCLUDED.status, ''), tv_library.status),
13
- adult = COALESCE(EXCLUDED.adult, tv_library.adult),
14
- updatedAt = strftime('%s', 'now')`),touchProgress:(r,e,t)=>s(r).run(i`
15
- UPDATE tv_library SET lastSeason = ${t.season}, lastEpisode = ${t.episode}, updatedAt = strftime('%s', 'now') WHERE id = ${e}`)},I={getByMedia:(r,e)=>s(r).all(i`
16
- SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${e} ORDER BY updatedAt DESC, rowid DESC`),getEpisode:async(r,e,t,o)=>(await s(r).all(i`
17
- SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${e} AND season = ${t} AND episode = ${o}`))[0],getLatest:async(r,e)=>(await s(r).all(i`
18
- SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${e} ORDER BY updatedAt DESC, rowid DESC LIMIT 1`))[0],upsert:(r,e)=>s(r).run(i`
19
- INSERT INTO tv_progress (mediaId, season, episode, currentTime, duration, completed, title, poster, backdrop, year, overview, tmdbId, mediaType, adult, updatedAt)
20
- VALUES (${e.mediaId}, ${e.season}, ${e.episode}, ${e.currentTime}, ${e.duration}, ${e.completed}, ${e.title??null}, ${e.poster??null}, ${e.backdrop??null}, ${e.year??null}, ${e.overview??null}, ${e.tmdbId??null}, ${e.mediaType??null}, ${e.adult??null}, strftime('%s', 'now'))
21
- ON CONFLICT(mediaId, season, episode) DO UPDATE SET
22
- currentTime = EXCLUDED.currentTime,
23
- duration = EXCLUDED.duration,
24
- completed = EXCLUDED.completed,
25
- title = COALESCE(EXCLUDED.title, tv_progress.title),
26
- poster = COALESCE(EXCLUDED.poster, tv_progress.poster),
27
- backdrop = COALESCE(EXCLUDED.backdrop, tv_progress.backdrop),
28
- year = COALESCE(EXCLUDED.year, tv_progress.year),
29
- overview = COALESCE(EXCLUDED.overview, tv_progress.overview),
30
- tmdbId = COALESCE(EXCLUDED.tmdbId, tv_progress.tmdbId),
31
- mediaType = COALESCE(EXCLUDED.mediaType, tv_progress.mediaType),
32
- adult = COALESCE(EXCLUDED.adult, tv_progress.adult),
33
- updatedAt = strftime('%s', 'now')`),deleteByMedia:(r,e)=>s(r).delete(a).where(n(a.mediaId,e)),deleteMany:(r,e)=>e.length===0?Promise.resolve():s(r).delete(a).where(d(a.mediaId,e)),deleteEpisode:(r,e,t,o)=>s(r).run(i`
34
- DELETE FROM tv_progress WHERE mediaId = ${e} AND season = ${t} AND episode = ${o}`),getContinueWatching:(r,e)=>{const t=typeof e=="number"?`LIMIT ${e}`:"";return s(r).all(i`
35
- SELECT p.mediaId as id,
36
- COALESCE(l.tmdbId, p.tmdbId, CAST(SUBSTR(REPLACE(p.mediaId, '-', ':'), INSTR(REPLACE(p.mediaId, '-', ':'), ':') + 1) AS INTEGER)) as tmdbId,
37
- COALESCE(l.mediaType, p.mediaType, SUBSTR(REPLACE(p.mediaId, '-', ':'), 1, INSTR(REPLACE(p.mediaId, '-', ':'), ':') - 1)) as mediaType,
38
- COALESCE(l.title, p.title) as title, COALESCE(l.poster, p.poster) as poster, COALESCE(l.backdrop, p.backdrop) as backdrop,
39
- COALESCE(l.year, p.year) as year, COALESCE(l.overview, p.overview) as overview,
40
- l.status as watchlistStatus, COALESCE(l.adult, p.adult) as adult,
41
- p.season, p.episode, p.currentTime, p.duration, p.completed, p.updatedAt as progressAt
42
- FROM (
43
- SELECT *, rowid AS progressRowid, ROW_NUMBER() OVER (PARTITION BY mediaId ORDER BY updatedAt DESC, rowid DESC) as rn
44
- FROM tv_progress
45
- ) p
46
- LEFT JOIN tv_library l ON p.mediaId = l.id
47
- WHERE p.rn = 1
48
- AND (l.status IS NULL OR l.status = 'Watching')
49
- ORDER BY p.updatedAt DESC, p.progressRowid DESC
50
- ${i.raw(t)}`)}};export{g as TV_STATUSES,T as TvLibraryRepository,I as TvProgressRepository,v as buildTvId,D as normalizeTvMediaId};