diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 83f68b3..ba14a94 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -17,6 +17,7 @@ "cheerio": "^1.1.2", "dotenv": "^17.2.3", "electron-log": "^5.4.3", + "epub": "^1.3.0", "fastify": "^5.6.2", "js-yaml": "^4.1.1", "jsonwebtoken": "^9.0.3", @@ -2695,6 +2696,16 @@ "node": ">=4" } }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2897,7 +2908,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -3707,6 +3717,27 @@ "node": ">=6" } }, + "node_modules/epub": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/epub/-/epub-1.3.0.tgz", + "integrity": "sha512-6BL8gIitljkTf4HW52Ast6wenPTkMKllU28bRc5awVsT+xCaPl6nWSaqSmHbRgPrl1+5uekOPvOxy7DQzbhM8Q==", + "dependencies": { + "adm-zip": "^0.4.11", + "xml2js": "^0.4.23" + }, + "optionalDependencies": { + "zipfile": "^0.5.11" + } + }, + "node_modules/epub/node_modules/adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "license": "MIT", + "engines": { + "node": ">=0.3.0" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -4668,6 +4699,29 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore-walk": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", + "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4790,6 +4844,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "optional": true + }, "node_modules/isbinaryfile": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", @@ -5420,12 +5481,60 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "license": "MIT", + "optional": true + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, + "node_modules/needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -5577,6 +5686,348 @@ "node": ">=18" } }, + "node_modules/node-pre-gyp": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", + "deprecated": "Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/node-pre-gyp/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-pre-gyp/node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "license": "ISC", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/node-pre-gyp/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/node-pre-gyp/node_modules/fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^2.6.0" + } + }, + "node_modules/node-pre-gyp/node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/node-pre-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-pre-gyp/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "license": "MIT", + "optional": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-pre-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-pre-gyp/node_modules/minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "license": "ISC", + "optional": true, + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "node_modules/node-pre-gyp/node_modules/minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^2.9.0" + } + }, + "node_modules/node-pre-gyp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/node-pre-gyp/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/node-pre-gyp/node_modules/npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/node-pre-gyp/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/node-pre-gyp/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/node-pre-gyp/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/node-pre-gyp/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/node-pre-gyp/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "license": "MIT", + "optional": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-pre-gyp/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-pre-gyp/node_modules/tar": { + "version": "4.4.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", + "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", + "license": "ISC", + "optional": true, + "dependencies": { + "chownr": "^1.1.4", + "fs-minipass": "^1.2.7", + "minipass": "^2.9.0", + "minizlib": "^1.3.3", + "mkdirp": "^0.5.5", + "safe-buffer": "^5.2.1", + "yallist": "^3.1.1" + }, + "engines": { + "node": ">=4.5" + } + }, + "node_modules/node-pre-gyp/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC", + "optional": true + }, "node_modules/nopt": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", @@ -5606,6 +6057,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "license": "ISC", + "optional": true + }, + "node_modules/npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "license": "ISC", + "optional": true, + "dependencies": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, "node_modules/npmlog": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", @@ -5635,6 +6115,26 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -5727,6 +6227,38 @@ "node": ">=8" } }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -6044,6 +6576,13 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT", + "optional": true + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -6433,7 +6972,6 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/secure-json-parse": { @@ -7938,6 +8476,28 @@ } } }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -8071,6 +8631,20 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zipfile": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/zipfile/-/zipfile-0.5.12.tgz", + "integrity": "sha512-zA60gW+XgQBu/Q4qV3BCXNIDRald6Xi5UOPj3jWGlnkjmBHaKDwIz7kyXWV3kq7VEsQN/2t/IWjdXdKeVNm6Eg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "nan": "~2.10.0", + "node-pre-gyp": "~0.10.2" + }, + "bin": { + "unzip.js": "bin/unzip.js" + } } } } diff --git a/desktop/package.json b/desktop/package.json index 8cce840..6042b1b 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -20,6 +20,7 @@ "cheerio": "^1.1.2", "dotenv": "^17.2.3", "electron-log": "^5.4.3", + "epub": "^1.3.0", "fastify": "^5.6.2", "js-yaml": "^4.1.1", "jsonwebtoken": "^9.0.3", diff --git a/desktop/src/api/local/local.controller.ts b/desktop/src/api/local/local.controller.ts index c07c070..908d669 100644 --- a/desktop/src/api/local/local.controller.ts +++ b/desktop/src/api/local/local.controller.ts @@ -1,21 +1,14 @@ import { FastifyRequest, FastifyReply } from 'fastify'; -import { getConfig as loadConfig, setConfig as saveConfig } from '../../shared/config.js'; +import { getConfig as loadConfig } from '../../shared/config.js'; import { queryOne, queryAll, run } from '../../shared/database.js'; import crypto from 'crypto'; import fs from "fs"; import { PathLike } from "node:fs"; import path from "path"; import {getAnimeById, searchAnimeLocal} from "../anime/anime.service"; -import {getBookById, searchBooksAniList, searchBooksLocal} from "../books/books.service"; +import {getBookById, searchBooksAniList} from "../books/books.service"; import AdmZip from 'adm-zip'; - -type SetConfigBody = { - library?: { - anime?: string | null; - manga?: string | null; - novels?: string | null; - }; -}; +import EPub from 'epub'; type ScanQuery = { mode?: 'full' | 'incremental'; @@ -37,10 +30,21 @@ async function resolveEntryMetadata(entry: any, type: string) { ? await searchAnimeLocal(query) : await searchBooksAniList(query); - const first = results?.[0]; + let picked = null; - if (first?.id) { - matchedId = first.id; + if (type !== 'anime' && Array.isArray(results)) { + console.log(type) + if (entry.type === 'novels') { + picked = results.find(r => r.format === 'NOVEL'); + } else if (entry.type === 'manga') { + picked = results.find(r => r.format !== 'NOVEL'); + } + } + + picked ??= results?.[0]; + + if (picked?.id) { + matchedId = picked.id; await run( `UPDATE local_entries @@ -104,9 +108,13 @@ export async function scanLibrary(request: FastifyRequest<{ Querystring: ScanQue } const files = fs.readdirSync(fullPath, { withFileTypes: true }) - .filter(f => f.isFile()) + .filter(f => + f.isFile() || + (type === 'manga' && f.isDirectory()) + ) .sort((a, b) => a.name.localeCompare(b.name)); + let unit = 1; for (const file of files) { @@ -191,27 +199,30 @@ export async function streamUnit(request: FastifyRequest, reply: FastifyReply) { const parts = range.replace(/bytes=/, '').split('-'); const start = Number(parts[0]); - let end = parts[1] ? Number(parts[1]) : stat.size - 1; + const end = parts[1] ? Number(parts[1]) : stat.size - 1; + // Validate range values if ( Number.isNaN(start) || Number.isNaN(end) || start < 0 || + start >= stat.size || end < start || end >= stat.size ) { - end = stat.size - 1; + return reply.status(416).send({ error: 'INVALID_RANGE' }); } + const contentLength = end - start + 1; + reply .status(206) .header('Content-Range', `bytes ${start}-${end}/${stat.size}`) .header('Accept-Ranges', 'bytes') - .header('Content-Length', end - start + 1) + .header('Content-Length', contentLength) .header('Content-Type', 'video/mp4'); return fs.createReadStream(file.file_path, { start, end }); - } type MatchBody = { @@ -247,17 +258,13 @@ export async function matchEntry( return { status: 'OK', matched: !!matched_id }; } -export async function getUnits( - request: FastifyRequest<{ Params: Params }>, - reply: FastifyReply -) { +export async function getUnits(request: FastifyRequest<{ Params: Params }>, reply: FastifyReply) { try { - const { type, id } = request.params as { type: string, id: string }; + const { id } = request.params as { id: string }; - // Buscar la entrada por matched_id const entry = await queryOne( - `SELECT id, type, matched_id FROM local_entries WHERE matched_id = ? AND type = ?`, - [Number(id), type], + `SELECT id, type, matched_id FROM local_entries WHERE matched_id = ?`, + [Number(id)], 'local_library' ); @@ -274,24 +281,82 @@ export async function getUnits( 'local_library' ); - // Formatear la respuesta según el tipo - const units = files.map((file: any) => { - const fileName = path.basename(file.file_path); - const fileExt = path.extname(file.file_path).toLowerCase(); + const MANGA_IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.webp']; + const MANGA_ARCHIVES = ['.cbz', '.cbr', '.zip']; - // Detectar si es un archivo comprimido (capítulo único) o carpeta - const isDirectory = fs.existsSync(file.file_path) && - fs.statSync(file.file_path).isDirectory(); + const NOVEL_EXTS = ['.epub', '.pdf', '.txt', '.md', '.docx', '.mobi']; + + function isImageFolder(folderPath: string): boolean { + if (!fs.existsSync(folderPath)) return false; + if (!fs.statSync(folderPath).isDirectory()) return false; + + const files = fs.readdirSync(folderPath); + return files.some(f => MANGA_IMAGE_EXTS.includes(path.extname(f).toLowerCase())); + } + + const units = files + .map((file: any) => { + const fileExt = path.extname(file.file_path).toLowerCase(); + const isDir = fs.existsSync(file.file_path) && + fs.statSync(file.file_path).isDirectory(); + + // ===== MANGA ===== + if (entry.type === 'manga') { + if (MANGA_ARCHIVES.includes(fileExt)) { + return { + id: file.id, + number: file.unit_number, + name: path.basename(file.file_path), + type: 'chapter', + format: fileExt.replace('.', ''), + path: file.file_path + }; + } + + if (isDir && isImageFolder(file.file_path)) { + return { + id: file.id, + number: file.unit_number, + name: path.basename(file.file_path), + type: 'chapter', + format: 'folder', + path: file.file_path + }; + } + + return null; + } + + if (entry.type === 'novels') { + if (NOVEL_EXTS.includes(fileExt)) { + return { + id: file.id, + number: file.unit_number, + name: path.basename(file.file_path), + type: 'chapter', + format: fileExt.replace('.', ''), + path: file.file_path + }; + } + + return null; + } + + if (entry.type === 'anime') { + return { + id: file.id, + number: file.unit_number, + name: path.basename(file.file_path), + type: 'episode', + format: fileExt.replace('.', ''), + path: file.file_path + }; + } + + return null; + }) + .filter(Boolean); - return { - id: file.id, - number: file.unit_number, - name: fileName, - type: type === 'anime' ? 'episode' : 'chapter', - format: fileExt === '.cbz' ? 'cbz' : 'file', - path: file.file_path - }; - }); return { entry_id: entry.id, @@ -306,7 +371,7 @@ export async function getUnits( } } -export async function getCbzPages(request: FastifyRequest, reply: FastifyReply) { +export async function getManifest(request: FastifyRequest, reply: FastifyReply) { const { unitId } = request.params as any; const file = await queryOne( @@ -319,20 +384,73 @@ export async function getCbzPages(request: FastifyRequest, reply: FastifyReply) return reply.status(404).send({ error: 'FILE_NOT_FOUND' }); } - const zip = new AdmZip(file.file_path); + const ext = path.extname(file.file_path).toLowerCase(); - const pages = zip.getEntries() - .filter(e => !e.isDirectory && /\.(jpg|jpeg|png|webp)$/i.test(e.entryName)) - .sort((a, b) => a.entryName.localeCompare(b.entryName, undefined, { numeric: true })) - .map((_, i) => - `/api/library/manga/cbz/${unitId}/page/${i}` - ); + // ===== MANGA ===== + if (['.cbz', '.cbr', '.zip'].includes(ext)) { + const zip = new AdmZip(file.file_path); - return { pages }; + const pages = zip.getEntries() + .filter(e => !e.isDirectory && /\.(jpg|jpeg|png|webp)$/i.test(e.entryName)) + .sort((a, b) => a.entryName.localeCompare(b.entryName, undefined, { numeric: true })) + .map((_, i) => ({ + id: i, + url: `/api/library/${unitId}/resource/${i}` + })); + + return { + type: 'manga', + format: 'archive', + pages + }; + } + + if (fs.statSync(file.file_path).isDirectory()) { + const pages = fs.readdirSync(file.file_path) + .filter(f => /\.(jpg|jpeg|png|webp)$/i.test(f)) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) + .map((_, i) => ({ + id: i, + url: `/api/library/${unitId}/resource/${i}` + })); + + return { + type: 'manga', + format: 'folder', + pages + }; + } + + // ===== NOVEL ===== + if (ext === '.epub') { + return { + type: 'ln', + format: 'epub', + url: `/api/library/${unitId}/resource/epub` + }; + } + + if (['.txt', '.md'].includes(ext)) { + return { + type: 'ln', + format: 'text', + url: `/api/library/${unitId}/resource/text` + }; + } + + if (ext === '.pdf') { + return { + type: 'ln', + format: 'pdf', + url: `/api/library/${unitId}/resource/pdf` + }; + } + + return reply.status(400).send({ error: 'UNSUPPORTED_FORMAT' }); } -export async function getCbzPage(request: FastifyRequest, reply: FastifyReply) { - const { unitId, page } = request.params as any; +export async function getPage(request: FastifyRequest, reply: FastifyReply) { + const { unitId, resId } = request.params as any; const file = await queryOne( `SELECT file_path FROM local_files WHERE id = ?`, @@ -342,16 +460,83 @@ export async function getCbzPage(request: FastifyRequest, reply: FastifyReply) { if (!file) return reply.status(404).send(); - const zip = new AdmZip(file.file_path); + const ext = path.extname(file.file_path).toLowerCase(); - const images = zip.getEntries() - .filter(e => !e.isDirectory && /\.(jpg|jpeg|png|webp)$/i.test(e.entryName)) - .sort((a, b) => a.entryName.localeCompare(b.entryName, undefined, { numeric: true })); + // ===== CBZ PAGE ===== + if (['.cbz', '.zip', '.cbr'].includes(ext)) { + const zip = new AdmZip(file.file_path); + const images = zip.getEntries() + .filter(e => !e.isDirectory && /\.(jpg|jpeg|png|webp)$/i.test(e.entryName)) + .sort((a, b) => a.entryName.localeCompare(b.entryName, undefined, { numeric: true })); - const entry = images[page]; - if (!entry) return reply.status(404).send(); + const entry = images[Number(resId)]; + if (!entry) return reply.status(404).send(); - reply - .header('Content-Type', 'image/jpeg') - .send(entry.getData()); + return reply + .header('Content-Type', 'image/jpeg') + .send(entry.getData()); + } + + if (fs.statSync(file.file_path).isDirectory()) { + const images = fs.readdirSync(file.file_path) + .filter(f => /\.(jpg|jpeg|png|webp)$/i.test(f)) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); + + const img = images[Number(resId)]; + if (!img) return reply.status(404).send(); + + const imgPath = path.join(file.file_path, img); + const stat = fs.statSync(imgPath); + + reply + .header('Content-Length', stat.size) + .header('Content-Type', 'image/jpeg'); + + return fs.createReadStream(imgPath); + } + + if (ext === '.epub') { + const html = await parseEpubToHtml(file.file_path); + + return reply + .header('Content-Type', 'text/html; charset=utf-8') + .send(html); + } + + // ===== TXT / MD ===== + if (['.txt', '.md'].includes(ext)) { + const text = fs.readFileSync(file.file_path, 'utf8'); + + return reply + .header('Content-Type', 'text/html; charset=utf-8') + .send(`
${text}
`); + } + + return reply.status(400).send(); +} + +function parseEpubToHtml(filePath: string): Promise { + return new Promise((resolve, reject) => { + const epub = new EPub(filePath); + + epub.on('end', async () => { + let html = ''; + + for (const id of epub.flow.map(f => f.id)) { + const chapter = await new Promise((res, rej) => { + epub.getChapter(id, (err, text) => { + if (err) rej(err); + else res(text); + }); + }); + + html += `
${chapter}
`; + } + + resolve(html); + }); + + epub.on('error', reject); + epub.parse(); + }); } diff --git a/desktop/src/api/local/local.routes.ts b/desktop/src/api/local/local.routes.ts index 0127890..5d18cf9 100644 --- a/desktop/src/api/local/local.routes.ts +++ b/desktop/src/api/local/local.routes.ts @@ -7,9 +7,9 @@ async function localRoutes(fastify: FastifyInstance) { fastify.get('/library/:type/:id', controller.getEntry); fastify.get('/library/stream/:type/:id/:unit', controller.streamUnit); fastify.post('/library/:type/:id/match', controller.matchEntry); - fastify.get('/library/:type/:id/units', controller.getUnits); - fastify.get('/library/:type/cbz/:unitId/pages', controller.getCbzPages); - fastify.get('/library/:type/cbz/:unitId/page/:page', controller.getCbzPage); + fastify.get('/library/:id/units', controller.getUnits); + fastify.get('/library/:unitId/manifest', controller.getManifest); + fastify.get('/library/:unitId/resource/:resId', controller.getPage); } export default localRoutes; \ No newline at end of file diff --git a/desktop/src/scripts/anime/player.js b/desktop/src/scripts/anime/player.js index 8f901d0..c76fde5 100644 --- a/desktop/src/scripts/anime/player.js +++ b/desktop/src/scripts/anime/player.js @@ -32,12 +32,12 @@ let localEntryId = null; async function checkLocal() { try { const res = await fetch(`/api/library/anime/${animeId}`); - if (!res.ok) return; - + if (!res.ok) return null; const data = await res.json(); - localEntryId = data.id; // ← ID interna - - } catch {} + return data.id; + } catch { + return null; + } } async function loadAniSkip(malId, episode, duration) { @@ -53,7 +53,7 @@ async function loadAniSkip(malId, episode, duration) { } async function loadMetadata() { - checkLocal(); + localEntryId = await checkLocal(); try { const sourceQuery = (extName === 'local' || !extName) ? "source=anilist" : `source=${extName}`; const res = await fetch(`/api/anime/${animeId}?${sourceQuery}`); @@ -133,6 +133,7 @@ async function loadMetadata() { } catch (error) { console.error('Error loading metadata:', error); } + await loadExtensions(); } async function applyAniSkip(video) { @@ -484,4 +485,3 @@ setInterval(() => { }, 60000); loadMetadata(); -loadExtensions(); \ No newline at end of file diff --git a/desktop/src/scripts/books/book.js b/desktop/src/scripts/books/book.js index 7384528..73a9643 100644 --- a/desktop/src/scripts/books/book.js +++ b/desktop/src/scripts/books/book.js @@ -18,7 +18,10 @@ document.addEventListener('DOMContentLoaded', () => { async function checkLocalLibraryEntry() { try { - const res = await fetch(`/api/library/manga/${bookId}`); + const libraryType = + bookData?.entry_type === 'NOVEL' ? 'novels' : 'manga'; + + const res = await fetch(`/api/library/${libraryType}/${bookId}`); if (!res.ok) return; const data = await res.json(); @@ -38,18 +41,6 @@ async function checkLocalLibraryEntry() { } } -function markAsLocal() { - isLocal = true; - const pill = document.getElementById('local-pill'); - if (pill) { - pill.textContent = 'Local'; - pill.style.display = 'inline-flex'; - pill.style.background = 'rgba(34, 197, 94, 0.2)'; - pill.style.color = '#22c55e'; - pill.style.borderColor = 'rgba(34, 197, 94, 0.3)'; - } -} - async function init() { try { const urlData = URLUtils.parseEntityPath('book'); @@ -61,8 +52,8 @@ async function init() { extensionName = urlData.extensionName; bookId = urlData.entityId; bookSlug = urlData.slug; - await checkLocalLibraryEntry(); await loadBookMetadata(); + await checkLocalLibraryEntry(); await loadAvailableExtensions(); await loadChapters(); @@ -220,7 +211,7 @@ async function loadChapters(targetProvider = null) { if (isLocalRequest) { // Nuevo endpoint para archivos locales - fetchUrl = `/api/library/manga/${bookId}/units`; + fetchUrl = `/api/library/${bookId}/units`; } else { const source = extensionName || 'anilist'; fetchUrl = `/api/book/${bookId}/chapters?source=${source}`; diff --git a/desktop/src/scripts/books/reader.js b/desktop/src/scripts/books/reader.js index 05d19d1..200911e 100644 --- a/desktop/src/scripts/books/reader.js +++ b/desktop/src/scripts/books/reader.js @@ -132,7 +132,7 @@ async function loadChapter() { let newEndpoint; if (provider === 'local') { - newEndpoint = `/api/library/manga/${bookId}/units`; + newEndpoint = `/api/library/${bookId}/units`; } else { newEndpoint = `/api/book/${bookId}/${chapter}/${provider}?source=${source}`; } @@ -142,29 +142,39 @@ async function loadChapter() { const data = await res.json(); if (provider === 'local') { const unit = data.units[Number(chapter)]; + if (!unit) return; - if (!unit) { - reader.innerHTML = '
Chapter not found
'; - return; - } + chapterLabel.textContent = unit.name; + document.title = unit.name; - if (unit.format === 'cbz') { - chapterLabel.textContent = unit.name; // ✅ - document.title = unit.name; - const pagesRes = await fetch( - `/api/library/manga/cbz/${unit.id}/pages` - ); - const pagesData = await pagesRes.json(); + const manifestRes = await fetch(`/api/library/${unit.id}/manifest`); + const manifest = await manifestRes.json(); + reader.innerHTML = ''; + + // ===== MANGA ===== + if (manifest.type === 'manga') { currentType = 'manga'; updateSettingsVisibility(); applyStyles(); - currentPages = pagesData.pages.map(url => ({ url })); - reader.innerHTML = ''; + currentPages = manifest.pages; loadManga(currentPages); return; } + + // ===== LN ===== + if (manifest.type === 'ln') { + currentType = 'ln'; + updateSettingsVisibility(); + applyStyles(); + + const contentRes = await fetch(manifest.url); + const html = await contentRes.text(); + + loadLN(html); + return; + } } @@ -205,13 +215,8 @@ async function loadChapter() { reader.innerHTML = ''; if (data.type === 'manga') { - if (provider === 'local' && data.format === 'cbz') { - currentPages = data.pages.map(url => ({ url })); - loadManga(currentPages); - } else { - currentPages = data.pages || []; - loadManga(currentPages); - } + currentPages = data.pages || []; + loadManga(currentPages); } else if (data.type === 'ln') { loadLN(data.content); } diff --git a/desktop/src/scripts/local-library-books.js b/desktop/src/scripts/local-library-books.js index 63e3f62..3ab311c 100644 --- a/desktop/src/scripts/local-library-books.js +++ b/desktop/src/scripts/local-library-books.js @@ -26,21 +26,37 @@ async function loadLocalEntries() { grid.innerHTML = '
'.repeat(6); try { - // Cambiado a endpoint de libros - const response = await fetch('/api/library/manga'); - const entries = await response.json(); - localEntries = entries; + const [mangaRes, novelRes] = await Promise.all([ + fetch('/api/library/manga'), + fetch('/api/library/novels') + ]); - if (entries.length === 0) { - grid.innerHTML = '

No books found in your local library.

'; + const [manga, novel] = await Promise.all([ + mangaRes.json(), + novelRes.json() + ]); + + localEntries = [ + ...manga.map(e => ({ ...e, type: 'manga' })), + ...novel.map(e => ({ ...e, type: 'novel' })) + ]; + + if (localEntries.length === 0) { + grid.innerHTML = '

No books found.

'; return; } - renderLocalEntries(entries); - } catch (err) { - grid.innerHTML = '

Error loading local books.

'; + + renderLocalEntries(localEntries); + } catch { + grid.innerHTML = '

Error loading library.

'; } } +function filterLocal(type) { + if (type === 'all') renderLocalEntries(localEntries); + else renderLocalEntries(localEntries.filter(e => e.type === type)); +} + function renderLocalEntries(entries) { const grid = document.getElementById('local-entries-grid'); grid.innerHTML = entries.map(entry => { @@ -58,6 +74,7 @@ function renderLocalEntries(entries) {

${chapters} Chapters

+
${entry.type}
${entry.matched ? '● Linked' : '○ Unlinked'}
diff --git a/docker/package-lock.json b/docker/package-lock.json index c491add..93995d1 100644 --- a/docker/package-lock.json +++ b/docker/package-lock.json @@ -15,6 +15,7 @@ "bindings": "^1.5.0", "cheerio": "^1.1.2", "dotenv": "^17.2.3", + "epub": "^1.3.0", "fastify": "^5.6.2", "js-yaml": "^4.1.1", "jsonwebtoken": "^9.0.3", @@ -874,6 +875,16 @@ "node": ">=6" } }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -941,6 +952,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT", + "optional": true + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1218,6 +1236,27 @@ "node": ">=6" } }, + "node_modules/epub": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/epub/-/epub-1.3.0.tgz", + "integrity": "sha512-6BL8gIitljkTf4HW52Ast6wenPTkMKllU28bRc5awVsT+xCaPl6nWSaqSmHbRgPrl1+5uekOPvOxy7DQzbhM8Q==", + "dependencies": { + "adm-zip": "^0.4.11", + "xml2js": "^0.4.23" + }, + "optionalDependencies": { + "zipfile": "^0.5.11" + } + }, + "node_modules/epub/node_modules/adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "license": "MIT", + "engines": { + "node": ">=0.3.0" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -1701,6 +1740,29 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore-walk": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", + "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1787,6 +1849,13 @@ "license": "MIT", "optional": true }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "optional": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2234,12 +2303,60 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "license": "MIT", + "optional": true + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, + "node_modules/needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2392,6 +2509,348 @@ "node": ">=18" } }, + "node_modules/node-pre-gyp": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", + "deprecated": "Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/node-pre-gyp/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-pre-gyp/node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "license": "ISC", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/node-pre-gyp/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/node-pre-gyp/node_modules/fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^2.6.0" + } + }, + "node_modules/node-pre-gyp/node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/node-pre-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-pre-gyp/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "license": "MIT", + "optional": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-pre-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-pre-gyp/node_modules/minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "license": "ISC", + "optional": true, + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "node_modules/node-pre-gyp/node_modules/minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^2.9.0" + } + }, + "node_modules/node-pre-gyp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/node-pre-gyp/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/node-pre-gyp/node_modules/npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/node-pre-gyp/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/node-pre-gyp/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/node-pre-gyp/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/node-pre-gyp/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/node-pre-gyp/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/node-pre-gyp/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "license": "MIT", + "optional": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-pre-gyp/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-pre-gyp/node_modules/tar": { + "version": "4.4.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", + "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", + "license": "ISC", + "optional": true, + "dependencies": { + "chownr": "^1.1.4", + "fs-minipass": "^1.2.7", + "minipass": "^2.9.0", + "minizlib": "^1.3.3", + "mkdirp": "^0.5.5", + "safe-buffer": "^5.2.1", + "yallist": "^3.1.1" + }, + "engines": { + "node": ">=4.5" + } + }, + "node_modules/node-pre-gyp/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC", + "optional": true + }, "node_modules/nopt": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", @@ -2408,6 +2867,35 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "license": "ISC", + "optional": true + }, + "node_modules/npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "license": "ISC", + "optional": true, + "dependencies": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, "node_modules/npmlog": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", @@ -2437,6 +2925,26 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -2455,6 +2963,38 @@ "wrappy": "1" } }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", @@ -2672,6 +3212,13 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT", + "optional": true + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -2913,6 +3460,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "license": "BlueOak-1.0.0" + }, "node_modules/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -4005,6 +4558,28 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -4020,6 +4595,20 @@ "engines": { "node": ">=6" } + }, + "node_modules/zipfile": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/zipfile/-/zipfile-0.5.12.tgz", + "integrity": "sha512-zA60gW+XgQBu/Q4qV3BCXNIDRald6Xi5UOPj3jWGlnkjmBHaKDwIz7kyXWV3kq7VEsQN/2t/IWjdXdKeVNm6Eg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "nan": "~2.10.0", + "node-pre-gyp": "~0.10.2" + }, + "bin": { + "unzip.js": "bin/unzip.js" + } } } } diff --git a/docker/package.json b/docker/package.json index 6c4cd76..6375879 100644 --- a/docker/package.json +++ b/docker/package.json @@ -18,6 +18,7 @@ "bindings": "^1.5.0", "cheerio": "^1.1.2", "dotenv": "^17.2.3", + "epub": "^1.3.0", "fastify": "^5.6.2", "js-yaml": "^4.1.1", "jsonwebtoken": "^9.0.3", diff --git a/docker/src/api/local/local.controller.ts b/docker/src/api/local/local.controller.ts index c07c070..908d669 100644 --- a/docker/src/api/local/local.controller.ts +++ b/docker/src/api/local/local.controller.ts @@ -1,21 +1,14 @@ import { FastifyRequest, FastifyReply } from 'fastify'; -import { getConfig as loadConfig, setConfig as saveConfig } from '../../shared/config.js'; +import { getConfig as loadConfig } from '../../shared/config.js'; import { queryOne, queryAll, run } from '../../shared/database.js'; import crypto from 'crypto'; import fs from "fs"; import { PathLike } from "node:fs"; import path from "path"; import {getAnimeById, searchAnimeLocal} from "../anime/anime.service"; -import {getBookById, searchBooksAniList, searchBooksLocal} from "../books/books.service"; +import {getBookById, searchBooksAniList} from "../books/books.service"; import AdmZip from 'adm-zip'; - -type SetConfigBody = { - library?: { - anime?: string | null; - manga?: string | null; - novels?: string | null; - }; -}; +import EPub from 'epub'; type ScanQuery = { mode?: 'full' | 'incremental'; @@ -37,10 +30,21 @@ async function resolveEntryMetadata(entry: any, type: string) { ? await searchAnimeLocal(query) : await searchBooksAniList(query); - const first = results?.[0]; + let picked = null; - if (first?.id) { - matchedId = first.id; + if (type !== 'anime' && Array.isArray(results)) { + console.log(type) + if (entry.type === 'novels') { + picked = results.find(r => r.format === 'NOVEL'); + } else if (entry.type === 'manga') { + picked = results.find(r => r.format !== 'NOVEL'); + } + } + + picked ??= results?.[0]; + + if (picked?.id) { + matchedId = picked.id; await run( `UPDATE local_entries @@ -104,9 +108,13 @@ export async function scanLibrary(request: FastifyRequest<{ Querystring: ScanQue } const files = fs.readdirSync(fullPath, { withFileTypes: true }) - .filter(f => f.isFile()) + .filter(f => + f.isFile() || + (type === 'manga' && f.isDirectory()) + ) .sort((a, b) => a.name.localeCompare(b.name)); + let unit = 1; for (const file of files) { @@ -191,27 +199,30 @@ export async function streamUnit(request: FastifyRequest, reply: FastifyReply) { const parts = range.replace(/bytes=/, '').split('-'); const start = Number(parts[0]); - let end = parts[1] ? Number(parts[1]) : stat.size - 1; + const end = parts[1] ? Number(parts[1]) : stat.size - 1; + // Validate range values if ( Number.isNaN(start) || Number.isNaN(end) || start < 0 || + start >= stat.size || end < start || end >= stat.size ) { - end = stat.size - 1; + return reply.status(416).send({ error: 'INVALID_RANGE' }); } + const contentLength = end - start + 1; + reply .status(206) .header('Content-Range', `bytes ${start}-${end}/${stat.size}`) .header('Accept-Ranges', 'bytes') - .header('Content-Length', end - start + 1) + .header('Content-Length', contentLength) .header('Content-Type', 'video/mp4'); return fs.createReadStream(file.file_path, { start, end }); - } type MatchBody = { @@ -247,17 +258,13 @@ export async function matchEntry( return { status: 'OK', matched: !!matched_id }; } -export async function getUnits( - request: FastifyRequest<{ Params: Params }>, - reply: FastifyReply -) { +export async function getUnits(request: FastifyRequest<{ Params: Params }>, reply: FastifyReply) { try { - const { type, id } = request.params as { type: string, id: string }; + const { id } = request.params as { id: string }; - // Buscar la entrada por matched_id const entry = await queryOne( - `SELECT id, type, matched_id FROM local_entries WHERE matched_id = ? AND type = ?`, - [Number(id), type], + `SELECT id, type, matched_id FROM local_entries WHERE matched_id = ?`, + [Number(id)], 'local_library' ); @@ -274,24 +281,82 @@ export async function getUnits( 'local_library' ); - // Formatear la respuesta según el tipo - const units = files.map((file: any) => { - const fileName = path.basename(file.file_path); - const fileExt = path.extname(file.file_path).toLowerCase(); + const MANGA_IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.webp']; + const MANGA_ARCHIVES = ['.cbz', '.cbr', '.zip']; - // Detectar si es un archivo comprimido (capítulo único) o carpeta - const isDirectory = fs.existsSync(file.file_path) && - fs.statSync(file.file_path).isDirectory(); + const NOVEL_EXTS = ['.epub', '.pdf', '.txt', '.md', '.docx', '.mobi']; + + function isImageFolder(folderPath: string): boolean { + if (!fs.existsSync(folderPath)) return false; + if (!fs.statSync(folderPath).isDirectory()) return false; + + const files = fs.readdirSync(folderPath); + return files.some(f => MANGA_IMAGE_EXTS.includes(path.extname(f).toLowerCase())); + } + + const units = files + .map((file: any) => { + const fileExt = path.extname(file.file_path).toLowerCase(); + const isDir = fs.existsSync(file.file_path) && + fs.statSync(file.file_path).isDirectory(); + + // ===== MANGA ===== + if (entry.type === 'manga') { + if (MANGA_ARCHIVES.includes(fileExt)) { + return { + id: file.id, + number: file.unit_number, + name: path.basename(file.file_path), + type: 'chapter', + format: fileExt.replace('.', ''), + path: file.file_path + }; + } + + if (isDir && isImageFolder(file.file_path)) { + return { + id: file.id, + number: file.unit_number, + name: path.basename(file.file_path), + type: 'chapter', + format: 'folder', + path: file.file_path + }; + } + + return null; + } + + if (entry.type === 'novels') { + if (NOVEL_EXTS.includes(fileExt)) { + return { + id: file.id, + number: file.unit_number, + name: path.basename(file.file_path), + type: 'chapter', + format: fileExt.replace('.', ''), + path: file.file_path + }; + } + + return null; + } + + if (entry.type === 'anime') { + return { + id: file.id, + number: file.unit_number, + name: path.basename(file.file_path), + type: 'episode', + format: fileExt.replace('.', ''), + path: file.file_path + }; + } + + return null; + }) + .filter(Boolean); - return { - id: file.id, - number: file.unit_number, - name: fileName, - type: type === 'anime' ? 'episode' : 'chapter', - format: fileExt === '.cbz' ? 'cbz' : 'file', - path: file.file_path - }; - }); return { entry_id: entry.id, @@ -306,7 +371,7 @@ export async function getUnits( } } -export async function getCbzPages(request: FastifyRequest, reply: FastifyReply) { +export async function getManifest(request: FastifyRequest, reply: FastifyReply) { const { unitId } = request.params as any; const file = await queryOne( @@ -319,20 +384,73 @@ export async function getCbzPages(request: FastifyRequest, reply: FastifyReply) return reply.status(404).send({ error: 'FILE_NOT_FOUND' }); } - const zip = new AdmZip(file.file_path); + const ext = path.extname(file.file_path).toLowerCase(); - const pages = zip.getEntries() - .filter(e => !e.isDirectory && /\.(jpg|jpeg|png|webp)$/i.test(e.entryName)) - .sort((a, b) => a.entryName.localeCompare(b.entryName, undefined, { numeric: true })) - .map((_, i) => - `/api/library/manga/cbz/${unitId}/page/${i}` - ); + // ===== MANGA ===== + if (['.cbz', '.cbr', '.zip'].includes(ext)) { + const zip = new AdmZip(file.file_path); - return { pages }; + const pages = zip.getEntries() + .filter(e => !e.isDirectory && /\.(jpg|jpeg|png|webp)$/i.test(e.entryName)) + .sort((a, b) => a.entryName.localeCompare(b.entryName, undefined, { numeric: true })) + .map((_, i) => ({ + id: i, + url: `/api/library/${unitId}/resource/${i}` + })); + + return { + type: 'manga', + format: 'archive', + pages + }; + } + + if (fs.statSync(file.file_path).isDirectory()) { + const pages = fs.readdirSync(file.file_path) + .filter(f => /\.(jpg|jpeg|png|webp)$/i.test(f)) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) + .map((_, i) => ({ + id: i, + url: `/api/library/${unitId}/resource/${i}` + })); + + return { + type: 'manga', + format: 'folder', + pages + }; + } + + // ===== NOVEL ===== + if (ext === '.epub') { + return { + type: 'ln', + format: 'epub', + url: `/api/library/${unitId}/resource/epub` + }; + } + + if (['.txt', '.md'].includes(ext)) { + return { + type: 'ln', + format: 'text', + url: `/api/library/${unitId}/resource/text` + }; + } + + if (ext === '.pdf') { + return { + type: 'ln', + format: 'pdf', + url: `/api/library/${unitId}/resource/pdf` + }; + } + + return reply.status(400).send({ error: 'UNSUPPORTED_FORMAT' }); } -export async function getCbzPage(request: FastifyRequest, reply: FastifyReply) { - const { unitId, page } = request.params as any; +export async function getPage(request: FastifyRequest, reply: FastifyReply) { + const { unitId, resId } = request.params as any; const file = await queryOne( `SELECT file_path FROM local_files WHERE id = ?`, @@ -342,16 +460,83 @@ export async function getCbzPage(request: FastifyRequest, reply: FastifyReply) { if (!file) return reply.status(404).send(); - const zip = new AdmZip(file.file_path); + const ext = path.extname(file.file_path).toLowerCase(); - const images = zip.getEntries() - .filter(e => !e.isDirectory && /\.(jpg|jpeg|png|webp)$/i.test(e.entryName)) - .sort((a, b) => a.entryName.localeCompare(b.entryName, undefined, { numeric: true })); + // ===== CBZ PAGE ===== + if (['.cbz', '.zip', '.cbr'].includes(ext)) { + const zip = new AdmZip(file.file_path); + const images = zip.getEntries() + .filter(e => !e.isDirectory && /\.(jpg|jpeg|png|webp)$/i.test(e.entryName)) + .sort((a, b) => a.entryName.localeCompare(b.entryName, undefined, { numeric: true })); - const entry = images[page]; - if (!entry) return reply.status(404).send(); + const entry = images[Number(resId)]; + if (!entry) return reply.status(404).send(); - reply - .header('Content-Type', 'image/jpeg') - .send(entry.getData()); + return reply + .header('Content-Type', 'image/jpeg') + .send(entry.getData()); + } + + if (fs.statSync(file.file_path).isDirectory()) { + const images = fs.readdirSync(file.file_path) + .filter(f => /\.(jpg|jpeg|png|webp)$/i.test(f)) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); + + const img = images[Number(resId)]; + if (!img) return reply.status(404).send(); + + const imgPath = path.join(file.file_path, img); + const stat = fs.statSync(imgPath); + + reply + .header('Content-Length', stat.size) + .header('Content-Type', 'image/jpeg'); + + return fs.createReadStream(imgPath); + } + + if (ext === '.epub') { + const html = await parseEpubToHtml(file.file_path); + + return reply + .header('Content-Type', 'text/html; charset=utf-8') + .send(html); + } + + // ===== TXT / MD ===== + if (['.txt', '.md'].includes(ext)) { + const text = fs.readFileSync(file.file_path, 'utf8'); + + return reply + .header('Content-Type', 'text/html; charset=utf-8') + .send(`
${text}
`); + } + + return reply.status(400).send(); +} + +function parseEpubToHtml(filePath: string): Promise { + return new Promise((resolve, reject) => { + const epub = new EPub(filePath); + + epub.on('end', async () => { + let html = ''; + + for (const id of epub.flow.map(f => f.id)) { + const chapter = await new Promise((res, rej) => { + epub.getChapter(id, (err, text) => { + if (err) rej(err); + else res(text); + }); + }); + + html += `
${chapter}
`; + } + + resolve(html); + }); + + epub.on('error', reject); + epub.parse(); + }); } diff --git a/docker/src/api/local/local.routes.ts b/docker/src/api/local/local.routes.ts index 0127890..5d18cf9 100644 --- a/docker/src/api/local/local.routes.ts +++ b/docker/src/api/local/local.routes.ts @@ -7,9 +7,9 @@ async function localRoutes(fastify: FastifyInstance) { fastify.get('/library/:type/:id', controller.getEntry); fastify.get('/library/stream/:type/:id/:unit', controller.streamUnit); fastify.post('/library/:type/:id/match', controller.matchEntry); - fastify.get('/library/:type/:id/units', controller.getUnits); - fastify.get('/library/:type/cbz/:unitId/pages', controller.getCbzPages); - fastify.get('/library/:type/cbz/:unitId/page/:page', controller.getCbzPage); + fastify.get('/library/:id/units', controller.getUnits); + fastify.get('/library/:unitId/manifest', controller.getManifest); + fastify.get('/library/:unitId/resource/:resId', controller.getPage); } export default localRoutes; \ No newline at end of file diff --git a/docker/src/scripts/anime/player.js b/docker/src/scripts/anime/player.js index da56b29..8023dfe 100644 --- a/docker/src/scripts/anime/player.js +++ b/docker/src/scripts/anime/player.js @@ -32,12 +32,12 @@ let localEntryId = null; async function checkLocal() { try { const res = await fetch(`/api/library/anime/${animeId}`); - if (!res.ok) return; - + if (!res.ok) return null; const data = await res.json(); - localEntryId = data.id; - - } catch {} + return data.id; + } catch { + return null; + } } async function loadAniSkip(malId, episode, duration) { @@ -53,7 +53,7 @@ async function loadAniSkip(malId, episode, duration) { } async function loadMetadata() { - checkLocal(); + localEntryId = await checkLocal(); try { const sourceQuery = (extName === 'local' || !extName) ? "source=anilist" : `source=${extName}`; const res = await fetch(`/api/anime/${animeId}?${sourceQuery}`); @@ -133,6 +133,7 @@ async function loadMetadata() { } catch (error) { console.error('Error loading metadata:', error); } + await loadExtensions(); } async function applyAniSkip(video) { @@ -439,9 +440,9 @@ document.getElementById('next-btn').onclick = () => { if (currentEpisode <= 1) document.getElementById('prev-btn').disabled = true; +// Actualizar progreso cada 1 minuto si el video está reproduciéndose setInterval(() => { if (plyrInstance && !plyrInstance.paused) sendProgress(); }, 60000); loadMetadata(); -loadExtensions(); \ No newline at end of file diff --git a/docker/src/scripts/books/book.js b/docker/src/scripts/books/book.js index 7384528..73a9643 100644 --- a/docker/src/scripts/books/book.js +++ b/docker/src/scripts/books/book.js @@ -18,7 +18,10 @@ document.addEventListener('DOMContentLoaded', () => { async function checkLocalLibraryEntry() { try { - const res = await fetch(`/api/library/manga/${bookId}`); + const libraryType = + bookData?.entry_type === 'NOVEL' ? 'novels' : 'manga'; + + const res = await fetch(`/api/library/${libraryType}/${bookId}`); if (!res.ok) return; const data = await res.json(); @@ -38,18 +41,6 @@ async function checkLocalLibraryEntry() { } } -function markAsLocal() { - isLocal = true; - const pill = document.getElementById('local-pill'); - if (pill) { - pill.textContent = 'Local'; - pill.style.display = 'inline-flex'; - pill.style.background = 'rgba(34, 197, 94, 0.2)'; - pill.style.color = '#22c55e'; - pill.style.borderColor = 'rgba(34, 197, 94, 0.3)'; - } -} - async function init() { try { const urlData = URLUtils.parseEntityPath('book'); @@ -61,8 +52,8 @@ async function init() { extensionName = urlData.extensionName; bookId = urlData.entityId; bookSlug = urlData.slug; - await checkLocalLibraryEntry(); await loadBookMetadata(); + await checkLocalLibraryEntry(); await loadAvailableExtensions(); await loadChapters(); @@ -220,7 +211,7 @@ async function loadChapters(targetProvider = null) { if (isLocalRequest) { // Nuevo endpoint para archivos locales - fetchUrl = `/api/library/manga/${bookId}/units`; + fetchUrl = `/api/library/${bookId}/units`; } else { const source = extensionName || 'anilist'; fetchUrl = `/api/book/${bookId}/chapters?source=${source}`; diff --git a/docker/src/scripts/books/reader.js b/docker/src/scripts/books/reader.js index f0ba41d..609165b 100644 --- a/docker/src/scripts/books/reader.js +++ b/docker/src/scripts/books/reader.js @@ -132,7 +132,7 @@ async function loadChapter() { let newEndpoint; if (provider === 'local') { - newEndpoint = `/api/library/manga/${bookId}/units`; + newEndpoint = `/api/library/${bookId}/units`; } else { newEndpoint = `/api/book/${bookId}/${chapter}/${provider}?source=${source}`; } @@ -142,29 +142,39 @@ async function loadChapter() { const data = await res.json(); if (provider === 'local') { const unit = data.units[Number(chapter)]; + if (!unit) return; - if (!unit) { - reader.innerHTML = '
Chapter not found
'; - return; - } + chapterLabel.textContent = unit.name; + document.title = unit.name; - if (unit.format === 'cbz') { - chapterLabel.textContent = unit.name; // ✅ - document.title = unit.name; - const pagesRes = await fetch( - `/api/library/manga/cbz/${unit.id}/pages` - ); - const pagesData = await pagesRes.json(); + const manifestRes = await fetch(`/api/library/${unit.id}/manifest`); + const manifest = await manifestRes.json(); + reader.innerHTML = ''; + + // ===== MANGA ===== + if (manifest.type === 'manga') { currentType = 'manga'; updateSettingsVisibility(); applyStyles(); - currentPages = pagesData.pages.map(url => ({ url })); - reader.innerHTML = ''; + currentPages = manifest.pages; loadManga(currentPages); return; } + + // ===== LN ===== + if (manifest.type === 'ln') { + currentType = 'ln'; + updateSettingsVisibility(); + applyStyles(); + + const contentRes = await fetch(manifest.url); + const html = await contentRes.text(); + + loadLN(html); + return; + } } @@ -193,13 +203,8 @@ async function loadChapter() { reader.innerHTML = ''; if (data.type === 'manga') { - if (provider === 'local' && data.format === 'cbz') { - currentPages = data.pages.map(url => ({ url })); - loadManga(currentPages); - } else { - currentPages = data.pages || []; - loadManga(currentPages); - } + currentPages = data.pages || []; + loadManga(currentPages); } else if (data.type === 'ln') { loadLN(data.content); } diff --git a/docker/src/scripts/local-library-books.js b/docker/src/scripts/local-library-books.js index 63e3f62..3ab311c 100644 --- a/docker/src/scripts/local-library-books.js +++ b/docker/src/scripts/local-library-books.js @@ -26,21 +26,37 @@ async function loadLocalEntries() { grid.innerHTML = '
'.repeat(6); try { - // Cambiado a endpoint de libros - const response = await fetch('/api/library/manga'); - const entries = await response.json(); - localEntries = entries; + const [mangaRes, novelRes] = await Promise.all([ + fetch('/api/library/manga'), + fetch('/api/library/novels') + ]); - if (entries.length === 0) { - grid.innerHTML = '

No books found in your local library.

'; + const [manga, novel] = await Promise.all([ + mangaRes.json(), + novelRes.json() + ]); + + localEntries = [ + ...manga.map(e => ({ ...e, type: 'manga' })), + ...novel.map(e => ({ ...e, type: 'novel' })) + ]; + + if (localEntries.length === 0) { + grid.innerHTML = '

No books found.

'; return; } - renderLocalEntries(entries); - } catch (err) { - grid.innerHTML = '

Error loading local books.

'; + + renderLocalEntries(localEntries); + } catch { + grid.innerHTML = '

Error loading library.

'; } } +function filterLocal(type) { + if (type === 'all') renderLocalEntries(localEntries); + else renderLocalEntries(localEntries.filter(e => e.type === type)); +} + function renderLocalEntries(entries) { const grid = document.getElementById('local-entries-grid'); grid.innerHTML = entries.map(entry => { @@ -58,6 +74,7 @@ function renderLocalEntries(entries) {

${chapters} Chapters

+
${entry.type}
${entry.matched ? '● Linked' : '○ Unlinked'}