@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,3 +0,0 @@
1
- import{AppCache as A}from"../utils/cache.utils.js";import{parseJsonBody as $}from"../utils/http.utils.js";const h=new A({ttlSeconds:10080*60,maxKeys:5e3});function k(e){return e.split(" ").map((t,i)=>i!==0&&["x","and","or","the","a","an","of","in","on","with"].includes(t.toLowerCase())?t.toLowerCase():t.charAt(0).toUpperCase()+t.slice(1)).join(" ").replace(/\bX\b/g,"x")}function C(e){const t=e.trim();return t&&k(t)}async function U(e,t,i){const d=e.map(r=>String(r).trim()).filter(Boolean),a={},c=[];for(const r of d){const n=`${t}:${i}:${r}`,l=h.get(n);l?a[r]=l:c.push(r)}if(c.length>0){const r=[];for(let n=0;n<c.length;n+=15)r.push(c.slice(n,n+15));for(const n of r){const l=n.join(`
2
- `),w=`https://clients5.google.com/translate_a/t?client=dict-chrome-ex&sl=${encodeURIComponent(t)}&tl=${encodeURIComponent(i)}&dt=t&q=${encodeURIComponent(l)}`;try{const o=await fetch(w,{headers:{"User-Agent":"Mozilla/5.0"},signal:AbortSignal.timeout(5e3)});if(!o.ok)throw new Error(`translate ${o.status}`);const s=await $(o);let g="";Array.isArray(s)&&typeof s[0]=="string"?g=s[0]:Array.isArray(s)&&Array.isArray(s[0])&&(g=s[0]?.map(u=>u[0]).join("")||"");const y=g.split(`
3
- `);n.forEach((f,u)=>{const m=(y[u]??"").trim(),p=m?C(m):f;a[f]=p,h.set(`${t}:${i}:${f}`,p)})}catch{for(const o of n)a[o]||(a[o]=o)}await new Promise(o=>setTimeout(o,80))}}return a}export{U as translateTexts};
@@ -1 +0,0 @@
1
- const y=20,g=21,p=58;function f(e,t){if(t>=e.length)return null;const l=e[t];let a=0;for(let r=1;r<=5;r++)if((l&128>>r-1)===0){a=r;break}if(a<1||t+a>e.length)return null;if(a===1)return[l,t+1];if(a===5)return[e[t+1]+e[t+2]*256+e[t+3]*65536+e[t+4]*16777216>>>0,t+5];const i=8-a;let n=l&(1<<i)-1;for(let r=1;r<a;r++)n+=e[t+r]*2**(i+8*(r-1));return[n,t+a]}function h(e){let t=0;for(;t<e.length;){const l=e[t++],a=l>>3;if((l&7)!==0)break;const n=f(e,t);if(!n)break;if(a===1)return n[0];t=n[1]}return 1}function k(e,t){const l=[];let a=1,i=t,n=0;if(i){const r=f(e,n),u=r&&f(e,r[1]);r&&u&&r[0]===20&&(n=u[1]+u[0]);const s=f(e,n),c=s&&f(e,s[1]);if(s&&c&&s[0]===i.pendingType){n=c[1];const o=Math.min(c[0],e.length-n);if(i.chunks.push(e.subarray(n,n+o)),i.remaining=c[0]-o,n+=o,i.remaining<=0){const m=Buffer.concat(i.chunks);i=null,s[0]===21&&m.length>1&&l.push(m.subarray(1))}}else i=null}for(;n<e.length;){const r=f(e,n);if(!r)break;const u=f(e,r[1]);if(!u)break;const[s,c]=[r[0],u[0]];n=u[1];const o=e.length-n;if(c>o){i={pendingType:s,remaining:c-o,chunks:[e.subarray(n)]};break}const m=e.subarray(n,n+c);n+=c,s===21?m.length>1&&l.push(m.subarray(1)):s===58&&(a=h(m))}return{media:Buffer.concat(l),status:a,carry:i}}export{k as parseUmpResponse};
@@ -1 +0,0 @@
1
- import o from"fs";import v from"path";import{CONFIG as d}from"../config.js";import l from"../logger.js";const a=v.join(d.ROOT,"ytmusic_cookie.txt");let f=null,u=null,h=0;const S=60*1e3;let n=null,m=null,g=!1;async function s(){return m||(m=await import("youtubei.js")),m}async function y(){if(g)return;const{Platform:t,Log:e}=await s();t.shim.eval=(async r=>new Function(r.output)()),e.setLevel(e.Level.ERROR),g=!0}function p(){try{return o.existsSync(a)&&o.readFileSync(a,"utf-8").trim()||null}catch{return null}}function T(){return p()}function I(t){if(!t||typeof t!="object")return!1;if(t.constructor?.name==="ParsingError")return!0;const r=t instanceof Error?t.message:String(t);return/Expected node of any type|Cannot cast \S+ to one of|got ItemSection|is not valid JSON|Incorrect JSPB formatting/i.test(r)}function C(){return f?Promise.resolve(f):Date.now()-h<S?Promise.reject(new Error("YouTube session unavailable (cooling down)")):(u||(u=(async()=>{const{Innertube:t}=await s();await y();try{const e=await t.create({generate_session_locally:!1});return f=e,e}catch(e){throw u=null,h=Date.now(),e}})()),u)}async function P(t=!1){const e=p();if(!e)return n=null,null;if(n&&!t)return n;await y();const{Innertube:r}=await s();return n=await r.create({cookie:e}),n}function k(){n=null}async function E(){k();try{return await P()}catch(t){return l.warn({err:t},"[ytmusic] saved-cookie session refresh failed"),null}}const x=300*1e3;let c=0,i=!1;async function _(){if(!p())return!1;const t=Date.now();if(t-c<x)return i;c=t;try{const e=await P();if(!e)return!1;await e.account.getInfo(),i=!0}catch{i=!1}return i}async function L(){return{authenticated:await _()}}async function D(t){const e=t.trim();if(!e)throw new Error("Cookie is empty");await y();const{Innertube:r}=await s(),b=await r.create({cookie:e});try{await b.music.getLibrary()}catch(w){if(!I(w))throw w;l.warn("[ytmusic] sign-in validation hit a layout variant, accepting cookie anyway")}try{o.mkdirSync(d.ROOT,{recursive:!0})}catch{}o.writeFileSync(a,e,"utf-8"),n=b,c=Date.now(),i=!0,l.info("[ytmusic] cookie sign-in validated and saved")}async function F(){try{o.existsSync(a)&&o.unlinkSync(a)}catch{}n=null,c=0,i=!1}export{P as getAuthedInnertube,C as getInnertube,L as getMusicAuthStatus,T as getMusicCookie,k as invalidateAuthedSession,I as isParserVariantError,E as refreshAuthedInnertube,D as saveMusicCookie,F as signOutMusic,_ as verifyMusicSession};
@@ -1 +0,0 @@
1
- import e from"pino";const o=process.argv.includes("--dev")||process.env.NODE_ENV==="development",t=o?e({level:"debug",transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):e({level:process.env.LOG_LEVEL||"info"});var n=t;export{n as default};
File without changes
File without changes
@@ -1,2 +0,0 @@
1
- import m from"fs";import y from"path";import E from"crypto";import{execFileSync as K}from"node:child_process";import{pathToFileURL as Q}from"url";import*as X from"cheerio";import{gotScraping as Y}from"got-scraping";import g from"../logger.js";import{CONFIG as M}from"../config.js";import{requestContext as Z}from"../utils/request-context.js";import{sanitizeCfClearance as ee,buildCfClearanceCookie as re}from"../utils/cookie.utils.js";import{decodeMaybeGzip as U,parseJsonBody as j}from"../utils/http.utils.js";import{buildQueryVariants as O,pickBestMatch as D}from"./title-matching.js";import{anilistRequest as te,parseMalId as ne,searchAnilistByTitle as oe}from"../lib/anilist.js";import{kitsuMetaByAnilistId as se}from"../lib/kitsu.js";import{getTmdbKey as ie,TMDB_BASE as N,TMDB_IMAGE as ae}from"../lib/tmdb.js";import{parseRegistry as H}from"./remote-types.js";const x=15e3,b="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";function T(r,n,e){let t;const o=new Promise((a,c)=>{t=setTimeout(()=>c(new Error(`${e} timed out after ${n}ms`)),n)});return Promise.race([r,o]).finally(()=>clearTimeout(t))}function $(r){return/^https?:\/\//i.test(r)}function S(r){return r.startsWith("file://")?new URL(r).pathname:r}function ce(r){return $(r)||r.startsWith("file://")?!1:r.includes("/")||r.includes("\\")||r.endsWith(".json")}async function de(r){if($(r)){const e=await T(fetch(r,{headers:{"User-Agent":b,Accept:"application/json"}}),x,"registry fetch");if(!e.ok)throw new Error(`registry HTTP ${e.status}: ${r}`);return U(Buffer.from(await e.arrayBuffer()))}const n=y.resolve(S(r));return await m.promises.readFile(n)}async function ue(r){if($(r)){const e=await T(fetch(r,{headers:{"User-Agent":b,Accept:"text/javascript,*/*"}}),x,"provider fetch");if(!e.ok)throw new Error(`provider HTTP ${e.status}: ${r}`);return U(Buffer.from(await e.arrayBuffer()))}const n=y.resolve(S(r));return await m.promises.readFile(n)}function le(r,n){const e=(r??"").trim();if($(e)||e.startsWith("file://"))return e;if($(n))return new URL(e,n).href;const t=y.dirname(y.resolve(S(n)));return y.resolve(t,e)}function I(r){return E.createHash("sha256").update(r).digest("hex")}function fe(r,n,e){let t;try{t=Buffer.from(r,"base64url")}catch{return null}if(!t.length||t.length%16!==0)return null;try{const o=Buffer.alloc(32);Buffer.from(n).copy(o);const a=E.createDecipheriv("aes-256-cbc",o,Buffer.from(e)),c=Buffer.concat([a.update(t),a.final()]);return JSON.parse(c.toString("utf8"))}catch{return null}}function W(r,n){const e=["-sSL","-A",b,...Object.entries(n??{}).flatMap(([c,i])=>["-H",`${c}: ${i}`]),"--max-time","15","-w",`
2
- __HTTP_STATUS__%{http_code}`,"--",r];let t;try{t=K("curl",e,{encoding:"utf-8",maxBuffer:10*1024*1024})}catch{return null}const o=t.match(/__HTTP_STATUS__(\d+)\s*$/);return{status:o?parseInt(o[1],10):0,body:o&&o.index!==void 0?t.slice(0,o.index):t}}function pe(r){const n=async(e,t,o=x)=>T(fetch(e,{...t,headers:{"User-Agent":b,...t?.headers}}),o,`fetch ${e}`);return{cache:{get:e=>r.get(e),set:(e,t,o)=>{r.set(e,t,o)}},logger:{info:(e,t)=>{g.info(e,t)},warn:(e,t)=>{g.warn(e,t)},error:(e,t)=>{g.error(e,t)},debug:(e,t)=>{g.debug(e,t)}},fetchText:async(e,t)=>{try{const o=await n(e,t);return o.ok?await o.text():null}catch{return null}},fetchJson:async(e,t)=>{const o=await n(e,t);if(!o.ok)throw new Error(`HTTP ${o.status}: ${e}`);return j(o)},proxyUrl:(e,t)=>`/api/proxy?url=${encodeURIComponent(e)}&referer=${encodeURIComponent(t)}`,userAgent:b,titleMatch:{buildQueryVariants:O,pickBestMatch:D},resolveBestShowId:async(e,t,o)=>{const a=[e,t].filter(c=>typeof c=="string"&&c.trim().length>0);if(a.length===0)return null;for(const c of O(e,t)){let i;try{i=await o(c)}catch(u){if(u.message==="AUTH_REQUIRED")throw u;continue}if(!i||i.length===0)continue;const l=D(i.filter(u=>u&&typeof u.id=="string"&&u.id.length>0),a);if(l)return l.item.id}return null},anilist:{request:(e,t)=>te(e,t),parseMalId:ne,searchByTitle:oe},kitsu:{metaByAnilistId:se},tmdb:{base:N,image:ae,get:async e=>{try{const t=await ie(),o=e.includes("?")?"&":"?",a=await n(`${N}${e}${o}api_key=${t}`);return a.ok?j(a):null}catch{return null}}},request:{get:e=>Z.getStore()?.get(e)},cookies:{sanitizeCfClearance:ee,buildCfClearanceCookie:re},crypto:{aes256CbcDecryptJson:fe,hmacSha256Base64Url:(e,t)=>E.createHmac("sha256",e).update(t).digest("base64url")},scraping:{fetch:async(e,t)=>{const o=typeof e=="string"?{url:e,...t??{}}:e,{url:a,...c}=o,i=await Y({url:a,method:"GET",responseType:"text",throwHttpErrors:!1,...c});return{statusCode:i.statusCode,body:String(i.body??""),headers:i.headers}}},curl:{getJson:async(e,t)=>{try{const o=W(e,t);return!o||o.status!==200?null:JSON.parse(o.body)}catch{return null}},getText:async(e,t)=>{const o=W(e,t);return!o||o.status!==200?null:o.body}},cheerio:{load:e=>X.load(e)}}}function A(r){if(!r||typeof r!="object")return null;const n=r;return typeof n.name=="string"?n:null}function G(r,n){return n.every(e=>typeof r[e]=="function")}function F(r){const n=A(r);return!!n&&G(n,["search","getEpisodes","getStreamUrls"])}function L(r){const n=A(r);return!!n&&G(n,["search","getDetail","getChapters","getPages"])}function _(r){const n=A(r);return!!n&&(typeof n.getSources=="function"||typeof n.getEmbedUrl=="function")}function ge(r,n){const e=n.trim().replace(/\/+$/,"");return{name:r,getEmbedUrl:async t=>{const o=Number(t.tmdbId);return o?t.type==="movie"?`${e}/movie/${o}`:`${e}/tv/${o}/${t.season||1}/${t.episode||1}`:null}}}async function me(r,n,e,t,o){if(r.embedBase){const f=ge(r.id,r.embedBase);return C.set(`${r.kind??"anime"}:${r.id}-${r.version}-embed`,{provider:f}),{provider:f}}const a=le(r.entry,n);await m.promises.mkdir(e,{recursive:!0});const c=`${r.id}-${r.version}.mjs`,i=y.join(e,c),l=`${r.kind??"anime"}:${r.id}-${r.version}-${(r.sha256??"nosha").slice(0,12)}`;let u=null;try{const f=await m.promises.readFile(i);(r.sha256&&I(f)===r.sha256||!r.sha256&&o)&&(u=f)}catch{}if(u){const f=C.get(l);if(f)return g.debug({provider:r.id,version:r.version},"remote provider reused cached instance"),{provider:f.provider,facets:f.facets}}else{try{u=await ue(a)}catch(f){return{provider:null,error:`download failed: ${f.message}`}}if(r.sha256){if(I(u)!==r.sha256)return{provider:null,error:`sha256 mismatch for ${r.id}`}}else if(!o)return{provider:null,error:`missing sha256 for ${r.id}`};await m.promises.writeFile(i,u)}const B=`${r.version}-${(r.sha256??"nosha").slice(0,12)}`,w=await J(i,B,t,r);return w.provider&&!w.error&&C.set(l,{provider:w.provider,facets:w.facets}),w}const C=new Map;function ve(r){let n,e;for(const[t,o]of Object.entries(r)){if(t==="default"||!Array.isArray(o)||o.length===0||!o.every(i=>typeof i=="string"&&i.length>0))continue;const a=o.filter(i=>i.length<=64).slice(0,500);if(a.length===0)continue;const c=t.toUpperCase();c.includes("GENRE")||c.includes("TAG")?n??=a:(c.includes("ORDER")||c.includes("SORT"))&&(e??=a)}if(!(!n&&!e))return{...n?{genres:n}:{},...e?{orders:e}:{}}}async function J(r,n,e,t){const o=t.id;let a;try{const l=`${Q(r).href}?v=${encodeURIComponent(n)}`;a=await T(import(l),x,`import ${o}`)}catch(l){return{provider:null,error:`import failed: ${l.message}`}}const c=a.default;if(typeof c!="function")return{provider:null,error:"missing default factory export"};let i;try{i=await T(Promise.resolve(c(e)),1e4,`factory ${o}`)}catch(l){return{provider:null,error:`factory failed: ${l.message}`}}return(t.kind==="manga"?!L(i):t.kind==="tv"?!_(i):!F(i))?{provider:null,error:"invalid provider shape"}:{provider:i,facets:ve(a)}}async function he(r,n,e,t){if(!n.sha256)return{provider:null,error:"no verified fallback"};const o=y.join(e,`${n.id}-${n.version}.mjs`);let a;try{a=await m.promises.readFile(o)}catch{return{provider:null,error:"no cached fallback file"}}if(I(a)!==n.sha256)return{provider:null,error:"cached fallback failed verification"};const c=`${n.version}-${n.sha256.slice(0,12)}`;return J(o,c,t,{...r,version:n.version})}async function Me(r){const n=(r.registryUrl??M.PROVIDER_REPO_URL??"").trim(),e={providers:{},mangaProviders:{},tvProviders:{},catalog:[],proxyRules:[]};if(!n)return e;const t=y.join(M.ROOT,"remote-providers"),o=y.join(t,"registry.json");let a=null;try{a=H(JSON.parse((await m.promises.readFile(o)).toString("utf8")))}catch{}const c=new Map((a?.providers??[]).map(s=>[s.id.toLowerCase(),s]));let i;try{const s=await de(n);i=JSON.parse(s.toString("utf8")),await m.promises.mkdir(t,{recursive:!0}),await m.promises.writeFile(o,JSON.stringify(i))}catch(s){g.error({err:s,registryUrl:n},"remote providers: manifest fetch failed, trying disk cache");try{i=JSON.parse((await m.promises.readFile(o)).toString("utf8"))}catch{return e}}let l;try{l=H(i)}catch(s){return g.error({err:s,registryUrl:n},"remote providers: manifest parse failed"),e}const u=new Set((r.enabledProviders??[]).map(s=>s.toLowerCase())),B=u.size>0,w=pe(r.cache),f=r.allowUnsigned??(process.env.DANGO_ALLOW_UNSIGNED_PROVIDERS==="1"||ce(n));for(const s of l.providers){const v=s.id.toLowerCase();if(s.enabledByDefault===!1&&(!B||!u.has(v))){e.catalog.push(P(s,!1,"disabled by default"));continue}if(B&&!u.has(v)){e.catalog.push(P(s,!1,"not in enabledProviders"));continue}const{provider:h,facets:d,error:k}=await me(s,n,t,w,f);if(h&&!k){if(s.kind==="manga"&&L(h))e.mangaProviders[v]=h;else if(s.kind==="tv"&&_(h))e.tvProviders[v]=h;else if(s.kind!=="manga"&&s.kind!=="tv"&&F(h))e.providers[v]=h;else{e.catalog.push(P(s,!1,"invalid provider shape for kind")),g.warn({provider:s.id},"remote provider kind mismatch, skipped");continue}e.catalog.push(V(d,q(s,h,P(s,!0)))),s.proxy&&e.proxyRules.push(s.proxy),g.info({provider:s.id,version:s.version},"remote provider loaded")}else{const R=c.get(v);if(R&&R.version!==s.version){const p=await he(s,R,t,w);if(p.provider&&!p.error&&(s.kind==="manga"&&L(p.provider)?e.mangaProviders[v]=p.provider:s.kind==="tv"&&_(p.provider)?e.tvProviders[v]=p.provider:s.kind!=="manga"&&s.kind!=="tv"&&F(p.provider)?e.providers[v]=p.provider:p.error="cached fallback failed validation"),p.provider&&!p.error){e.catalog.push({...V(p.facets,q(s,p.provider,P({...s,version:R.version},!0))),error:`serving cached ${R.version}: ${k}`}),s.proxy&&e.proxyRules.push(s.proxy),g.warn({provider:s.id,error:k,fallback:R.version},"remote provider update failed, serving cached version");continue}}e.catalog.push(P(s,!1,k)),g.warn({provider:s.id,error:k},"remote provider skipped")}}const z=new Set(e.catalog.filter(s=>!s.loaded).map(s=>`${s.id}-`));try{const s=new Set(l.providers.map(d=>`${d.id}-${d.version}.mjs`.toLowerCase())),v=new Set(l.providers.map(d=>`${d.kind??"anime"}:${d.id}-${d.version}-${(d.sha256??"nosha").slice(0,12)}`.toLowerCase()));for(const d of C.keys())v.has(d.toLowerCase())||C.delete(d);const h=await m.promises.readdir(t);for(const d of h)if(d.toLowerCase().endsWith(".mjs")&&!s.has(d.toLowerCase())){if([...z].some(k=>d.toLowerCase().startsWith(k)))continue;await m.promises.unlink(y.join(t,d)),g.info({file:d},"remote providers: removed superseded module")}}catch(s){g.warn({err:s},"remote providers: superseded module cleanup failed")}return e}function q(r,n,e){if(r.kind!=="tv"||!n||typeof n!="object")return e;const t=n.servers;return Array.isArray(t)&&t.every(o=>typeof o=="string")&&t.length>0?{...e,servers:[...t]}:e}function V(r,n){const e=(r?.genres??[]).length>0?r?.genres:void 0,t=(r?.orders??[]).length>0?r?.orders:void 0;return!e&&!t?n:{...n,facets:{...e?{genres:e}:{},...t?{orders:t}:{}}}}function P(r,n,e){return{id:r.id.toLowerCase(),label:r.label,version:r.version,mature:r.mature,kind:r.kind??"anime",sub:r.sub,tier:r.tier,modes:r.modes??(r.kind==="tv"?[]:["sub","dub"]),browse:r.browse,enabledByDefault:r.enabledByDefault!==!1,loaded:n,remote:!0,error:e}}export{Me as loadRemoteProviders};
@@ -1 +0,0 @@
1
- const f=/^[a-z0-9][a-z0-9-]*$/i,m=/^[0-9a-f]{64}$/i;function g(t){if(!t||typeof t!="object")throw new Error("registry: not an object");const o=t;if(typeof o.version!="number")throw new Error("registry.version must be a number");if(!Array.isArray(o.providers))throw new Error("registry.providers must be an array");const e=o.providers.map((r,s)=>p(r,s)),n=new Set;for(const r of e){const s=r.id.toLowerCase();if(n.has(s))throw new Error(`registry: duplicate provider id "${r.id}"`);n.add(s)}return{version:o.version,updatedAt:typeof o.updatedAt=="string"?o.updatedAt:void 0,providers:e}}function p(t,o){if(!t||typeof t!="object")throw new Error(`registry.providers[${o}]: not an object`);const e=t,n=e.id,r=e.entry,s=e.label,u=e.version;if(typeof n!="string"||!f.test(n.trim()))throw new Error(`registry.providers[${o}].id must match ${f}`);if(typeof s!="string"||!s.trim())throw new Error(`registry.providers[${o}].label must be a non-empty string`);if(typeof u!="string"||!u.trim())throw new Error(`registry.providers[${o}].version must be a non-empty string`);const i=e.embedBase;if(i!==void 0&&(typeof i!="string"||!/^https:\/\/[^/]+/i.test(i.trim())))throw new Error(`registry.providers[${o}].embedBase must be an https URL`);if((typeof r!="string"||!r.trim())&&i===void 0)throw new Error(`registry.providers[${o}].entry must be a non-empty string`);if(e.sha256!==void 0&&(typeof e.sha256!="string"||!m.test(e.sha256.trim())))throw new Error(`registry.providers[${o}].sha256 must be 64 hex chars`);if(e.mature!==void 0&&typeof e.mature!="boolean")throw new Error(`registry.providers[${o}].mature must be boolean`);if(e.kind!==void 0&&e.kind!=="anime"&&e.kind!=="asmr"&&e.kind!=="manga"&&e.kind!=="tv")throw new Error(`registry.providers[${o}].kind must be anime|asmr|manga|tv`);if(e.sub!==void 0&&e.sub!=="soft"&&e.sub!=="hard"&&e.sub!=="mixed")throw new Error(`registry.providers[${o}].sub must be soft|hard|mixed`);if(e.tier!==void 0&&e.tier!=="direct"&&e.tier!=="embed"&&e.tier!=="cookie")throw new Error(`registry.providers[${o}].tier must be direct|embed|cookie`);let a;if(e.modes!==void 0){if(!Array.isArray(e.modes))throw new Error(`registry.providers[${o}].modes must be an array`);a=[];for(const d of e.modes){if(d!=="sub"&&d!=="dub")throw new Error(`registry.providers[${o}].modes must contain only sub|dub`);a.includes(d)||a.push(d)}}return{id:n.trim(),label:s.trim(),version:u.trim(),entry:typeof r=="string"?r.trim():void 0,embedBase:typeof i=="string"?i.trim().replace(/\/+$/,""):void 0,sha256:typeof e.sha256=="string"?e.sha256.trim().toLowerCase():void 0,minDango:typeof e.minDango=="string"?e.minDango:void 0,mature:typeof e.mature=="boolean"?e.mature:!1,kind:e.kind==="asmr"||e.kind==="manga"||e.kind==="tv"?e.kind:"anime",sub:e.sub,tier:e.tier,modes:a,browse:y(e.browse,o),proxy:b(e.proxy,o),auth:e.auth==="cookie"?"cookie":void 0,enabledByDefault:e.enabledByDefault!==!1}}function b(t,o){if(t===void 0)return;if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`registry.providers[${o}].proxy must be an object`);const e=t,n={};for(const r of["hosts","suffixes","mirrors"])if(e[r]!==void 0){if(!Array.isArray(e[r])||!e[r].every(s=>typeof s=="string"&&s))throw new Error(`registry.providers[${o}].proxy.${r} must be a non-empty string array`);n[r]=e[r].map(s=>s.trim().toLowerCase())}for(const r of["referer","origin"])if(e[r]!==void 0){if(typeof e[r]!="string"||!/^https:\/\//i.test(e[r].trim()))throw new Error(`registry.providers[${o}].proxy.${r} must be an https URL`);n[r]=e[r].trim()}for(const r of["originFromReferer","scrape","cfCookie","embedPatch"])if(e[r]!==void 0){if(typeof e[r]!="boolean")throw new Error(`registry.providers[${o}].proxy.${r} must be boolean`);n[r]=e[r]}return n}function y(t,o){if(t===void 0)return;if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`registry.providers[${o}].browse must be an object`);const e=t,n={};for(const r of["genre","order","studio","sort"])if(e[r]!==void 0){if(typeof e[r]!="boolean")throw new Error(`registry.providers[${o}].browse.${r} must be boolean`);n[r]=e[r]}if(e.pageSize!==void 0){if(typeof e.pageSize!="number"||!Number.isInteger(e.pageSize)||e.pageSize<1)throw new Error(`registry.providers[${o}].browse.pageSize must be a positive integer`);n.pageSize=e.pageSize}return n}export{g as parseRegistry};
@@ -1 +0,0 @@
1
- function h(e){return e.toLowerCase().replace(/[^a-z0-9]/g,"")}function u(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(n=>n.length>=2)}function x(e,n){if(e.length<2||n.length<2)return e===n?1:0;const c=i=>{const g=new Map;for(let l=0;l<i.length-1;l++){const f=i.slice(l,l+2);g.set(f,(g.get(f)||0)+1)}return g},o=c(e),t=c(n);let s=0,r=0,a=0;return o.forEach(i=>s+=i),t.forEach(i=>r+=i),o.forEach((i,g)=>{const l=t.get(g);l&&(a+=Math.min(i,l))}),2*a/(s+r)}const v=new Set(["the","animation","no","wa","ga","wo","ni","de","to","e","o","x","ii","yo","ova","oad","hd","ep","episode","season","part","hen","a"]),S=new Set(["season","2nd","3rd","4th","5th","ii","iii","iv","v","vi","vii","viii","ix","x","two","three","four","five","six","seven","eight","nine","ten"]);function d(e){return[...new Set(u(e).filter(n=>!v.has(n)&&!S.has(n)))]}const m={ii:2,iii:3,iv:4,v:5,vi:6,vii:7,viii:8,ix:9,x:10},b={two:2,three:3,four:4,five:5,six:6,seven:7,eight:8,nine:9,ten:10};function p(e){const n=e.toLowerCase(),c=n.match(/\bseason\s*(\d+|ii|iii|iv|v|vi|vii|viii|ix|x|two|three|four|five|six|seven|eight|nine|ten)\b/);if(c){const r=c[1],a=m[r],i=b[r];return String(a??i??r)}const o=n.match(/\b(\d+)(st|nd|rd|th)\b/);if(o)return o[1];const t=n.match(/\b(ii|iii|iv|v|vi|vii|viii|ix|x)\b(?!\s*(st|nd|rd|th|season))/);if(t)return String(m[t[1]]);const s=n.match(/\b(\d+)\s*(season)?\s*$/);return s?s[1]:""}function w(e){return[...new Set(u(e).filter(n=>!v.has(n)))]}function M(e,n){const c=h(e),o=h(n);if(!c||!o)return 0;if(c===o)return 1;let t=x(c,o);if(c.startsWith(o)||o.startsWith(c)){const i=.5+.5*(Math.min(c.length,o.length)/Math.max(c.length,o.length));i>t&&(t=i)}const s=d(e),r=d(n);if(s.length>0&&r.length>=2){const i=new Set(r);let g=0;for(const f of s)i.has(f)&&g++;const l=g/Math.min(s.length,r.length);l>t&&(t=l)}const a=p(e);if(a){const i=p(n);i===a?t+=.25:!i&&t>=.5&&(t-=.25)}return t>=1?.99:t}function T(e,n,c=.6){if(!e.length)return null;const o=n.filter(s=>!!s&&s.trim().length>0);if(!o.length)return null;let t=null;for(const s of e){if(!s.title)continue;let r=0;for(const a of o){const i=M(a,s.title);i>r&&(r=i)}(!t||r>t.score)&&(t={item:s,score:r})}return t&&t.score>=c?t:null}function k(e,n){const c=new Set,o=[],t=g=>{const l=g.replace(/\s+/g," ").trim();l&&!c.has(l.toLowerCase())&&(c.add(l.toLowerCase()),o.push(l))};n&&n.trim()&&t(n),t(e),/^re\s*zero$/i.test(e.trim())&&t("Re Zero");const s=g=>g.replace(/[^\p{L}\p{N}\s]/gu," ");t(s(e)),n&&t(s(n));const r=s(e).split(/\s+/).filter(Boolean);r.length>3&&t(r.slice(0,3).join(" ")),r.length>4&&t(r.slice(0,4).join(" "));const a=w(e).concat(n?w(n):[]).filter(g=>g.length>=4),i=[...new Set(a)];i.sort((g,l)=>l.length-g.length);for(const g of i.slice(0,4))t(g);return o}export{k as buildQueryVariants,T as pickBestMatch};
File without changes
@@ -1,2 +0,0 @@
1
- import{spawn as l,execFile as m}from"child_process";import a from"./logger.js";import{CONFIG as c}from"./config.js";class u{activeRemote=null;executeCommand(t){return new Promise((e,r)=>{m("rclone",t,(i,o,n)=>{if(i)return n&&a.warn({stderr:n},"Rclone command warning"),r(new Error(n||i.message));e(o.trim())})})}executeRcloneArgs(t){return new Promise((e,r)=>{const i=l("rclone",t,{stdio:"ignore"});i.on("close",o=>{o===0?e():r(new Error(`Rclone exited with code ${o}`))}),i.on("error",o=>r(o))})}async listRemotes(){try{return(await this.executeCommand(["listremotes"])).split(`
2
- `).map(e=>e.trim()).filter(e=>e!=="").map(e=>e.replace(/:$/,""))}catch{return[]}}async init(){try{await this.executeCommand(["version"]);const t=await this.listRemotes();if(c.RCLONE_REMOTE){const e=t.find(r=>r.toLowerCase()===c.RCLONE_REMOTE?.toLowerCase());if(e)return this.activeRemote=e,a.info(`Rclone initialized with manual remote: ${this.activeRemote}`),!0;a.warn(`Configured RCLONE_REMOTE '${c.RCLONE_REMOTE}' not found in rclone listremotes.`)}return t.length>0&&!c.RCLONE_REMOTE&&a.info({remotes:t},"Rclone available but no manual remote is configured in settings."),!1}catch(t){return a.warn({err:t},"Rclone initialization failed"),!1}}isActive(){return this.activeRemote!==null}getRemoteName(){return this.activeRemote||"unknown"}async downloadFile(t,e,r){if(!this.activeRemote)throw new Error("Rclone not active");const i=`${this.activeRemote}:${t}/${e}`;await this.executeRcloneArgs(["copyto",i,r])}async uploadFile(t,e,r){if(!this.activeRemote)throw new Error("Rclone not active");const i=`${this.activeRemote}:${e}/${r}`;await this.executeRcloneArgs(["copyto",t,i])}executeRcloneArgsWithOutput(t){return new Promise((e,r)=>{const i=l("rclone",t,{stdio:"pipe"});let o="",n="";i.stdout?.on("data",s=>o+=s),i.stderr?.on("data",s=>n+=s),i.on("close",s=>{s===0?e(o.trim()):(n&&a.warn({stderr:n},"Rclone command warning"),r(new Error(n||`Rclone exited with code ${s}`)))}),i.on("error",s=>r(s))})}async fileExists(t,e){if(!this.activeRemote)return!1;try{const r=`${this.activeRemote}:${t}/${e}`,i=await this.executeRcloneArgsWithOutput(["lsjson",r]),o=JSON.parse(i);return o&&o.length>0}catch{return!1}}}const w=new u;export{w as rcloneService};
@@ -1,42 +0,0 @@
1
- import{eq as i,inArray as l,sql as a}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";import{asmrProgress as n}from"../db/schema-asmr.js";import{makeLibraryRepo as u}from"./library.repository.js";const C=["Listening","Completed","On-Hold","Dropped","Planned"];function A(t){return String(t).trim().toUpperCase()}const g={...u("asmr_library"),upsert:(t,r)=>s(t).run(a`
2
- INSERT INTO asmr_library (id, rjCode, title, thumbnail, status, isAdult, updatedAt)
3
- VALUES (${r.id}, ${r.rjCode}, ${r.title}, ${r.thumbnail}, ${r.status}, ${r.isAdult?1:0}, strftime('%s', 'now'))
4
- ON CONFLICT(id) DO UPDATE SET
5
- rjCode = COALESCE(NULLIF(EXCLUDED.rjCode, ''), asmr_library.rjCode),
6
- title = COALESCE(NULLIF(EXCLUDED.title, ''), asmr_library.title),
7
- thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), asmr_library.thumbnail),
8
- status = COALESCE(NULLIF(EXCLUDED.status, ''), asmr_library.status),
9
- isAdult = COALESCE(EXCLUDED.isAdult, asmr_library.isAdult),
10
- updatedAt = strftime('%s', 'now')`),touchProgress:(t,r,e)=>s(t).run(a`
11
- UPDATE asmr_library SET lastTrackIndex = ${e.trackIndex}, lastTrackLabel = ${e.trackLabel}, lastPosition = ${e.position}, updatedAt = strftime('%s', 'now') WHERE id = ${r}`)},c={getByWork:(t,r)=>s(t).all(a`
12
- SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${r} ORDER BY updatedAt DESC`),getTrack:async(t,r,e)=>(await s(t).all(a`
13
- SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${r} AND trackIndex = ${e}`))[0],getLatest:async(t,r)=>(await s(t).all(a`
14
- SELECT workId, trackIndex, trackLabel, currentTime, duration, updatedAt FROM asmr_progress WHERE workId = ${r} ORDER BY updatedAt DESC LIMIT 1`))[0],upsert:(t,r)=>s(t).run(a`
15
- INSERT INTO asmr_progress (workId, trackIndex, trackLabel, currentTime, duration, title, thumbnail, rjCode, isAdult, updatedAt)
16
- VALUES (${r.workId}, ${r.trackIndex}, ${r.trackLabel}, ${r.currentTime}, ${r.duration}, ${r.title??null}, ${r.thumbnail??null}, ${r.rjCode??null}, ${r.isAdult??null}, strftime('%s', 'now'))
17
- ON CONFLICT(workId, trackIndex) DO UPDATE SET
18
- trackLabel = COALESCE(NULLIF(EXCLUDED.trackLabel, ''), asmr_progress.trackLabel),
19
- currentTime = EXCLUDED.currentTime,
20
- duration = EXCLUDED.duration,
21
- title = COALESCE(EXCLUDED.title, asmr_progress.title),
22
- thumbnail = COALESCE(EXCLUDED.thumbnail, asmr_progress.thumbnail),
23
- rjCode = COALESCE(EXCLUDED.rjCode, asmr_progress.rjCode),
24
- isAdult = COALESCE(EXCLUDED.isAdult, asmr_progress.isAdult),
25
- updatedAt = strftime('%s', 'now')`),deleteByWork:(t,r)=>s(t).delete(n).where(i(n.workId,r)),deleteMany:(t,r)=>r.length===0?Promise.resolve():s(t).delete(n).where(l(n.workId,r)),deleteTrack:(t,r,e)=>s(t).run(a`
26
- DELETE FROM asmr_progress WHERE workId = ${r} AND trackIndex = ${e}`),getContinueListening:(t,r)=>{const e=typeof r=="number"?`LIMIT ${r}`:"";return s(t).all(a`
27
- SELECT p.workId as id,
28
- COALESCE(l.rjCode, p.rjCode, p.workId) as rjCode,
29
- COALESCE(l.title, p.title) as title,
30
- COALESCE(l.thumbnail, p.thumbnail) as thumbnail,
31
- COALESCE(l.isAdult, p.isAdult) as isAdult,
32
- l.status as watchlistStatus,
33
- p.trackIndex, p.trackLabel, p.currentTime, p.duration, p.updatedAt as progressAt
34
- FROM (
35
- SELECT *, ROW_NUMBER() OVER (PARTITION BY workId ORDER BY updatedAt DESC) as rn
36
- FROM asmr_progress
37
- ) p
38
- LEFT JOIN asmr_library l ON p.workId = l.id
39
- WHERE p.rn = 1
40
- AND (l.status IS NULL OR l.status = 'Listening')
41
- ORDER BY p.updatedAt DESC
42
- ${a.raw(e)}`)}};export{C as ASMR_STATUSES,g as AsmrLibraryRepository,c as AsmrProgressRepository,A as buildAsmrId};
@@ -1,92 +0,0 @@
1
- import{sql as t}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";const w={getCoreStats:async e=>(await s(e).all(t`
2
- SELECT
3
- (SELECT SUM(
4
- CASE WHEN we.currentTime > 0 THEN we.currentTime
5
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
6
- COALESCE(sm.episodeDuration * 60,
7
- CASE UPPER(COALESCE(sm.type, ''))
8
- WHEN 'MOVIE' THEN 6000
9
- WHEN 'TV_SHORT' THEN 720
10
- WHEN 'MUSIC' THEN 300
11
- ELSE 1440 END)
12
- ELSE 0 END)
13
- FROM watched_episodes we
14
- LEFT JOIN shows_meta sm ON sm.id = we.showId) as totalSeconds,
15
- (SELECT COUNT(*) FROM watched_episodes) as totalEpisodes,
16
- (SELECT COUNT(DISTINCT showId) FROM watched_episodes) as totalAnime,
17
- (SELECT COUNT(*) FROM watchlist WHERE status = 'Completed') as completedCount,
18
- (SELECT COUNT(*) FROM watchlist) as totalWatchlist`))[0],getActivityGrid:e=>s(e).all(t`
19
- SELECT date(watchedAt) as day, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY day`),getHourlyDist:e=>s(e).all(t`
20
- SELECT strftime('%H', watchedAt) as hour, COUNT(*) as count FROM watched_episodes WHERE NOT (currentTime = 0 AND duration = 0) GROUP BY hour`),getSeasonality:e=>s(e).all(t`
21
- SELECT strftime('%m', we.watchedAt) as month, SUM(
22
- CASE WHEN we.currentTime > 0 THEN we.currentTime
23
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
24
- COALESCE(sm.episodeDuration * 60,
25
- CASE UPPER(COALESCE(sm.type, ''))
26
- WHEN 'MOVIE' THEN 6000
27
- WHEN 'TV_SHORT' THEN 720
28
- WHEN 'MUSIC' THEN 300
29
- ELSE 1440 END)
30
- ELSE 0 END) as seconds
31
- FROM watched_episodes we
32
- LEFT JOIN shows_meta sm ON sm.id = we.showId
33
- GROUP BY month`),getAllWatches:e=>s(e).all(t`
34
- SELECT we.watchedAt, we.currentTime,
35
- CASE WHEN we.currentTime > 0 THEN we.currentTime
36
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
37
- COALESCE(sm.episodeDuration * 60,
38
- CASE UPPER(COALESCE(sm.type, ''))
39
- WHEN 'MOVIE' THEN 6000
40
- WHEN 'TV_SHORT' THEN 720
41
- WHEN 'MUSIC' THEN 300
42
- ELSE 1440 END)
43
- ELSE 0 END as effectiveSeconds
44
- FROM watched_episodes we
45
- LEFT JOIN shows_meta sm ON sm.id = we.showId
46
- ORDER BY we.watchedAt ASC`),getWatchedShowsMeta:e=>s(e).all(t`
47
- SELECT DISTINCT sm.id, sm.genres, sm.popularityScore
48
- FROM shows_meta sm
49
- JOIN watched_episodes we ON sm.id = we.showId`),getDroppedShows:e=>s(e).all(t`
50
- SELECT w.id, w.name, MAX(we.watchedAt) as lastActivity
51
- FROM watchlist w
52
- JOIN watched_episodes we ON w.id = we.showId
53
- WHERE w.status = 'Watching' AND NOT (we.currentTime = 0 AND we.duration = 0)
54
- GROUP BY w.id
55
- HAVING lastActivity < date('now', '-90 days')`),getCompletionVelocities:e=>s(e).all(t`
56
- SELECT
57
- (julianday(MAX(we.watchedAt)) - julianday(MIN(we.watchedAt))) as daysToFinish
58
- FROM watchlist w
59
- JOIN watched_episodes we ON w.id = we.showId
60
- WHERE w.status = 'Completed'
61
- GROUP BY w.id
62
- HAVING SUM(we.currentTime) > 0`),getWatchedEpisodesWithMeta:e=>s(e).all(t`
63
- SELECT
64
- we.showId,
65
- we.currentTime,
66
- we.duration,
67
- CASE WHEN we.currentTime > 0 THEN we.currentTime
68
- WHEN we.currentTime = 0 AND we.duration = 0 THEN
69
- COALESCE(sm.episodeDuration * 60,
70
- CASE UPPER(COALESCE(sm.type, ''))
71
- WHEN 'MOVIE' THEN 6000
72
- WHEN 'TV_SHORT' THEN 720
73
- WHEN 'MUSIC' THEN 300
74
- ELSE 1440 END)
75
- ELSE 0 END as effectiveSeconds,
76
- sm.genres,
77
- sm.popularityScore,
78
- sm.name,
79
- sm.nativeName,
80
- sm.englishName,
81
- sm.thumbnail
82
- FROM watched_episodes we
83
- JOIN shows_meta sm ON we.showId = sm.id`),getLibraryShowsWithGenres:e=>s(e).all(t`
84
- SELECT
85
- w.id,
86
- w.status,
87
- COALESCE(NULLIF(sm.englishName, ''), sm.name, w.name) as title,
88
- sm.genres,
89
- (SELECT COUNT(*) FROM watched_episodes we WHERE we.showId = w.id) as episodesWatched,
90
- COALESCE(sm.popularityScore, 0) as popularityScore
91
- FROM watchlist w
92
- LEFT JOIN shows_meta sm ON sm.id = w.id`),getAllAnilistIds:e=>s(e).all(t`SELECT anilistId FROM shows_meta WHERE anilistId IS NOT NULL`)};export{w as InsightsRepository};
@@ -1,3 +0,0 @@
1
- import{sql as a}from"drizzle-orm";import{getDrizzle as n}from"../db/drizzle.js";const l=["watchlist","watched_episodes","queue","shows_meta","dismissed_notifications","discovered_notifications","legacy_id_mapping","temp_show_ids"],b=["manga_library","manga_progress"],m=["tv_library","tv_progress"],E=["asmr_library","asmr_progress"];async function u(e,o){const t={};for(const r of o)try{const s=await n(e).all(a`SELECT COUNT(*) AS rows FROM ${a.raw(`"${r}"`)}`);t[r]=s[0]?.rows??0}catch{t[r]=0}return t}async function c(e,o){for(const t of o)try{await n(e).run(a`DELETE FROM ${a.raw(`"${t}"`)}`)}catch{}}function A(e){const o=a.raw(`"${e}"`);return{getById:async(t,r)=>(await n(t).all(a`SELECT * FROM ${o} WHERE id = ${r}`))[0],getAll:(t,r,s,i)=>{const p=a`SELECT * FROM ${o}`;return r&&r!=="All"&&p.append(a` WHERE status = ${r}`),p.append(a` ORDER BY updatedAt DESC`),s!==void 0&&i!==void 0&&p.append(a` LIMIT ${s} OFFSET ${i}`),n(t).all(p)},getCount:async(t,r)=>r&&r!=="All"?(await n(t).all(a`SELECT COUNT(*) as total FROM ${o} WHERE status = ${r}`))[0]?.total||0:(await n(t).all(a`SELECT COUNT(*) as total FROM ${o}`))[0]?.total||0,getIds:async t=>(await n(t).all(a`SELECT id FROM ${o}`)).map(s=>s.id),updateStatus:(t,r,s)=>n(t).run(a`
2
- UPDATE ${o} SET status = ${s}, updatedAt = strftime('%s', 'now') WHERE id = ${r}`),updateStatusMany:(t,r,s)=>r.length===0?Promise.resolve():n(t).run(a`
3
- UPDATE ${o} SET status = ${s}, updatedAt = strftime('%s', 'now') WHERE id IN (${a.join(r.map(i=>a`${i}`),a`, `)})`),delete:(t,r)=>n(t).run(a`DELETE FROM ${o} WHERE id = ${r}`),deleteMany:(t,r)=>r.length===0?Promise.resolve():n(t).run(a`DELETE FROM ${o} WHERE id IN (${a.join(r.map(s=>a`${s}`),a`, `)})`)}}const L={countAll:e=>u(e,l),clearAll:e=>c(e,l),countManga:e=>u(e,b),clearManga:e=>c(e,b),countTv:e=>u(e,m),clearTv:e=>c(e,m),countAsmr:e=>u(e,E),clearAsmr:e=>c(e,E)};export{L as LibraryRepository,A as makeLibraryRepo};
@@ -1 +0,0 @@
1
- import{DatabaseSync as p}from"node:sqlite";import T from"fs";import m from"path";import{CONFIG as l}from"../config.js";import{DatabaseWrapper as u}from"../db.js";import{dbGet as a,dbRun as n}from"../utils/db-utils.js";const s=5e3,S=6*3600;function h(){return m.join(l.ROOT,l.IS_DEV?"mal-cache.dev.db":"mal-cache.db")}let o=null;function c(){if(!o){const e=h();T.mkdirSync(m.dirname(e),{recursive:!0}),o=new u(e,new p(e)),n(o,"CREATE TABLE IF NOT EXISTS mal_cache (key TEXT PRIMARY KEY, payload TEXT NOT NULL, fetched_at TEXT NOT NULL, expires_at TEXT NOT NULL)")}return o}function f(e){const t=a(c(),"SELECT payload, fetched_at AS fetchedAt, expires_at AS expiresAt FROM mal_cache WHERE key = ?",[e]);return t?{payload:t.payload,fresh:Date.now()<Date.parse(t.expiresAt)}:null}function O(e,t,i=S){const r=c(),E=new Date;n(r,"INSERT OR REPLACE INTO mal_cache (key, payload, fetched_at, expires_at) VALUES (?, ?, ?, ?)",[e,t,E.toISOString(),new Date(E.getTime()+i*1e3).toISOString()]),!((a(r,"SELECT COUNT(*) AS rows FROM mal_cache")?.rows??0)<s||(d(),(a(r,"SELECT COUNT(*) AS rows FROM mal_cache")?.rows??0)<s))&&n(r,`DELETE FROM mal_cache WHERE key NOT IN (SELECT key FROM mal_cache ORDER BY fetched_at DESC LIMIT ${s})`)}function d(){const e=new Date().toISOString(),t=a(c(),"SELECT COUNT(*) AS rows FROM mal_cache WHERE expires_at < ?",[e]);return n(c(),"DELETE FROM mal_cache WHERE expires_at < ?",[e]),t?.rows??0}function N(){return{get:e=>f(e),put:(e,t,i)=>O(e,t,i)}}export{s as MAL_CACHE_MAX_ROWS,f as malCacheGet,d as malCachePruneExpired,O as malCachePut,N as malCacheStore};
@@ -1,51 +0,0 @@
1
- import{eq as l,inArray as p,sql as n}from"drizzle-orm";import{getDrizzle as e}from"../db/drizzle.js";import{mangaLibrary as i,mangaProgress as s}from"../db/schema-manga.js";import{makeLibraryRepo as o}from"./library.repository.js";const I=["Reading","Completed","On-Hold","Dropped","Planned"];function L(a,t){return`${a.toLowerCase()}:${t}`}const D={...o("manga_library"),upsert:(a,t)=>e(a).run(n`
2
- INSERT INTO manga_library (id, provider, mangaId, title, cover, status, author, altTitle, contentRating, anilistId, anilistIdSource, updatedAt)
3
- VALUES (${t.id}, ${t.provider}, ${t.mangaId}, ${t.title}, ${t.cover}, ${t.status}, ${t.author||null}, ${t.altTitle||null}, ${t.contentRating||null}, ${t.anilistId??null}, ${t.anilistIdSource??null}, strftime('%s', 'now'))
4
- ON CONFLICT(id) DO UPDATE SET
5
- provider = COALESCE(NULLIF(EXCLUDED.provider, ''), manga_library.provider),
6
- title = COALESCE(NULLIF(EXCLUDED.title, ''), manga_library.title),
7
- cover = COALESCE(NULLIF(EXCLUDED.cover, ''), manga_library.cover),
8
- status = COALESCE(NULLIF(EXCLUDED.status, ''), manga_library.status),
9
- author = COALESCE(NULLIF(EXCLUDED.author, ''), manga_library.author),
10
- altTitle = COALESCE(NULLIF(EXCLUDED.altTitle, ''), manga_library.altTitle),
11
- contentRating = COALESCE(EXCLUDED.contentRating, manga_library.contentRating),
12
- anilistId = COALESCE(EXCLUDED.anilistId, manga_library.anilistId),
13
- anilistIdSource = COALESCE(EXCLUDED.anilistIdSource, manga_library.anilistIdSource),
14
- updatedAt = strftime('%s', 'now')`),setAnilistId:(a,t,r,g)=>g?e(a).update(i).set({anilistId:r,anilistIdSource:g}).where(l(i.id,t)):e(a).update(i).set({anilistId:r}).where(l(i.id,t)),touchProgress:(a,t,r)=>e(a).run(n`
15
- UPDATE manga_library SET lastChapterId = ${r.chapterId}, lastChapterNumber = ${r.chapterNumber}, lastPage = ${r.page}, updatedAt = strftime('%s', 'now') WHERE id = ${t}`),setProgressPointer:(a,t,r)=>e(a).run(n`
16
- UPDATE manga_library SET lastChapterNumber = ${r}, updatedAt = strftime('%s', 'now') WHERE id = ${t}`)},b={getByManga:(a,t)=>e(a).all(n`
17
- SELECT mangaId, chapterId, chapterNumber, page, pageCount, updatedAt FROM manga_progress WHERE mangaId = ${t} ORDER BY updatedAt DESC`),getChapter:async(a,t,r)=>(await e(a).all(n`
18
- SELECT mangaId, chapterId, chapterNumber, page, pageCount, updatedAt FROM manga_progress WHERE mangaId = ${t} AND chapterId = ${r}`))[0],getLatest:async(a,t)=>(await e(a).all(n`
19
- SELECT mangaId, chapterId, chapterNumber, page, pageCount, updatedAt FROM manga_progress WHERE mangaId = ${t} ORDER BY updatedAt DESC LIMIT 1`))[0],upsert:(a,t)=>e(a).run(n`
20
- INSERT INTO manga_progress (mangaId, chapterId, chapterNumber, page, pageCount, title, cover, provider, altTitle, contentRating, updatedAt)
21
- VALUES (${t.mangaId}, ${t.chapterId}, ${t.chapterNumber}, ${t.page}, ${t.pageCount}, ${t.title??null}, ${t.cover??null}, ${t.provider??null}, ${t.altTitle??null}, ${t.contentRating??null}, strftime('%s', 'now'))
22
- ON CONFLICT(mangaId, chapterId) DO UPDATE SET
23
- chapterNumber = COALESCE(NULLIF(EXCLUDED.chapterNumber, ''), manga_progress.chapterNumber),
24
- page = EXCLUDED.page,
25
- pageCount = EXCLUDED.pageCount,
26
- title = COALESCE(EXCLUDED.title, manga_progress.title),
27
- cover = COALESCE(EXCLUDED.cover, manga_progress.cover),
28
- provider = COALESCE(EXCLUDED.provider, manga_progress.provider),
29
- altTitle = COALESCE(EXCLUDED.altTitle, manga_progress.altTitle),
30
- contentRating = COALESCE(EXCLUDED.contentRating, manga_progress.contentRating),
31
- updatedAt = strftime('%s', 'now')`),deleteByManga:(a,t)=>e(a).delete(s).where(l(s.mangaId,t)),deleteMany:(a,t)=>t.length===0?Promise.resolve():e(a).delete(s).where(p(s.mangaId,t)),deleteChapter:(a,t,r)=>e(a).run(n`
32
- DELETE FROM manga_progress WHERE mangaId = ${t} AND chapterId = ${r}`),deleteSyntheticChapters:(a,t,r)=>e(a).run(n`
33
- DELETE FROM manga_progress WHERE mangaId = ${t} AND chapterId LIKE 'anilist:ch:%' AND chapterId != ${r}`),moveSyntheticChapters:(a,t,r)=>e(a).run(n`
34
- UPDATE manga_progress SET mangaId = ${r}, updatedAt = strftime('%s', 'now') WHERE mangaId = ${t} AND chapterId LIKE 'anilist:ch:%'`),getContinueReading:(a,t)=>{const r=typeof t=="number"?`LIMIT ${t}`:"";return e(a).all(n`
35
- SELECT p.mangaId as id,
36
- COALESCE(l.mangaId, SUBSTR(p.mangaId, INSTR(p.mangaId, ':') + 1)) as mangaId,
37
- COALESCE(l.provider, p.provider) as provider,
38
- COALESCE(l.title, p.title) as title, COALESCE(l.cover, p.cover) as cover,
39
- COALESCE(l.author, '') as author, COALESCE(l.altTitle, p.altTitle) as altTitle,
40
- COALESCE(l.contentRating, p.contentRating) as contentRating,
41
- l.status as watchlistStatus,
42
- p.chapterId, p.chapterNumber, p.page, p.pageCount, p.updatedAt as progressAt
43
- FROM (
44
- SELECT *, ROW_NUMBER() OVER (PARTITION BY mangaId ORDER BY updatedAt DESC) as rn
45
- FROM manga_progress
46
- ) p
47
- LEFT JOIN manga_library l ON p.mangaId = l.id
48
- WHERE p.rn = 1
49
- AND (l.status IS NULL OR l.status = 'Reading')
50
- ORDER BY p.updatedAt DESC
51
- ${n.raw(r)}`)}};export{I as MANGA_STATUSES,D as MangaLibraryRepository,b as MangaProgressRepository,L as buildMangaId};
@@ -1,14 +0,0 @@
1
- import{and as n,eq as t,sql as o}from"drizzle-orm";import{getDrizzle as i}from"../db/drizzle.js";import{discoveredNotifications as d,dismissedNotifications as a}from"../db/schema-anime.js";const N={getDismissedByShow:(e,s)=>i(e).all(o`SELECT episodeNumber FROM dismissed_notifications WHERE showId = ${s}`),getDiscoveredByShow:(e,s)=>i(e).all(o`SELECT episodeNumber FROM discovered_notifications WHERE showId = ${s}`),addDiscovered:(e,s,r)=>i(e).insert(d).values({showId:s,episodeNumber:r}).onConflictDoNothing(),addDismissed:(e,s,r)=>i(e).insert(a).values({showId:s,episodeNumber:r}).onConflictDoNothing(),dismissFromDiscovered:(e,s)=>s?i(e).run(o`
2
- INSERT OR IGNORE INTO dismissed_notifications (showId, episodeNumber) SELECT showId, episodeNumber FROM discovered_notifications WHERE showId = ${s}`):i(e).run(o`
3
- INSERT OR IGNORE INTO dismissed_notifications (showId, episodeNumber) SELECT showId, episodeNumber FROM discovered_notifications`),deleteByShow:(e,s)=>Promise.all([i(e).delete(a).where(t(a.showId,s)),i(e).delete(d).where(t(d.showId,s))]),deleteSpecificDismissed:(e,s,r)=>i(e).delete(a).where(n(t(a.showId,s),t(a.episodeNumber,r))),deleteDiscovered:(e,s,r)=>i(e).delete(d).where(n(t(d.showId,s),t(d.episodeNumber,r))),cleanupWatchedNotifications:e=>Promise.all([i(e).run(o`
4
- DELETE FROM dismissed_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = dismissed_notifications.showId AND we.episodeNumber = dismissed_notifications.episodeNumber)`),i(e).run(o`
5
- DELETE FROM discovered_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = discovered_notifications.showId AND we.episodeNumber = discovered_notifications.episodeNumber)`)]),hasAnyDiscovered:async e=>((await i(e).all(o`SELECT COUNT(*) as count FROM discovered_notifications`))[0]?.count||0)>0,hasActiveNotifications:async e=>((await i(e).all(o`
6
- SELECT COUNT(*) as count FROM discovered_notifications dn
7
- WHERE NOT EXISTS (
8
- SELECT 1 FROM dismissed_notifications dn2
9
- WHERE dn2.showId = dn.showId AND dn2.episodeNumber = dn.episodeNumber
10
- )
11
- AND NOT EXISTS (
12
- SELECT 1 FROM watched_episodes we
13
- WHERE we.showId = dn.showId AND we.episodeNumber = dn.episodeNumber
14
- )`))[0]?.count||0)>0};export{N as NotificationsRepository};
@@ -1,19 +0,0 @@
1
- import{and as n,eq as o,sql as u}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";import{queue as i}from"../db/schema-anime.js";const d={getAll:e=>s(e).all(u`
2
- SELECT
3
- q.id,
4
- q.showId,
5
- q.episodeNumber,
6
- q.queue_order,
7
- COALESCE(NULLIF(sm.name, ''), w.name) as name,
8
- COALESCE(NULLIF(sm.thumbnail, ''), NULLIF(w.thumbnail, ''), '') as thumbnail,
9
- COALESCE(NULLIF(sm.nativeName, ''), w.nativeName) as nativeName,
10
- COALESCE(NULLIF(sm.englishName, ''), w.englishName) as englishName,
11
- COALESCE(NULLIF(sm.type, ''), w.type) as type
12
- FROM queue q
13
- LEFT JOIN shows_meta sm ON q.showId = sm.id
14
- LEFT JOIN watchlist w ON q.showId = w.id
15
- ORDER BY q.queue_order ASC, q.id ASC`),getByEpisode:async(e,a,r)=>(await s(e).all(u`SELECT * FROM queue WHERE showId = ${a} AND episodeNumber = ${r}`))[0],getByShow:(e,a)=>s(e).all(u`SELECT * FROM queue WHERE showId = ${a} ORDER BY queue_order ASC`),getMaxOrder:async e=>(await s(e).all(u`SELECT COALESCE(MAX(queue_order), -1) as maxOrder FROM queue`))[0]?.maxOrder??-1,addToEnd:(e,a,r)=>s(e).run(u`
16
- INSERT INTO queue (showId, episodeNumber, queue_order) VALUES
17
- (${a}, ${r}, (SELECT COALESCE(MAX(queue_order), -1) + 1 FROM queue))`),removeEpisode:(e,a,r)=>s(e).delete(i).where(n(o(i.showId,a),o(i.episodeNumber,r))),addManyToEnd:async(e,a)=>{if(a.length===0)return;let r=await d.getMaxOrder(e)+1;for(const t of a)await d.getByEpisode(e,t.showId,t.episodeNumber)||(await s(e).insert(i).values({showId:t.showId,episodeNumber:t.episodeNumber,queueOrder:r}),r+=1)},removeMany:(e,a,r)=>r.length===0?Promise.resolve():s(e).run(u`
18
- DELETE FROM queue WHERE showId = ${a} AND episodeNumber IN (${u.join(r.map(t=>u`${t}`),u`, `)})`),clear:e=>s(e).delete(i),reorder:(e,a)=>Promise.all(a.map((r,t)=>r.id!==void 0?s(e).update(i).set({queueOrder:t}).where(o(i.id,r.id)):s(e).update(i).set({queueOrder:t}).where(n(o(i.showId,r.showId??""),o(i.episodeNumber,r.episodeNumber??""))))),cleanupOrphanedShowsMeta:e=>s(e).run(u`
19
- DELETE FROM shows_meta WHERE id NOT IN (SELECT id FROM watchlist) AND id NOT IN (SELECT showId FROM queue)`)};export{d as QueueRepository};
@@ -1,3 +0,0 @@
1
- import{eq as g,sql as r}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";import{settings as n,watchlist as p}from"../db/schema-anime.js";const o={getByKey:async(t,e)=>(await s(t).all(r`SELECT value FROM settings WHERE key = ${e}`))[0],upsert:(t,e,a)=>s(t).insert(n).values({key:e,value:a}).onConflictDoUpdate({target:n.key,set:{value:a}}),deleteByKey:(t,e)=>s(t).delete(n).where(g(n.key,e)),readContinueFlags:async(t,e,a)=>{const l=await o.getByKey(t,e),i=await o.getByKey(t,a);return{ignoreAdult:l?l.value!=="false":!0,listOnly:i?i.value==="true"||i.value==="1":!1}},clearWatchlist:t=>s(t).delete(p),upsertWatchlistBatch:(t,e)=>e.length===0?Promise.resolve():s(t).run(r`
2
- INSERT OR REPLACE INTO watchlist (id, name, thumbnail, status) VALUES
3
- ${r.join(e.map(a=>r`(${a.id}, ${a.name}, ${a.thumbnail??null}, ${a.status})`),r`, `)}`)};export{o as SettingsRepository};
@@ -1,5 +0,0 @@
1
- import{eq as r,sql as i}from"drizzle-orm";import{getDrizzle as a}from"../db/drizzle.js";import{showsMeta as s}from"../db/schema-anime.js";const E={getById:async(n,e)=>(await a(n).all(i`SELECT * FROM shows_meta WHERE id = ${e}`))[0],getStatus:async(n,e)=>(await a(n).all(i`SELECT status FROM shows_meta WHERE id = ${e}`))[0]?.status,upsert:(n,e)=>{const t={id:e.id,name:e.name??null,thumbnail:e.thumbnail??null,nativeName:e.nativeName??null,englishName:e.englishName??null,genres:e.genres??null,popularityScore:e.popularityScore??null,status:e.status??null,episodeCount:e.episodeCount??null,type:e.type??null,anilistId:e.anilistId??null,isAdult:e.isAdult??null,episodeDuration:e.episodeDuration??null};return a(n).insert(s).values(t).onConflictDoUpdate({target:s.id,set:{name:i`COALESCE(NULLIF(${t.name}, ''), ${s.name})`,thumbnail:i`COALESCE(NULLIF(${t.thumbnail}, ''), ${s.thumbnail})`,nativeName:i`COALESCE(NULLIF(${t.nativeName}, ''), ${s.nativeName})`,englishName:i`COALESCE(NULLIF(${t.englishName}, ''), ${s.englishName})`,genres:i`COALESCE(NULLIF(${t.genres}, ''), ${s.genres})`,popularityScore:i`COALESCE(${t.popularityScore}, ${s.popularityScore})`,status:i`COALESCE(NULLIF(${t.status}, ''), ${s.status})`,episodeCount:i`COALESCE(${t.episodeCount}, ${s.episodeCount})`,type:i`COALESCE(NULLIF(${t.type}, ''), ${s.type})`,anilistId:i`COALESCE(${t.anilistId}, ${s.anilistId})`,isAdult:i`COALESCE(${t.isAdult}, ${s.isAdult})`,episodeDuration:i`COALESCE(NULLIF(${t.episodeDuration}, 0), ${s.episodeDuration})`}})},updateEpisodeCount:(n,e,t)=>a(n).update(s).set({episodeCount:t}).where(r(s.id,e)),updateStatus:(n,e,t)=>a(n).update(s).set({status:t}).where(r(s.id,e)),cleanupOrphanedMeta:n=>a(n).run(i`
2
- DELETE FROM shows_meta WHERE id NOT IN (SELECT id FROM watchlist) AND id NOT IN (SELECT showId FROM queue)`),getShowIdsMissingMeta:n=>a(n).all(i`
3
- SELECT DISTINCT we.showId as id FROM watched_episodes we
4
- LEFT JOIN shows_meta sm ON sm.id = we.showId
5
- WHERE sm.thumbnail IS NULL OR TRIM(sm.thumbnail) = ''`)};export{E as ShowsMetaRepository};
@@ -1,3 +0,0 @@
1
- import{sql as a}from"drizzle-orm";import{getDrizzle as i}from"../db/drizzle.js";import{tempShowIds as p}from"../db/schema-anime.js";import{TEMP_SHOW_ID_PREFIX as o}from"../lib/temp-ids.js";const n={getById:async(t,e)=>(await i(t).all(a`SELECT * FROM temp_show_ids WHERE id = ${e}`))[0],getByProviderNative:async(t,e,r)=>(await i(t).all(a`SELECT * FROM temp_show_ids WHERE provider = ${e} AND nativeId = ${r}`))[0],count:async t=>(await i(t).all(a`SELECT COUNT(*) as total FROM temp_show_ids`))[0]?.total||0,allocate:async(t,e)=>{const r=await n.getByProviderNative(t,e.provider,e.nativeId);if(r)return r;const s=o.length+1,m=((await i(t).all(a`
2
- SELECT MAX(CAST(SUBSTR(id, ${s}) AS INTEGER)) as maxId FROM temp_show_ids WHERE id LIKE ${`${o}%`}`))[0]?.maxId||0)+1,E=`${o}${m}`;try{await i(t).insert(p).values({id:E,provider:e.provider,nativeId:e.nativeId,title:e.title,thumbnail:e.thumbnail||"",createdAt:Date.now()})}catch{const w=await n.getByProviderNative(t,e.provider,e.nativeId);if(w)return w;throw new Error("temp id allocation failed")}return await n.getById(t,E)},purge:async t=>{const e=await n.count(t);if(e===0)return 0;const r=`${o}%`;return await i(t).run(a`DELETE FROM watchlist WHERE id LIKE ${r}`),await i(t).run(a`DELETE FROM watched_episodes WHERE showId LIKE ${r}`),await i(t).run(a`DELETE FROM queue WHERE showId LIKE ${r}`),await i(t).run(a`DELETE FROM shows_meta WHERE id LIKE ${r}`),await i(t).run(a`DELETE FROM dismissed_notifications WHERE showId LIKE ${r}`),await i(t).run(a`DELETE FROM discovered_notifications WHERE showId LIKE ${r}`),await i(t).run(a`
3
- DELETE FROM legacy_id_mapping WHERE legacyId LIKE ${r} OR numericId LIKE ${r}`),await i(t).delete(p),e}};export{n as TempShowIdsRepository};
@@ -1,50 +0,0 @@
1
- import{eq as n,inArray as d,sql as i}from"drizzle-orm";import{getDrizzle as s}from"../db/drizzle.js";import{tvProgress as a}from"../db/schema-tv.js";import{makeLibraryRepo as p}from"./library.repository.js";const g=["Watching","Completed","On-Hold","Dropped","Planned"];function v(r,e){return`${String(r).toLowerCase()==="movie"?"movie":"tv"}:${e}`}function D(r){const e=String(r??"").trim(),t=/^(movie|tv)[:-](\d+)$/i.exec(e);return t?`${t[1].toLowerCase()}:${t[2]}`:e}const T={...p("tv_library"),upsert:(r,e)=>s(r).run(i`
2
- INSERT INTO tv_library (id, tmdbId, mediaType, title, poster, backdrop, year, overview, status, adult, updatedAt)
3
- VALUES (${e.id}, ${e.tmdbId}, ${e.mediaType}, ${e.title}, ${e.poster}, ${e.backdrop||null}, ${e.year||null}, ${e.overview||null}, ${e.status}, ${e.adult?1:0}, strftime('%s', 'now'))
4
- ON CONFLICT(id) DO UPDATE SET
5
- tmdbId = EXCLUDED.tmdbId,
6
- mediaType = EXCLUDED.mediaType,
7
- title = COALESCE(NULLIF(EXCLUDED.title, ''), tv_library.title),
8
- poster = COALESCE(NULLIF(EXCLUDED.poster, ''), tv_library.poster),
9
- backdrop = COALESCE(NULLIF(EXCLUDED.backdrop, ''), tv_library.backdrop),
10
- year = COALESCE(NULLIF(EXCLUDED.year, ''), tv_library.year),
11
- overview = COALESCE(NULLIF(EXCLUDED.overview, ''), tv_library.overview),
12
- status = COALESCE(NULLIF(EXCLUDED.status, ''), tv_library.status),
13
- adult = COALESCE(EXCLUDED.adult, tv_library.adult),
14
- updatedAt = strftime('%s', 'now')`),touchProgress:(r,e,t)=>s(r).run(i`
15
- UPDATE tv_library SET lastSeason = ${t.season}, lastEpisode = ${t.episode}, updatedAt = strftime('%s', 'now') WHERE id = ${e}`)},I={getByMedia:(r,e)=>s(r).all(i`
16
- SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${e} ORDER BY updatedAt DESC, rowid DESC`),getEpisode:async(r,e,t,o)=>(await s(r).all(i`
17
- SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${e} AND season = ${t} AND episode = ${o}`))[0],getLatest:async(r,e)=>(await s(r).all(i`
18
- SELECT mediaId, season, episode, currentTime, duration, completed, updatedAt FROM tv_progress WHERE mediaId = ${e} ORDER BY updatedAt DESC, rowid DESC LIMIT 1`))[0],upsert:(r,e)=>s(r).run(i`
19
- INSERT INTO tv_progress (mediaId, season, episode, currentTime, duration, completed, title, poster, backdrop, year, overview, tmdbId, mediaType, adult, updatedAt)
20
- VALUES (${e.mediaId}, ${e.season}, ${e.episode}, ${e.currentTime}, ${e.duration}, ${e.completed}, ${e.title??null}, ${e.poster??null}, ${e.backdrop??null}, ${e.year??null}, ${e.overview??null}, ${e.tmdbId??null}, ${e.mediaType??null}, ${e.adult??null}, strftime('%s', 'now'))
21
- ON CONFLICT(mediaId, season, episode) DO UPDATE SET
22
- currentTime = EXCLUDED.currentTime,
23
- duration = EXCLUDED.duration,
24
- completed = EXCLUDED.completed,
25
- title = COALESCE(EXCLUDED.title, tv_progress.title),
26
- poster = COALESCE(EXCLUDED.poster, tv_progress.poster),
27
- backdrop = COALESCE(EXCLUDED.backdrop, tv_progress.backdrop),
28
- year = COALESCE(EXCLUDED.year, tv_progress.year),
29
- overview = COALESCE(EXCLUDED.overview, tv_progress.overview),
30
- tmdbId = COALESCE(EXCLUDED.tmdbId, tv_progress.tmdbId),
31
- mediaType = COALESCE(EXCLUDED.mediaType, tv_progress.mediaType),
32
- adult = COALESCE(EXCLUDED.adult, tv_progress.adult),
33
- updatedAt = strftime('%s', 'now')`),deleteByMedia:(r,e)=>s(r).delete(a).where(n(a.mediaId,e)),deleteMany:(r,e)=>e.length===0?Promise.resolve():s(r).delete(a).where(d(a.mediaId,e)),deleteEpisode:(r,e,t,o)=>s(r).run(i`
34
- DELETE FROM tv_progress WHERE mediaId = ${e} AND season = ${t} AND episode = ${o}`),getContinueWatching:(r,e)=>{const t=typeof e=="number"?`LIMIT ${e}`:"";return s(r).all(i`
35
- SELECT p.mediaId as id,
36
- COALESCE(l.tmdbId, p.tmdbId, CAST(SUBSTR(REPLACE(p.mediaId, '-', ':'), INSTR(REPLACE(p.mediaId, '-', ':'), ':') + 1) AS INTEGER)) as tmdbId,
37
- COALESCE(l.mediaType, p.mediaType, SUBSTR(REPLACE(p.mediaId, '-', ':'), 1, INSTR(REPLACE(p.mediaId, '-', ':'), ':') - 1)) as mediaType,
38
- COALESCE(l.title, p.title) as title, COALESCE(l.poster, p.poster) as poster, COALESCE(l.backdrop, p.backdrop) as backdrop,
39
- COALESCE(l.year, p.year) as year, COALESCE(l.overview, p.overview) as overview,
40
- l.status as watchlistStatus, COALESCE(l.adult, p.adult) as adult,
41
- p.season, p.episode, p.currentTime, p.duration, p.completed, p.updatedAt as progressAt
42
- FROM (
43
- SELECT *, rowid AS progressRowid, ROW_NUMBER() OVER (PARTITION BY mediaId ORDER BY updatedAt DESC, rowid DESC) as rn
44
- FROM tv_progress
45
- ) p
46
- LEFT JOIN tv_library l ON p.mediaId = l.id
47
- WHERE p.rn = 1
48
- AND (l.status IS NULL OR l.status = 'Watching')
49
- ORDER BY p.updatedAt DESC, p.progressRowid DESC
50
- ${i.raw(t)}`)}};export{g as TV_STATUSES,T as TvLibraryRepository,I as TvProgressRepository,v as buildTvId,D as normalizeTvMediaId};
@@ -1,33 +0,0 @@
1
- import{eq as o,sql as s}from"drizzle-orm";import{getDrizzle as r}from"../db/drizzle.js";import{watchedEpisodes as i}from"../db/schema-anime.js";const p={getByShowAndEpisode:async(t,e,a)=>(await r(t).all(s`
2
- SELECT currentTime, duration FROM watched_episodes WHERE showId = ${e} AND episodeNumber = ${a}`))[0],getWatchedEpisodeNumbers:async(t,e)=>(await r(t).all(s`SELECT episodeNumber FROM watched_episodes WHERE showId = ${e}`)).map(n=>n.episodeNumber),getByShow:(t,e)=>r(t).all(s`
3
- SELECT showId, episodeNumber, currentTime, duration, watchedAt FROM watched_episodes WHERE showId = ${e} ORDER BY CAST(episodeNumber AS REAL) ASC`),getLatestResumeProgress:async(t,e)=>(await r(t).all(s`
4
- SELECT showId, episodeNumber, currentTime, duration, watchedAt
5
- FROM watched_episodes
6
- WHERE showId = ${e} AND currentTime > 5 AND (duration <= 0 OR currentTime < duration * 0.8)
7
- ORDER BY watchedAt DESC
8
- LIMIT 1`))[0],upsert:(t,e)=>r(t).insert(i).values({showId:e.showId,episodeNumber:e.episodeNumber,watchedAt:s`CURRENT_TIMESTAMP`,currentTime:e.currentTime,duration:e.duration}).onConflictDoUpdate({target:[i.showId,i.episodeNumber],set:{watchedAt:s`CURRENT_TIMESTAMP`,currentTime:e.currentTime,duration:e.duration}}),insertIfMissing:(t,e)=>r(t).insert(i).values({showId:e.showId,episodeNumber:e.episodeNumber,watchedAt:e.watchedAt?s`${e.watchedAt}`:s`CURRENT_TIMESTAMP`,currentTime:0,duration:0}).onConflictDoNothing(),deleteByShow:(t,e)=>r(t).delete(i).where(o(i.showId,e)),getContinueWatching:(t,e)=>{const a=typeof e=="number"?`LIMIT ${e}`:"";return r(t).all(s`
9
- SELECT
10
- we.showId as _id,
11
- we.showId as id,
12
- COALESCE(NULLIF(w.name, ''), sm.name) as name,
13
- COALESCE(NULLIF(w.thumbnail, ''), NULLIF(sm.thumbnail, ''), '') as thumbnail,
14
- COALESCE(NULLIF(w.nativeName, ''), sm.nativeName) as nativeName,
15
- COALESCE(NULLIF(w.englishName, ''), sm.englishName) as englishName,
16
- COALESCE(NULLIF(w.type, ''), sm.type) as type,
17
- sm.episodeCount,
18
- sm.type as smType,
19
- sm.isAdult as isAdult,
20
- w.status as watchlistStatus,
21
- (SELECT COUNT(DISTINCT episodeNumber) FROM watched_episodes WHERE showId = we.showId) as watchedCount,
22
- we.episodeNumber, we.currentTime, we.duration, we.watchedAt
23
- FROM (
24
- SELECT *, ROW_NUMBER() OVER(PARTITION BY showId ORDER BY (currentTime > 0 OR duration > 0) DESC, watchedAt DESC) as rn
25
- FROM watched_episodes
26
- ) we
27
- LEFT JOIN watchlist w ON we.showId = w.id
28
- LEFT JOIN shows_meta sm ON we.showId = sm.id
29
- WHERE we.rn = 1
30
- AND (w.status IS NULL OR w.status = 'Watching')
31
- AND (w.id IS NOT NULL OR sm.id IS NOT NULL)
32
- ORDER BY we.watchedAt DESC
33
- ${s.raw(a)}`)}};export{p as WatchedEpisodesRepository};
@@ -1,9 +0,0 @@
1
- import{eq as E,inArray as m,sql as n}from"drizzle-orm";import{getDrizzle as i}from"../db/drizzle.js";import{watchlist as s}from"../db/schema-anime.js";const L={getById:async(e,t)=>(await i(e).all(n`SELECT * FROM watchlist WHERE id = ${t}`))[0],exists:async(e,t)=>{const a=await i(e).all(n`SELECT EXISTS(SELECT 1 FROM watchlist WHERE id = ${t}) as inWatchlist`);return!!(a[0]&&a[0].inWatchlist)},getAll:(e,t,a,l)=>{const r=n`SELECT * FROM watchlist`;return t&&t!=="All"&&r.append(n` WHERE status = ${t}`),r.append(n` ORDER BY rowid DESC`),a!==void 0&&l!==void 0&&r.append(n` LIMIT ${a} OFFSET ${l}`),i(e).all(r)},getCount:async(e,t)=>{const a=n`SELECT COUNT(*) as total FROM watchlist`;return t&&t!=="All"&&a.append(n` WHERE status = ${t}`),(await i(e).all(a))[0]?.total||0},upsert:(e,t)=>i(e).run(n`
2
- INSERT INTO watchlist (id, name, thumbnail, status, nativeName, englishName, type) VALUES (${t.id}, ${t.name}, ${t.thumbnail}, ${t.status}, ${t.nativeName}, ${t.englishName}, ${t.type})
3
- ON CONFLICT(id) DO UPDATE SET
4
- name = COALESCE(NULLIF(EXCLUDED.name, ''), watchlist.name),
5
- thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), watchlist.thumbnail),
6
- status = COALESCE(NULLIF(EXCLUDED.status, ''), watchlist.status),
7
- nativeName = COALESCE(NULLIF(EXCLUDED.nativeName, ''), watchlist.nativeName),
8
- englishName = COALESCE(NULLIF(EXCLUDED.englishName, ''), watchlist.englishName),
9
- type = COALESCE(NULLIF(EXCLUDED.type, ''), watchlist.type)`),updateStatus:(e,t,a)=>i(e).update(s).set({status:a}).where(E(s.id,t)),updateThumbnail:(e,t,a)=>!a||a.trim()===""?Promise.resolve():i(e).update(s).set({thumbnail:a}).where(E(s.id,t)),delete:(e,t)=>i(e).delete(s).where(E(s.id,t)),deleteMany:(e,t)=>t.length===0?Promise.resolve():i(e).delete(s).where(m(s.id,t)),updateStatusMany:(e,t,a)=>t.length===0?Promise.resolve():i(e).update(s).set({status:a}).where(m(s.id,t)),getWatchingShows:e=>i(e).all(n`SELECT id, name, thumbnail, nativeName, englishName FROM watchlist WHERE status = 'Watching'`),getMissingThumbnails:e=>i(e).all(n`SELECT id, name FROM watchlist WHERE thumbnail IS NULL OR TRIM(thumbnail) = ''`)};export{L as WatchlistRepository};
@@ -1 +0,0 @@
1
- import g from"node:path";import{CONFIG as s}from"../config.js";import{DatabaseWrapper as p}from"../db.js";import{anilistRequest as f}from"../lib/anilist.js";import{dbAll as l,dbRun as E}from"../utils/db-utils.js";const a=50;async function h(){const n=s.IS_DEV?s.DB_NAME_DEV:s.DB_NAME_PROD,o=await p.create(g.join(s.ROOT,n));o.configure("busyTimeout",1e4),l(o,"PRAGMA table_info(shows_meta)").some(i=>i.name==="episodeDuration")||o.run("ALTER TABLE shows_meta ADD COLUMN episodeDuration INTEGER");const e=l(o,"SELECT id, anilistId FROM shows_meta WHERE anilistId IS NOT NULL AND episodeDuration IS NULL");if(e.length===0){console.log("Nothing to backfill."),o.close();return}let r=0;for(let i=0;i<e.length;i+=a){const d=e.slice(i,i+a),c=await f(`query ($ids: [Int]) { Page(perPage: ${a}) { media(id_in: $ids, type: ANIME) { id duration } } }`,{ids:d.map(t=>t.anilistId)});for(const t of c?.data?.Page?.media??[]){if(!t.duration||t.duration<=0)continue;const m=d.find(u=>u.anilistId===t.id);m&&(E(o,"UPDATE shows_meta SET episodeDuration = ? WHERE id = ?",[t.duration,m.id]),r++)}console.log(`Progress: ${Math.min(i+a,e.length)}/${e.length} (updated ${r})`)}console.log(`Done. Updated ${r} of ${e.length} shows.`),o.close()}h().catch(n=>{console.error(n?.message||n),process.exit(1)});
@@ -1 +0,0 @@
1
- import S from"path";import f from"./logger.js";import{CONFIG as u}from"./config.js";import{dbAll as w,dbGet as _}from"./utils/db-utils.js";import{isTempSyncRow as d}from"./lib/temp-ids.js";const R=f.child({module:"SyncPayload"}),$=["watchlist","watched_episodes","queue","settings","shows_meta","sync_metadata","dismissed_notifications","discovered_notifications"],L=["manga_library","manga_progress","sync_metadata"],P=["tv_library","tv_progress","sync_metadata"],k=["asmr_library","asmr_progress","sync_metadata"];function x(e){const n=e.tables.sync_metadata?.find(t=>t.key==="db_version")?.value;return typeof n=="number"?n:Number(n||e.version||0)}function C(e,o,a){if(!e||typeof e!="object"||!("tables"in e))throw new Error(`Invalid ${a} sync payload.`);const n=e;for(const t of o)if(!Array.isArray(n.tables?.[t]))throw new Error(`Invalid ${a} sync payload: missing ${String(t)}.`);return n}function O(e,o){const a={};for(const n of o){const t=w(e,`SELECT * FROM "${String(n).replace(/"/g,'""')}"`);a[n]=t.filter(y=>!d(y))}return{version:x({version:0,exportedAt:"",tables:a}),exportedAt:new Date().toISOString(),tables:a}}function M(e,o,a,n){const t=a.tables,y=n.libraryTables.reduce((r,s)=>r+(t[s]?.length??0),0),p=n.libraryTables.length===0?1:_(e,`SELECT ${n.libraryTables.map(r=>`(SELECT COUNT(*) FROM "${String(r).replace(/"/g,'""')}")`).join(" + ")} AS n`)?.n??0;if(y===0&&p>0)throw new Error("Sync down refused: remote library is empty while local library has data");try{e.backup(S.join(u.ROOT,n.backupName))}catch(r){R.warn({err:r},"Pre-sync backup failed, continuing without backup")}const b=new Map;for(const r of o){const s=e.all(`PRAGMA table_info("${String(r)}")`);b.set(String(r),new Set(s.map(i=>i.name)))}e.serialize(()=>{for(const r of o)e.run(`DELETE FROM "${String(r).replace(/"/g,'""')}"`);for(const r of o){const s=b.get(String(r));for(const i of t[String(r)]||[]){if(d(i))continue;const c=Object.keys(i).filter(l=>s?.has(l));if(c.length===0)continue;const T=c.map(l=>`"${l.replace(/"/g,'""')}"`).join(", "),m=c.map(()=>"?").join(", "),g=c.map(l=>i[l]);e.run(`INSERT INTO "${String(r).replace(/"/g,'""')}" (${T}) VALUES (${m})`,g)}}})}export{$ as ANIME_SYNC_TABLES,k as ASMR_SYNC_TABLES,L as MANGA_SYNC_TABLES,P as TV_SYNC_TABLES,O as exportTables,M as importTables,C as normalizePayload,x as readPayloadVersion};
@@ -1,13 +0,0 @@
1
- import*as u from"fs/promises";import{existsSync as _}from"fs";import b from"path";import m from"./logger.js";import{googleDriveService as E}from"./google.js";import{rcloneService as l}from"./rclone.js";import{githubSyncService as T}from"./github-sync.js";import{CONFIG as d}from"./config.js";import{DatabaseWrapper as L}from"./db.js";import{runTx as F}from"./db/drizzle.js";import{dbGet as S}from"./utils/db-utils.js";import{TempShowIdsRepository as G}from"./repositories/temp-show-ids.repository.js";import{malCachePruneExpired as W}from"./repositories/mal-cache.repository.js";import{notifySyncStart as I,notifySyncEnd as N}from"./lib/ipc.js";import{ANIME_SYNC_TABLES as x,exportTables as k,importTables as Y,MANGA_SYNC_TABLES as D,TV_SYNC_TABLES as O,ASMR_SYNC_TABLES as w,normalizePayload as B,readPayloadVersion as v}from"./sync-payload.js";const o=m.child({module:"Sync"}),V=x;function y(a){return a.length===0?a:a[0].toUpperCase()+a.slice(1)}const A={anime:{mid:"",Head:"",rcloneLabel:"rclone",tables:V,libraryTables:["watchlist","watched_episodes"],backupName:"pre-sync-backup.db",manifestPath:d.LOCAL_MANIFEST_PATH,rcloneFile:d.RCLONE_SYNC_FILENAME,githubDown:a=>T.syncDown(a),githubUp:a=>T.syncUp(a),googleDown:a=>E.syncDown(a),githubVersion:()=>T.getRemoteVersion(),googleVersion:()=>E.getRemoteVersion(),googleUp:a=>E.syncUp(a)},manga:{mid:"manga ",Head:"Manga ",rcloneLabel:"rclone manga",tables:D,libraryTables:["manga_library","manga_progress"],backupName:"pre-sync-manga-backup.db",manifestPath:d.MANGA_LOCAL_MANIFEST_PATH,rcloneFile:d.MANGA_RCLONE_SYNC_FILENAME,githubDown:a=>T.syncMangaDown(a),githubUp:a=>T.syncMangaUp(a),googleDown:a=>E.syncMangaDown(a),githubVersion:()=>T.getMangaRemoteVersion(),googleVersion:()=>E.getMangaRemoteVersion(),googleUp:a=>E.syncMangaUp(a)},tv:{mid:"TV ",Head:"TV ",rcloneLabel:"rclone tv",tables:O,libraryTables:["tv_library","tv_progress"],backupName:"pre-sync-tv-backup.db",manifestPath:d.TV_LOCAL_MANIFEST_PATH,rcloneFile:d.TV_RCLONE_SYNC_FILENAME,githubDown:a=>T.syncTvDown(a),githubUp:a=>T.syncTvUp(a),googleDown:a=>E.syncTvDown(a),githubVersion:()=>T.getTvRemoteVersion(),googleVersion:()=>E.getTvRemoteVersion(),googleUp:a=>E.syncTvUp(a)},asmr:{mid:"ASMR ",Head:"ASMR ",rcloneLabel:"rclone asmr",tables:w,libraryTables:["asmr_library","asmr_progress"],backupName:"pre-sync-asmr-backup.db",manifestPath:d.ASMR_LOCAL_MANIFEST_PATH,rcloneFile:d.ASMR_RCLONE_SYNC_FILENAME,githubDown:a=>T.syncAsmrDown(a),githubUp:a=>T.syncAsmrUp(a),googleDown:a=>E.syncAsmrDown(a),githubVersion:()=>T.getAsmrRemoteVersion(),googleVersion:()=>E.getAsmrRemoteVersion(),googleUp:a=>E.syncAsmrUp(a)}};async function $(a,e=d.RCLONE_SYNC_FILENAME){if(!await l.fileExists(a,e))return 0;const r=b.join(d.ROOT,`temp_${Date.now()}_rclone_sync.json`);try{await l.downloadFile(a,e,r);const n=await u.readFile(r,"utf-8"),i=JSON.parse(n);return v(i)}catch{return 0}finally{_(r)&&await u.unlink(r)}}async function H(a,e,t){const r=A[e],n=k(a,r.tables),i=b.join(d.ROOT,`temp_${Date.now()}_rclone_${e}_up.json`);try{await u.writeFile(i,JSON.stringify(n,null,2)),await l.uploadFile(i,t,r.rcloneFile)}finally{_(i)&&await u.unlink(i).catch(()=>{})}}async function K(a,e,t){const r=A[e],n=b.join(d.ROOT,`temp_${Date.now()}_rclone_${e}_down.json`);try{await l.downloadFile(t,r.rcloneFile,n);const i=await u.readFile(n,"utf-8"),c=B(JSON.parse(i),r.tables,r.rcloneLabel);return Y(a,r.tables,c,{libraryTables:r.libraryTables,backupName:r.backupName}),v(c)}finally{_(n)&&await u.unlink(n).catch(()=>{})}}class j{_locked=!1;_waiting=[];async lock(){return new Promise(e=>{this._locked?this._waiting.push(e):(this._locked=!0,e())})}unlock(){const e=this._waiting.shift();e?e():this._locked=!1}}const p=new j;let R=!1,s="none";async function me(){await p.lock(),p.unlock()}function z(){return s}async function q(a){const e=a||process.env.SYNC_PROVIDER;if(e==="github"&&T.isAuthenticated()){s="github",o.info("Sync Provider: GitHub (Selected)");return}if(e==="google"&&E.isAuthenticated()){s="google",o.info("Sync Provider: Google Drive API (Selected)");return}if(e==="rclone"&&await l.init()){s="rclone",o.info(`Sync Provider: Rclone (${l.getRemoteName()}) (Selected)`);return}if(e==="none"){s="none",o.info("Sync Provider: None (Forced)");return}if(T.isAuthenticated()){s="github",o.info("Sync Provider: GitHub (Fallback)");return}if(E.isAuthenticated()){s="google",o.info("Sync Provider: Google Drive API (Fallback)");return}if(await l.init()){s="rclone",o.info(`Sync Provider: Rclone (${l.getRemoteName()}) (Fallback)`);return}s="none",o.info("No sync provider available.")}async function J(a=d.LOCAL_MANIFEST_PATH){if(_(a))try{const e=await u.readFile(a,"utf-8");return JSON.parse(e).version||0}catch{return 0}return 0}async function h(a,e=d.LOCAL_MANIFEST_PATH){await u.writeFile(e,JSON.stringify({version:a}))}function M(a){return J(A[a].manifestPath)}function g(a,e){return h(e,A[a].manifestPath)}async function X(a,e="anime"){const t=A[e],r=e==="anime"?"manifest":`${t.mid.trim()} manifest`;try{if(s==="github")return T.isAuthenticated()?{version:await t.githubVersion()}:{version:0};if(s==="google")return E.isAuthenticated()?{version:await t.googleVersion()}:{version:0};if(s==="rclone")return{version:await $(a,t.rcloneFile)}}catch(n){o.warn({err:n},`Could not read remote ${r}.`)}return{version:0}}async function P(a,e,t){const r=A[e];let n=await M(e);if(n===0&&a&&(n=S(a,"SELECT value FROM sync_metadata WHERE key = 'db_version'")?.value??0,n>0&&await g(e,n)),s!=="none"){if(await p.lock(),R){p.unlock();return}R=!0;try{I(`Initial ${r.mid}sync check (${s})`);const{version:i}=await X(t,e);if(N(),o.info(`${r.Head}Sync Check: Local v${n} vs Remote v${i}`),i>n){if(s==="github"){if(!T.isAuthenticated())return;I(`Importing GitHub ${r.mid}sync data (Remote v${i})`);const c=await r.githubDown(a);await g(e,c||i),N(),o.info(`GitHub ${r.mid}sync down complete.`);return}if(s==="google"){if(!E.isAuthenticated())return;I(`Importing Google ${r.mid}sync data (Remote v${i})`);const c=await r.googleDown(a);await g(e,c||i),N(),o.info(`Google ${r.mid}sync down complete.`);return}if(s==="rclone"){I(`Importing Rclone ${r.mid}sync data (Remote v${i})`);const c=await K(a,e,t);await g(e,c||i),N(),o.info(`Rclone ${r.mid}sync down complete.`);return}}else o.info(`Local ${r.mid}DB is up to date.`)}catch(i){N(),o.error({err:i},`${y(`${r.mid}sync`)} boot error.`)}finally{R=!1,p.unlock()}}}async function C(a,e,t){const r=A[e];if(s!=="none"){if(await p.lock(),R){p.unlock();return}R=!0;try{const n=await M(e),{version:i}=await X(t,e);if(n>i){if(I(`Syncing ${r.mid}up (Local v${n})`),s==="github"){if(!T.isAuthenticated())return;await r.githubUp(a)}else if(s==="google"){if(!E.isAuthenticated())return;await r.googleUp(a)}else s==="rclone"&&await H(a,e,t);N(),o.info(`${y(`${r.mid}sync`)} up complete.`)}else o.info(`No ${r.mid}changes to sync up or remote is newer.`)}catch(n){N(),o.error({err:n},`${y(`${r.mid}sync`)} up failed.`)}finally{R=!1,p.unlock()}}}async function ue(a,e){if(await q(e.preferredProvider),z()==="github"&&T.isAuthenticated())try{await T.migrateFromAniWebSync()}catch(r){m.error({err:r},"GitHub sync migration from ani-web failed")}await P(a.db,"anime",e.remoteFolder);try{await C(a.db,"anime",e.remoteFolder)}catch(r){m.error({err:r},"Sync up on boot failed")}const t=[{kind:"manga",db:a.mangaDb},{kind:"tv",db:a.tvDb},{kind:"asmr",db:a.asmrDb}];for(const{kind:r,db:n}of t){const i=A[r];try{await P(n,r,e.remoteFolder)}catch(c){m.error({err:c},`${i.Head}sync down on boot failed`)}try{await C(n,r,e.remoteFolder)}catch(c){m.error({err:c},`${i.Head}sync up on boot failed`)}}}async function f(a,e,t=r=>h(r)){await F(a,async i=>{await e(i),i.run("UPDATE sync_metadata SET value = value + 1 WHERE key = 'db_version'")});const n=S(a,"SELECT value FROM sync_metadata WHERE key = 'db_version'")?.value??1;await t(n)}async function le(a,e){await f(a,e,t=>g("manga",t))}async function Ae(a,e){await f(a,e,t=>g("tv",t))}async function pe(a,e){await f(a,e,t=>g("asmr",t))}async function Ne(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS manga_library (id TEXT PRIMARY KEY, provider TEXT NOT NULL, mangaId TEXT NOT NULL, title TEXT, cover TEXT, status TEXT DEFAULT 'Reading', author TEXT, contentRating TEXT, lastChapterId TEXT, lastChapterNumber TEXT, lastPage INTEGER, updatedAt INTEGER, altTitle TEXT, anilistId INTEGER, anilistIdSource TEXT)"),e.run("CREATE TABLE IF NOT EXISTS manga_progress (mangaId TEXT NOT NULL, chapterId TEXT NOT NULL, chapterNumber TEXT, page INTEGER DEFAULT 0, pageCount INTEGER DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (mangaId, chapterId))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_manga_library_provider_manga ON manga_library(provider, mangaId)"),e.run("CREATE INDEX IF NOT EXISTS idx_manga_library_status ON manga_library(status)"),e.run("CREATE INDEX IF NOT EXISTS idx_manga_progress_manga ON manga_progress(mangaId, updatedAt)");const t=e.all("PRAGMA table_info(manga_library)");t.some(n=>n.name==="altTitle")||e.run("ALTER TABLE manga_library ADD COLUMN altTitle TEXT"),t.some(n=>n.name==="anilistId")||e.run("ALTER TABLE manga_library ADD COLUMN anilistId INTEGER"),t.some(n=>n.name==="anilistIdSource")||e.run("ALTER TABLE manga_library ADD COLUMN anilistIdSource TEXT"),e.run("CREATE INDEX IF NOT EXISTS idx_manga_library_anilist ON manga_library(anilistId)");const r=e.all("PRAGMA table_info(manga_progress)");return r.some(n=>n.name==="title")||e.run("ALTER TABLE manga_progress ADD COLUMN title TEXT"),r.some(n=>n.name==="cover")||e.run("ALTER TABLE manga_progress ADD COLUMN cover TEXT"),r.some(n=>n.name==="provider")||e.run("ALTER TABLE manga_progress ADD COLUMN provider TEXT"),r.some(n=>n.name==="altTitle")||e.run("ALTER TABLE manga_progress ADD COLUMN altTitle TEXT"),r.some(n=>n.name==="contentRating")||e.run("ALTER TABLE manga_progress ADD COLUMN contentRating TEXT"),e}catch(e){throw o.error({err:e},"Manga database opening error"),e}}async function ge(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS tv_library (id TEXT PRIMARY KEY, tmdbId INTEGER NOT NULL, mediaType TEXT NOT NULL, title TEXT, poster TEXT, backdrop TEXT, year TEXT, overview TEXT, status TEXT DEFAULT 'Watching', adult INTEGER DEFAULT 0, lastSeason INTEGER, lastEpisode INTEGER, updatedAt INTEGER)"),e.run("CREATE TABLE IF NOT EXISTS tv_progress (mediaId TEXT NOT NULL, season INTEGER NOT NULL, episode INTEGER NOT NULL, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, completed INTEGER NOT NULL DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (mediaId, season, episode))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE INDEX IF NOT EXISTS idx_tv_library_status ON tv_library(status)"),e.run("CREATE INDEX IF NOT EXISTS idx_tv_library_tmdb ON tv_library(tmdbId, mediaType)"),e.run("CREATE INDEX IF NOT EXISTS idx_tv_progress_media ON tv_progress(mediaId, updatedAt)");const t=e.all("PRAGMA table_info(tv_progress)");return t.some(r=>r.name==="title")||e.run("ALTER TABLE tv_progress ADD COLUMN title TEXT"),t.some(r=>r.name==="poster")||e.run("ALTER TABLE tv_progress ADD COLUMN poster TEXT"),t.some(r=>r.name==="backdrop")||e.run("ALTER TABLE tv_progress ADD COLUMN backdrop TEXT"),t.some(r=>r.name==="year")||e.run("ALTER TABLE tv_progress ADD COLUMN year TEXT"),t.some(r=>r.name==="overview")||e.run("ALTER TABLE tv_progress ADD COLUMN overview TEXT"),t.some(r=>r.name==="tmdbId")||e.run("ALTER TABLE tv_progress ADD COLUMN tmdbId INTEGER"),t.some(r=>r.name==="mediaType")||e.run("ALTER TABLE tv_progress ADD COLUMN mediaType TEXT"),t.some(r=>r.name==="adult")||e.run("ALTER TABLE tv_progress ADD COLUMN adult INTEGER"),t.some(r=>r.name==="completed")||e.run("ALTER TABLE tv_progress ADD COLUMN completed INTEGER NOT NULL DEFAULT 0"),e.run("UPDATE tv_progress SET completed = 1 WHERE completed = 0 AND duration > 0 AND currentTime >= duration * 0.8"),e.run(`CREATE TRIGGER IF NOT EXISTS trg_tv_progress_completed_insert
2
- AFTER INSERT ON tv_progress
3
- WHEN NEW.duration > 0 AND NEW.currentTime >= NEW.duration * 0.8
4
- BEGIN
5
- UPDATE tv_progress SET completed = 1
6
- WHERE mediaId = NEW.mediaId AND season = NEW.season AND episode = NEW.episode;
7
- END`),e.run("DROP TRIGGER IF EXISTS trg_tv_progress_completed_update"),e.run(`CREATE TRIGGER IF NOT EXISTS trg_tv_progress_completed_update
8
- AFTER UPDATE OF currentTime, duration ON tv_progress
9
- WHEN NEW.duration > 0 AND NEW.currentTime >= NEW.duration * 0.8
10
- BEGIN
11
- UPDATE tv_progress SET completed = 1
12
- WHERE mediaId = NEW.mediaId AND season = NEW.season AND episode = NEW.episode;
13
- END`),e.run("UPDATE tv_progress SET completed = 0 WHERE completed = 1 AND NOT (duration > 0 AND currentTime >= duration * 0.8)"),e.run("UPDATE OR IGNORE tv_progress SET mediaId = REPLACE(mediaId, '-', ':') WHERE mediaId LIKE 'tv-%' OR mediaId LIKE 'movie-%'"),e.run("DELETE FROM tv_progress WHERE mediaId LIKE 'tv-%' OR mediaId LIKE 'movie-%'"),e}catch(e){throw o.error({err:e},"TV database opening error"),e}}async function Re(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS asmr_library (id TEXT PRIMARY KEY, rjCode TEXT NOT NULL, title TEXT, thumbnail TEXT, status TEXT DEFAULT 'Listening', isAdult INTEGER DEFAULT 0, lastTrackIndex INTEGER, lastTrackLabel TEXT, lastPosition REAL, updatedAt INTEGER)"),e.run("CREATE TABLE IF NOT EXISTS asmr_progress (workId TEXT NOT NULL, trackIndex INTEGER NOT NULL, trackLabel TEXT, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, updatedAt INTEGER, PRIMARY KEY (workId, trackIndex))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE INDEX IF NOT EXISTS idx_asmr_library_status ON asmr_library(status)"),e.run("CREATE INDEX IF NOT EXISTS idx_asmr_library_rjcode ON asmr_library(rjCode)"),e.run("CREATE INDEX IF NOT EXISTS idx_asmr_progress_work ON asmr_progress(workId, updatedAt)");const t=e.all("PRAGMA table_info(asmr_progress)");return t.some(r=>r.name==="title")||e.run("ALTER TABLE asmr_progress ADD COLUMN title TEXT"),t.some(r=>r.name==="thumbnail")||e.run("ALTER TABLE asmr_progress ADD COLUMN thumbnail TEXT"),t.some(r=>r.name==="rjCode")||e.run("ALTER TABLE asmr_progress ADD COLUMN rjCode TEXT"),t.some(r=>r.name==="isAdult")||e.run("ALTER TABLE asmr_progress ADD COLUMN isAdult INTEGER"),e}catch(e){throw o.error({err:e},"ASMR database opening error"),e}}async function Ie(a){try{const e=await L.create(a);e.configure("busyTimeout",5e3),e.run("PRAGMA journal_mode = WAL;"),e.run("PRAGMA synchronous = NORMAL;"),e.run("PRAGMA cache_size = -20000;"),e.run("PRAGMA temp_store = MEMORY;"),e.run("PRAGMA mmap_size = 268435456;"),e.run("PRAGMA foreign_keys = ON;"),e.run("CREATE TABLE IF NOT EXISTS watchlist (id TEXT NOT NULL, name TEXT, thumbnail TEXT, status TEXT, nativeName TEXT, englishName TEXT, type TEXT, PRIMARY KEY (id))"),e.run("CREATE TABLE IF NOT EXISTS watched_episodes (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, watchedAt DATETIME DEFAULT CURRENT_TIMESTAMP, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, PRIMARY KEY (showId, episodeNumber))"),e.run("CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY, showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, queue_order INTEGER NOT NULL)"),e.run("CREATE TABLE IF NOT EXISTS settings (key TEXT NOT NULL, value TEXT, PRIMARY KEY (key))"),e.run("CREATE TABLE IF NOT EXISTS shows_meta (id TEXT PRIMARY KEY, name TEXT, thumbnail TEXT, nativeName TEXT, englishName TEXT, episodeCount INTEGER, status TEXT, genres TEXT, popularityScore INTEGER, type TEXT)"),e.run("CREATE TABLE IF NOT EXISTS dismissed_notifications (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, dismissedAt DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (showId, episodeNumber))"),e.run("CREATE TABLE IF NOT EXISTS discovered_notifications (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, discoveredAt DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (showId, episodeNumber))"),e.run("CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)"),e.run("INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)"),e.run("CREATE TABLE IF NOT EXISTS legacy_id_mapping (legacyId TEXT PRIMARY KEY, numericId TEXT)"),e.run("CREATE TABLE IF NOT EXISTS temp_show_ids (id TEXT PRIMARY KEY, provider TEXT NOT NULL, nativeId TEXT NOT NULL, title TEXT NOT NULL, thumbnail TEXT, createdAt INTEGER NOT NULL)"),e.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_temp_show_ids_provider_native ON temp_show_ids(provider, nativeId)");try{const r=await G.purge(e);r>0&&m.info({purged:r},"Purged stale temp show ids on boot")}catch(r){m.warn({err:r},"Temp show purge on boot failed")}try{const r=W();r>0&&m.info({pruned:r},"Pruned expired mal cache rows on boot")}catch(r){m.warn({err:r},"Mal cache prune on boot failed")}e.run("CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId ON watched_episodes(showId)"),e.run("CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId_episodeNumber ON watched_episodes(showId, episodeNumber)"),e.run("CREATE INDEX IF NOT EXISTS idx_watched_episodes_watchedAt ON watched_episodes(watchedAt)"),e.run("CREATE INDEX IF NOT EXISTS idx_watchlist_status ON watchlist(status)"),e.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_queue_show_episode ON queue(showId, episodeNumber)"),e.run("CREATE INDEX IF NOT EXISTS idx_queue_order ON queue(queue_order)"),e.run("DELETE FROM dismissed_notifications WHERE showId NOT IN (SELECT id FROM watchlist)"),e.run("DELETE FROM discovered_notifications WHERE showId NOT IN (SELECT id FROM watchlist)"),e.run("DELETE FROM legacy_id_mapping WHERE legacyId NOT IN (SELECT id FROM watchlist) AND numericId NOT IN (SELECT id FROM watchlist)"),e.run("DELETE FROM dismissed_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = dismissed_notifications.showId AND we.episodeNumber = dismissed_notifications.episodeNumber)"),e.run("DELETE FROM discovered_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = discovered_notifications.showId AND we.episodeNumber = discovered_notifications.episodeNumber)"),e.run("UPDATE discovered_notifications SET discoveredAt = datetime('now') WHERE discoveredAt IS NULL");const t=(r,n,i)=>{e.all(`PRAGMA table_info(${r})`).some(U=>U.name===n)||e.run(`ALTER TABLE ${r} ADD COLUMN ${n} ${i}`)};return t("watchlist","nativeName","TEXT"),t("watchlist","englishName","TEXT"),t("shows_meta","nativeName","TEXT"),t("shows_meta","englishName","TEXT"),t("shows_meta","episodeCount","INTEGER"),t("shows_meta","status","TEXT"),t("shows_meta","genres","TEXT"),t("shows_meta","popularityScore","INTEGER"),t("watchlist","type","TEXT"),t("shows_meta","type","TEXT"),t("shows_meta","anilistId","INTEGER"),t("shows_meta","isAdult","INTEGER"),t("shows_meta","episodeDuration","INTEGER"),e}catch(e){throw o.error({err:e},"Database opening error"),e}}export{w as ASMR_SYNC_TABLES,A as KIND_SYNC,D as MANGA_SYNC_TABLES,V as SYNC_TABLES,O as TV_SYNC_TABLES,z as getActiveProvider,M as getKindManifestVersion,J as getLocalManifestVersion,q as initSyncProvider,Re as initializeAsmrDatabase,Ie as initializeDatabase,Ne as initializeMangaDatabase,ge as initializeTvDatabase,pe as performAsmrWriteTransactionAsync,le as performMangaWriteTransactionAsync,Ae as performTvWriteTransactionAsync,f as performWriteTransactionAsync,ue as runFullSyncSequence,g as setKindManifestVersion,h as setLocalManifestVersion,P as syncDownOnBoot,C as syncUp,me as waitForSync};
@@ -1 +0,0 @@
1
- import{getTokenFromHeaders as t,hasAppPassword as o,validateLanSession as p}from"../app-auth.js";function n(e,r,a){return{...e,lanAuthed:!o()||p(t(r,a))}}export{n as buildTrpcContext};
@@ -1 +0,0 @@
1
- import{TRPCError as r}from"@trpc/server";function E(e){return new r({code:"FORBIDDEN",message:"AUTH_REQUIRED",cause:{code:"AUTH_REQUIRED",provider:e}})}function R(){return new r({code:"UNAUTHORIZED",message:"LAN_AUTH_REQUIRED",cause:{code:"LAN_AUTH_REQUIRED"}})}export{E as authRequiredError,R as lanAuthRequiredError};
@@ -1 +0,0 @@
1
- import{initTRPC as o}from"@trpc/server";import{lanAuthRequiredError as n}from"./errors.js";const e=o.context().create({errorFormatter:({shape:r,error:t})=>({...r,data:{...r.data,cause:t.cause??null}})}),d=e.router,c=e.middleware(({ctx:r,next:t})=>{if(!r.lanAuthed)throw n();return t({ctx:r})}),p=e.procedure.use(c);export{p as protectedProcedure,d as router};
@@ -1 +0,0 @@
1
- import{router as r}from"./index.js";import{settingsRouter as o}from"./routers/settings.js";import{queueRouter as t}from"./routers/queue.js";import{watchlistRouter as m}from"./routers/watchlist.js";import{progressRouter as i}from"./routers/progress.js";import{notificationsRouter as e}from"./routers/notifications.js";import{continueWatchingRouter as a}from"./routers/continue-watching.js";import{discoveryRouter as s}from"./routers/discovery.js";import{mangaLibraryRouter as u}from"./routers/manga-library.js";import{mangaProgressRouter as p}from"./routers/manga-progress.js";import{asmrLibraryRouter as f}from"./routers/asmr-library.js";import{asmrProgressRouter as R}from"./routers/asmr-progress.js";import{tvLibraryRouter as n}from"./routers/tv-library.js";import{tvProgressRouter as g}from"./routers/tv-progress.js";import{tvRouter as c}from"./routers/tv.js";import{mangaRouter as d}from"./routers/manga.js";import{asmrRouter as y}from"./routers/asmr.js";import{musicRouter as h}from"./routers/music.js";import{radioRouter as v}from"./routers/radio.js";import{trackerRouter as b}from"./routers/tracker.js";import{insightsRouter as L}from"./routers/insights.js";import{authRouter as P}from"./routers/auth.js";import{dataRouter as k}from"./routers/data.js";import{discordRouter as l}from"./routers/discord.js";const V=r({settings:o,queue:t,watchlist:m,progress:i,notifications:e,continueWatching:a,discovery:s,mangaLibrary:u,mangaProgress:p,asmrLibrary:f,asmrProgress:R,tvLibrary:n,tvProgress:g,tv:c,manga:d,asmr:y,music:h,radio:v,tracker:b,insights:L,auth:P,data:k,discord:l});export{V as appRouter};
@@ -1 +0,0 @@
1
- import{TRPCError as b}from"@trpc/server";import{protectedProcedure as e,router as L}from"../index.js";import{performAsmrWriteTransactionAsync as o}from"../../sync.js";import{buildAsmrId as p,ASMR_STATUSES as A,AsmrLibraryRepository as i,AsmrProgressRepository as g}from"../../repositories/asmr.repository.js";import{defineSchema as n,reqObj as u,reqStr as m,optStr as d,reqStrArray as l,readPageLimit as w}from"../validation.js";const h=()=>n(r=>{const t=u(r),s={...w(t)},a=d(t,"status");return a!==void 0&&(s.status=a),s}),I=()=>n(r=>({id:m(u(r),"id")})),f=()=>n(r=>{const t=u(r);return{rjCode:d(t,"rjCode"),id:d(t,"id"),title:d(t,"title"),thumbnail:d(t,"thumbnail"),status:d(t,"status"),isAdult:t.isAdult}}),D=()=>n(r=>{const t=u(r);return{id:m(t,"id"),status:m(t,"status")}}),j=()=>n(r=>{const t=u(r);return{ids:l(t,"ids",!0),status:m(t,"status")}}),q=()=>n(r=>({ids:l(u(r),"ids",!0)})),T=L({list:e.input(h()).query(async({ctx:r,input:t})=>{const s=Math.max(parseInt(String(t.page??1))||1,1),a=Math.min(Math.max(parseInt(String(t.limit??24))||24,1),100),c=(s-1)*a,y=await i.getAll(r.asmrDb,t.status,a,c),S=await i.getCount(r.asmrDb,t.status);return{data:y,total:S,page:s,limit:a}}),ids:e.query(async({ctx:r})=>({ids:await i.getIds(r.asmrDb)})),check:e.input(I()).query(async({ctx:r,input:t})=>{const s=await i.getById(r.asmrDb,t.id);return{inLibrary:!!s,status:s?.status??null}}),add:e.input(f()).mutation(async({ctx:r,input:t})=>{const s=String(t.rjCode||t.id||"").trim();if(!s)throw new b({code:"BAD_REQUEST",message:"rjCode is required"});const a=t.id&&t.id.trim()?p(t.id):p(s),c=typeof t.status=="string"&&A.includes(t.status)?t.status:"Listening";return await o(r.asmrDb,async y=>{await i.upsert(y,{id:a,rjCode:p(s),title:String(t.title||""),thumbnail:String(t.thumbnail||""),status:c,isAdult:t.isAdult===!0||t.isAdult===1})}),{success:!0,id:a}}),remove:e.input(I()).mutation(async({ctx:r,input:t})=>(await o(r.asmrDb,async s=>{await i.delete(s,String(t.id)),await g.deleteByWork(s,String(t.id))}),{success:!0})),setStatus:e.input(D()).mutation(async({ctx:r,input:t})=>{if(!t.id||!A.includes(t.status))throw new b({code:"BAD_REQUEST",message:"id and a valid status are required"});return await o(r.asmrDb,async s=>{await i.updateStatus(s,String(t.id),String(t.status))}),{success:!0}}),batchStatus:e.input(j()).mutation(async({ctx:r,input:t})=>{if(!t.status||!A.includes(t.status))throw new b({code:"BAD_REQUEST",message:"a valid status is required"});const s=t.ids.map(a=>String(a));return await o(r.asmrDb,async a=>{await i.updateStatusMany(a,s,String(t.status))}),{success:!0,updated:s.length}}),removeMany:e.input(q()).mutation(async({ctx:r,input:t})=>{const s=t.ids.map(a=>String(a));return await o(r.asmrDb,async a=>{await i.deleteMany(a,s),await g.deleteMany(a,s)}),{success:!0,removed:s.length}})});export{T as asmrLibraryRouter};