fix(connect): queue new files with the audiobookshelf watcher instead of a full scan #10
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
@@ -1228,12 +1229,13 @@ def test_connections_hear_about_committed_rows(client, monkeypatch):
|
||||
from wordarr import connect
|
||||
|
||||
calls = []
|
||||
watcher = {"disableWatcher": False}
|
||||
|
||||
def handler(request):
|
||||
calls.append((request.method, str(request.url)))
|
||||
calls.append((request.method, str(request.url), request.content))
|
||||
if request.url.path == "/api/libraries":
|
||||
return httpx.Response(200, json={"libraries": [
|
||||
{"id": "lib-audio", "name": "Audio",
|
||||
{"id": "lib-audio", "name": "Audio", "settings": watcher,
|
||||
"folders": [{"fullPath": str(client.tmp_path / "library")}]},
|
||||
{"id": "lib-pod", "name": "Pod", "folders": [{"fullPath": "/podcasts"}]},
|
||||
]})
|
||||
@@ -1261,16 +1263,29 @@ def test_connections_hear_about_committed_rows(client, monkeypatch):
|
||||
"path": str(src), "is_dir": False, "files": [str(src)], "request_id": req_id,
|
||||
}]}).json()["results"][0]["ok"]
|
||||
|
||||
assert calls == [
|
||||
# no full scan while the watcher runs: the two raced and added the book twice
|
||||
dest = str(root / "Momo" / "Momo.epub")
|
||||
assert [c[:2] for c in calls] == [
|
||||
("POST", "http://hook/in"),
|
||||
("GET", "http://abs/api/libraries"),
|
||||
("POST", "http://abs/api/watcher/update"),
|
||||
]
|
||||
assert json.loads(calls[2][2]) == {
|
||||
"libraryId": "lib-audio", "path": dest, "type": "add"}
|
||||
|
||||
calls.clear()
|
||||
client.delete(f"/api/requests/{req_id}")
|
||||
assert [c[:2] for c in calls] == [("POST", "http://hook/in")] # nothing new on disk
|
||||
|
||||
calls.clear()
|
||||
watcher["disableWatcher"] = True
|
||||
connect.notify([{"event": "imported", "dest": dest}])
|
||||
assert [c[:2] for c in calls] == [
|
||||
("POST", "http://hook/in"),
|
||||
("GET", "http://abs/api/libraries"),
|
||||
("POST", "http://abs/api/libraries/lib-audio/scan"),
|
||||
]
|
||||
|
||||
calls.clear()
|
||||
client.delete(f"/api/requests/{req_id}")
|
||||
assert calls == [("POST", "http://hook/in")] # nothing new on disk, no scan
|
||||
|
||||
|
||||
def test_bulk_move_delete_and_count(client):
|
||||
def lib(name, media_type):
|
||||
|
||||
+30
-5
@@ -1,9 +1,10 @@
|
||||
"""Tell other services about history rows once they are committed: a generic
|
||||
webhook gets every row, Audiobookshelf a scan of the libraries holding new
|
||||
files. Runs as a background task, so a slow or dead service never fails the
|
||||
webhook gets every row, Audiobookshelf the new files of the libraries holding
|
||||
them. Runs as a background task, so a slow or dead service never fails the
|
||||
request that caused it; errors only reach the log."""
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from fastapi import BackgroundTasks
|
||||
@@ -46,15 +47,39 @@ def notify(events: list[dict]) -> None:
|
||||
|
||||
|
||||
def _scan_audiobookshelf(client: httpx.Client, dests: list[str]) -> None:
|
||||
"""Scan only the libraries whose folders contain a destination. That needs
|
||||
both containers to mount the media under the same path."""
|
||||
"""Tell the libraries whose folders contain a destination about the new
|
||||
files. That needs both containers to mount the media under the same path."""
|
||||
headers = {"Authorization": f"Bearer {ABS_TOKEN}"}
|
||||
resp = client.get(f"{ABS_URL}/api/libraries", headers=headers)
|
||||
resp.raise_for_status()
|
||||
for lib in resp.json().get("libraries") or []:
|
||||
folders = [str(f.get("fullPath") or "").rstrip("/")
|
||||
for f in lib.get("folders") or []]
|
||||
if any(f and (d == f or d.startswith(f + "/")) for f in folders for d in dests):
|
||||
mine = [d for d in dests
|
||||
if any(f and (d == f or d.startswith(f + "/")) for f in folders)]
|
||||
if not mine:
|
||||
continue
|
||||
if (lib.get("settings") or {}).get("disableWatcher"):
|
||||
# the watcher endpoint drops paths of unwatched libraries
|
||||
client.post(f"{ABS_URL}/api/libraries/{lib.get('id')}/scan",
|
||||
headers=headers).raise_for_status()
|
||||
log.info("audiobookshelf scans library %s", lib.get("name"))
|
||||
continue
|
||||
# a full scan racing the watcher adds the same folder twice, and it
|
||||
# walks the whole library; this queues the files with the watcher
|
||||
for path in _files(mine):
|
||||
client.post(f"{ABS_URL}/api/watcher/update", headers=headers,
|
||||
json={"libraryId": lib.get("id"), "path": path,
|
||||
"type": "add"}).raise_for_status()
|
||||
log.info("audiobookshelf watches new files in library %s", lib.get("name"))
|
||||
|
||||
|
||||
def _files(dests: list[str]) -> list[str]:
|
||||
"""Imports are flat: a destination is a file or a folder of files."""
|
||||
out = []
|
||||
for d in map(Path, dests):
|
||||
if d.is_file():
|
||||
out.append(str(d))
|
||||
elif d.is_dir():
|
||||
out += sorted(str(f) for f in d.iterdir() if f.is_file())
|
||||
return out
|
||||
Reference in new issue
Block a user