add(metadata) search also for narrator if same title
This commit is contained in:
+8
-2
@@ -127,6 +127,7 @@ $("#search-form").addEventListener("submit", async (e) => {
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<strong>${esc(r.title)}</strong>
|
<strong>${esc(r.title)}</strong>
|
||||||
<span>${esc(r.authors)}</span>
|
<span>${esc(r.authors)}</span>
|
||||||
|
${r.narrator ? `<span class="muted">🎙 ${esc(r.narrator)}</span>` : ""}
|
||||||
<span class="muted">${r.year ?? ""} ${r.external_id ? "· " + esc(r.external_id) : ""}</span>
|
<span class="muted">${r.year ?? ""} ${r.external_id ? "· " + esc(r.external_id) : ""}</span>
|
||||||
${type === "comic" ? `<input type="number" min="0" placeholder="Band" class="volume" data-vol="${i}">` : ""}
|
${type === "comic" ? `<input type="number" min="0" placeholder="Band" class="volume" data-vol="${i}">` : ""}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -149,7 +150,8 @@ $("#search-form").addEventListener("submit", async (e) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
library_id: parseInt(libId),
|
library_id: parseInt(libId),
|
||||||
title: r.title, authors: r.authors, external_id: r.external_id,
|
title: r.title, authors: r.authors, narrator: r.narrator || "",
|
||||||
|
external_id: r.external_id,
|
||||||
year: r.year, series: r.series, cover_url: r.cover_url,
|
year: r.year, series: r.series, cover_url: r.cover_url,
|
||||||
volume: volInput && volInput.value ? parseInt(volInput.value) : null,
|
volume: volInput && volInput.value ? parseInt(volInput.value) : null,
|
||||||
}),
|
}),
|
||||||
@@ -192,6 +194,7 @@ $("#manual-form").addEventListener("submit", async (e) => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
library_id: parseInt(data.library_id),
|
library_id: parseInt(data.library_id),
|
||||||
series: data.series || data.title, authors: data.authors,
|
series: data.series || data.title, authors: data.authors,
|
||||||
|
narrator: data.narrator || "",
|
||||||
volume_from: parseInt(data.volume),
|
volume_from: parseInt(data.volume),
|
||||||
volume_to: parseInt(data.volume_to),
|
volume_to: parseInt(data.volume_to),
|
||||||
year: data.year ? parseInt(data.year) : null,
|
year: data.year ? parseInt(data.year) : null,
|
||||||
@@ -204,6 +207,7 @@ $("#manual-form").addEventListener("submit", async (e) => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
library_id: parseInt(data.library_id),
|
library_id: parseInt(data.library_id),
|
||||||
title: data.title, authors: data.authors, series: data.series,
|
title: data.title, authors: data.authors, series: data.series,
|
||||||
|
narrator: data.narrator || "",
|
||||||
external_id: data.external_id,
|
external_id: data.external_id,
|
||||||
year: data.year ? parseInt(data.year) : null,
|
year: data.year ? parseInt(data.year) : null,
|
||||||
volume: data.volume ? parseInt(data.volume) : null,
|
volume: data.volume ? parseInt(data.volume) : null,
|
||||||
@@ -393,6 +397,7 @@ async function runQuickSearch() {
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<strong>${esc(r.title)}</strong>
|
<strong>${esc(r.title)}</strong>
|
||||||
<span>${esc(r.authors)}</span>
|
<span>${esc(r.authors)}</span>
|
||||||
|
${r.narrator ? `<span class="muted">🎙 ${esc(r.narrator)}</span>` : ""}
|
||||||
<span class="muted">${r.year ?? ""}</span>
|
<span class="muted">${r.year ?? ""}</span>
|
||||||
<div class="row"><button data-pick="${j}">Anfragen & verbinden</button></div>
|
<div class="row"><button data-pick="${j}">Anfragen & verbinden</button></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -423,7 +428,8 @@ async function pickQuickResult(r) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
library_id: parseInt(libId),
|
library_id: parseInt(libId),
|
||||||
title: r.title, authors: r.authors, external_id: r.external_id,
|
title: r.title, authors: r.authors, narrator: r.narrator || "",
|
||||||
|
external_id: r.external_id,
|
||||||
year: r.year, series: r.series, cover_url: r.cover_url,
|
year: r.year, series: r.series, cover_url: r.cover_url,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-1
@@ -46,6 +46,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>Titel <input name="title" required /></label>
|
<label>Titel <input name="title" required /></label>
|
||||||
<label>Autor(en) <input name="authors" /></label>
|
<label>Autor(en) <input name="authors" /></label>
|
||||||
|
<label>Sprecher <input name="narrator" /></label>
|
||||||
<label>Serie <input name="series" /></label>
|
<label>Serie <input name="series" /></label>
|
||||||
<label>Band <input name="volume" type="number" min="0" /></label>
|
<label>Band <input name="volume" type="number" min="0" /></label>
|
||||||
<label
|
<label
|
||||||
@@ -180,7 +181,8 @@
|
|||||||
<button type="submit">Anlegen</button>
|
<button type="submit">Anlegen</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
Platzhalter: {Author} {Authors} {Title} {Year} {Series} {Volume}
|
Platzhalter: {Author} {Authors} {Narrator} {Title} {Year} {Series}
|
||||||
|
{Volume}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -168,3 +168,37 @@ def test_audio_tags_written_on_import(client):
|
|||||||
assert str(tags["TXXX:SERIES"]) == "Die drei ??? Kids"
|
assert str(tags["TXXX:SERIES"]) == "Die drei ??? Kids"
|
||||||
assert str(tags["TXXX:SERIES-PART"]) == "85"
|
assert str(tags["TXXX:SERIES-PART"]) == "85"
|
||||||
assert str(tags["TRCK"]) == "1/2"
|
assert str(tags["TRCK"]) == "1/2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_narrator_in_path_and_tags(client):
|
||||||
|
from mutagen.id3 import ID3
|
||||||
|
|
||||||
|
root = client.tmp_path / "library" / "hp"
|
||||||
|
lib_id = client.post("/api/libraries", json={
|
||||||
|
"name": "HP", "media_type": "audiobook", "root_path": str(root),
|
||||||
|
"folder_template": "{Author}/{Title} ({Narrator})", "file_template": "{Title}",
|
||||||
|
}).json()["id"]
|
||||||
|
beck = client.post("/api/requests", json={
|
||||||
|
"library_id": lib_id, "title": "Harry Potter und der Stein der Weisen",
|
||||||
|
"authors": "J.K. Rowling", "narrator": "Rufus Beck",
|
||||||
|
}).json()
|
||||||
|
client.post("/api/requests", json={
|
||||||
|
"library_id": lib_id, "title": "Harry Potter und der Stein der Weisen",
|
||||||
|
"authors": "J.K. Rowling", "narrator": "Felix von Manteuffel",
|
||||||
|
})
|
||||||
|
|
||||||
|
f = client.downloads / "Harry Potter Stein der Weisen (Rufus Beck).mp3"
|
||||||
|
f.write_bytes(b"")
|
||||||
|
items = client.get("/api/import/scan").json()["items"]
|
||||||
|
assert items[0]["suggested_request_id"] == beck["id"]
|
||||||
|
|
||||||
|
resp = client.post("/api/import", json={"items": [{
|
||||||
|
"path": items[0]["path"], "is_dir": False,
|
||||||
|
"files": items[0]["files"], "request_id": beck["id"],
|
||||||
|
}]})
|
||||||
|
assert resp.json()["results"][0]["ok"]
|
||||||
|
dest = (root / "J.K. Rowling"
|
||||||
|
/ "Harry Potter und der Stein der Weisen (Rufus Beck)"
|
||||||
|
/ "Harry Potter und der Stein der Weisen.mp3")
|
||||||
|
assert dest.is_file()
|
||||||
|
assert str(ID3(dest)["TCOM"]) == "Rufus Beck"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class RequestIn(BaseModel):
|
|||||||
library_id: int
|
library_id: int
|
||||||
title: str
|
title: str
|
||||||
authors: str = ""
|
authors: str = ""
|
||||||
|
narrator: str = ""
|
||||||
external_id: str = ""
|
external_id: str = ""
|
||||||
year: int | None = None
|
year: int | None = None
|
||||||
series: str = ""
|
series: str = ""
|
||||||
@@ -65,6 +66,7 @@ class BulkRequestIn(BaseModel):
|
|||||||
library_id: int
|
library_id: int
|
||||||
series: str
|
series: str
|
||||||
authors: str = ""
|
authors: str = ""
|
||||||
|
narrator: str = ""
|
||||||
volume_from: int
|
volume_from: int
|
||||||
volume_to: int
|
volume_to: int
|
||||||
year: int | None = None
|
year: int | None = None
|
||||||
@@ -88,6 +90,7 @@ def create_bulk(data: BulkRequestIn, session: Session = Depends(get_session)):
|
|||||||
library_id=lib.id,
|
library_id=lib.id,
|
||||||
title=f"{data.series} Folge {n:0{width}d}",
|
title=f"{data.series} Folge {n:0{width}d}",
|
||||||
authors=data.authors,
|
authors=data.authors,
|
||||||
|
narrator=data.narrator,
|
||||||
series=data.series,
|
series=data.series,
|
||||||
volume=n,
|
volume=n,
|
||||||
year=data.year,
|
year=data.year,
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ def score(item_name: str, request) -> float:
|
|||||||
result = max(s, title_only * 0.95)
|
result = max(s, title_only * 0.95)
|
||||||
# for numbered volumes/episodes the number must match, otherwise every
|
# for numbered volumes/episodes the number must match, otherwise every
|
||||||
# episode of a series scores alike on the shared title tokens
|
# episode of a series scores alike on the shared title tokens
|
||||||
|
# narrator in the file name (e.g. "gelesen von Rufus Beck") disambiguates
|
||||||
|
# otherwise identical titles
|
||||||
|
narrator = getattr(request, "narrator", "") or ""
|
||||||
|
if narrator and fuzz.partial_ratio(normalize(narrator), name) > 80:
|
||||||
|
result = min(100.0, result + 5)
|
||||||
if request.volume is not None:
|
if request.volume is not None:
|
||||||
nums = _numbers(item_name)
|
nums = _numbers(item_name)
|
||||||
if request.volume in nums:
|
if request.volume in nums:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from mutagen.flac import FLAC
|
from mutagen.flac import FLAC
|
||||||
from mutagen.id3 import ID3, TALB, TIT2, TPE1, TRCK, TXXX
|
from mutagen.id3 import ID3, TALB, TCOM, TIT2, TPE1, TRCK, TXXX
|
||||||
from mutagen.mp4 import MP4
|
from mutagen.mp4 import MP4
|
||||||
from mutagen.oggopus import OggOpus
|
from mutagen.oggopus import OggOpus
|
||||||
from mutagen.oggvorbis import OggVorbis
|
from mutagen.oggvorbis import OggVorbis
|
||||||
@@ -23,6 +23,7 @@ def tag_audio(path: Path, request, track: int | None = None, total: int | None =
|
|||||||
|
|
||||||
def _tag_audio(path: Path, request, track, total):
|
def _tag_audio(path: Path, request, track, total):
|
||||||
author = (request.authors or "").split(",")[0].strip()
|
author = (request.authors or "").split(",")[0].strip()
|
||||||
|
narrator = getattr(request, "narrator", "") or ""
|
||||||
title = request.title or ""
|
title = request.title or ""
|
||||||
series = request.series or ""
|
series = request.series or ""
|
||||||
part = str(request.volume) if request.volume is not None else ""
|
part = str(request.volume) if request.volume is not None else ""
|
||||||
@@ -40,6 +41,8 @@ def _tag_audio(path: Path, request, track, total):
|
|||||||
tags.setall("TPE1", [TPE1(encoding=3, text=author)])
|
tags.setall("TPE1", [TPE1(encoding=3, text=author)])
|
||||||
if track:
|
if track:
|
||||||
tags.setall("TRCK", [TRCK(encoding=3, text=f"{track}/{total}" if total else str(track))])
|
tags.setall("TRCK", [TRCK(encoding=3, text=f"{track}/{total}" if total else str(track))])
|
||||||
|
if narrator: # Audiobookshelf reads the narrator from the composer tag
|
||||||
|
tags.setall("TCOM", [TCOM(encoding=3, text=narrator)])
|
||||||
if series:
|
if series:
|
||||||
tags.setall("TXXX:SERIES", [TXXX(encoding=3, desc="SERIES", text=series)])
|
tags.setall("TXXX:SERIES", [TXXX(encoding=3, desc="SERIES", text=series)])
|
||||||
if part:
|
if part:
|
||||||
@@ -53,6 +56,8 @@ def _tag_audio(path: Path, request, track, total):
|
|||||||
mp4["\xa9ART"] = [author]
|
mp4["\xa9ART"] = [author]
|
||||||
if track:
|
if track:
|
||||||
mp4["trkn"] = [(track, total or 0)]
|
mp4["trkn"] = [(track, total or 0)]
|
||||||
|
if narrator:
|
||||||
|
mp4["\xa9wrt"] = [narrator]
|
||||||
if series:
|
if series:
|
||||||
mp4["----:com.apple.iTunes:SERIES"] = [series.encode()]
|
mp4["----:com.apple.iTunes:SERIES"] = [series.encode()]
|
||||||
if part:
|
if part:
|
||||||
@@ -66,6 +71,8 @@ def _tag_audio(path: Path, request, track, total):
|
|||||||
audio["ARTIST"] = author
|
audio["ARTIST"] = author
|
||||||
if track:
|
if track:
|
||||||
audio["TRACKNUMBER"] = str(track)
|
audio["TRACKNUMBER"] = str(track)
|
||||||
|
if narrator:
|
||||||
|
audio["COMPOSER"] = narrator
|
||||||
if series:
|
if series:
|
||||||
audio["SERIES"] = series
|
audio["SERIES"] = series
|
||||||
if part:
|
if part:
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ async def search(query: str) -> list[MetadataResult]:
|
|||||||
media_type="audiobook",
|
media_type="audiobook",
|
||||||
title=p.get("title", ""),
|
title=p.get("title", ""),
|
||||||
authors=", ".join(a.get("name", "") for a in p.get("authors") or []),
|
authors=", ".join(a.get("name", "") for a in p.get("authors") or []),
|
||||||
|
narrator=", ".join(n.get("name", "") for n in p.get("narrators") or []),
|
||||||
external_id=asin,
|
external_id=asin,
|
||||||
year=int(release[:4]) if release[:4].isdigit() else None,
|
year=int(release[:4]) if release[:4].isdigit() else None,
|
||||||
cover_url=next(iter(images.values()), ""),
|
cover_url=next(iter(images.values()), ""),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class MetadataResult(BaseModel):
|
|||||||
media_type: str
|
media_type: str
|
||||||
title: str
|
title: str
|
||||||
authors: str = ""
|
authors: str = ""
|
||||||
|
narrator: str = ""
|
||||||
external_id: str = ""
|
external_id: str = ""
|
||||||
year: int | None = None
|
year: int | None = None
|
||||||
series: str = ""
|
series: str = ""
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ def render_template(template: str, request) -> str:
|
|||||||
"Authors": request.authors or author,
|
"Authors": request.authors or author,
|
||||||
"Title": request.title or "Unknown Title",
|
"Title": request.title or "Unknown Title",
|
||||||
"Year": str(request.year) if request.year else "",
|
"Year": str(request.year) if request.year else "",
|
||||||
|
"Narrator": getattr(request, "narrator", "") or "",
|
||||||
"Series": request.series or request.title or "Unknown Series",
|
"Series": request.series or request.title or "Unknown Series",
|
||||||
"Volume": f"{request.volume:02d}" if request.volume is not None else "",
|
"Volume": f"{request.volume:02d}" if request.volume is not None else "",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user