Files

98 lines
3.5 KiB
Python

import sys
import urllib.parse
from dataclasses import dataclass, field
from typing import Callable
import library_api
SECTIONS = {
"media": {"label": "shows and movies", "path": "/api/library/media/"},
"books": {"label": "books", "path": "/api/library/books/"},
"games": {"label": "games", "path": "/api/library/games/"},
}
@dataclass
class PushPlan:
source: library_api.Site
target: library_api.Site
entries: dict[str, list[dict]]
removals: dict[str, list[str]]
def describe(self) -> str:
pushed = ", ".join(f"{len(self.entries[name])} {section['label']}" for name, section in SECTIONS.items())
removed = sum(len(identifiers) for identifiers in self.removals.values())
removal_text = f" and remove {removed} entries that only exist there" if removed else ""
return f"Push {pushed} from {self.source.url} to {self.target.url}{removal_text}?"
@dataclass
class PushResult:
pushed: int = 0
removed: int = 0
errors: list[str] = field(default_factory=list)
def entry_path(section: str, identifier: str) -> str:
return f"{SECTIONS[section]['path']}{urllib.parse.quote(identifier, safe='')}"
def plan_push() -> PushPlan:
source = library_api.local_site()
target = library_api.push_site()
if source.url == target.url:
raise library_api.LibraryError("LIBRARY_PUSH_URL is the same site as LIBRARY_API_URL")
local = library_api.fetch_export(source)
if not any(local[section] for section in SECTIONS):
raise library_api.LibraryError(f"The library at {source.url} is empty, so nothing was pushed")
remote = library_api.fetch_export(target)
removals = {}
for section in SECTIONS:
local_ids = {entry["id"] for entry in local[section]}
removals[section] = [entry["id"] for entry in remote[section] if entry["id"] not in local_ids]
return PushPlan(source, target, {section: local[section] for section in SECTIONS}, removals)
def run_push(plan: PushPlan, on_progress: Callable[[str], None] = lambda message: None) -> PushResult:
result = PushResult()
for section, identifiers in plan.removals.items():
for identifier in identifiers:
try:
library_api.send("DELETE", entry_path(section, identifier), site=plan.target)
result.removed += 1
except library_api.LibraryError as error:
result.errors.append(f"remove {identifier}: {error}")
for section, entries in plan.entries.items():
on_progress(f"Pushing {len(entries)} {SECTIONS[section]['label']}...")
for entry in entries:
try:
library_api.send("PUT", entry_path(section, entry["id"]), entry["fields"], site=plan.target)
result.pushed += 1
except library_api.LibraryError as error:
result.errors.append(f"{entry['id']}: {error}")
return result
def main() -> int:
try:
plan = plan_push()
except (OSError, ValueError) as error:
print(f"Push stopped: {error}", file=sys.stderr)
return 1
print(plan.describe())
if input("Type yes to continue: ").strip().lower() != "yes":
print("Nothing was pushed")
return 0
result = run_push(plan, print)
for message in result.errors:
print(f"Error: {message}", file=sys.stderr)
print(f"Pushed {result.pushed}, removed {result.removed}, {len(result.errors)} failed")
return 1 if result.errors else 0
if __name__ == "__main__":
sys.exit(main())