add(core): open filesystem while adding library path
This commit is contained in:
@@ -84,6 +84,24 @@ $("#lib-form").addEventListener("submit", async (e) => {
|
||||
} catch (err) { toast(err.message, true); }
|
||||
});
|
||||
|
||||
// path autocompletion for the library form
|
||||
{
|
||||
const pathInput = document.querySelector('#lib-form [name=root_path]');
|
||||
const datalist = $("#path-suggestions");
|
||||
let pathTimer;
|
||||
pathInput.addEventListener("input", () => {
|
||||
clearTimeout(pathTimer);
|
||||
const value = pathInput.value;
|
||||
if (!value.startsWith("/")) return;
|
||||
pathTimer = setTimeout(async () => {
|
||||
try {
|
||||
const res = await api("/api/fs/browse?path=" + encodeURIComponent(value));
|
||||
datalist.innerHTML = res.dirs.map((d) => `<option value="${esc(d)}">`).join("");
|
||||
} catch {}
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
function libOptions(mediaType) {
|
||||
return libraries
|
||||
.filter((l) => l.media_type === mediaType)
|
||||
|
||||
+148
-108
@@ -1,116 +1,156 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>wordarr</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>📚 wordarr</h1>
|
||||
<nav>
|
||||
<button data-view="search" class="active">Suche</button>
|
||||
<button data-view="missing">Missing</button>
|
||||
<button data-view="imported">Importiert</button>
|
||||
<button data-view="import">Import</button>
|
||||
<button data-view="settings">Libraries</button>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section id="view-search">
|
||||
<form id="search-form" class="bar">
|
||||
<select id="search-type">
|
||||
<option value="ebook">Ebook</option>
|
||||
<option value="audiobook">Audiobook</option>
|
||||
<option value="comic">Comic/Manga</option>
|
||||
</select>
|
||||
<input id="search-q" placeholder="Titel, Autor oder ISBN…" required>
|
||||
<button type="submit">Suchen</button>
|
||||
<button type="button" id="manual-btn" class="secondary">Manuell anlegen</button>
|
||||
</form>
|
||||
<div id="search-results" class="cards"></div>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>wordarr</title>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>📚 wordarr</h1>
|
||||
<nav>
|
||||
<button data-view="search" class="active">Suche</button>
|
||||
<button data-view="missing">Missing</button>
|
||||
<button data-view="imported">Importiert</button>
|
||||
<button data-view="import">Import</button>
|
||||
<button data-view="settings">Libraries</button>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section id="view-search">
|
||||
<form id="search-form" class="bar">
|
||||
<select id="search-type">
|
||||
<option value="ebook">Ebook</option>
|
||||
<option value="audiobook">Audiobook</option>
|
||||
<option value="comic">Comic/Manga</option>
|
||||
</select>
|
||||
<input id="search-q" placeholder="Titel, Autor oder ISBN…" required />
|
||||
<button type="submit">Suchen</button>
|
||||
<button type="button" id="manual-btn" class="secondary">
|
||||
Manuell anlegen
|
||||
</button>
|
||||
</form>
|
||||
<div id="search-results" class="cards"></div>
|
||||
|
||||
<dialog id="manual-dialog">
|
||||
<form id="manual-form" method="dialog">
|
||||
<h3>Manuell anfragen</h3>
|
||||
<label>Typ
|
||||
<dialog id="manual-dialog">
|
||||
<form id="manual-form" method="dialog">
|
||||
<h3>Manuell anfragen</h3>
|
||||
<label
|
||||
>Typ
|
||||
<select name="media_type">
|
||||
<option value="ebook">Ebook</option>
|
||||
<option value="audiobook">Audiobook</option>
|
||||
<option value="comic">Comic/Manga</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Titel <input name="title" required /></label>
|
||||
<label>Autor(en) <input name="authors" /></label>
|
||||
<label>Serie <input name="series" /></label>
|
||||
<label>Band <input name="volume" type="number" min="0" /></label>
|
||||
<label>Jahr <input name="year" type="number" /></label>
|
||||
<label>ISBN/ID <input name="external_id" /></label>
|
||||
<label
|
||||
>Library
|
||||
<select name="library_id" id="manual-library"></select
|
||||
></label>
|
||||
<div class="row">
|
||||
<button value="cancel" class="secondary">Abbrechen</button>
|
||||
<button value="ok" id="manual-submit">Anfragen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</section>
|
||||
|
||||
<section id="view-missing" hidden>
|
||||
<div class="bar">
|
||||
<select id="missing-filter-type">
|
||||
<option value="">Alle Typen</option>
|
||||
<option value="ebook">Ebook</option>
|
||||
<option value="audiobook">Audiobook</option>
|
||||
<option value="comic">Comic/Manga</option>
|
||||
</select>
|
||||
<select id="missing-filter-library">
|
||||
<option value="">Alle Libraries</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="missing-list" class="cards"></div>
|
||||
</section>
|
||||
|
||||
<section id="view-imported" hidden>
|
||||
<div id="imported-list" class="cards"></div>
|
||||
</section>
|
||||
|
||||
<section id="view-import" hidden>
|
||||
<div class="bar">
|
||||
<button id="scan-btn">Download-Ordner scannen</button>
|
||||
<span id="scan-info" class="muted"></span>
|
||||
</div>
|
||||
<table id="import-table" hidden>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Datei/Ordner</th>
|
||||
<th>Typ</th>
|
||||
<th>Zuordnung</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div class="bar">
|
||||
<button id="import-btn" hidden>Ausgewählte importieren</button>
|
||||
</div>
|
||||
<div id="import-results"></div>
|
||||
</section>
|
||||
|
||||
<section id="view-settings" hidden>
|
||||
<h3>Libraries</h3>
|
||||
<table id="lib-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Typ</th>
|
||||
<th>Pfad</th>
|
||||
<th>Ordner-Schema</th>
|
||||
<th>Datei-Schema</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<h4>Neue Library</h4>
|
||||
<form id="lib-form" class="bar wrap">
|
||||
<input name="name" placeholder="Name (z.B. Kids)" required />
|
||||
<select name="media_type">
|
||||
<option value="ebook">Ebook</option>
|
||||
<option value="audiobook">Audiobook</option>
|
||||
<option value="comic">Comic/Manga</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Titel <input name="title" required></label>
|
||||
<label>Autor(en) <input name="authors"></label>
|
||||
<label>Serie <input name="series"></label>
|
||||
<label>Band <input name="volume" type="number" min="0"></label>
|
||||
<label>Jahr <input name="year" type="number"></label>
|
||||
<label>ISBN/ID <input name="external_id"></label>
|
||||
<label>Library <select name="library_id" id="manual-library"></select></label>
|
||||
<div class="row">
|
||||
<button value="cancel" class="secondary">Abbrechen</button>
|
||||
<button value="ok" id="manual-submit">Anfragen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</section>
|
||||
|
||||
<section id="view-missing" hidden>
|
||||
<div class="bar">
|
||||
<select id="missing-filter-type">
|
||||
<option value="">Alle Typen</option>
|
||||
<option value="ebook">Ebook</option>
|
||||
<option value="audiobook">Audiobook</option>
|
||||
<option value="comic">Comic/Manga</option>
|
||||
</select>
|
||||
<select id="missing-filter-library"><option value="">Alle Libraries</option></select>
|
||||
</div>
|
||||
<div id="missing-list" class="cards"></div>
|
||||
</section>
|
||||
|
||||
<section id="view-imported" hidden>
|
||||
<div id="imported-list" class="cards"></div>
|
||||
</section>
|
||||
|
||||
<section id="view-import" hidden>
|
||||
<div class="bar">
|
||||
<button id="scan-btn">Download-Ordner scannen</button>
|
||||
<span id="scan-info" class="muted"></span>
|
||||
</div>
|
||||
<table id="import-table" hidden>
|
||||
<thead><tr><th></th><th>Datei/Ordner</th><th>Typ</th><th>Zuordnung</th><th>Score</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div class="bar">
|
||||
<button id="import-btn" hidden>Ausgewählte importieren</button>
|
||||
</div>
|
||||
<div id="import-results"></div>
|
||||
</section>
|
||||
|
||||
<section id="view-settings" hidden>
|
||||
<h3>Libraries</h3>
|
||||
<table id="lib-table">
|
||||
<thead><tr><th>Name</th><th>Typ</th><th>Pfad</th><th>Ordner-Schema</th><th>Datei-Schema</th><th></th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<h4>Neue Library</h4>
|
||||
<form id="lib-form" class="bar wrap">
|
||||
<input name="name" placeholder="Name (z.B. Kids)" required>
|
||||
<select name="media_type">
|
||||
<option value="ebook">Ebook</option>
|
||||
<option value="audiobook">Audiobook</option>
|
||||
<option value="comic">Comic/Manga</option>
|
||||
</select>
|
||||
<input name="root_path" placeholder="/library/ebooks/kids" required class="wide">
|
||||
<input name="folder_template" placeholder="Ordner-Schema (optional)" class="wide">
|
||||
<input name="file_template" placeholder="Datei-Schema (optional)" class="wide">
|
||||
<button type="submit">Anlegen</button>
|
||||
</form>
|
||||
<p class="muted">Platzhalter: {Author} {Authors} {Title} {Year} {Series} {Volume}</p>
|
||||
</section>
|
||||
</main>
|
||||
<div id="toast" hidden></div>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
<input
|
||||
name="root_path"
|
||||
placeholder="/ebooks/kids"
|
||||
required
|
||||
class="wide"
|
||||
list="path-suggestions"
|
||||
autocomplete="off" />
|
||||
<datalist id="path-suggestions"></datalist>
|
||||
<input
|
||||
name="folder_template"
|
||||
placeholder="Ordner-Schema (optional)"
|
||||
class="wide" />
|
||||
<input
|
||||
name="file_template"
|
||||
placeholder="Datei-Schema (optional)"
|
||||
class="wide" />
|
||||
<button type="submit">Anlegen</button>
|
||||
</form>
|
||||
<p class="muted">
|
||||
Platzhalter: {Author} {Authors} {Title} {Year} {Series} {Volume}
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
<div id="toast" hidden></div>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix="/api/fs", tags=["fs"])
|
||||
|
||||
|
||||
@router.get("/browse")
|
||||
def browse(path: str = "/"):
|
||||
"""List subdirectories for path autocompletion in the library form.
|
||||
|
||||
If `path` ends with '/', its children are listed; otherwise the children
|
||||
of the parent that match the typed last segment.
|
||||
"""
|
||||
p = Path(path)
|
||||
if path.endswith("/"):
|
||||
base, prefix = p, ""
|
||||
else:
|
||||
base, prefix = p.parent, p.name.lower()
|
||||
try:
|
||||
dirs = sorted(
|
||||
str(d) for d in base.iterdir()
|
||||
if d.is_dir() and not d.name.startswith(".")
|
||||
and d.name.lower().startswith(prefix)
|
||||
)
|
||||
except OSError:
|
||||
dirs = []
|
||||
return {"dirs": dirs[:50]}
|
||||
+2
-1
@@ -5,7 +5,7 @@ from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import db
|
||||
from .api import imports, libraries, requests, search
|
||||
from .api import fs, imports, libraries, requests, search
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -20,6 +20,7 @@ app.include_router(libraries.router)
|
||||
app.include_router(search.router)
|
||||
app.include_router(requests.router)
|
||||
app.include_router(imports.router)
|
||||
app.include_router(fs.router)
|
||||
|
||||
|
||||
static_dir = Path(__file__).parent.parent / "static"
|
||||
|
||||
Reference in New Issue
Block a user