support for novels and lot of formats for books

This commit is contained in:
2025-12-28 18:31:54 +01:00
parent 6222e7736f
commit 48e1939d2a
16 changed files with 1798 additions and 236 deletions

589
docker/package-lock.json generated
View File

@@ -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"
}
}
}
}

View File

@@ -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",

View File

@@ -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(`<div class="ln-content"><pre>${text}</pre></div>`);
}
return reply.status(400).send();
}
function parseEpubToHtml(filePath: string): Promise<string> {
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<string>((res, rej) => {
epub.getChapter(id, (err, text) => {
if (err) rej(err);
else res(text);
});
});
html += `<section class="ln-chapter">${chapter}</section>`;
}
resolve(html);
});
epub.on('error', reject);
epub.parse();
});
}

View File

@@ -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;

View File

@@ -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();

View File

@@ -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}`;

View File

@@ -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 = '<div class="loading-container"><span>Chapter not found</span></div>';
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);
}

View File

@@ -26,21 +26,37 @@ async function loadLocalEntries() {
grid.innerHTML = '<div class="skeleton-card"></div>'.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 = '<p style="grid-column: 1/-1; text-align: center; color: var(--color-text-secondary); padding: 3rem;">No books found in your local library.</p>';
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 = '<p style="grid-column:1/-1;text-align:center;padding:3rem;">No books found.</p>';
return;
}
renderLocalEntries(entries);
} catch (err) {
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: var(--color-danger); padding: 3rem;">Error loading local books.</p>';
renderLocalEntries(localEntries);
} catch {
grid.innerHTML = '<p style="grid-column:1/-1;text-align:center;color:var(--color-danger);padding:3rem;">Error loading library.</p>';
}
}
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) {
<p style="font-size: 0.85rem; color: var(--color-text-secondary); margin: 0;">
${chapters} Chapters
</p>
<div class="badge">${entry.type}</div>
<div class="match-status ${entry.matched ? 'status-linked' : 'status-unlinked'}">
${entry.matched ? '● Linked' : '○ Unlinked'}
</div>