@serifpersia/dango 3.6.8 → 3.6.9

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 +1 -1
  2. package/client/dist/assets/{AnimeCard-DraA_TF9.js → AnimeCard-DKDJK8Mp.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-BJ7pzs-P.js → AnimeInfo-CTcvfjva.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-B_esUr5C.js → AnimeInfoPage-B_UiacIN.js} +2 -2
  5. package/client/dist/assets/{Asmr-Ckuh2xfW.js → Asmr-DMSN2vxl.js} +2 -2
  6. package/client/dist/assets/{AsmrResetProgressModal-BS6UFJN4.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-DwHCd9yB.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
  8. package/client/dist/assets/{Home-DuM1EuvN.js → Home-pwmK9_Eh.js} +1 -1
  9. package/client/dist/assets/{Insights-D3Pd9c9V.js → Insights-m7kFzg2A.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-Dm2yVn0-.js → LibraryListPage-C1wpPvu6.js} +1 -1
  11. package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
  12. package/client/dist/assets/{Manga-JwprMKBk.js → Manga-ChRCExhK.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-DaeyJl2p.js → MangaInfoPage-DSHJPo2A.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-COg0fHI9.js → MangaReadPage-CzOVY1wA.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-kguXv0_M.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
  16. package/client/dist/assets/{Mature-IHDaasrs.js → Mature-CifQMfhs.js} +1 -1
  17. package/client/dist/assets/{MediaCard-CpwsrXo5.js → MediaCard-CM10yB9V.js} +1 -1
  18. package/client/dist/assets/{OptionListMenu-11CcAsy8.js → OptionListMenu-6Kovbsqw.js} +1 -1
  19. package/client/dist/assets/{Player-DQoEQkHL.js → Player-CK0hT-u4.js} +2 -2
  20. package/client/dist/assets/{PlayerSettings-D4Ud4kv4.js → PlayerSettings-Bb66uqT9.js} +1 -1
  21. package/client/dist/assets/{QueueOptionsButton-B8PlPKjC.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
  22. package/client/dist/assets/{QueueRail-B4Dok7U3.js → QueueRail-DUK7URAE.js} +1 -1
  23. package/client/dist/assets/{ReadingList-BCsxIz57.js → ReadingList-Ck9LJThU.js} +1 -1
  24. package/client/dist/assets/{ResetProgressModal-BXHxSfy-.js → ResetProgressModal-BUphIv0k.js} +1 -1
  25. package/client/dist/assets/{Search-PK1ds07O.js → Search-zJyP15h6.js} +1 -1
  26. package/client/dist/assets/{Settings-Bxk_x55l.js → Settings-BYdoSEUh.js} +1 -1
  27. package/client/dist/assets/{SkeletonGrid-CpJCZSRN.js → SkeletonGrid-CqgbOhjX.js} +1 -1
  28. package/client/dist/assets/{Trackers-RieCh_84.js → Trackers-BVjyv6CM.js} +1 -1
  29. package/client/dist/assets/{Tv-DLytPJUj.js → Tv-L13ylswR.js} +1 -1
  30. package/client/dist/assets/{TvInfo-C8djKebR.js → TvInfo-Cvcm5U2D.js} +1 -1
  31. package/client/dist/assets/{TvPopup-40JGGJDk.js → TvPopup-DhxfKvYm.js} +1 -1
  32. package/client/dist/assets/{TvSearch-A3hbospf.js → TvSearch-P8bFE6_X.js} +1 -1
  33. package/client/dist/assets/{TvWatchlist-DuXjqmZM.js → TvWatchlist-D5gVPjtb.js} +1 -1
  34. package/client/dist/assets/{Watchlist-B9wGBh0u.js → Watchlist-Cjq8l3rU.js} +1 -1
  35. package/client/dist/assets/{hls-CZ5X3Ya-.js → hls-BkzuANAg.js} +1 -1
  36. package/client/dist/assets/{index-dRhc9-ol.js → index-CLVgzKLk.js} +19 -19
  37. package/client/dist/assets/{useAnimeInfoData-ZzDtLfvD.js → useAnimeInfoData-DZJZu-so.js} +1 -1
  38. package/client/dist/assets/{useAsmr-CKOdHgol.js → useAsmr-CvrMGHuo.js} +1 -1
  39. package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
  40. package/client/dist/assets/{useMangaLibrary-CKATe31P.js → useMangaLibrary--MiN11uR.js} +1 -1
  41. package/client/dist/assets/{useMangaPopup-C75QuYgY.js → useMangaPopup-CR5XhKKS.js} +1 -1
  42. package/client/dist/assets/{useProviders-CMtvZbEi.js → useProviders-DqdT4fA5.js} +1 -1
  43. package/client/dist/assets/{useTv-DUX4zYMr.js → useTv-BL0HMDyP.js} +1 -1
  44. package/client/dist/assets/{useTvLibrary-By4Gt-R_.js → useTvLibrary-D4U4iqN8.js} +1 -1
  45. package/client/dist/index.html +1 -1
  46. package/package.json +3 -5
  47. package/server/dist/server.js +1089 -1
  48. package/server/package.json +2 -4
  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,5 +0,0 @@
1
- import x from"../logger.js";import{fetchWithRetry as j,linkAbort as E,setProxyHeaders as W}from"../utils/http.utils.js";import{URL as R}from"url";import{isSafeExternalUrl as $}from"../utils/security.utils.js";function S(o,u,e){const t=e?`&provider=${encodeURIComponent(e)}`:"";return`/api/tv/stream-proxy?url=${encodeURIComponent(o)}&referer=${encodeURIComponent(u)}${t}`}function A(o,u,e,t){return o.split(`
2
- `).map(s=>{const n=s.trim();if(!n)return s;if(n.startsWith("#"))return n.replace(/URI="([^"]+)"/g,(i,p)=>{const h=new R(p,u).href;return`URI="${S(h,e,t)}"`});const r=new R(n,u).href;return S(r,e,t)}).join(`
3
- `)}function H(o,u){const e=o.split(`
4
- `);let t=-1,s=!1;const n=[];for(const r of e){const i=r.trim();if(i.startsWith("#EXT-X-STREAM-INF")){t++,s=t!==u,s||n.push(r);continue}if(s&&i&&!i.startsWith("#")){s=!1;continue}n.push(r)}return t>=0&&u<=t?n.join(`
5
- `):o}function N(o){return o.length>=7&&o.subarray(0,32).toString("utf8").replace(/^\uFEFF/,"").trimStart().startsWith("#EXTM3U")}function V(o,u){o.get("/api/tv/embed/:provider/:type/:tmdbId",async e=>{const t=e.req.param("provider"),s=e.req.param("type"),n=e.req.param("tmdbId"),r=s==="movie"?"movie":"tv",i=parseInt(n,10);if(!i)return e.json({url:null,error:"Bad tmdb id"});const p=u(String(t).toLowerCase());if(!p||typeof p.getEmbedUrl!="function")return e.json({url:null,error:`Unknown TV provider ${t}`});const h={tmdbId:i,type:r,season:parseInt(String(e.req.query("season")||"1"),10)||1,episode:parseInt(String(e.req.query("episode")||"1"),10)||1,title:"",year:"",imdbId:"",totalSeasons:1};try{const m=await p.getEmbedUrl(h);return m?e.json({url:m}):e.json({url:null,error:"No embed url"})}catch(m){return e.json({url:null,error:m.message})}}),o.get("/api/tv/stream-proxy",async e=>{const t=e.req.query("url")??"",s=t.match(/#v=(\d+)$/),n=s?parseInt(s[1],10):NaN,r=t.replace(/#.*$/,""),i=e.req.query("referer")||"",p=(e.req.query("provider")||"").toLowerCase();if(!r)return e.text("URL required",400);const h=p?u(p):void 0,m=(g,l)=>{const f=h?.rewritePlaylist?.(g,l.href)??g,U=Number.isInteger(n)?H(f,n):f;return A(U,l,i,p)},v=$(r);if(!v.safe)return e.text(v.error||"Invalid URL",400);const b=new AbortController,B=setTimeout(()=>b.abort(),3e4);E(e.req.raw.signal,b);try{const g={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"};i&&(g.Referer=i);const l=await j(r,{headers:g,redirect:"follow"},{retries:3,baseDelayMs:750,signal:b.signal}),f=l.status;if(f!==200&&f!==206)return e.text("Upstream error",f||502);const U=l.headers.get("content-type")||"application/octet-stream",d=new Headers;if(d.set("Content-Type",U),W(d,c=>l.headers.get(c)),d.set("Connection","keep-alive"),r.includes(".m3u8")||/mpegurl|m3u8/i.test(U)){const c=await l.text(),y=new R(l.url||r),q=m(c,y),a=Buffer.from(q,"utf8");return d.set("Content-Type","application/vnd.apple.mpegurl"),d.set("Content-Length",String(a.length)),new Response(a,{status:f,headers:d})}const w=l.body?.getReader();if(!w)return x.warn({host:v.url?.hostname},"tv stream-proxy: upstream sent no body"),e.text("Upstream sent no body",502);try{const c=await w.read(),y=c.done?Buffer.alloc(0):Buffer.from(c.value);if(N(y)){const a=[y];for(;;){const{done:M,value:k}=await w.read();if(M)break;a.push(Buffer.from(k))}const I=Buffer.concat(a),T=new R(l.url||r),L=m(I.toString("utf8"),T),C=Buffer.from(L,"utf8");return d.set("Content-Type","application/vnd.apple.mpegurl"),d.set("Content-Length",String(C.length)),new Response(C,{status:f,headers:d})}const q=new ReadableStream({start(a){y.length&&a.enqueue(y)},async pull(a){const{done:I,value:T}=await w.read();I?a.close():a.enqueue(T)},cancel(a){return w.cancel(a)}});return new Response(q,{status:f,headers:d})}catch(c){return x.warn({err:c,host:v.url?.hostname},"tv stream-proxy: upstream body stream failed"),e.text("Upstream stream failed",502)}}catch(g){return b.signal.aborted?new Response(null,{status:499}):(x.warn({err:g,host:v.url?.hostname},"tv stream-proxy: upstream fetch failed"),e.text("Upstream fetch failed",502))}finally{clearTimeout(B)}})}export{V as registerTv};
@@ -1,5 +0,0 @@
1
- import H from"../logger.js";import{WatchlistRepository as et}from"../repositories/watchlist.repository.js";import{WatchedEpisodesRepository as j}from"../repositories/watched-episodes.repository.js";import{ShowsMetaRepository as k}from"../repositories/shows-meta.repository.js";import{NotificationsRepository as b}from"../repositories/notifications.repository.js";import{dbAll as st}from"../utils/db-utils.js";import{AppCache as F}from"../utils/cache.utils.js";import{getAiredEpisodesForShows as J,isAnilistRateLimited as nt,batchGetShowStatuses as it}from"../lib/anilist.js";import{parseJsonBody as z}from"../utils/http.utils.js";import{pickBestMatch as ot}from"../providers/title-matching.js";const rt=300*1e3,at=120*1e3,ct=300*1e3;let K=null,G=0,y=!1,V=0,N="idle",R=0,U=0,P=!1,X=null;function St(){return X?.()??!1}function It(){P=!0,K!==null&&(clearInterval(K),K=null)}function At(){return{running:y,state:N,total:R,done:Math.min(U,R),lastRunAt:V}}function Dt(g){const n=new Map,w=async o=>{if(n.has(o))return n.get(o)||null;const s=g(),l=await k.getById(s,o);if(l?.anilistId)return n.set(o,l.anilistId),l.anilistId;if(/^\d+$/.test(o)){const d=parseInt(o);return n.set(o,d),d}return n.set(o,null),null},h=async(o=!1)=>{if(y||P)return;y=!0,N="running",R=0,U=0;const s=g();if(!s||s.isClosedCheck()){y=!1;return}const l=Date.now(),d=o?9e4:ct;try{const i=await et.getWatchingShows(s);if(R=i.length,i.length===0){y=!1;return}const r=new Map,E=o?4:2,D=[];let S=0;const M=async()=>{for(;S<i.length;){if(Date.now()-l>d)return;const t=i[S];S+=1;const e=await w(t.id);U+=1,D.push({show:t,id:e})}};await Promise.all(Array.from({length:Math.min(E,i.length)},()=>M()));for(const{show:t,id:e}of D)e&&r.set(t.id,e);if(Date.now()-l>d||r.size===0){y=!1;return}const m=new Date,A=new Date(m);A.setDate(m.getDate()-7),A.setHours(0,0,0,0);const $=new Date(m);$.setHours(23,59,59,999);const Q=await J(Array.from(r.values()),A,$),T=Math.floor(Date.now()/1e3),O=new Map;for(const[t,e]of r.entries())O.set(e,t);const x=new Set,_=[];for(const[t,e]of r.entries()){const a=await k.getById(s,t);a?.status==="FINISHED"?x.add(e):a?.status||_.push({watchlistId:t,anilistId:e})}if(_.length>0&&!nt()){const t=await it(_.map(a=>a.anilistId));let e=0;for(const{watchlistId:a,anilistId:u}of _){const f=t.get(u);f==="FINISHED"?(x.add(u),await k.upsert(s,{id:a,status:"FINISHED"}),e++):f&&(await k.upsert(s,{id:a,status:f}),e++)}}if(x.size>0){const t=new Date(m);t.setDate(m.getDate()-30),t.setHours(0,0,0,0);const e=new Date(m);e.setHours(23,59,59,999);const a=await J(Array.from(x),t,e),u=new Map,f=new Set;for(const p of a){if(p.airingAt>T)continue;const c=O.get(p.mediaId);if(!c)continue;const v=String(Math.round(p.episode)),C=`${c}:${v}`;if(f.has(C))continue;f.add(C);const W=u.get(c);W?W.push({episodeKey:v,airingAt:p.airingAt}):u.set(c,[{episodeKey:v,airingAt:p.airingAt}])}for(const[p,c]of u){const[v,C]=await Promise.all([j.getWatchedEpisodeNumbers(s,p),b.getDismissedByShow(s,p)]),W=new Set(v.map(I=>I.toString())),Z=new Set(C.map(I=>I.episodeNumber.toString()));for(const{episodeKey:I,airingAt:tt}of c)T-tt>720*60*60||!W.has(I)&&!Z.has(I)&&await b.addDiscovered(s,p,I)}}const L=new Map,B=new Set;for(const t of Q){if(t.airingAt>T)continue;const e=O.get(t.mediaId);if(!e)continue;const a=String(Math.round(t.episode)),u=`${e}:${a}`;if(B.has(u))continue;B.add(u);const f=L.get(e);f?f.push({episodeKey:a,airingAt:t.airingAt}):L.set(e,[{episodeKey:a,airingAt:t.airingAt}])}for(const[t,e]of L){const[a,u]=await Promise.all([j.getWatchedEpisodeNumbers(s,t),b.getDismissedByShow(s,t)]),f=new Set(a.map(c=>c.toString())),p=new Set(u.map(c=>c.episodeNumber.toString()));for(const{episodeKey:c}of e)!f.has(c)&&!p.has(c)&&await b.addDiscovered(s,t,c)}await b.cleanupWatchedNotifications(s)}catch(i){N="error",i?.message==="Database is closed"?H.info("Notification discovery stopped: database is closed"):H.error({err:i},"AniList notification discovery failed")}finally{y=!1,V=Date.now(),N==="running"&&(N=R===0?"empty":"complete")}};X=()=>P||y||Date.now()-G<at?!1:(G=Date.now(),h(!0),!0),K=setInterval(()=>{P||h(!1)},rt)}async function vt(g){return(await st(g,`SELECT DISTINCT we.showId as showId
2
- FROM watched_episodes we
3
- JOIN shows_meta sm ON sm.id = we.showId AND sm.isAdult = 1
4
- LEFT JOIN watchlist w ON w.id = we.showId
5
- WHERE w.id IS NULL`)).map(w=>w.showId)}const Y=new F({ttlSeconds:3600,maxKeys:2e3}),q=new F({ttlSeconds:3600,maxKeys:2e3});async function bt(g){const n=g.trim().toLowerCase();if(!n)return null;const w=q.get(n);if(w!==void 0)return w;try{const h=new URLSearchParams({title:g.trim(),limit:"5","includes[]":"cover_art","order[relevance]":"desc"});for(const r of["safe","suggestive","erotica","pornographic"])h.append("contentRating[]",r);const o=await fetch(`https://api.mangadex.org/manga?${h.toString()}`,{headers:{"User-Agent":"Dango/3.0",Accept:"application/json"},signal:AbortSignal.timeout(1e4)});if(!o.ok)return null;const s=await z(o),l=[];for(const r of s.data||[]){const E=r.relationships?.find(m=>m.type==="cover_art")?.attributes?.fileName;if(!E)continue;const D=`https://uploads.mangadex.org/covers/${r.id}/${E}.256.jpg`,S=r.attributes?.title,M=S?.en||S?.["ja-ro"]||Object.values(S||{})[0];M&&l.push({title:M,cover:D});for(const m of r.attributes?.altTitles||[]){const A=Object.values(m)[0];A&&l.push({title:A,cover:D})}}const d=ot(l,[g]),i=d?d.item.cover:null;return q.set(n,i),i}catch{return null}}async function Nt(g){const n=String(g).trim().toUpperCase();if(!/^RJ\d{5,}$/.test(n))return null;const w=Y.get(n);if(w!==void 0)return w;try{const h=await fetch(`https://www.dlsite.com/maniax/product/info/ajax?product_id=${n}`,{headers:{"User-Agent":"Mozilla/5.0",Accept:"application/json"},signal:AbortSignal.timeout(5e3)});if(!h.ok)return null;const s=await z(h),d=s[n]??s,i=typeof d?.work_image=="string"?d.work_image:void 0;if(!i)return null;const r=i.startsWith("//")?`https:${i}`:i;return Y.set(n,r),r}catch{return null}}export{vt as getAdultNonWatchlistShowIds,Nt as getDlsitePoster,bt as getMangadexCover,At as getWatchlistDiscoveryStatus,Dt as startWatchlistDiscovery,It as stopWatchlistDiscovery,St as triggerWatchlistDiscovery};
@@ -1,151 +0,0 @@
1
- import{buildQueryVariants as qe,pickBestMatch as je}from"../providers/title-matching.js";import U from"../logger.js";import{parseJsonBody as Be}from"../utils/http.utils.js";import{findTmdbDefaultBackdrop as Ge}from"./tmdb.js";import{getScheduleFromAniSchedule as Ye,getAiredEpisodesFromAniScheduleFeed as He,withScheduleFields as ce}from"./anischedule.js";import{kitsuSearchAnime as ee,kitsuTrending as ge,kitsuSeasonal as te,kitsuSpotlight as Ve,kitsuLatestReleases as We,kitsuMetaByAnilistId as Ke,kitsuMetaByMalId as me,kitsuEpisodes as fe}from"./kitsu.js";import{malSearchMedia as pe,malSeasonal as he,malTop as k,malLatestReleases as Je,malSearchAnime as Ae,malAnimeDetail as ne,enrichEnglishFromMalDetail as ye,EN_TITLE_CACHE_PREFIX as ze,toAnilistSearchMedia as Qe,toAnilistDetailMedia as Xe}from"./mal.js";import{malCacheStore as S}from"../repositories/mal-cache.repository.js";import{offlineDb as G}from"./offline-db.js";const Se="https://graphql.anilist.co";let D=0,L=!1,be=!1;const ie=600*1e3,Ze=120*1e3,et=30*1e3;function _(){return!!(D>Date.now()||L&&x-Date.now()>et)}function ae(){return L&&x>Date.now()}async function vt(){const e=await tt();return e||(be=!0),e}async function tt(){try{const e=new AbortController,t=setTimeout(()=>e.abort(),5e3),n=await fetch(Se,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({query:"query { __typename }"}),signal:e.signal});clearTimeout(t);const i=n.ok;return i?(D=0,L=!1):(D=Date.now()+ie,L=!1),i}catch{return D=Date.now()+ie,L=!1,!1}}function Nt(){return be}const Me=8,nt=1500;let F=nt,z=Me,re=Date.now(),x=0;function it(){const e=Date.now();if(e<re)return;const t=e-re,n=Math.floor(t/F);n>0&&(z=Math.min(Me,z+n),re+=n*F)}function at(e){const t=a=>{const r=e.get(a);if(r===null||r==="")return null;const s=Number.parseInt(r,10);return Number.isFinite(s)&&s>=0?s:null},n=t("X-RateLimit-Limit"),i=t("X-RateLimit-Remaining");if(n!==null&&n>0){const a=Math.max(1,Math.round(n*.2)),r=Math.max(1,n-a);F=Math.max(300,Math.min(6e4,Math.round(6e4/r)))}if(i!==null&&i<=2){const a=t("X-RateLimit-Reset");if(a!==null){const r=a*1e3;r>Date.now()&&(x=Math.max(x,r))}}}const se=new Map,rt=3600*1e3,oe=new Map,st=3600*1e3,Q=new Map;function Y(){return`
2
- id
3
- idMal
4
- title { romaji english native }
5
- bannerImage
6
- coverImage { extraLarge large medium color }
7
- description
8
- genres
9
- averageScore
10
- meanScore
11
- format
12
- status
13
- episodes
14
- duration
15
- season
16
- seasonYear
17
- startDate { year month day }
18
- endDate { year month day }
19
- countryOfOrigin
20
- isAdult
21
- synonyms
22
- trending
23
- popularity
24
- favourites
25
- tags { id name rank isMediaSpoiler }
26
- studios(isMain: true) { nodes { id name } }
27
- nextAiringEpisode { episode timeUntilAiring airingAt }
28
- trailer { id site thumbnail }
29
- rankings { id rank type format allTime context }
30
- siteUrl
31
- `}function q(){return`
32
- id
33
- title { romaji english native }
34
- coverImage { extraLarge large }
35
- status
36
- averageScore
37
- episodes
38
- seasonYear
39
- season
40
- genres
41
- isAdult
42
- format
43
- nextAiringEpisode { episode }
44
- `}async function v(e,t){for(const n of[e,t])try{const i=await n();if(i&&i.length>0)return i}catch{}return[]}function ot(e){const t=new Set,n=[];for(const i of e)!i.id||t.has(i.id)||(t.add(i.id),n.push(h(i)));return n}async function Ct(e){const t=new Date,n=t.getFullYear(),i=t.getMonth()+1,a=i<=3?"WINTER":i<=6?"SPRING":i<=9?"SUMMER":"FALL",r=e&&e!=="ALL"?e.toUpperCase():void 0,s=r?"format: $seasonFormat,":"",o=`
45
- query ($season: MediaSeason, $seasonYear: Int, $seasonFormat: MediaFormat) {
46
- trending: Page(perPage: 20) {
47
- media(sort: TRENDING_DESC, type: ANIME, isAdult: false, status: RELEASING) {
48
- ${q()}
49
- }
50
- }
51
- seasonal: Page(perPage: 14) {
52
- media(season: $season, seasonYear: $seasonYear, type: ANIME, isAdult: false, ${s} sort: [POPULARITY_DESC, SCORE_DESC]) {
53
- ${q()}
54
- }
55
- }
56
- spotlight: Page(perPage: 20) {
57
- media(sort: TRENDING_DESC, type: ANIME, isAdult: false) {
58
- ${Y()}
59
- }
60
- }
61
- }
62
- `,c=await b(o,{season:a,seasonYear:n,seasonFormat:r});if(!c?.data){if(!_())return{trending:[],seasonal:[],spotlight:[]};try{const[d,f,g]=await Promise.all([v(()=>k(S(),"airing",1).then(u=>u.map(h)),()=>ge(1,20).then(u=>u.map(h))),v(()=>he(S(),a,n,r,1,14).then(u=>u.map(h)),()=>te(a,n,r,1,14).then(u=>u.map(h))),v(()=>k(S(),"airing",1).then(u=>u.map(h)),()=>Ve(1,20).then(u=>u.map(h)))]);return{trending:d,seasonal:f,spotlight:g}}catch{return{trending:[],seasonal:[],spotlight:[]}}}return{trending:(c.data.trending?.media??[]).map(h),seasonal:(c.data.seasonal?.media??[]).map(h),spotlight:(c.data.spotlight?.media??[]).map(h)}}function lt(e,t){return`${e}:${JSON.stringify(t||{})}`}function j(e){const t=se.get(e);return t&&Date.now()<t.expiry?t.data:(se.delete(e),null)}function M(e,t){se.set(e,{data:t,expiry:Date.now()+rt})}function ut(e){const t=oe.get(e);return t&&Date.now()<t.expiry?t.data:(oe.delete(e),null)}function we(e,t){oe.set(e,{data:t,expiry:Date.now()+st})}async function dt(){for(;;){const e=Date.now();if(e<x){await new Promise(t=>setTimeout(t,x-e));continue}if(it(),z>=1){z-=1;return}await new Promise(t=>setTimeout(t,F))}}async function Ie(e,t,n=0){await dt();try{const i=await fetch(Se,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({query:e,variables:t})});if(at(i.headers),i.status===429){if(L=!0,n>=5)return{data:null,errors:[{message:"AniList rate limited"}]};const r=Number.parseInt(i.headers.get("Retry-After")||"",10),s=Number.isFinite(r)&&r>=0?r:5,o=Math.min(s*1e3,6e4);return x=Math.max(x,Date.now()+o),F=Math.min(6e4,Math.max(F,1500)*1.15),U.warn({retryAfter:s,retryCount:n},"AniList rate limited, retrying"),Ie(e,t,n+1)}i.status===403||i.status>=500?(D=Date.now()+ie,L=!1):i.status>=200&&i.status<300&&(D=0,L=!1);const a=await Be(i);return a.errors&&!a.data&&U.warn({status:i.status,errors:a.errors},"AniList request failed"),{data:a.data??null,errors:a.errors}}catch(i){return D=Math.max(D,Date.now()+Ze),U.error({err:i},"AniList request error"),null}}function b(e,t){if(_())return Promise.resolve({data:null,errors:[{message:"AniList unavailable"}]});const n=lt(e,t),i=Q.get(n);if(i)return i;const a=Ie(e,t);return Q.set(n,a),a.then(()=>Q.delete(n),()=>Q.delete(n)),a}const ct=["Action","Adventure","Comedy","Drama","Fantasy","Horror","Mahou Shoujo","Mecha","Music","Mystery","Psychological","Romance","Sci-Fi","Slice of Life","Sports","Supernatural","Thriller"];let H=null;const gt=24*3600*1e3;async function Rt(){if(H&&Date.now()-H.at<gt)return{genres:H.genres,tags:H.tags};try{const e=await b("query { GenreCollection MediaTagCollection { name isAdult } }"),t=e?.data?.GenreCollection?.filter(i=>typeof i=="string"),n=(e?.data?.MediaTagCollection??[]).filter(i=>i&&!i.isAdult&&typeof i.name=="string").map(i=>i.name);if(t&&t.length>0)return H={genres:t,tags:n,at:Date.now()},{genres:t,tags:n}}catch{}return{genres:ct,tags:[]}}function mt(e){return e?e.replace(/<br\s*\/?>/gi,`
63
- `).replace(/<[^>]+>/g,"").replace(/&amp;/g,"&").replace(/&quot;/g,'"').replace(/&#0?39;|&apos;/g,"'").replace(/&lt;/g,"<").replace(/&gt;/g,">").trim():""}function h(e){const t=e.id<0?`mal-${Math.abs(e.id)}`:e.id.toString(),n=e.title,i=n?.romaji||n?.english||n?.native||"Unknown",a=e.nextAiringEpisode?.airingAt!=null?new Date(e.nextAiringEpisode.airingAt*1e3).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric",timeZone:"UTC"}):void 0;return{_id:t,id:t,anilistId:e.id>0?e.id:void 0,name:i,englishName:n?.english,nativeName:n?.native,names:{romaji:n?.romaji,english:n?.english,native:n?.native,synonyms:e.synonyms},thumbnail:e.coverImage?.extraLarge||e.coverImage?.large||"",bannerImage:e.bannerImage||void 0,description:mt(e.description),genres:(e.genres?.slice(0,6)||[]).map(r=>({name:r})),score:e.averageScore!=null?Number((e.averageScore/10).toFixed(1)):void 0,type:e.format,status:e.status,episodeCount:e.episodes??void 0,year:e.seasonYear??void 0,season:e.season?{season:e.season,title:e.season,year:e.seasonYear??void 0}:void 0,nextAiring:e.nextAiringEpisode?{episode:e.nextAiringEpisode.episode,timeUntilAiring:e.nextAiringEpisode.timeUntilAiring}:void 0,nextEpisodeAirDate:a,studios:e.studios?.nodes?.map(r=>({name:r.name})),isAdult:e.isAdult,tags:e.tags?.map(r=>({name:r.name})),averageScore:e.averageScore??void 0,country:e.countryOfOrigin??void 0,airedStart:e.startDate?{year:e.startDate.year??void 0,month:e.startDate.month??void 0,date:e.startDate.day??void 0}:void 0,airedEnd:e.endDate?{year:e.endDate.year??void 0,month:e.endDate.month??void 0,date:e.endDate.day??void 0}:void 0}}function $e(e){const t=String(e.anilistId),n=e.title||"Unknown";let i;if(e.genres)try{const a=JSON.parse(e.genres);if(Array.isArray(a)){const r=a.filter(s=>typeof s=="string");r.length>0&&(i=r.map(s=>({name:s})))}}catch{}return{_id:t,id:t,anilistId:e.anilistId,name:n,names:{romaji:e.title||void 0},thumbnail:e.thumbnail||"",genres:i,type:e.type}}async function kt(e="TV",t=1,n=12){if(e==="ADULT")return Pe(t,n);const i=Math.floor(Date.now()/1e3),a=i-30*86400,r=t*n,s=[],o=new Set;let c=1,d=!1;for(;s.length<r&&c<=10;){const g=`
64
- query ($page: Int, $perPage: Int, $airingAtGt: Int, $airingAtLt: Int) {
65
- Page(page: $page, perPage: $perPage) {
66
- pageInfo { hasNextPage }
67
- airingSchedules(sort: TIME_DESC, airingAt_lesser: $airingAtLt, airingAt_greater: $airingAtGt) {
68
- id episode airingAt
69
- media {
70
- ${q()}
71
- }
72
- }
73
- }
74
- }
75
- `,u=await b(g,{page:c,perPage:50,airingAtLt:i,airingAtGt:a});if(!u?.data){d=!0;break}const l=u?.data?.Page?.airingSchedules;if(!l||l.length===0)break;for(const m of l)if(m.media&&!(e!=="ALL"&&m.media.format!==e)&&!m.media.isAdult&&!(!m.media.id||o.has(m.media.id))&&(o.add(m.media.id),s.push(h(m.media)),s.length>=r))break;if(!u.data?.Page?.pageInfo?.hasNextPage)break;c++}if(d)return(await v(()=>Je(S(),e,t,n),()=>We(e,t,n))).map(h);const f=(t-1)*n;return s.slice(f,f+n)}async function Pe(e,t){const n=`
76
- query ($page: Int, $perPage: Int) {
77
- Page(page: $page, perPage: $perPage) {
78
- pageInfo { total hasNextPage }
79
- media(sort: [START_DATE_DESC, ID_DESC], type: ANIME, isAdult: true) {
80
- ${q()}
81
- }
82
- }
83
- }
84
- `,i=await b(n,{page:e,perPage:t});if(!i?.data)return[];const a=i?.data?.Page?.media;return a?a.map(h):[]}async function Ot(e=1,t=14,n){if(n==="ADULT")return Pe(e,t);const i=new Date,a=i.getFullYear(),r=i.getMonth()+1,s=r<=3?"WINTER":r<=6?"SPRING":r<=9?"SUMMER":"FALL",o=`
85
- query ($page: Int, $perPage: Int, $season: MediaSeason, $seasonYear: Int, $format: MediaFormat) {
86
- Page(page: $page, perPage: $perPage) {
87
- pageInfo { hasNextPage total }
88
- media(season: $season, seasonYear: $seasonYear, type: ANIME, format: $format, isAdult: false, sort: [POPULARITY_DESC, SCORE_DESC]) {
89
- ${q()}
90
- }
91
- }
92
- }
93
- `,c=n&&n!=="ALL"?n.toUpperCase():void 0,d=await b(o,{page:e,perPage:t,season:s,seasonYear:a,format:c});if(!d?.data)return _()?(await v(()=>he(S(),s,a,n,e,t),()=>te(s,a,n,e,t))).map(h):[];const f=d?.data?.Page?.media;return f?f.map(h):[]}async function Ut(e=1,t=20,n="TRENDING_DESC",i){const a=await Ee(e,t,n,i);return a&&a.length>0?a.map(h):_()?(await v(async()=>{if(i==="RELEASING")return k(S(),"airing",e);switch(n){case"POPULARITY_DESC":case"FAVOURITES_DESC":return k(S(),"bypopularity",e);case"SCORE_DESC":return k(S(),null,e);default:return k(S(),"airing",e)}},()=>ge(e,t))).map(h):[]}async function Ee(e=1,t=20,n="TRENDING_DESC",i){const a=i?`status: ${i},`:"",r=`
94
- query ($page: Int, $perPage: Int) {
95
- Page(page: $page, perPage: $perPage) {
96
- media(sort: ${n}, type: ANIME, isAdult: false, ${a}) {
97
- ${Y()}
98
- }
99
- }
100
- }
101
- `,s=await b(r,{page:e,perPage:t});return s?.data?s?.data?.Page?.media??[]:null}async function Ft(e=1,t=20){const n=await Ee(e,t);if(n){const u=[],l=[];for(const p of n){if(u.length>=6&&l.length>=6)break;const A=h(p);A.bannerImage?u.length<6&&u.push(A):l.length<6&&l.push(A)}const m=[...u];for(const p of l){if(m.length>=6)break;p.thumbnail&&m.push({...p,bannerImage:p.thumbnail})}return m}const i=new Date,a=i.getFullYear(),r=i.getMonth()+1,s=r<=3?"WINTER":r<=6?"SPRING":r<=9?"SUMMER":"FALL",o=await v(()=>k(S(),"airing",1),()=>te(s,a,"TV",1,20));if(!o||o.length===0)return[];const c=(u,l=4e3)=>Promise.race([u,new Promise(m=>setTimeout(()=>m(null),l))]),d=[...o].sort(()=>Math.random()-.5),f=await Promise.allSettled(d.slice(0,10).map(u=>c(Ge({english:u.title?.english,romaji:u.title?.romaji,native:u.title?.native})).then(l=>({m:u,artwork:l})))),g=[];for(const u of f){if(g.length>=6)break;if(u.status!=="fulfilled"||!u.value?.artwork)continue;const l=h(u.value.m);g.push({...l,bannerImage:u.value.artwork.backdrop,description:l.description||u.value.artwork.overview||""})}if(g.length<6){const u=new Set(g.map(l=>l._id));for(const l of d){if(g.length>=6)break;const m=h(l);u.has(m._id)||!m.thumbnail||(u.add(m._id),g.push({...m,bannerImage:m.thumbnail}))}}return g}function Te(e){if(typeof e=="number")return e<0?Math.abs(e):null;const t=/^mal-(\d+)$/i.exec(e.trim());return t?parseInt(t[1],10):/^-\d+$/.test(e.trim())?Math.abs(parseInt(e,10)):null}function T(e){return!!e?.thumbnail&&e.thumbnail.trim()!==""}function ft(e){const t=e?.coverImage?.extraLarge||e?.coverImage?.large||e?.coverImage?.medium||e?.coverImage?.original||e?.coverImage?.small||e?.coverImage?.tiny;return t&&t.trim()!==""?t:null}function _e(e){return new Set(e.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g,"").split(/[^a-z0-9]+/).filter(t=>t.length>=2))}function pt(e,t){if(!e.trim()||!t.trim())return 0;const n=e.trim().toLowerCase(),i=t.trim().toLowerCase();if(n===i||n.includes(i)||i.includes(n))return 1;const a=_e(e),r=_e(t);return a.size===0||r.size===0?0:[...a].filter(o=>r.has(o)).length/Math.min(a.size,r.size)}function ht(e,t){return e?t.some(n=>pt(e,n)>=.6):!1}async function At(e,t){const n=e?.trim();if(!n)return null;const i=(t?.titles??[]).filter(a=>a.trim()!=="");try{const{entries:a}=await Ae(S(),n,1);for(const r of a)if(!(!r.imageUrl||r.imageUrl.trim()==="")&&(t?.malId&&r.idMal===t.malId||t&&ht(r.title,i)||!t))return r.imageUrl}catch{}try{const a=await ee({query:n,page:1,perPage:5});for(const r of a){const s=ft(r);if(s&&(t?.anilistId&&r.id===t.anilistId||t?.malId&&r.idMal===t.malId))return s}}catch{}return null}async function N(e,t,n){if(!e)return null;if(T(e))return M(t,e),e;if(n&&(n.anilistId||n.malId)){const o=((n.anilistId?G.getByAnilistId(n.anilistId):null)??(n.malId?G.getByMalId(n.malId):null))?.thumbnail;if(o&&o.trim()!=="")return e.thumbnail=o,M(t,e),e}const i=[e.names?.english,e.englishName,e.name,e.names?.romaji].filter(s=>!!s&&s.trim().length>=4).filter((s,o,c)=>c.findIndex(d=>d.toLowerCase()===s.toLowerCase())===o),a=n??{titles:[]};a.titles.length===0&&(a.titles=[...i]),e.names?.native&&a.titles.push(e.names.native);const r=new Set;for(const s of i){const o=s.toLowerCase();if(!r.has(o)){r.add(o);try{const c=await At(s,a);if(c)return e.thumbnail=c,M(t,e),e}catch{}}}return e}async function De(e,t){try{const n=await ne(S(),t);n.detail?.imageUrl&&(e.thumbnail=n.detail.imageUrl);try{S().put(`${ze}${t}`,(n.detail?.titleEnglish??"").trim(),168*3600)}catch{}const i=n.detail?.titleEnglish?.trim();i&&(!e.names?.english||e.names.english.trim()===""||e.names.english===e.names?.romaji)&&(e.names={...e.names,english:i},e.englishName=i);const a=n.detail?.titleJapanese?.trim();a&&!e.names?.native&&(e.names={...e.names,native:a},e.nativeName=a),(!e.name||e.name==="Unknown")&&(e.names?.english||e.names?.native)&&(e.name=e.names.english??e.names.native??e.name)}catch{}}async function yt(e){const t=Te(e);if(t)return Le(t);const n=parseInt(e);if(isNaN(n))return null;if(n<0)return Le(Math.abs(n));const i=Y(),a=`meta:v2:${e}`,r=j(`meta:${e}`),s=j(a)??r;if(s&&T(s))return M(a,s),s;const o={anilistId:n,titles:[]},c=`query ($id: Int) { Media(id: $id, type: ANIME) { ${i} } }`,d=`query ($id: Int) { Media(idMal: $id, type: ANIME) { ${i} } }`,f=await b(c,{id:n});if(f?.data?.Media){const l=h(f.data.Media);return N(l,a,o)}const g=await b(d,{id:n});if(g?.data?.Media){const l=h(g.data.Media);return N(l,a,o)}if(!f?.data&&!g?.data&&_()){const l=G.getByAnilistId(n)??G.getByMalId(n),m=l?$e(l):null;if(m&&T(m))return M(a,m),m;const p=await Ke(n)??await me(n);if(p){const A=h(p);return T(A)?(p.idMal&&await De(A,p.idMal),M(a,A),A):N(A,a,o)}if(m)return M(a,m),m}return s&&!T(s)?N(s,a,o):null}async function Le(e){const t=Math.abs(e);if(!t)return null;const n=`meta:v2:-${t}`,i=j(n)??j(`meta:-${t}`),a={malId:t,titles:[]};if(i&&i.anilistId!=null&&i.anilistId>0&&T(i))return M(n,i),i;const r=!_();if(i&&!r&&T(i))return M(n,i),i;if(r)try{const c=await b(`query ($id: Int) { Media(idMal: $id, type: ANIME) { ${Y()} } }`,{id:t});if(c?.data?.Media){const d=h(c.data.Media);return N(d,n,a)}}catch{}const s=r?null:G.getByMalId(t),o=s?$e(s):null;if(o&&T(o))return M(n,o),o;try{const c=await me(t);if(c){const d=h(c);return await De(d,t),T(d)?(M(n,d),d):N(d,n,a)}}catch{}try{const c=await ne(S(),t);if(c.detail){const d=h(Xe(c.detail));return N(d,n,a)}}catch{}return i&&!T(i)?N(i,n,a):o?(M(n,o),o):(U.warn({malId:t},"MAL id unresolvable on all metadata providers"),null)}async function qt(e){const t=new Map;if(e.length===0)return t;if(!ae()){for(let a=0;a<e.length;a+=10){const r=e.slice(a,a+10),s=r.map((c,d)=>`a${d}: Media(id: $id${d}, type: ANIME) { id status }`).join(`
102
- `),o={};r.forEach((c,d)=>{o[`id${d}`]=c});try{const c=await b(`query (${r.map((d,f)=>`$id${f}: Int`).join(", ")}) { ${s} }`,o);for(let d=0;d<r.length;d++){const f=`a${d}`,g=c?.data?.[f];g?.status&&t.set(g.id,g.status)}}catch{break}}if(t.size===e.length)return t}const n=e.filter(i=>!t.has(i));if(n.length>0)try{const{kitsuBatchGetStatuses:i}=await import("./kitsu.js"),a=await i(n);for(const[r,s]of a)t.set(r,s)}catch{}return t}async function jt(e){const t=Te(e);if(t)return xe(t);const n=parseInt(e);if(isNaN(n))return[];if(n<0)return xe(Math.abs(n));const i=`eps:${e}`,a=j(i);if(a)return a;const r="id episodes status nextAiringEpisode { episode airingAt } airingSchedule(perPage: 100) { nodes { episode airingAt } }",s=`query ($id: Int) { Media(id: $id, type: ANIME) { ${r} } }`,o=`query ($id: Int) { Media(idMal: $id, type: ANIME) { ${r} } }`,d=(await b(s,{id:n}))?.data?.Media;if(!d){const g=await b(o,{id:n});if(!g?.data){if(_()){const l=await fe(n,n);if(l.length>0)return M(i,l),l}return[]}if(!g?.data?.Media)return[];const u=ve(g.data.Media);return M(i,u),u}const f=ve(d);return M(i,f),f}async function xe(e){const t=`eps:-${e}`,n=j(t);if(n)return n;try{const i=await fe(0,e);if(i.length>0)return M(t,i),i}catch{}try{const a=(await ne(S(),e)).detail?.episodes;if(a&&a>0&&a<=2e3){const r=Array.from({length:a},(s,o)=>(o+1).toString());return M(t,r),r}}catch{}return[]}function ve(e){const{status:t,episodes:n,nextAiringEpisode:i,airingSchedule:a}=e;if(t==="FINISHED"&&n)return Array.from({length:n},(r,s)=>(s+1).toString());if(t==="RELEASING"&&i?.episode){const r=i.episode-1;if(r>0)return Array.from({length:r},(s,o)=>(o+1).toString())}if(a?.nodes){const r=Date.now()/1e3,s=a.nodes.filter(o=>o.airingAt<r);if(s.length>0){const o=Math.max(...s.map(c=>c.episode));return Array.from({length:o},(c,d)=>(d+1).toString())}}return n?Array.from({length:n},(r,s)=>(s+1).toString()):[]}async function St(e,t="ANIME"){const n=`
103
- query ($search: String) {
104
- Page(page: 1, perPage: 5) {
105
- media(search: $search, type: ${t}) {
106
- id
107
- title { romaji english native }
108
- }
109
- }
110
- }
111
- `,i=e.normalize("NFD").replace(/[\u0300-\u036f]/g,""),a=i.split(/\s+/).filter(l=>l.length>=3),r=[e,i,i.replace(/:/g,""),a.slice(0,5).join(" "),a.slice(0,4).join(" "),a.slice(0,3).join(" "),a.slice(0,2).join(" ").replace(/:/g,""),i.replace(/\s*\d+(?:st|nd|rd|th)?\s*Season\s*\d*/gi,"").trim()].filter((l,m,p)=>l&&p.indexOf(l)===m);let s,o=!1;for(const l of r){const m=await b(n,{search:l});if(!m?.data){o=!0;break}if(s=m?.data?.Page?.media,s?.length)break}if(!s||s.length===0){if(o&&t==="ANIME"&&_()){const l=await ee({query:e,page:1,perPage:5});if(l.length>0){const m=l[0];return{id:m.id,title:m.title??{}}}}return null}const c=e.toLowerCase(),d=s.find(l=>l.title?.romaji?.toLowerCase()===c||l.title?.english?.toLowerCase()===c||l.title?.native===e);if(d)return{id:d.id,title:d.title};const f=new Set(i.toLowerCase().split(/\s+/).filter(l=>l.length>=2));let g=s[0],u=0;for(const l of s){const p=(l.title?.romaji||l.title?.english||"").toLowerCase().split(/\s+/).filter(A=>f.has(A)).length;p>u&&(u=p,g=l)}return{id:g.id,title:g.title}}async function Bt(e){return St(e,"MANGA")}const Ne="id title { romaji english native } coverImage { large }";async function Gt(e){if(!e)return null;try{const n=(await b(`query ($id: Int) { Media(id: $id, type: MANGA) { ${Ne} } }`,{id:e}))?.data?.Media;return n?{id:n.id,title:n.title??{},cover:n.coverImage?.large}:null}catch{return null}}async function Yt(e){const t=Math.abs(e);if(!t)return null;try{const i=(await b(`query ($id: Int) { Media(idMal: $id, type: MANGA) { ${Ne} } }`,{id:t}))?.data?.Media;return i?{id:i.id,title:i.title??{},cover:i.coverImage?.large}:null}catch{return null}}async function Ht(e,t,n){const i=Math.floor(t.getTime()/1e3),a=Math.floor(n.getTime()/1e3),r=`aired:${i}:${a}:${Array.from(e).sort((g,u)=>g-u).join(",")}`,s=ut(r);if(s)return s;try{const g=await He(e,i,a);return we(r,g),g}catch(g){U.warn({err:g},"AniSchedule episode feed failed, falling back to AniList aired episodes")}if(ae())return[];const o=Math.floor(Date.now()/1e3),c=[],d=new Set,f=5;for(let g=0;g<e.length;g+=f){const u=e.slice(g,g+f),l=u.map((A,I)=>`a${I}: Media(id: $id${I}, type: ANIME) { id airingSchedule(perPage: 100) { nodes { episode airingAt } } }`).join(`
112
- `),m={};u.forEach((A,I)=>{m[`id${I}`]=A});const p=await b(`query (${u.map((A,I)=>`$id${I}: Int`).join(", ")}) { ${l} }`,m);for(let A=0;A<u.length;A++){const I=`a${A}`,P=p?.data?.[I];if(P?.airingSchedule?.nodes){for(const y of P.airingSchedule.nodes)if(y.airingAt>=i&&y.airingAt<=a&&y.airingAt<=o){const V=`${P.id}:${y.episode}`;d.has(V)||(d.add(V),c.push({mediaId:P.id,episode:y.episode,airingAt:y.airingAt}))}}}}return we(r,c),c}async function Vt(e,t,n=!1){t==="ADULT"&&(n=!0,t=void 0);try{const i=await Ye(e,t,n),a=i.entries.filter(r=>r.show).map(r=>ce(r.show,r.episode,r.airingAt));if(i.missingMeta.length>0)for(const r of i.missingMeta)try{const s=await yt(String(r.id));if(!s||(n?!s.isAdult:s.isAdult))continue;a.push(ce(s,r.episode,r.airingAt))}catch{}return a}catch(i){return U.warn({err:i},"AniSchedule schedule failed, falling back to AniList"),ae()?[]:bt(e,t,n)}}async function bt(e,t,n=!1){const i=Math.floor(e.getTime()/1e3),a=i+86400,r=`
113
- query ($dayStart: Int, $dayEnd: Int) {
114
- Page(perPage: 50) {
115
- airingSchedules(airingAt_greater: $dayStart, airingAt_lesser: $dayEnd) {
116
- episode
117
- airingAt
118
- media {
119
- ${Y()}
120
- }
121
- }
122
- }
123
- }
124
- `,o=(await b(r,{dayStart:i,dayEnd:a}))?.data?.Page?.airingSchedules;if(!o||o.length===0)return[];const c=new Set,d=[],f=Math.floor(Date.now()/1e3);for(const g of o){const u=g.media;if(!u||t&&t!=="ALL"&&u.format!==t)continue;const l=!!u.isAdult;if(n?!l:l)continue;const m=u.idMal??u.id;if(c.has(m))continue;c.add(m);const p=h(u);p.episodeNumber=g.episode,p.aired=g.airingAt<=f,p.nextAiring={episode:g.episode,timeUntilAiring:g.airingAt-f},p.nextEpisodeAirDate=new Date(g.airingAt*1e3).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric",timeZone:"UTC"}),p.airTime=new Date(g.airingAt*1e3).toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",timeZone:"UTC"}),d.push(p)}return d}function Ce(e){const t=Array.isArray(e)?e:typeof e=="string"?e.split(","):[];return[...new Set(t.map(n=>n.trim()).filter(Boolean))]}async function Wt(e={}){const{query:t,page:n=1,perPage:i=14,format:a,status:r,season:s,seasonYear:o,countryOfOrigin:c,genre:d,genre_in:f,tag:g,tag_in:u,genre_not_in:l,tag_not_in:m,averageScore_greater:p,episodes_greater:A,isAdult:I,sort:P}=e,y={page:n,perPage:i};t&&(y.search=t),a&&a!=="ALL"&&(a==="ADULT"?y.isAdult=!0:y.format=a.toUpperCase()),r&&(y.status=r),s&&s!=="ALL"&&(y.season=s.toUpperCase()),o&&(y.seasonYear=o),c&&c!=="ALL"&&(y.countryOfOrigin=c);const V=[...Ce(d),...(f??[]).map(w=>w.trim()).filter(Boolean)],W=[...new Set(V)];W.length===1?y.genre=W[0]:W.length>1&&(y.genre_in=W);const Re=[...Ce(g),...(u??[]).map(w=>w.trim()).filter(Boolean)],K=[...new Set(Re)];K.length===1?y.tag=K[0]:K.length>1&&(y.tag_in=K),l&&l.length>0&&(y.genre_not_in=l),m&&m.length>0&&(y.tag_not_in=m),p&&(y.averageScore_greater=p),A&&(y.episodes_greater=A),I!==void 0&&(y.isAdult=I),P&&(y.sort=[P]);const ke=`
125
- query ($page: Int, $perPage: Int, $search: String, $format: MediaFormat, $status: MediaStatus, $season: MediaSeason, $seasonYear: Int, $countryOfOrigin: CountryCode, $genre: String, $genre_in: [String], $tag: String, $tag_in: [String], $genre_not_in: [String], $tag_not_in: [String], $averageScore_greater: Int, $episodes_greater: Int, $isAdult: Boolean, $sort: [MediaSort]) {
126
- Page(page: $page, perPage: $perPage) {
127
- pageInfo { hasNextPage total }
128
- media(
129
- search: $search,
130
- type: ANIME,
131
- format: $format,
132
- status: $status,
133
- season: $season,
134
- seasonYear: $seasonYear,
135
- countryOfOrigin: $countryOfOrigin,
136
- genre: $genre,
137
- genre_in: $genre_in,
138
- tag: $tag,
139
- tag_in: $tag_in,
140
- genre_not_in: $genre_not_in,
141
- tag_not_in: $tag_not_in,
142
- averageScore_greater: $averageScore_greater,
143
- episodes_greater: $episodes_greater,
144
- isAdult: $isAdult,
145
- sort: $sort
146
- ) {
147
- ${q()}
148
- }
149
- }
150
- }
151
- `,Oe=t?.toLowerCase().replace(/[^a-z0-9]/g,""),Ue=t?.trim()?qe(t).filter(w=>w.toLowerCase().replace(/[^a-z0-9]/g,"")===Oe):[""],X=new Map;let B=null,le=!1;for(const[w,C]of Ue.entries()){if(B=await b(ke,{...y,...C?{search:C}:{}}),!B?.data)break;for(const E of B.data.Page.media??[])E.id&&X.set(E.id,E);if(w>0&&(le=!0),(B.data.Page.media?.length??0)>=i)break}if(!B?.data&&X.size===0){if(_()){const C=await v(async()=>{const R=S();let $=await pe(R,{query:t,page:n,perPage:i,format:a&&a!=="ALL"&&a!=="ADULT"?a:void 0,status:r,season:s,seasonYear:o,genre:d,genre_in:f,genre_not_in:l,averageScore_greater:p,episodes_greater:A,isAdult:a==="ADULT"?!0:I,sort:P});if($.length>0)try{const O=await pe(R,{query:t,page:n,perPage:i,format:a&&a!=="ALL"&&a!=="ADULT"?a:void 0,status:r,season:s,seasonYear:o,genre:d,genre_in:f,averageScore_greater:p,episodes_greater:A,sort:P,isAdult:!0},!1);if(O.length>0){const Fe=new Set(O.map(Z=>Z.id));$=$.filter(Z=>!Fe.has(Z.id))}}catch{}return $},()=>ee({query:t,page:n,perPage:i,format:a&&a!=="ALL"&&a!=="ADULT"?a:void 0,status:r,season:s,seasonYear:o,genre:d,genre_in:f,genre_not_in:l,averageScore_greater:p,episodes_greater:A,isAdult:a==="ADULT"?!0:I,sort:P}));try{await ye(S(),C)}catch{}let E=ot(C);if(E.length===0&&t?.trim())try{const $=(await Ae(S(),t,n)).entries.map(O=>Qe(O));try{await ye(S(),$)}catch{}E=$.map(O=>h(O))}catch{}return E}return[]}const J=[...X.values()];if(!J||J.length===0)return[];le&&J.sort((w,C)=>{const E=R=>Math.max(0,...[R.title?.romaji,R.title?.english,R.title?.native].filter($=>!!$).map($=>je([{title:$}],[t??""])?.score??0));return E(C)-E(w)});const ue=new Set,de=[];for(const w of J)!w.id||ue.has(w.id)||(ue.add(w.id),de.push(h(w)));return de.slice(0,i)}export{b as anilistRequest,_ as anilistUnavailable,at as applyRateLimitHeaders,qt as batchGetShowStatuses,vt as checkAnilistStatus,At as findPosterByTitle,h as fromAnilistMedia,Ht as getAiredEpisodesForShows,jt as getAnilistEpisodes,Ct as getBatchedHomeData,Rt as getGenreTagLists,kt as getLatestReleases,Gt as getMangaMetaById,Yt as getMangaMetaByMalId,Vt as getSchedule,Ot as getSeasonal,yt as getShowMetaById,Le as getShowMetaByMalId,Ft as getSpotlightBanners,Ut as getTrending,ae as isAnilistRateLimited,Y as mediaFields,q as mediaFieldsLight,Te as parseMalId,Wt as searchAnilist,St as searchAnilistByTitle,Bt as searchAnilistMangaByTitle,M as setCachedAnilist,$e as showFromOfflineEntry,dt as waitForAnilistSlot,Nt as wasAnilistDownAtBoot};
@@ -1 +0,0 @@
1
- import{parseJsonBody as L}from"../utils/http.utils.js";const _="https://raw.githubusercontent.com/RockinChaos/AniSchedule/master/raw/sub-schedule.json",v="https://raw.githubusercontent.com/RockinChaos/AniSchedule/master/raw/sub-episode-feed.json",j="https://raw.githubusercontent.com/RockinChaos/AniSchedule/master/raw/hentai-episode-feed.json",R=900*1e3,F=600*1e3,N=1e4,k={data:null,expiry:0},B={data:null,expiry:0},H={data:null,expiry:0},S=new Map;async function J(e){const n=new AbortController,r=setTimeout(()=>n.abort(),N);try{const i=await fetch(e,{signal:n.signal,headers:{"User-Agent":"dango"}});if(!i.ok)throw new Error(`AniSchedule fetch failed with status ${i.status}`);const a=await L(i);if(Array.isArray(a)&&a.length===0)throw new Error("AniSchedule returned an empty payload");return a}finally{clearTimeout(r)}}function b(e,n,r,i){if(r.data!==null&&Date.now()<r.expiry)return Promise.resolve(r.data);const a=S.get(e);if(a)return a;const u=J(n).then(s=>(r.data=s,r.expiry=Date.now()+i,s)).catch(s=>{throw S.delete(e),s});return S.set(e,u),u}function Y(){return b("anischedule:shows",_,k,R)}function D(){return b("anischedule:subfeed",v,B,F)}function M(){return b("anischedule:hentaifeed",j,H,F)}function G(e,n,r){const i=Math.floor(Date.now()/1e3);return{...e,episodeNumber:n,aired:r<=i,nextAiring:{episode:n,timeUntilAiring:r-i},nextEpisodeAirDate:new Date(r*1e3).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric",timeZone:"UTC"}),airTime:new Date(r*1e3).toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",timeZone:"UTC"})}}function Z(e){const n=e.title,r=n?.romaji||n?.english||n?.native||"Unknown";return{_id:e.id.toString(),id:e.id.toString(),anilistId:e.id,name:r,englishName:n?.english,nativeName:n?.native,names:{romaji:n?.romaji,english:n?.english,native:n?.native},thumbnail:e.coverImage?.extraLarge||e.coverImage?.medium||"",bannerImage:e.bannerImage||void 0,genres:e.genres?.map(i=>({name:i})),type:e.format,year:e.seasonYear??void 0,isAdult:e.isAdult}}async function K(e,n,r=!1){const i=Math.floor(e.getTime()/1e3),a=i+86400,u=Math.floor(Date.now()/1e3),s=n&&n!=="ALL"?n:void 0,d=Y(),l=D(),P=r?M():Promise.resolve([]),[A,w,C]=await Promise.all([d,l,P]),p=new Map;for(const t of A)p.set(t.id,t);const U=t=>r?!!t.isAdult:!t.isAdult,h=new Map,g=new Map,y=(t,o,m,f)=>{const c=p.get(t);if(c){if(s&&c.format!==s||!U(c))return;const T=c.idMal??c.id,x=h.get(T);(!x||m>x.airingAt)&&h.set(T,{show:Z(c),episode:o,airingAt:m,aired:m<=u});return}if(r||s&&f&&f!==s)return;const E=g.get(t);(!E||m>E.airingAt)&&g.set(t,{id:t,episode:o,airingAt:m,format:f})};for(const t of A)for(const o of t.airingSchedule.nodes)o.airingAt>=i&&o.airingAt<a&&y(t.id,o.episode,o.airingAt);const I=r?[...w,...C]:w;for(const t of I){const o=Date.parse(t.episode.airedAt)/1e3;o>=i&&o<a&&y(t.id,t.episode.aired,o,t.format)}return{entries:Array.from(h.entries()).map(([,t])=>t).sort((t,o)=>t.airingAt-o.airingAt),missingMeta:Array.from(g.values())}}async function Q(e,n,r){const i=new Set(e),[a,u]=await Promise.all([D(),M()]),s=[];for(const d of[...a,...u]){if(!i.has(d.id))continue;const l=Date.parse(d.episode.airedAt)/1e3;l>=n&&l<=r&&s.push({mediaId:d.id,episode:d.episode.aired,airingAt:l})}return s}export{Q as getAiredEpisodesFromAniScheduleFeed,M as getAniScheduleHentaiFeed,Y as getAniScheduleShows,D as getAniScheduleSubFeed,K as getScheduleFromAniSchedule,G as withScheduleFields};
@@ -1 +0,0 @@
1
- import w from"../logger.js";import{InsightsRepository as l}from"../repositories/insights.repository.js";async function F(c){const[m,i,h,n,e,u,a,o]=await Promise.all([l.getCoreStats(c),l.getActivityGrid(c),l.getHourlyDist(c),l.getSeasonality(c),l.getAllWatches(c),l.getWatchedShowsMeta(c),l.getDroppedShows(c),l.getCompletionVelocities(c)]),b=i.length>0?Math.max(...i.map(t=>t.count)):0,g=[];if(e.length>0){let t=e[0].effectiveSeconds??e[0].currentTime;for(let r=1;r<e.length;r++){const s=new Date(e[r-1].watchedAt).getTime(),S=new Date(e[r].watchedAt).getTime()-s;Number.isFinite(S)&&S>0&&S<36e5?t+=e[r].effectiveSeconds??e[r].currentTime:(g.push(t),t=e[r].effectiveSeconds??e[r].currentTime)}g.push(t)}const W=g.length>0?Math.round(g.reduce((t,r)=>t+r,0)/g.length/60):0,p={};let y=0,f=0;for(const t of u){let r=[];if(t.genres)try{t.genres.startsWith("[")?r=JSON.parse(t.genres):r=t.genres.split(",").map(s=>s.trim())}catch(s){w.warn({err:s,showId:t.id},"Failed to parse genres for insights")}for(const s of r)p[s]=(p[s]||0)+1;if(t.popularityScore){let s=t.popularityScore;s>10&&(s=s/10),y+=s,f++}}const M=Object.entries(p).sort((t,r)=>r[1]-t[1])[0]?.[0],v={Action:"Shonen Warrior",Romance:"Hopeless Romantic",Comedy:"Chaos Enjoyer","Slice of Life":"Vibe Seeker",Horror:"Fearless Watcher",Fantasy:"Isekai Traveller","Sci-Fi":"Future Scientist",Drama:"Feels Collector"}[M||""]||"Anime Enthusiast",C=o.length>0?Math.round(o.reduce((t,r)=>t+r.daysToFinish,0)/o.length):0;return{totalHours:Math.round((m?.totalSeconds||0)/3600),totalEpisodes:m?.totalEpisodes||0,totalAnime:m?.totalAnime||0,completedAnime:m?.completedCount||0,completionRate:m?.totalWatchlist>0?Math.round(m.completedCount/m.totalWatchlist*100):0,persona:v,bingeFactor:b,avgSessionMinutes:W,avgCompletionDays:C,popularityScore:f>0?Math.round(y/f):0,genreSplit:Object.entries(p).map(([t,r])=>({name:t,count:r})).sort((t,r)=>r.count-t.count).slice(0,8)||[],activityGrid:i||[],hourlyDist:Array.from({length:24},(t,r)=>{const s=r.toString().padStart(2,"0");return{hour:s,count:h?.find(d=>d.hour===s)?.count||0}})||[],seasonality:Array.from({length:12},(t,r)=>{const s=(r+1).toString().padStart(2,"0");return{month:s,seconds:n?.find(d=>d.month===s)?.seconds||0}})||[],droppedShows:(a||[]).slice(0,5)}}async function E(c){const m=await l.getWatchedEpisodesWithMeta(c),i={};for(const n of m){let e=[];if(n.genres)try{n.genres.startsWith("[")?e=JSON.parse(n.genres):e=n.genres.split(",").map(o=>o.trim())}catch(o){w.warn({err:o,showId:n.showId},"Failed to parse genres for genre cards")}const u=(n.effectiveSeconds??n.currentTime)||0;let a=n.popularityScore||0;a>10&&(a=a/10);for(const o of e)i[o]||(i[o]={count:0,totalTime:0,scores:[],showWatches:{},showMeta:{}}),i[o].count++,i[o].totalTime+=u,a>0&&i[o].scores.push(a),i[o].showWatches[n.showId]=(i[o].showWatches[n.showId]||0)+1,(n.name||n.thumbnail)&&(i[o].showMeta[n.showId]={name:n.name,nativeName:n.nativeName,englishName:n.englishName,thumbnail:n.thumbnail})}const h=Object.entries(i).map(([n,e])=>{const u=Object.entries(e.showWatches).sort((a,o)=>o[1]-a[1]).slice(0,4).map(([a])=>({id:a,name:e.showMeta[a]?.name||"",nativeName:e.showMeta[a]?.nativeName,englishName:e.showMeta[a]?.englishName,thumbnail:e.showMeta[a]?.thumbnail||""}));return{rank:0,name:n,count:e.count,meanScore:e.scores.length>0?e.scores.reduce((a,o)=>a+o,0)/e.scores.length:0,timeWatched:T(e.totalTime),topShows:u}});return h.sort((n,e)=>e.count-n.count),h.forEach((n,e)=>{n.rank=e+1}),h}function T(c){const m=Math.floor(c/86400),i=Math.floor(c%86400/3600),h=Math.floor(c%3600/60);return m>0?`${m} days ${i>0?`${i} hours`:""}`:i>0?`${i} hours${h>0?` ${h} mins`:""}`:`${h} mins`}export{E as getGenreCards,F as getWatchInsights};
@@ -1 +0,0 @@
1
- import n from"../logger.js";function e(t){try{typeof process.send=="function"&&process.send(t)}catch{}}function i(t){e({type:"SYNC_START",message:t}),n.info(`[SYNC_START] ${t}`)}function r(){e({type:"SYNC_END"}),n.info("[SYNC_END]")}function S(){e({type:"SERVER_EXIT"}),n.info("[SERVER_EXIT]")}export{S as notifyServerExit,r as notifySyncEnd,i as notifySyncStart};
@@ -1 +0,0 @@
1
- import x from"../logger.js";import{parseJsonBody as G}from"../utils/http.utils.js";const B="https://kitsu.app/api/edge",H={Accept:"application/vnd.api+json","User-Agent":"dango"},q=15e3,C=3;async function g(e){let t=null;for(let i=1;i<=C;i++)try{const n=new AbortController,s=setTimeout(()=>n.abort(),q);try{const r=await fetch(`${B}${e}`,{headers:H,signal:n.signal});if(r.status===429||r.status>=500){t=`kitsu fetch failed with status ${r.status}`,await new Promise(l=>setTimeout(l,1e3*i));continue}return r.ok?G(r):(x.warn({status:r.status,path:e},"Kitsu request failed"),null)}finally{clearTimeout(s)}}catch(n){t=n instanceof Error?n.message:String(n),i<C&&await new Promise(s=>setTimeout(s,500*i))}return x.warn({lastError:t},"Kitsu request error"),null}function p(e){return e?Array.isArray(e)?e:[e]:[]}function L(e,t,i){if(!t||!e.relationships)return[];const n=e.relationships[i];if(!n?.data)return[];const s=new Set;for(const r of Array.isArray(n.data)?n.data:[n.data])s.add(r.id);return t.filter(r=>r.type===i&&s.has(r.id))}const V={current:"RELEASING",finished:"FINISHED",upcoming:"NOT_YET_RELEASED",unreleased:"NOT_YET_RELEASED",tba:"NOT_YET_RELEASED"},J={tv:"TV",ova:"OVA",ona:"ONA",movie:"MOVIE",special:"SPECIAL",music:"MUSIC"},W=["WINTER","WINTER","SPRING","SPRING","SPRING","SUMMER","SUMMER","SUMMER","FALL","FALL","FALL","WINTER"];function D(e){if(typeof e!="string"||!e)return;const t=e.match(/^(\d{4})-(\d{2})-(\d{2})/);if(t)return{year:Number(t[1]),month:Number(t[2]),day:Number(t[3])}}function X(e){if(e)return V[e.toLowerCase()]??e.toUpperCase()}function Q(e){if(e)return J[e.toLowerCase()]??e.toUpperCase()}function Z(e){switch(e){case"SCORE_DESC":return"-averageRating";case"FAVOURITES_DESC":return"-favoritesCount";case"START_DATE_DESC":return"-startDate";case"END_DATE_DESC":return"-endDate";case"EPISODES_DESC":return"-episodeCount";case"ID_DESC":return"-id";case"UPDATED_AT_DESC":return"-updatedAt";default:return"-userCount"}}function K(e){if(!e)return;const t=e.toUpperCase();if(t==="RELEASING"||t==="ONGOING")return"current";if(t==="FINISHED")return"finished";if(t==="NOT_YET_RELEASED"||t==="UPCOMING"||t==="HIATUS")return"upcoming"}function U(e){if(e)return e.toLowerCase()}const z={"Sci-Fi":"Science Fiction","Sci Fi":"Science Fiction",Shounen:"Shounen"};function ee(e){const t=e.trim(),i=Object.entries(z).find(([n])=>n.toLowerCase()===t.toLowerCase());return i?i[1]:t}function N(e,t){const i=e.relationships?.item?.data;if(i&&!Array.isArray(i))return t.find(n=>n.type===i.type&&n.id===i.id)}async function se(e={}){const{query:t,page:i=1,perPage:n=14,format:s,status:r,season:l,seasonYear:a,genre:o,genre_in:c,genre_not_in:u,averageScore_greater:d,episodes_greater:f,isAdult:A,sort:m}=e,E=[];t&&E.push(`filter[text]=${encodeURIComponent(t)}`),s&&s!=="ALL"&&s!=="ADULT"&&E.push(`filter[subtype]=${s.toLowerCase()}`);const b=K(r);b&&E.push(`filter[status]=${b}`);const I=U(l);I&&I!=="all"&&E.push(`filter[season]=${I}`),a&&E.push(`filter[seasonYear]=${a}`);const T=[...Array.isArray(o)?o:typeof o=="string"?o.split(","):[],...c??[]].map(w=>w.trim()).filter(Boolean).join(",");T&&E.push(`filter[categories]=${encodeURIComponent(T)}`);const M=(i-1)*n,y=`/anime?${E.join("&")}${E.length?"&":""}sort=${Z(m)}&page[limit]=${n}&page[offset]=${M}&include=mappings,categories`,R=await g(y);if(!R)return[];const F=p(R.data),v=R.included??[],$=[];for(const w of F){const S=h(w,v);S&&(u?.length&&(S.genres??[]).some(O=>u.some(Y=>Y.toLowerCase()===O.toLowerCase()))||d!=null&&(S.averageScore??0)<d||f!=null&&(S.episodes??0)<f||A===!0&&!S.isAdult||A===!1&&S.isAdult||$.push(S))}return $}function _(e){const t=new Set,i=[];for(const n of e){const s=n.id>0?n.id:n.idMal??n.id;t.has(s)||(t.add(s),i.push(n))}return i}function h(e,t,i){const n=e.attributes,s=L(e,t,"mappings"),r=s.find(y=>y.attributes?.externalSite==="anilist/anime"),l=s.find(y=>y.attributes?.externalSite==="myanimelist/anime"),a=r?Number(r.attributes.externalId):i?.anilistId??null,o=l?Number(l.attributes.externalId):i?.malId??null;if(!a&&!o)return null;const c=a??-Math.abs(o),u={},d=n.titles??{};u.romaji=d.en_jp||d.en||e.attributes.canonicalTitle,u.english=d.en||d.en_us||u.romaji,u.native=d.ja_jp||d.ja||u.romaji;const f=D(n.startDate),A=D(n.endDate),m=n.posterImage??{},E=n.coverImage??{},b=m.large||m.original||m.medium||m.small||m.tiny||null,I=E.large||E.original,T=L(e,t,"categories").map(y=>y.attributes?.title).filter(Boolean).map(y=>ee(y)),M=n.nextRelease!=null?Math.floor(new Date(n.nextRelease).getTime()/1e3):null;return{id:c,idMal:o,title:u,bannerImage:I||null,coverImage:b?{extraLarge:b,large:b,medium:b}:void 0,description:n.synopsis||n.description||null,genres:T.length>0?T:void 0,averageScore:n.averageRating!=null?Math.round(Number(n.averageRating)):null,format:Q(n.subtype),status:X(n.status),episodes:n.episodeCount??null,duration:n.episodeLength??null,season:f?.month?W[f.month-1]:void 0,seasonYear:f?.year??void 0,startDate:f,endDate:A,isAdult:!!n.nsfw,synonyms:n.abbreviatedTitles??void 0,popularity:n.userCount??void 0,favourites:n.favoritesCount??void 0,nextAiringEpisode:M!=null?{episode:0,timeUntilAiring:M-Math.floor(Date.now()/1e3),airingAt:M}:null}}async function j(e){const t=await g(`/mappings?filter[externalSite]=anilist%2Fanime&filter[externalId]=${e}&include=item`),i=p(t?.data);return i.length===0?null:N(i[0],t?.included??[])?.id??null}const ne={current:"RELEASING",finished:"FINISHED",upcoming:"NOT_YET_RELEASED",unreleased:"NOT_YET_RELEASED",tba:"NOT_YET_RELEASED"};async function ae(e){const t=new Map;for(const i of e)try{const n=await g(`/mappings?filter[externalSite]=anilist%2Fanime&filter[externalId]=${i}&include=item`),s=p(n?.data);if(s.length===0)continue;const r=n?.included??[],a=s[0].relationships?.item?.data;if(!a)continue;const o=Array.isArray(a)?a[0]:a;if(!o)continue;const c=r.find(f=>f.id===o.id&&f.type==="anime");if(!c)continue;const u=c.attributes?.status?.toLowerCase(),d=u?ne[u]??u.toUpperCase():void 0;d&&t.set(i,d)}catch{}return t}async function P(e,t){const[i,n]=await Promise.all([g(`/anime/${e}?include=categories,mappings`),g(`/anime/${e}/anime-productions?include=producer&page[limit]=10`)]);if(!i)return null;const s=p(i.data)[0];if(!s)return null;const r=h(s,i.included??[],t);if(!r)return null;const l=(n?.included??[]).filter(a=>a.type==="producers").map(a=>({id:0,name:a.attributes?.name??""})).filter(a=>a.name.length>0);return l.length>0&&(r.studios={nodes:l}),r}async function oe(e){const t=await j(e);return t?P(t,{anilistId:e}):null}async function ue(e){const t=await g(`/mappings?filter[externalSite]=myanimelist%2Fanime&filter[externalId]=${e}&include=item`),i=p(t?.data);if(i.length===0)return null;const n=N(i[0],t?.included??[]);return n?P(n.id,{malId:e}):null}async function le(e){const t=new Map,i=[...new Set(e.filter(r=>Number.isFinite(r)&&r>0))],n=20,s=[];for(let r=0;r<i.length;r+=n)s.push(i.slice(r,r+n));return await Promise.all(s.map(async r=>{try{const l=await g(`/mappings?filter[externalSite]=myanimelist%2Fanime&filter[externalId]=${r.join(",")}&include=item&page[limit]=${n}`);if(!l)return;const a=new Map;for(const o of l.included??[])o.type==="anime"&&a.set(o.id,o);for(const o of p(l.data)){const c=o.relationships?.item?.data,u=Array.isArray(c)?c[0]:c;if(!u)continue;const d=a.get(u.id)?.attributes?.titles??{},f=d.en||d.en_us||void 0,A=d.ja_jp||void 0;if(!f&&!A)continue;const m=Number(o.attributes?.externalId);!Number.isFinite(m)||m<=0||t.set(m,{english:f,native:A})}}catch{}})),t}async function de(e,t){let i=null;if(e>0&&(i=await j(e)),!i&&t!=null){const a=await g(`/mappings?filter[externalSite]=myanimelist%2Fanime&filter[externalId]=${t}&include=item`),o=p(a?.data);o.length>0&&(i=N(o[0],a?.included??[])?.id??null)}if(!i)return[];const n=[],s=20;let r=0;for(;r<1e3;){const a=await g(`/anime/${i}/episodes?page[limit]=${s}&page[offset]=${r}&sort=number`);if(!a)break;const o=p(a.data);if(o.length===0)break;for(const c of o){const u=c.attributes?.number;typeof u=="number"&&Number.isFinite(u)&&u>0&&n.push(u)}if(o.length<s)break;r+=s}if(n.length===0)return[];const l=Math.max(...n);return Array.from({length:l},(a,o)=>(o+1).toString())}async function ce(e=1,t=20,i){const n=K(i),s=n?`filter[status]=${n}`:"",r=(e-1)*t,l=await g(`/anime?${s}${s?"&":""}sort=-userCount&page[limit]=${t}&page[offset]=${r}&include=mappings,categories`);return l?_(p(l.data).map(a=>h(a,l.included??[])).filter(a=>a!==null)):[]}async function fe(e=1,t=20){const i=(e-1)*t,n=await g(`/anime?sort=-userCount&page[limit]=${t}&page[offset]=${i}&include=mappings,categories`);return n?_(p(n.data).map(s=>h(s,n.included??[])).filter(s=>s!==null).filter(s=>!!s.bannerImage)):[]}async function me(e="TV",t=1,i=12){const n=t*i,s=e&&e!=="ALL"?`filter[subtype]=${e.toLowerCase()}`:"",r=[];let l=0;const a=20;for(;r.length<n&&l<500;){const c=await g(`/anime?filter[status]=current${s?`&${s}`:""}&sort=-startDate&page[limit]=${a}&page[offset]=${l}&include=mappings,categories`);if(!c)break;const u=p(c.data);if(u.length===0)break;for(const d of u){const f=h(d,c.included??[]);f&&r.push(f)}l+=a}r.sort((c,u)=>{const d=c.nextAiringEpisode?.airingAt??Number.MAX_SAFE_INTEGER,f=u.nextAiringEpisode?.airingAt??Number.MAX_SAFE_INTEGER;if(d!==f)return d-f;const A=k(c),m=k(u);return A!==m?m-A:(u.popularity??0)-(c.popularity??0)});const o=(t-1)*i;return _(r.slice(o,o+i))}function k(e){return(e.startDate?.year??0)*1e4+(e.startDate?.month??0)*100+(e.startDate?.day??0)}async function ge(e,t,i,n=1,s=14){const r=[`filter[season]=${U(e)}`,`filter[seasonYear]=${t}`];i&&i!=="ALL"&&i!=="ADULT"&&r.push(`filter[subtype]=${i.toLowerCase()}`);const l=(n-1)*s,a=await g(`/anime?${r.join("&")}&sort=-userCount&page[limit]=${s}&page[offset]=${l}&include=mappings,categories`);return a?_(p(a.data).map(o=>h(o,a.included??[])).filter(o=>o!==null)):[]}export{_ as dedupeKitsuMedia,ae as kitsuBatchGetStatuses,de as kitsuEpisodes,me as kitsuLatestReleases,oe as kitsuMetaByAnilistId,ue as kitsuMetaByMalId,se as kitsuSearchAnime,ge as kitsuSeasonal,fe as kitsuSpotlight,le as kitsuTitlesByMalIds,ce as kitsuTrending};
@@ -1 +0,0 @@
1
- import{XMLParser as D}from"fast-xml-parser";import{searchAnilistByTitle as j,isAnilistRateLimited as E,getShowMetaById as O}from"./anilist.js";import{kitsuSearchAnime as R}from"./kitsu.js";import{offlineDb as V}from"./offline-db.js";import{SettingsRepository as v}from"../repositories/settings.repository.js";import{ShowsMetaRepository as x}from"../repositories/shows-meta.repository.js";import{performWriteTransactionAsync as N}from"../sync.js";import B from"../logger.js";let u={running:!1,current:0,total:0,phase:"done",source:null},L=!1;function $(){return u}function tt(){return u.running?(L=!0,{success:!0,message:"Import cancellation requested"}):{success:!0,message:"No import running"}}function et(t,b){if(u.running)return{ok:!1,status:409,body:{error:"An import is already in progress"}};if(!t)return{ok:!1,status:400,body:{error:"No file"}};const{erase:a,useOfflineDb:c,skipFallback:p}=b,h=c!=="false"&&c!==!1&&c!=="0",y=p==="true"||p===!0||p==="1";let s;try{s=q.parse(t.toString())}catch{return{ok:!1,status:400,body:{error:"Invalid XML"}}}const l=s?.myanimelist?.anime||[];return l.length===0?{ok:!1,status:400,body:{error:"No anime found in XML"}}:(L=!1,u={running:!0,current:0,total:l.length,phase:"offline",source:"offline"},{ok:!0,import:{animeList:l,erase:a,offlineEnabled:h,shouldSkipFallback:y}})}async function nt(t,b,a){const{animeList:c,erase:p,offlineEnabled:h,shouldSkipFallback:y}=b,s=c.length;let l=0;const o=[],C=[],k=[];for(const r of c){const e=I(r.series_title),g=X(I(r.my_status));let T=!1;if(h){const m=parseInt(I(r.series_animedb_id),10);if(m&&!Number.isNaN(m)){const i=V.getByMalId(m);if(i){T=!0;const M=String(i.anilistId),n=i.title||e;o.push({id:M,name:n,thumbnail:i.thumbnail,status:g}),(i.thumbnail||i.type||i.genres)&&C.push({id:M,thumbnail:i.thumbnail,type:i.type,genres:i.genres}),u={running:!0,current:o.length,total:s,phase:"offline",source:"offline",title:e,matchedTitle:n,status:g,found:!0,imported:o.length},a("progress",{current:o.length,total:s,title:e,matchedTitle:n,status:g,source:"offline",found:!0})}}}T||k.push(r)}if(o.length>0||p)try{await N(t,async r=>{p&&await v.clearWatchlist(r),await v.upsertWatchlistBatch(r,o);for(const e of C)(e.thumbnail||e.type||e.genres)&&await x.upsert(r,{id:e.id,thumbnail:e.thumbnail,type:e.type,genres:e.genres})})}catch(r){B.error({err:r},"Failed to commit offline import matches")}let w=0;if(y||L)l+=k.length;else if(k.length>0){u={...u,phase:"fallback",source:null};const r=5;for(let e=0;e<k.length;e+=r){if(L){l+=k.length-e;break}const g=k.slice(e,e+r),T=await Promise.allSettled(g.map(n=>H(I(n.series_title)))),m=[],i=[],M=[];if(T.forEach((n,f)=>{const S=I(g[f].series_title),A=X(I(g[f].my_status)),P=o.length+w+f+1;if(n.status==="fulfilled"&&n.value){const d=n.value,_=d.title?.english||d.title?.romaji||S;m.push({id:String(d.id),name:_,status:A}),u={running:!0,current:P,total:s,phase:"fallback",source:d.source,title:S,matchedTitle:_,status:A,found:!0,imported:o.length+w+1},a("progress",{current:P,total:s,title:S,matchedTitle:_,status:A,source:d.source,found:!0}),M.push(O(String(d.id)).then(F=>{F&&i.push({id:String(d.id),thumbnail:F.thumbnail||void 0,type:F.type||void 0})}).catch(()=>{}))}else l++,u={running:!0,current:P,total:s,phase:"fallback",source:null,title:S,matchedTitle:null,status:A,found:!1,imported:o.length+w},a("progress",{current:P,total:s,title:S,matchedTitle:null,status:A,source:null,found:!1})}),await Promise.allSettled(M),m.length>0)try{await N(t,async n=>{await v.upsertWatchlistBatch(n,m);for(const f of i)f.thumbnail&&await x.upsert(n,{id:f.id,thumbnail:f.thumbnail,type:f.type})}),w+=m.length}catch(n){B.error({err:n},"Failed to commit fallback import batch"),l+=m.length}}}const W=o.length+w;u={running:!1,current:s,total:s,phase:"done",source:null,imported:W,skipped:l},a("complete",{imported:W,skipped:l})}function I(t){return Array.isArray(t)?String(t[0]??""):String(t??"")}function X(t){switch(t){case"Plan to Watch":return"Planned";case"On Hold":return"On-Hold";default:return t}}const q=new D({isArray:t=>t==="anime",parseTagValue:!1,trimValues:!0});async function H(t){const b=await j(t);if(b)return{...b,source:"anilist"};if(E()){B.debug({title:t},"AniList rate limited, trying Kitsu fallback for MAL import");const a=await R({query:t,page:1,perPage:5});if(a.length>0){const c=t.toLowerCase(),h=a.find(y=>y.title?.romaji?.toLowerCase()===c||y.title?.english?.toLowerCase()===c)||a[0];if(h.id>0)return{id:h.id,title:h.title??{},source:"kitsu"}}}return null}export{nt as executeMalImport,$ as getMalImportStatus,et as prepareMalImport,tt as requestMalImportCancel};
@@ -1 +0,0 @@
1
- import{kitsuTitlesByMalIds as st}from"./kitsu.js";import{parseJsonBody as B}from"../utils/http.utils.js";const P="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",at=1500,H=25e3,D=6*3600;let Q=0;async function Y(){const t=at-(Date.now()-Q);t>0&&await new Promise(e=>setTimeout(e,t)),Q=Date.now()}async function I(t){await Y();const e=await fetch(t,{headers:{"User-Agent":P,Accept:"text/html,application/xhtml+xml"},signal:AbortSignal.timeout(H),redirect:"follow"}),n=await e.text();return{status:e.status,html:n,url:e.url}}function g(t){return t.replace(/&#(\d+);/g,(e,n)=>String.fromCharCode(Number(n))).replace(/&#x([0-9a-fA-F]+);/g,(e,n)=>String.fromCharCode(parseInt(n,16))).replace(/&amp;/g,"&").replace(/&quot;/g,'"').replace(/&#0?39;|&apos;/g,"'").replace(/&lt;/g,"<").replace(/&gt;/g,">").trim()}function W(t){const e=t.match(/\bdata-src=["']([^"']+)["']/i)||t.match(/\bsrc=["']((?!data:)[^"']+)["']/i);if(!e)return null;const n=e[1].replace(/&amp;/g,"&"),r=n.match(/[?&]src=([^&]+)/i);let i=n;if(r)try{i=decodeURIComponent(r[1])}catch{i=r[1]}return/^https:\/\/cdn\.myanimelist\.net\/r\/\d+x\d+\//i.test(i)&&(i=i.replace(/\/r\/\d+x\d+\//i,"/").split("?")[0]),i}function M(t){if(!t)return null;const e=t.replace(/,/g,"").trim();if(/^(unknown|n\/a|-)$/i.test(e))return null;const n=Number(e);return Number.isFinite(n)?n:null}function lt(t){const e=t.match(/id="sarea(\d+)"/);if(!e)return null;const n=Number(e[1]),r=t.match(new RegExp(`href="(https://myanimelist\\.net/anime/${n}/[^"]*)"`,"i"))?.[1],i=g(t.match(/<strong>([^<]+)<\/strong>/i)?.[1]??"");if(!n||!i)return null;const a=t.match(/class="pt4">([\s\S]*?)<a\s/i)?.[1]??null,s=a&&g(a).replace(/\s*read more\.?$/i,"").replace(/\.{3}$/,"").trim()||null,o=[...t.matchAll(/<td class="borderClass ac bgColor\d"[^>]*>\s*([\s\S]*?)\s*<\/td>/gi)].map(c=>g(c[1]));return{id:-n,idMal:n,url:r??`https://myanimelist.net/anime/${n}`,title:i,imageUrl:W(t),synopsis:s,type:o[0]||null,episodes:M(o[1]),score:M(o[2])}}function U(t){if(!t.includes("Anime - MyAnimeList.net"))return[];const e=[];for(const n of t.split("<tr>").slice(1)){const r=lt(n);r&&!e.some(i=>i.idMal===r.idMal)&&e.push(r)}return e}function $(t,e){const n=t.match(new RegExp(`<meta\\s+property=["']${e}["']\\s+content=["']([^"']*)["']`,"i"));return n?g(n[1]):null}function E(t,e){const n=t.match(new RegExp(`<span class="dark_text">${e}:</span>\\s*([^<]*?)(?:<|$)`,"i"));return n&&g(n[1]).trim()||null}function G(t,e){const n=$(t,"og:title")||g(t.match(/<h1[^>]*>([^<]+)<\/h1>/i)?.[1]??"");if(!n||/^(404\b|not found\b)/i.test(n.trim()))return null;const r=$(t,"og:image"),i=r&&!/apple-touch-icon|404|error/i.test(r)?r:null,a=[...t.matchAll(/\/anime\/genre\/\d+\/[^"']*"[^>]*>([^<]+)</gi)].map(s=>g(s[1]).trim()).filter((s,o,c)=>s&&c.indexOf(s)===o).slice(0,10);return{id:-e,idMal:e,url:$(t,"og:url")||`https://myanimelist.net/anime/${e}`,title:n,titleEnglish:E(t,"English"),titleJapanese:E(t,"Japanese"),imageUrl:i,synopsis:$(t,"og:description"),type:E(t,"Type"),episodes:M(E(t,"Episodes")),status:E(t,"Status"),score:M(E(t,"Score")),rating:E(t,"Rating"),genres:a}}function ot(t){return{id:t.id,idMal:t.idMal,url:t.url,title:t.title,imageUrl:t.imageUrl,synopsis:t.synopsis,type:t.type,episodes:t.episodes,score:t.score}}function ct(t){if(!t)return;const e=t.toUpperCase().replace(/\s+/g,"_");return e.includes("FINISHED")?"FINISHED":e.includes("CURRENTLY_AIRING")||e==="AIRING"?"RELEASING":e.includes("NOT_YET_AIRED")||e==="UPCOMING"?"NOT_YET_RELEASED":e}function O(t){return{id:t.id,idMal:t.idMal,title:{romaji:t.title,english:t.title,native:t.title},coverImage:t.imageUrl?{extraLarge:t.imageUrl,large:t.imageUrl,medium:t.imageUrl}:void 0,description:t.synopsis,averageScore:t.score!=null?Math.round(t.score*10):null,format:t.type?.toUpperCase(),episodes:t.episodes,siteUrl:t.url}}function ut(t){const e=t.rating||"";return{id:t.id,idMal:t.idMal,title:{romaji:t.title,english:t.titleEnglish||t.title,native:t.titleJapanese||t.title},coverImage:t.imageUrl?{extraLarge:t.imageUrl,large:t.imageUrl,medium:t.imageUrl}:void 0,description:t.synopsis,genres:t.genres.length>0?t.genres:void 0,averageScore:t.score!=null?Math.round(t.score*10):null,format:t.type?.toUpperCase(),status:ct(t.status),episodes:t.episodes,isAdult:e.includes("Rx")||e.includes("R+"),siteUrl:t.url}}async function Rt(t,e,n=1){const r=`mal:search:anime:${e.toLowerCase()}:page:${n}`,i=t.get(r);if(i&&i.fresh)return{entries:JSON.parse(i.payload),cached:!0,ms:0};const a=Date.now(),s=(n-1)*50,o=new URLSearchParams({q:e,cat:"anime"});s>0&&o.set("show",String(s));const c=await I(`https://myanimelist.net/anime.php?${o.toString()}`),l=Date.now()-a;if(c.status!==200){if(i)return{entries:JSON.parse(i.payload),cached:!0,ms:l};throw new Error(`MAL search failed with status ${c.status}`)}const u=c.url.match(/myanimelist\.net\/anime\/(\d+)/i);if(u&&!c.html.includes("Search Anime - MyAnimeList.net")){const f=G(c.html,Number(u[1])),y=f?[ot(f)]:[];return y.length>0&&t.put(r,JSON.stringify(y),D),{entries:y,cached:!1,ms:l}}const d=U(c.html);return t.put(r,JSON.stringify(d),D),{entries:d,cached:!1,ms:l}}async function xt(t,e){const n=`mal:detail:anime:${e}`,r=t.get(n);if(r&&r.fresh)return{detail:JSON.parse(r.payload),cached:!0,ms:0};const i=Date.now(),{status:a,html:s}=await I(`https://myanimelist.net/anime/${e}`),o=Date.now()-i;if(a!==200){if(r)return{detail:JSON.parse(r.payload),cached:!0,ms:o};throw new Error(`MAL detail failed with status ${a}`)}const c=G(s,e);return c&&t.put(n,JSON.stringify(c),D),{detail:c,cached:!1,ms:o}}const mt={tv:"1",ova:"2",movie:"3",special:"4",ona:"5",music:"6"},dt={airing:"1",complete:"2",finished:"2",upcoming:"3",hiatus:"3",cancelled:void 0,not_yet_released:"3"},pt={start_date:"2",score:"3",episodes:"4",end_date:"5",type:"6",members:"7",rating:"8",mal_id:"9"},gt={POPULARITY_DESC:"members",SCORE_DESC:"score",TRENDING_DESC:"members",START_DATE_DESC:"start_date",END_DATE_DESC:"end_date",EPISODES_DESC:"episodes",FAVOURITES_DESC:"members",UPDATED_AT_DESC:"start_date"},ft={WINTER:[1,2,3],SPRING:[4,5,6],SUMMER:[7,8,9],FALL:[10,11,12]},ht={WINTER:"01",SPRING:"04",SUMMER:"07",FALL:"10"},z={jan:1,january:1,feb:2,february:2,mar:3,march:3,apr:4,april:4,may:5,jun:6,june:6,jul:7,july:7,aug:8,august:8,sep:9,sept:9,september:9,oct:10,october:10,nov:11,november:11,dec:12,december:12},X=["monday","tuesday","wednesday","thursday","friday","saturday","sunday"],yt={0:"Unknown",1:"TV",2:"OVA",3:"Movie",4:"Special",5:"ONA",9:"TV Special"};async function St(t,e){const n=t.get("taxonomy:anime:genres");let r;if(n&&n.fresh)r=JSON.parse(n.payload);else{const{status:a,html:s}=await I("https://myanimelist.net/anime.php?cat=genre");if(a!==200)if(n)r=JSON.parse(n.payload);else return null;else{r={};for(const o of s.matchAll(/\/anime\/genre\/(\d+)\/[^"']*"[^>]*>([^<]+)</gi))r[g(o[2]).trim().toLowerCase()]=Number(o[1]);t.put("taxonomy:anime:genres",JSON.stringify(r),24*3600)}}const i=[];for(const a of e.split(",")){const s=a.trim().toLowerCase();if(!s)continue;if(/^\d+$/.test(s)){i.push(parseInt(s,10));continue}const o=r[s];if(o==null)return null;i.push(o)}return i.length>0?[...new Set(i)].join(","):null}function _t(t){const e=t.match(/<a href="https:\/\/myanimelist\.net\/anime\/(\d+)\/[^"]*" class="link-title">([^<]+)<\/a>/i);if(!e)return null;const n=t.slice(0,200).match(/js-anime-type-(\d+)/)?.[1];return{malId:Number(e[1]),title:g(e[2]),imageUrl:W(t),score:M(t.match(/class="js-score">([\d.]+)<\/span>/i)?.[1]??null),type:n===void 0?null:yt[n]??null,episodes:M(t.match(/<span>(\d+)\s*eps?<\/span>/i)?.[1]??null),startDate:t.match(/class="js-start_date">(\d+)<\/span>/i)?.[1]??null,members:M(t.match(/class="js-members">(\d+)<\/span>/i)?.[1]??null),genreIds:(t.match(/data-genre="([\d,]+)"/i)?.[1]??"").split(",").map(r=>Number(r)).filter(r=>Number.isFinite(r)&&r>0)}}function tt(t){const e=new Map;for(const n of t.split("js-seasonal-anime").slice(1)){const r=_t(n);r&&e.set(r.malId,r)}return[...e.values()]}function Mt(t){const e=t?.match(/^(\d{4})(\d{2})(\d{2})/);if(e)return{year:Number(e[1]),month:Number(e[2]),day:Number(e[3])};const n=t?.match(/([A-Za-z]+)\s+(\d{4})/);if(n){const r=z[n[1].toLowerCase()];if(r)return{year:Number(n[2]),month:r}}return{}}function et(t){const e=Mt(t.startDate),n=t.imageUrl||void 0;return{id:-t.malId,idMal:t.malId,title:{romaji:t.title,english:t.title,native:t.title},coverImage:n?{extraLarge:n,large:n,medium:n}:void 0,description:null,averageScore:t.score!=null?Math.round(t.score*10):null,format:t.type?.toUpperCase(),episodes:t.episodes,seasonYear:e.year,startDate:e,popularity:t.members??void 0,isAdult:t.genreIds.includes(12)}}function q(t,e,n){const r=t.get(e);return r&&r.fresh?Promise.resolve({html:r.payload,cached:!0}):I(n).then(({status:i,html:a})=>{if(i!==200){if(r)return{html:r.payload,cached:!0};throw new Error(`MAL fetch failed with status ${i} for ${n}`)}return t.put(e,a,D),{html:a,cached:!1}})}const At=168*3600,Et=168*3600,bt=1e4,wt=10,Tt="mal:en-title:";function Lt(t){const e=t.title?.romaji?.trim()??"",n=t.title?.english?.trim()??"";return!!t.idMal&&(!n||!!e&&n===e)}function nt(t,e){const n=(t??"").trim();return!n||/^(unknown|n\/a|-)$/i.test(n)||e&&n.toLowerCase()===e.trim().toLowerCase()?null:n}async function Pt(t,e,n=wt){const r=e.filter(Lt).slice(0,Math.max(0,n));return r.length===0||await Promise.all(r.map(async i=>{const a=i.idMal,s=`${Tt}${a}`;try{const o=t.get(s);if(o&&typeof o.payload=="string"){const f=nt(o.payload,i.title?.romaji);f&&i.title&&(i.title={...i.title,english:f});return}const c=await fetch(`https://myanimelist.net/anime/${a}`,{headers:{"User-Agent":P,Accept:"text/html,application/xhtml+xml"},signal:AbortSignal.timeout(bt),redirect:"follow"});if(c.status!==200)return;const l=await c.text(),u=E(l,"English");try{t.put(s,(u??"").trim(),Et)}catch{}const d=nt(u,i.title?.romaji);if(!d)return;i.title&&(i.title={...i.title,english:d})}catch{}})),e}async function v(t,e){const n=new Map,r=[];for(const a of e){if(!a.idMal)continue;const s=t.get(`mal:title:${a.idMal}`);if(s)try{n.set(a.idMal,JSON.parse(s.payload));continue}catch{}r.push(a.idMal)}let i=new Map;if(r.length>0){try{i=await st(r)}catch{}for(const[a,s]of i)t.put(`mal:title:${a}`,JSON.stringify(s),At)}for(const a of e){if(!a.idMal)continue;const s=n.get(a.idMal)??i.get(a.idMal);(s?.english||s?.native)&&(a.title={romaji:a.title?.romaji,english:s.english??a.title?.english,native:s.native??a.title?.native})}return e}async function Ct(t,e={},n=!0){const{query:r,page:i=1,format:a,status:s,season:o,seasonYear:c,genre:l,genre_in:u,genre_not_in:d,averageScore_greater:f,episodes_greater:y,isAdult:A,sort:b}=e,p=new URLSearchParams,T=r?.trim(),L=!!T;T&&p.set("q",T),p.set("cat","anime"),i>1&&p.set("show",String((i-1)*50));const C=A?a:void 0;if(a&&a!=="ALL"&&a!=="ADULT"&&!A){const m=mt[a.toLowerCase()];m&&p.set("type",m)}if(s){const m=dt[s.toLowerCase()];m&&p.set("status",m)}const w=[...Array.isArray(l)?l:typeof l=="string"?l.split(","):[],...u??[]].map(m=>m.trim()).filter(Boolean).join(",");if(w){const m=await St(t,w);if(!m)return[];for(const h of m.split(","))p.append("genre[]",h)}if(c){const m=o&&o!=="ALL"?ht[o.toUpperCase()]??"01":"01";p.set("sm",String(Number(m))),p.set("sd","1"),p.set("sy",String(c))}f&&p.set("score",String(f)),A&&(p.set("r","6"),p.append("genre[]","12"));const N=b?gt[b]||"members":void 0,R=b&&b.includes("ASC")?"2":"1";N&&(p.set("o",pt[N]),p.set("w",R));const J=`https://myanimelist.net/anime.php?${p.toString()}`,_=`mal:search:${p.toString()}:page:${i}`,S=m=>n?v(t,m):Promise.resolve(m);let F=!1,K;try{const m=t.get(_);if(m&&m.fresh)return S(j(U(m.payload).map(O),{genre_not_in:d,episodes_greater:y,format:C,seasonYear:c,season:o,sort:F?void 0:b,hasQuery:L}));const h=await I(J);if(h.status!==200){if(m)return S(j(U(m.payload).map(O),{genre_not_in:d,episodes_greater:y,format:C,seasonYear:c,season:void 0,sort:void 0,hasQuery:L}));throw new Error(`MAL search failed with status ${h.status}`)}const x=h.url.match(/myanimelist\.net\/anime\/(\d+)/i);if(x&&!h.html.includes("Search Anime - MyAnimeList.net")){const V=G(h.html,Number(x[1])),Z=V?[ut(V)]:[];return t.put(_,JSON.stringify(Z.map(it=>it.idMal))),Z}F=!!N,t.put(_,h.html,D),K=h.html}catch{const m=t.get(_);if(m){try{const h=JSON.parse(m.payload);if(Array.isArray(h)&&h.every(x=>typeof x=="number"))return[]}catch{}return S(j(U(m.payload).map(O),{genre_not_in:d,episodes_greater:y,format:C,seasonYear:c,season:void 0,sort:void 0,hasQuery:L}))}return[]}return S(j(U(K).map(O),{genre_not_in:d,episodes_greater:y,format:C,seasonYear:c,season:o,sort:F?void 0:b,hasQuery:L}))}function j(t,e){let n=t;if(e.format&&e.format!=="ALL"&&e.format!=="ADULT"){const r=e.format.toUpperCase();n=n.filter(i=>(i.format??"").toUpperCase()===r)}if(e.episodes_greater!=null&&(n=n.filter(r=>(r.episodes??0)>e.episodes_greater)),e.seasonYear&&!e.hasQuery&&(n=n.filter(r=>r.startDate?.year===e.seasonYear)),e.season&&e.season!=="ALL"&&!e.hasQuery){const r=ft[e.season.toUpperCase()]??[];n=n.filter(i=>i.startDate?.month!=null&&r.includes(i.startDate.month))}if(e.sort&&!e.sort.startsWith("POPULARITY_")&&!e.sort.startsWith("TRENDING_")&&!e.sort.startsWith("FAVOURITES_")&&!e.sort.startsWith("START_DATE_")&&!e.sort.startsWith("END_DATE_")){const i=!e.sort.includes("ASC")?-1:1;if(e.sort.startsWith("SCORE_")){const a=s=>s.averageScore??s.meanScore??-1;n=[...n].sort((s,o)=>(a(s)-a(o))*i)}else e.sort.startsWith("EPISODES_")&&(n=[...n].sort((a,s)=>((a.episodes??-1)-(s.episodes??-1))*i))}return n}async function $t(t,e){const r=(await Ct(t,{query:e,page:1,perPage:5},!1))[0];return r?.idMal?`mal-${r.idMal}`:null}async function Ot(t,e,n=1){const r=(n-1)*50,i=new URLSearchParams;e&&i.set("type",e),r>0&&i.set("limit",String(r));const a=i.toString(),s=a?`https://myanimelist.net/topanime.php?${a}`:"https://myanimelist.net/topanime.php",o=`mal:top:${e??"all"}:page:${n}`,{html:c}=await q(t,o,s),l=[];for(const u of c.matchAll(/<tr class="ranking-list">([\s\S]*?)<\/tr>/gi)){const d=u[1],f=Number(d.match(/href="https:\/\/myanimelist\.net\/anime\/(\d+)\//i)?.[1]);if(!f)continue;const y=d.match(/<img[^>]*alt="Anime: ([^"]+)"[^>]*>/i),A=y?g(y[1]):"";if(!A)continue;const p=(d.match(/<div class="information[^"]*">([\s\S]*?)<\/div>/i)?.[1]??"").split(/<br\s*\/?>/i).map(S=>g(S)),T=p[0]??"",L=p[1]??"",C=p[2]??"",w=L.match(/([A-Za-z]+)\s+(\d{4})/),N=w?z[w[1].toLowerCase()]:void 0,R=M(d.match(/class="text on score-label[^"]*">([\d.]+)<\/span>/i)?.[1]??null);l.push({id:-f,idMal:f,title:{romaji:A,english:A,native:A},coverImage:void 0,description:null,averageScore:R!=null?Math.round(R*10):null,format:T.match(/^(\S+)/)?.[1]?.toUpperCase(),episodes:(()=>{const S=T.match(/\((\d+)/);return S?Number(S[1]):null})(),startDate:N!=null&&w?{year:Number(w[2]),month:N}:void 0,popularity:M(C.match(/([\d,]+)/)?.[1]??null)??void 0});const J=d.match(/<img[^>]*>/i)?.[0]??"",_=W(J);if(_){const S=l[l.length-1];S.coverImage={extraLarge:_,large:_,medium:_}}}return v(t,l)}async function vt(t,e,n,r,i=1,a=14){const s=`mal:season:${n}:${e.toLowerCase()}`,{html:o}=await q(t,s,`https://myanimelist.net/anime/season/${n}/${e.toLowerCase()}`);let c=tt(o).map(et);if(r&&r!=="ALL"&&r!=="ADULT"){const u=r.toUpperCase();c=c.filter(d=>(d.format??"").toUpperCase()===u)}c=[...c].sort((u,d)=>(d.popularity??0)-(u.popularity??0));const l=(i-1)*a;return v(t,c.slice(l,l+a))}async function Nt(t){const e="mal:schedule:week",{html:n}=await q(t,e,"https://myanimelist.net/anime/season/schedule"),r={};for(const i of n.split('class="anime-header">').slice(1)){const a=i.slice(0,40).match(/^([A-Za-z]+)</)?.[1]?.toLowerCase();!a||!X.includes(a)||(r[a]=tt(i).map(et))}return r}const k=300,rt=100;async function jt(t){const e=t.trim();if(!e||/[^A-Za-z0-9_-]/.test(e))throw new Error("Invalid MAL username");const n=[];let r=0;for(let i=0;i<rt;i++){const a=`https://myanimelist.net/animelist/${encodeURIComponent(e)}/load.json?status=7&offset=${r}`;await Y();const s=await fetch(a,{headers:{"User-Agent":P,Accept:"application/json"},signal:AbortSignal.timeout(H),redirect:"manual"});if(s.status===400||s.status===404)throw new Error(`MAL user "${e}" not found`);if(s.status===403||s.status===429)throw new Error(`MAL blocked the request (HTTP ${s.status}) \u2014 try again later`);if(s.status>=300&&s.status<400)throw new Error(`MAL list for "${e}" is private or unavailable`);if(!s.ok)throw new Error(`MAL list fetch failed (HTTP ${s.status})`);const o=await B(s);if(!Array.isArray(o)||o.length===0)break;for(const c of o){const l=c,u=Number(l.anime_id);if(!u)continue;const d=typeof l.anime_title=="string"&&l.anime_title.trim()!==""?l.anime_title:typeof l.anime_title_eng=="string"?l.anime_title_eng:"";n.push({malId:u,title:g(d),titleEnglish:typeof l.anime_title_eng=="string"&&l.anime_title_eng.trim()!==""?g(l.anime_title_eng):null,status:Number(l.status)||0,score:Number(l.score)||0,watchedEpisodes:Number(l.num_watched_episodes)||0,totalEpisodes:Number(l.anime_num_episodes)||0,type:typeof l.anime_media_type_string=="string"?l.anime_media_type_string:null,imageUrl:typeof l.anime_image_path=="string"?l.anime_image_path:null})}if(o.length<k)break;r+=k}return n}function kt(t){switch(t){case 1:return"Reading";case 2:return"Completed";case 3:return"On-Hold";case 4:return"Dropped";case 6:return"Planned";default:return null}}async function Jt(t){const e=t.trim();if(!e||/[^A-Za-z0-9_-]/.test(e))throw new Error("Invalid MAL username");const n=[];let r=0;for(let i=0;i<rt;i++){const a=`https://myanimelist.net/mangalist/${encodeURIComponent(e)}/load.json?status=7&offset=${r}`;await Y();const s=await fetch(a,{headers:{"User-Agent":P,Accept:"application/json"},signal:AbortSignal.timeout(H),redirect:"manual"});if(s.status===400||s.status===404)throw new Error(`MAL user "${e}" not found`);if(s.status===403||s.status===429)throw new Error(`MAL blocked the request (HTTP ${s.status}) \u2014 try again later`);if(s.status>=300&&s.status<400)throw new Error(`MAL list for "${e}" is private or unavailable`);if(!s.ok)throw new Error(`MAL list fetch failed (HTTP ${s.status})`);const o=await B(s);if(!Array.isArray(o)||o.length===0)break;for(const c of o){const l=c,u=Number(l.manga_id);if(!u)continue;const d=typeof l.manga_title=="string"&&l.manga_title.trim()!==""?l.manga_title:typeof l.manga_title_eng=="string"?l.manga_title_eng:"";n.push({malId:u,title:g(d),titleEnglish:typeof l.manga_title_eng=="string"&&l.manga_title_eng.trim()!==""?g(l.manga_title_eng):null,status:Number(l.status)||0,chaptersRead:Number(l.num_read_chapters??l.num_chapters_read)||0})}if(o.length<k)break;r+=k}return n}async function Ft(t,e="TV",n=1,r=12){const i=await Nt(t),a=new Map;for(const l of X)for(const u of i[l]??[])a.has(u.id)||a.set(u.id,u);let s=[...a.values()];if(e&&e!=="ALL"){const l=e.toUpperCase();s=s.filter(u=>(u.format??"").toUpperCase()===l)}const o=l=>(l.startDate?.year??0)*1e4+(l.startDate?.month??0)*100+(l.startDate?.day??0);s.sort((l,u)=>o(u)-o(l)||(u.popularity??0)-(l.popularity??0));const c=(n-1)*r;return v(t,s.slice(c,c+r))}export{Tt as EN_TITLE_CACHE_PREFIX,v as enrichCardTitles,Pt as enrichEnglishFromMalDetail,Jt as fetchMalMangaList,jt as fetchMalUserList,xt as malAnimeDetail,Ft as malLatestReleases,Nt as malScheduleWeek,Rt as malSearchAnime,Ct as malSearchMedia,$t as malSearchTitle,vt as malSeasonal,Ot as malTop,kt as mapMalMangaStatusCode,G as parseDetail,U as parseSearchResults,ut as toAnilistDetailMedia,O as toAnilistSearchMedia};
@@ -1 +0,0 @@
1
- import{performWriteTransactionAsync as I}from"../sync.js";import{searchAnilistByTitle as D,getShowMetaById as f}from"./anilist.js";import{isTempShowId as p}from"./temp-ids.js";import{WatchlistRepository as d}from"../repositories/watchlist.repository.js";import{ShowsMetaRepository as l}from"../repositories/shows-meta.repository.js";import{dbGet as m,dbRun as S}from"../utils/db-utils.js";import h from"../logger.js";const w=new Map;async function O(a,i){const u=m(a,"SELECT anilistId FROM shows_meta WHERE id = ? AND anilistId IS NOT NULL",[i]);let t;if(u?.anilistId!=null&&String(u.anilistId)!==i)t=u.anilistId;else if(!u&&await d.exists(a,i)){const s=await f(i);s?.anilistId&&s.anilistId!==parseInt(i)&&(t=s.anilistId)}if(t){const s=String(t);if(await d.exists(a,i))return h.info({aliasId:i,canonicalId:s},"Migrating watchlist from MAL alias to canonical ID"),await I(a,async e=>{const E=await d.getById(e,i),o=await l.getById(e,i);e.run("UPDATE OR IGNORE watchlist SET id = ?, thumbnail = ? WHERE id = ?",[s,E?.thumbnail||"",i]),e.run("DELETE FROM watchlist WHERE id = ?",[i]),e.run("UPDATE OR IGNORE shows_meta SET id = ?, anilistId = ?, thumbnail = ? WHERE id = ?",[s,t,o?.thumbnail||"",i]),e.run("DELETE FROM shows_meta WHERE id = ?",[i]),e.run("UPDATE OR IGNORE watched_episodes SET showId = ? WHERE showId = ?",[s,i]),e.run("DELETE FROM watched_episodes WHERE showId = ?",[i]),e.run("UPDATE OR IGNORE queue SET showId = ? WHERE showId = ?",[s,i]),e.run("DELETE FROM queue WHERE showId = ?",[i]),e.run("UPDATE OR IGNORE dismissed_notifications SET showId = ? WHERE showId = ?",[s,i]),e.run("DELETE FROM dismissed_notifications WHERE showId = ?",[i]),e.run("UPDATE OR IGNORE discovered_notifications SET showId = ? WHERE showId = ?",[s,i]),e.run("DELETE FROM discovered_notifications WHERE showId = ?",[i]),e.run("INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)",[i,s])}),s;if(await d.exists(a,s)){const e=m(a,"SELECT numericId FROM legacy_id_mapping WHERE legacyId = ?",[i]);return(!e||e.numericId!==s)&&(S(a,"INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)",[i,s]),h.info({aliasId:i,canonicalId:s},"Mapped numeric alias to canonical ID")),s}}if(await d.exists(a,i))return i;let n;const R=m(a,"SELECT legacyId FROM legacy_id_mapping WHERE numericId = ? AND legacyId IN (SELECT id FROM watchlist)",[i]);if(R&&(n=R.legacyId),!n&&t){const s=m(a,"SELECT id FROM shows_meta WHERE anilistId = ? AND id IN (SELECT id FROM watchlist)",[t]);s&&(n=s.id)}if(!n)return i;h.info({legacyId:n,numericId:i},"Consolidating watchlist entry from legacy to numeric ID");const c=parseInt(i);return await I(a,async s=>{const e=await d.getById(s,n),E=await l.getById(s,n);s.run("UPDATE OR IGNORE watchlist SET id = ?, thumbnail = ? WHERE id = ?",[i,e?.thumbnail||"",n]),s.run("DELETE FROM watchlist WHERE id = ?",[n]),s.run("UPDATE OR IGNORE shows_meta SET id = ?, anilistId = ?, thumbnail = ? WHERE id = ?",[i,c,E?.thumbnail||"",n]),s.run("DELETE FROM shows_meta WHERE id = ?",[n]),s.run("UPDATE OR IGNORE watched_episodes SET showId = ? WHERE showId = ?",[i,n]),s.run("DELETE FROM watched_episodes WHERE showId = ?",[n]),s.run("UPDATE OR IGNORE queue SET showId = ? WHERE showId = ?",[i,n]),s.run("DELETE FROM queue WHERE showId = ?",[n]),s.run("UPDATE OR IGNORE dismissed_notifications SET showId = ? WHERE showId = ?",[i,n]),s.run("DELETE FROM dismissed_notifications WHERE showId = ?",[n]),s.run("UPDATE OR IGNORE discovered_notifications SET showId = ? WHERE showId = ?",[i,n]),s.run("DELETE FROM discovered_notifications WHERE showId = ?",[n]),s.run("INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)",[n,i])}),i}async function g(a,i){if(/^\d+$/.test(i))return O(a,i);try{const t=m(a,"SELECT numericId FROM legacy_id_mapping WHERE legacyId = ?",[i]);if(t){const E=await l.getById(a,t.numericId),o=E?.anilistId?String(E.anilistId):void 0;return!o||o===t.numericId?O(a,t.numericId):(await I(a,async r=>{const T=await d.getById(r,t.numericId),W=await l.getById(r,t.numericId);r.run("UPDATE OR IGNORE watchlist SET id = ?, thumbnail = ? WHERE id = ?",[o,T?.thumbnail||"",t.numericId]),r.run("DELETE FROM watchlist WHERE id = ?",[t.numericId]),r.run("UPDATE OR IGNORE shows_meta SET id = ?, anilistId = ?, thumbnail = ? WHERE id = ?",[o,parseInt(o),W?.thumbnail||"",t.numericId]),r.run("DELETE FROM shows_meta WHERE id = ?",[t.numericId]),r.run("UPDATE OR IGNORE watched_episodes SET showId = ? WHERE showId = ?",[o,t.numericId]),r.run("DELETE FROM watched_episodes WHERE showId = ?",[t.numericId]),r.run("UPDATE OR IGNORE queue SET showId = ? WHERE showId = ?",[o,t.numericId]),r.run("DELETE FROM queue WHERE showId = ?",[t.numericId]),r.run("UPDATE OR IGNORE dismissed_notifications SET showId = ? WHERE showId = ?",[o,t.numericId]),r.run("DELETE FROM dismissed_notifications WHERE showId = ?",[t.numericId]),r.run("UPDATE OR IGNORE discovered_notifications SET showId = ? WHERE showId = ?",[o,t.numericId]),r.run("DELETE FROM discovered_notifications WHERE showId = ?",[t.numericId]),r.run("UPDATE legacy_id_mapping SET numericId = ? WHERE legacyId = ?",[o,i])}),h.info({legacyId:i,previousId:t.numericId,newId:o},"Canonicalized migrated show ID"),o)}let n;const R=await l.getById(a,i);if(R&&R.name)n=R.name;else{const E=m(a,"SELECT id, name FROM shows_meta WHERE anilistId = ?",[parseInt(i)||0]);if(E&&E.name)n=E.name;else{const o=await d.getById(a,i);o&&o.name&&(n=o.name)}}if(!n)return h.warn({id:i},"No show name found for legacy ID migration"),i;const c=await D(n);if(!c)return h.warn({id:i,showName:n},"No AniList match found for legacy ID migration"),i;const s=String(c.id);let e;try{e=(await f(s))?.thumbnail}catch{e=void 0}return await I(a,async E=>{E.run("INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)",[i,s]);const o=await d.getById(E,i);o&&(await d.getById(E,s)?await d.delete(E,i):E.run("UPDATE watchlist SET id = ?, thumbnail = ? WHERE id = ?",[s,e||o.thumbnail||"",i]));const r=await l.getById(E,i);r&&(await l.getById(E,s)?E.run("DELETE FROM shows_meta WHERE id = ?",[i]):E.run("UPDATE shows_meta SET id = ?, anilistId = ?, thumbnail = ? WHERE id = ?",[s,c.id,e||r.thumbnail||"",i])),E.run("UPDATE OR IGNORE watched_episodes SET showId = ? WHERE showId = ?",[s,i]),E.run("DELETE FROM watched_episodes WHERE showId = ?",[i]),E.run("UPDATE OR IGNORE queue SET showId = ? WHERE showId = ?",[s,i]),E.run("DELETE FROM queue WHERE showId = ?",[i]),E.run("UPDATE OR IGNORE dismissed_notifications SET showId = ? WHERE showId = ?",[s,i]),E.run("DELETE FROM dismissed_notifications WHERE showId = ?",[i]),E.run("UPDATE OR IGNORE discovered_notifications SET showId = ? WHERE showId = ?",[s,i]),E.run("DELETE FROM discovered_notifications WHERE showId = ?",[i])}),h.info({legacyId:i,newId:s,showName:n},"Successfully migrated legacy show ID to numeric ID"),s}catch(t){return h.error({err:t,legacyId:i},"Error migrating legacy show ID"),i}}function G(a,i){if(p(i))return Promise.resolve(i);if(/^(mal-\d+|-\d+)$/i.test(i)){const n=m(a,"SELECT numericId FROM legacy_id_mapping WHERE legacyId = ?",[i]);return n?O(a,n.numericId):Promise.resolve(i)}if(/^\d+$/.test(i)){const n=w.get(i);if(n)return n;const R=g(a,i);return w.set(i,R),R.then(()=>w.delete(i),()=>w.delete(i)),R}const u=w.get(i);if(u)return u;const t=g(a,i);return w.set(i,t),t.then(()=>w.delete(i),()=>w.delete(i)),t}export{G as getMigratedId};
@@ -1,15 +0,0 @@
1
- import C from"crypto";import U from"../logger.js";import{SettingsRepository as i}from"../repositories/settings.repository.js";import{decodeMaybeGzip as k}from"../utils/http.utils.js";const o=U.child({module:"OfflineDb"}),$=["https://github.com/cedya77/anime-offline-database/releases/latest/download/anime-offline-database-minified.json","https://raw.githubusercontent.com/cedya77/anime-offline-database/master/anime-offline-database-minified.json","https://github.com/manami-project/anime-offline-database/releases/latest/download/anime-offline-database-minified.json","https://raw.githubusercontent.com/manami-project/anime-offline-database/master/anime-offline-database-minified.json"],v=6e4,B=1800*1e3,I=100*1024*1024,_=1e3,F=3600*1e3,P=["Action","Adventure","Comedy","Drama","Fantasy","Horror","Mahou Shoujo","Mecha","Music","Mystery","Psychological","Romance","Sci-Fi","Slice of Life","Sports","Supernatural","Thriller"],W=new Map(P.map(r=>[r.toLowerCase(),r])),z=1,p="anime_id_map",A="anime_id_map_new";function y(r,e=p){if(!r.get("SELECT name FROM sqlite_master WHERE type='table' AND name=?",[e]))r.run(`CREATE TABLE ${e} (id INTEGER PRIMARY KEY AUTOINCREMENT, anilist_id INTEGER, mal_id INTEGER, title TEXT, thumbnail TEXT, type TEXT, genres TEXT)`);else{const t=r.all(`PRAGMA table_info(${e})`),n=new Set(t.map(l=>l.name));n.has("mal_id")||r.run(`ALTER TABLE ${e} ADD COLUMN mal_id INTEGER`),n.has("type")||r.run(`ALTER TABLE ${e} ADD COLUMN type TEXT`),n.has("genres")||r.run(`ALTER TABLE ${e} ADD COLUMN genres TEXT`)}r.run(`CREATE INDEX IF NOT EXISTS idx_anime_id_map_anilist ON ${e}(anilist_id)`),r.run(`CREATE INDEX IF NOT EXISTS idx_anime_id_map_mal ON ${e}(mal_id)`)}function M(){const r=new Date,e=(r.getUTCDay()+1)%7;return new Date(Date.UTC(r.getUTCFullYear(),r.getUTCMonth(),r.getUTCDate()-e,0,0,0,0))}async function G(r){const e=new AbortController,a=setTimeout(()=>e.abort(),B);let t;const n=()=>{t&&clearTimeout(t),t=setTimeout(()=>e.abort(),v)};try{const l=await fetch(r,{signal:e.signal,headers:{"User-Agent":"Dango-Media-Client/1.0",Accept:"application/json"}});if(!l.ok)throw new Error(`HTTP ${l.status}`);const d=l.headers.get("content-length");if(d&&Number(d)>I)throw new Error("Response too large");if(!l.body)throw new Error("Empty response body");n();const T=[];let g=0;const w=l.body.getReader();try{for(;;){const{done:c,value:D}=await w.read();if(c)break;if(n(),g+=D.byteLength,g>I)throw new Error("Response too large");T.push(Buffer.from(D))}}finally{w.releaseLock()}const b=k(Buffer.concat(T)).toString("utf8");if(b.length>I)throw new Error("Response too large");return b}finally{t&&clearTimeout(t),clearTimeout(a)}}class x{malToEntry=new Map;anilistToMal=new Map;anilistToEntry=new Map;isInitialized=!1;isRefreshing=!1;async init(e){try{y(e),this.loadCache(e),o.info(`OfflineDb loaded ${this.malToEntry.size} MAL mappings from SQLite`);const a=(await i.getByKey(e,"offlineDbLastStatus"))?.value==="updating",t=await i.getByKey(e,"offlineDbAutoUpdateEnabled"),n=!t||t.value!=="false";this.backfillShowsMetaGenres(e),this.malToEntry.size===0?(o.info("OfflineDb cache is empty. Starting background download..."),this.refreshDatabase(e).catch(l=>{o.warn({err:l?.message},"Background offline database download failed")})):a&&n&&await this.interruptedRetryDue(e)?(o.info("Previous offline database refresh was interrupted. Retrying in background..."),await i.upsert(e,"offlineDbLastStatus","idle"),await i.upsert(e,"offlineDbLastMessage","Previous refresh was interrupted by a restart. Retrying automatically..."),this.refreshDatabase(e).catch(l=>{o.warn({err:l?.message},"Interrupted offline database retry failed")})):a&&(await i.upsert(e,"offlineDbLastStatus","idle"),await i.upsert(e,"offlineDbLastMessage","Previous refresh was interrupted by a restart. Trigger an update to retry.")),this.isInitialized=!0}catch(a){o.error({err:a},"Failed to initialize OfflineDb")}}backfillShowsMetaGenres(e){try{return y(e),e.run(`
2
- UPDATE shows_meta
3
- SET genres = (
4
- SELECT genres FROM ${p}
5
- WHERE anilist_id = CAST(shows_meta.id AS INTEGER)
6
- AND genres IS NOT NULL AND genres != '[]'
7
- LIMIT 1
8
- )
9
- WHERE (genres IS NULL OR genres = '' OR genres = '[]')
10
- AND EXISTS (
11
- SELECT 1 FROM ${p}
12
- WHERE anilist_id = CAST(shows_meta.id AS INTEGER)
13
- AND genres IS NOT NULL AND genres != '[]'
14
- )
15
- `),1}catch(a){return o.warn({err:a},"Failed to backfill shows_meta genres from offline db"),0}}loadCache(e){try{y(e);const a=e.all(`SELECT anilist_id, mal_id, title, thumbnail, type, genres FROM ${p} WHERE anilist_id IS NOT NULL AND mal_id IS NOT NULL`);this.malToEntry.clear(),this.anilistToMal.clear(),this.anilistToEntry.clear();for(const t of a){if(!t.anilist_id||!t.mal_id)continue;const n={anilistId:t.anilist_id,malId:t.mal_id,title:t.title||"",thumbnail:t.thumbnail||void 0,type:t.type||void 0,genres:t.genres||void 0};this.malToEntry.set(t.mal_id,n),this.anilistToMal.set(t.anilist_id,t.mal_id),this.anilistToEntry.set(t.anilist_id,n)}}catch(a){o.error({err:a},"Failed to load offline mappings into memory")}}getByMalId(e){return this.malToEntry.get(e)??null}getByAnilistId(e){return this.anilistToEntry.get(e)??null}async getOfflineDbInfo(e){const a=async d=>(await i.getByKey(e,d))?.value??null,t=await a("offlineDbAutoUpdateEnabled"),n=await a("offlineDbLastStatus"),l=Number(await a("offlineDbEntryCount")??this.malToEntry.size)||0;return{totalMapped:this.malToEntry.size,totalMalMapped:this.malToEntry.size,isInitialized:this.isInitialized,isRefreshing:this.isRefreshing,autoUpdateEnabled:t?t!=="false":!0,lastCheckedAt:await a("offlineDbLastCheckedAt"),lastUpdatedAt:await a("offlineDbLastUpdatedAt"),lastStatus:n||(this.isRefreshing?"updating":"idle"),lastMessage:await a("offlineDbLastMessage"),sourceUrl:await a("offlineDbSourceUrl"),releaseTag:await a("offlineDbReleaseTag"),contentSha256:await a("offlineDbContentSha256"),entryCount:l,schemaVersion:z,nextDueAt:M().toISOString()}}async checkWeeklyUpdateDue(e){const a=await i.getByKey(e,"offlineDbAutoUpdateEnabled");if(a&&a.value==="false")return!1;const t=await i.getByKey(e,"offlineDbLastCheckedAt");if(!t?.value)return!0;const n=new Date(t.value).getTime();return Number.isNaN(n)?!0:n<M().getTime()}async interruptedRetryDue(e){const a=await i.getByKey(e,"offlineDbLastCheckedAt");if(!a?.value)return!0;const t=new Date(a.value).getTime();return Number.isNaN(t)?!0:Date.now()-t>F}async executeScheduledUpdate(e){if(this.isRefreshing){o.info("Scheduled offline database update skipped: refresh already in progress");return}o.info("Starting scheduled weekly offline database update...");const a=await this.refreshDatabase(e);a.success?o.info(`Scheduled offline database update succeeded: ${a.count} entries`):o.warn(`Scheduled offline database update failed: ${a.error}`)}async refreshDatabase(e){if(this.isRefreshing)return{success:!1,count:this.malToEntry.size,error:"Refresh already in progress"};this.isRefreshing=!0;const a=Date.now();await i.upsert(e,"offlineDbLastStatus","updating"),await i.upsert(e,"offlineDbLastCheckedAt",new Date().toISOString()),await i.upsert(e,"offlineDbLastMessage","Downloading and parsing database...");try{let t=null,n=null,l=null;for(const u of $)try{o.info(`Fetching anime-offline-database from ${u}...`),t=await G(u);const s=JSON.parse(t);if(s&&Array.isArray(s.data)){n=u;break}t=null}catch(s){l=s,o.warn(`Failed downloading from ${u}: ${s.message}`)}if(!t)throw l||new Error("Could not download anime-offline-database from any source");const d=C.createHash("sha256").update(t).digest("hex"),T=(await i.getByKey(e,"offlineDbContentSha256"))?.value,g=JSON.parse(t).data;o.info(`Parsing ${g.length} entries from anime-offline-database...`);const w=/anilist\.co\/anime\/(\d+)/i,b=/myanimelist\.net\/anime\/(\d+)/i,c=[];for(const u of g){const s=u;if(!Array.isArray(s.sources))continue;let f=null,L=null;for(const m of s.sources){if(typeof m!="string")continue;const h=m.match(w);h&&(f=parseInt(h[1],10));const E=m.match(b);E&&(L=parseInt(E[1],10))}if(!f||!L)continue;let S=typeof s.thumbnail=="string"?s.thumbnail:typeof s.picture=="string"?s.picture:"";S.includes("cdn.myanimelist.net/images/anime/")&&(S=S.replace(/t\.(jpe?g|png|webp)$/i,"l.$1"));let R;if(Array.isArray(s.tags)){const m=new Set;for(const h of s.tags){if(typeof h!="string")continue;const E=W.get(h.trim().toLowerCase());E&&m.add(E)}m.size>0&&(R=JSON.stringify([...m]))}c.push({anilistId:f,malId:L,title:typeof s.title=="string"?s.title:"",thumbnail:S||void 0,type:typeof s.type=="string"?s.type:void 0,genres:R})}o.info(`Saving ${c.length} mapped entries to SQLite...`),e.run(`DROP TABLE IF EXISTS ${A}`),y(e,A);const D=e.prepare(`INSERT INTO ${A} (anilist_id, mal_id, title, thumbnail, type, genres) VALUES (?, ?, ?, ?, ?, ?)`);for(let u=0;u<c.length;u+=_){const s=c.slice(u,u+_);e.serialize(()=>{for(const f of s)D.run(f.anilistId,f.malId,f.title,f.thumbnail??null,f.type??null,f.genres??null)}),await new Promise(f=>setImmediate(f))}e.run(`DROP TABLE IF EXISTS ${p}`),e.run(`ALTER TABLE ${A} RENAME TO ${p}`),y(e),this.loadCache(e),this.backfillShowsMetaGenres(e);const N=n?.includes("/releases/")?"latest":"raw";await i.upsert(e,"offlineDbLastStatus","success"),await i.upsert(e,"offlineDbLastUpdatedAt",new Date().toISOString()),await i.upsert(e,"offlineDbSourceUrl",n??""),await i.upsert(e,"offlineDbReleaseTag",N),await i.upsert(e,"offlineDbContentSha256",d),await i.upsert(e,"offlineDbEntryCount",String(c.length));const O=T===d;return await i.upsert(e,"offlineDbLastMessage",`Updated ${c.length} entries in ${Math.round((Date.now()-a)/1e3)}s${O?" (unchanged upstream hash)":""}.`),o.info(`OfflineDb refresh completed: ${c.length} entries from ${n}`),{success:!0,count:c.length}}catch(t){const n=t.message||"Failed to refresh offline database";return o.error({err:t},"Failed to refresh anime-offline-database"),await i.upsert(e,"offlineDbLastStatus","failed"),await i.upsert(e,"offlineDbLastMessage",n),{success:!1,count:this.malToEntry.size,error:n}}finally{this.isRefreshing=!1}}}const J=new x;export{J as offlineDb};
@@ -1 +0,0 @@
1
- import S from"../logger.js";import{parseJsonBody as E}from"../utils/http.utils.js";import{getInnertube as M}from"./ytmusic.js";const C="O43z0dpjhgX20SCx4KAo",A=600*60*1e3,R=360*60*1e3;let s=null,c=0,a=null,h=0;const k=60*1e3,l=new Map;async function D(){const t=globalThis;if("navigator"in t&&t.yt)return;const{JSDOM:e,VirtualConsole:i}=await import("jsdom"),o=new i,n=new e('<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>',{url:"https://www.youtube.com",referrer:"https://www.youtube.com/",virtualConsole:o});Object.assign(t,{window:n.window,document:n.window.document,location:n.window.location,origin:n.window.origin}),"navigator"in t||Object.defineProperty(t,"navigator",{value:n.window.navigator,writable:!0,configurable:!0})}async function N(){const[{BotGuardClient:t},{WebPoMinter:e},{buildURL:i,getHeaders:o,USER_AGENT:n}]=await Promise.all([import("bgutils-js/botguard"),import("bgutils-js/webpo"),import("bgutils-js/utils")]),f=await M(),g=(await(await fetch("https://www.youtube.com",{headers:{accept:"*/*","accept-language":"en-US,en;q=0.7","user-agent":n}})).text()).match(/ytcfg\.set\(({.+?})\);/s)?.[1];if(g){const d=globalThis.window;d&&(d.yt={config_:JSON.parse(g)})}const r=(await f.getAttestationChallenge("ENGAGEMENT_TYPE_UNBOUND")).bg_challenge,u=r?.interpreter_url?.private_do_not_access_or_else_trusted_resource_url_wrapped_value;if(!r?.program||!r.global_name||!u)throw new Error("No BotGuard challenge in response");const w=await(await fetch(`https:${u}`)).text();if(!w)throw new Error("Could not load BotGuard VM");new Function(w)();const _=await t.create({program:r.program,globalName:r.global_name,globalObject:globalThis}),p=[],y=await _.snapshot({webPoSignalOutput:p}),P=await fetch(i("GenerateIT",!0),{method:"POST",headers:o(),body:JSON.stringify([C,y])}),[m,b,T,O]=await E(P);if(!m)throw new Error("Integrity token request failed");return S.info("[pot] minter ready (integrity ttl %ss)",b),e.create({integrityToken:m,estimatedTtlSecs:b,mintRefreshThreshold:T,websafeFallbackToken:O},p)}async function W(){if(s&&Date.now()-c<A)return s;if(Date.now()-h<k)throw new Error("PoToken bootstrap cooling down");return a||(a=(async()=>{await D();const t=await N();return s=t,c=Date.now(),t})().catch(t=>{throw a=null,h=Date.now(),t})),a}async function F(t){const e=l.get(t);if(e&&Date.now()-e.fetchedAt<R)return e.pot;const o=await(await W()).mintAsWebsafeString(t);return l.set(t,{pot:o,fetchedAt:Date.now()}),o}function H(t){t?l.delete(t):(l.clear(),s=null,c=0,a=null)}export{F as mintPoToken,H as resetPoTokens};
@@ -1 +0,0 @@
1
- import P from"../logger.js";import{InsightsRepository as h}from"../repositories/insights.repository.js";import{anilistRequest as w}from"../lib/anilist.js";function R(n){if(!n)return[];try{if(n.startsWith("[")){const r=JSON.parse(n);return Array.isArray(r)?r.map(String):[]}return n.split(",").map(r=>r.trim()).filter(Boolean)}catch{return[]}}async function S(n){const[r,c,d]=await Promise.all([h.getLibraryShowsWithGenres(n),h.getAllAnilistIds(n),h.getWatchedEpisodesWithMeta(n)]),o={},s={};for(const e of r){const i=R(e.genres);if(e.status==="Completed")for(const a of i)o[a]=(o[a]||0)+1;else if(e.status==="Dropped")for(const a of i)s[a]=(s[a]||0)+1}const g={};for(const e of d){const i=e.englishName||e.name||e.showId;for(const a of R(e.genres)){g[a]||(g[a]=new Map);const l=g[a].get(e.showId)||{title:i,count:0};l.count+=1,g[a].set(e.showId,l)}}const m={},u=Object.entries(o).sort((e,i)=>i[1]-e[1]).slice(0,6);for(const[e]of u)m[e]=[...g[e]?.values()||[]].sort((i,a)=>a.count-i.count).slice(0,2).map(i=>i.title);const t=new Set;for(const e of r)/^\d+$/.test(e.id)&&t.add(Number(e.id));for(const e of c)e.anilistId&&t.add(e.anilistId);return{genreComp:o,genreDrop:s,exemplars:m,knownIds:[...t]}}const C="id title { romaji english } coverImage { large } averageScore popularity episodes seasonYear format genres isAdult status";function v(n,r){const c=Math.max(1,...Object.values(n)),d={};for(const[o,s]of Object.entries(n)){const g=r[o]||0;d[o]=s/c*(s/(s+g))}return d}function M(n){const r=Object.entries(n).sort((o,s)=>s[1]-o[1]).slice(0,4).map(([o])=>o);for(;r.length<2;)r.push("Fantasy");const c=[[r[0],r[1]],[r[0],r[2]||r[1]],[r[1],r[2]||r[0]],[r[0],r[3]||r[1]]],d=new Set;return c.filter(o=>{const s=[...o].sort().join("|");return d.has(s)?!1:(d.add(s),!0)})}function A(n,r,c=2){const d=new Map;for(const o of r)for(const s of n[o]||[])d.set(s,(d.get(s)||0)+1);return[...d.entries()].sort((o,s)=>s[1]-o[1]).slice(0,c).map(([o])=>o)}async function E(n){const r=new Set(n.knownIds||[]);if(r.size===0)return[];const c=v(n.genreComp,n.genreDrop),d=.15,o=Object.values(c).reduce((t,e)=>t+e,0)||1,s=new Map;for(const t of M(c))try{const e=await w(`query ($g: [String]) { Page(page: 1, perPage: 20) { media(genre_in: $g, type: ANIME, status: FINISHED, isAdult: false, averageScore_greater: 68, sort: SCORE_DESC) { ${C} } } }`,{g:t});for(const i of e?.data?.Page?.media||[])s.has(i.id)||s.set(i.id,i)}catch{}if(s.size===0)return[];const g=[];for(const t of s.values()){if(r.has(t.id)||t.isAdult||t.format==="MUSIC"||t.format==="SPECIAL")continue;const e=t.genres||[],a=.65*(e.reduce((l,p)=>l+(c[p]??d),0)/o)+.25*((t.averageScore||70)/100)+.1*Math.min(1,(t.popularity||0)/2e5);g.push({m:t,score:a,overlap:e.filter(l=>(c[l]??0)>.3)})}g.sort((t,e)=>e.score-t.score);const m=g.slice(0,15),u=new Map;try{const t=await w("query ($ids: [Int]) { Page(page: 1, perPage: 50) { media(id_in: $ids, type: ANIME) { id relations { edges { relationType node { id title { romaji english } type } } } } } }",{ids:m.map(e=>e.m.id)});for(const e of t?.data?.Page?.media||[])u.set(e.id,e)}catch{}return m.map(t=>{const i=u.get(t.m.id)?.relations?.edges?.find(y=>y.relationType==="PREQUEL"&&y.node?.type==="ANIME")?.node;let a=t.score,l="entry",p=null;i&&i.id&&!r.has(i.id)?(l="sequel",p=i.title?.english||i.title?.romaji||null,a-=.3):i&&(l="continue",a+=.05);const b=t.m.genres||[],I=t.m.title?.english||t.m.title?.romaji||"Unknown";return{id:t.m.id,title:I,url:`https://anilist.co/anime/${t.m.id}`,cover:t.m.coverImage?.large||null,year:t.m.seasonYear??null,format:t.m.format||"TV",episodes:t.m.episodes??null,averageScore:t.m.averageScore??null,genres:b,matchPct:Math.round(a*100),sharedGenres:t.overlap,exemplars:A(n.exemplars,b),kind:l,prequelTitle:p}})}let f=null;function T(n){return JSON.stringify([n.genreComp,n.genreDrop,n.knownIds.length])}async function j(n,r){if(!f||f.hash!==r)try{const c=await E(n);c.length>0&&(f={hash:r,candidates:c})}catch(c){P.warn({err:c?.message},"Recommendation candidates failed, reusing previous")}return f?.candidates||[]}async function q(n){const r=await S(n);return j(r,T(r))}export{E as computeRecCandidates,S as computeTasteProfile,q as getCachedRecommendations};
@@ -1 +0,0 @@
1
- const n="dango-mt-",e=o=>!!o&&/^dango-mt-\d+$/.test(o),s=o=>Object.values(o).some(t=>typeof t=="string"&&t.startsWith(n));export{n as TEMP_SHOW_ID_PREFIX,e as isTempShowId,s as isTempSyncRow};
@@ -1 +0,0 @@
1
- import{parseJsonBody as d}from"../utils/http.utils.js";const i="https://api.themoviedb.org/3",b="https://image.tmdb.org/t/p",p="9e7096a7575623aa30c66e9cc987e411",l=["fb7bb23f03b6994dafc674c074d01761","e55425032d3d0f371fc776f302e7c09b","8301a21598f8b45668d5711a814f01f6","8cf43ad9c085135b9479ad5cf6bbcbda","da63548086e399ffc910fbc08526df05","13e53ff644a8bd4ba37b3e1044ad24f3","269890f657dddf4635473cf4cf456576","a2f888b27315e62e471b2d587048f32e","8476a7ab80ad76f0936744df0430e67c","5622cafbfe8f8cfe358a29c53e19bba0","ae4bd1b6fce2a5648671bfc171d15ba4","257654f35e3dff105574f97fb4b97035","2f4038e83265214a0dcd6ec2eb3276f5","9e43f45f94705cc8e1d5a0400d19a7b7","af6887753365e14160254ac7f4345dd2","06f10fc8741a672af455421c239a1ffc","09ad8ace66eec34302943272db0e8d2c"];let s=null;async function u(){return s||(s=process.env.TMDB_API_KEY||l[Math.floor(Math.random()*l.length)]||p),s}async function v(t){const n=await u(),a=`${i}/search/multi?api_key=${n}&query=${encodeURIComponent(t)}&include_adult=false`,e=await fetch(a);return e.ok?(await d(e)).results??null:null}async function g(t){const n=await u(),a=`${i}/tv/${t}?api_key=${n}`,e=await fetch(a);return e.ok?await d(e):null}async function T(t){const n=[t.english,t.romaji,t.native].filter(Boolean);for(const a of n){const e=await v(a);if(!e)continue;const c=e.filter(o=>o.media_type==="tv");if(c.length===0)continue;const f=c.sort((o,m)=>(m.vote_count||0)-(o.vote_count||0))[0],r=await g(f.id);if(r?.backdrop_path)return{backdrop:`${b}/original${r.backdrop_path}`,overview:r.overview||f.overview||void 0}}return null}export{i as TMDB_BASE,b as TMDB_IMAGE,T as findTmdbDefaultBackdrop,u as getTmdbKey,v as tmdbSearch,g as tmdbTvDetails};
@@ -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};