fix(connect): queue new files with the audiobookshelf watcher instead of a full scan
This commit is contained in:
1 parent
173e24a362
commit
377d2c8557
2 files changed
+52
-12
No files matched your search
@@ -1,4 +1,5 @@
|
|||||||
import importlib
|
import importlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -1228,12 +1229,13 @@ def test_connections_hear_about_committed_rows(client, monkeypatch):
|
|||||||
from wordarr import connect
|
from wordarr import connect
|
||||||
|
|
||||||
calls = []
|
calls = []
|
||||||
|
watcher = {"disableWatcher": False}
|
||||||
|
|
||||||
def handler(request):
|
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":
|
if request.url.path == "/api/libraries":
|
||||||
return httpx.Response(200, json={"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")}]},
|
"folders": [{"fullPath": str(client.tmp_path / "library")}]},
|
||||||
{"id": "lib-pod", "name": "Pod", "folders": [{"fullPath": "/podcasts"}]},
|
{"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,
|
"path": str(src), "is_dir": False, "files": [str(src)], "request_id": req_id,
|
||||||
}]}).json()["results"][0]["ok"]
|
}]}).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"),
|
("POST", "http://hook/in"),
|
||||||
("GET", "http://abs/api/libraries"),
|
("GET", "http://abs/api/libraries"),
|
||||||
("POST", "http://abs/api/libraries/lib-audio/scan"),
|
("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 test_bulk_move_delete_and_count(client):
|
||||||
def lib(name, media_type):
|
def lib(name, media_type):
|
||||||
|
|||||||
+30
-5
@@ -1,9 +1,10 @@
|
|||||||
"""Tell other services about history rows once they are committed: a generic
|
"""Tell other services about history rows once they are committed: a generic
|
||||||
webhook gets every row, Audiobookshelf a scan of the libraries holding new
|
webhook gets every row, Audiobookshelf the new files of the libraries holding
|
||||||
files. Runs as a background task, so a slow or dead service never fails the
|
them. Runs as a background task, so a slow or dead service never fails the
|
||||||
request that caused it; errors only reach the log."""
|
request that caused it; errors only reach the log."""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import BackgroundTasks
|
from fastapi import BackgroundTasks
|
||||||
@@ -46,15 +47,39 @@ def notify(events: list[dict]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _scan_audiobookshelf(client: httpx.Client, dests: list[str]) -> None:
|
def _scan_audiobookshelf(client: httpx.Client, dests: list[str]) -> None:
|
||||||
"""Scan only the libraries whose folders contain a destination. That needs
|
"""Tell the libraries whose folders contain a destination about the new
|
||||||
both containers to mount the media under the same path."""
|
files. That needs both containers to mount the media under the same path."""
|
||||||
headers = {"Authorization": f"Bearer {ABS_TOKEN}"}
|
headers = {"Authorization": f"Bearer {ABS_TOKEN}"}
|
||||||
resp = client.get(f"{ABS_URL}/api/libraries", headers=headers)
|
resp = client.get(f"{ABS_URL}/api/libraries", headers=headers)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
for lib in resp.json().get("libraries") or []:
|
for lib in resp.json().get("libraries") or []:
|
||||||
folders = [str(f.get("fullPath") or "").rstrip("/")
|
folders = [str(f.get("fullPath") or "").rstrip("/")
|
||||||
for f in lib.get("folders") or []]
|
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",
|
client.post(f"{ABS_URL}/api/libraries/{lib.get('id')}/scan",
|
||||||
headers=headers).raise_for_status()
|
headers=headers).raise_for_status()
|
||||||
log.info("audiobookshelf scans library %s", lib.get("name"))
|
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