"""REST API Endpoints fuer die Video-Bibliothek""" import asyncio import logging from aiohttp import web from app.config import Config from app.services.library import LibraryService from app.services.tvdb import TVDBService from app.services.queue import QueueService from app.services.cleaner import CleanerService from app.services.importer import ImporterService def setup_library_routes(app: web.Application, config: Config, library_service: LibraryService, tvdb_service: TVDBService, queue_service: QueueService, cleaner_service: CleanerService = None, importer_service: ImporterService = None ) -> None: """Registriert Bibliotheks-API-Routes""" # === Scan-Pfade === async def get_paths(request: web.Request) -> web.Response: """GET /api/library/paths""" paths = await library_service.get_paths() return web.json_response({"paths": paths}) async def post_path(request: web.Request) -> web.Response: """POST /api/library/paths""" try: data = await request.json() except Exception: return web.json_response( {"error": "Ungueltiges JSON"}, status=400 ) name = data.get("name", "").strip() path = data.get("path", "").strip() media_type = data.get("media_type", "").strip() if not name or not path: return web.json_response( {"error": "Name und Pfad erforderlich"}, status=400 ) if media_type not in ("series", "movie"): return web.json_response( {"error": "media_type muss 'series' oder 'movie' sein"}, status=400, ) path_id = await library_service.add_path(name, path, media_type) if path_id: return web.json_response( {"message": "Pfad hinzugefuegt", "id": path_id} ) return web.json_response( {"error": "Pfad konnte nicht hinzugefuegt werden"}, status=500 ) async def put_path(request: web.Request) -> web.Response: """PUT /api/library/paths/{path_id}""" path_id = int(request.match_info["path_id"]) try: data = await request.json() except Exception: return web.json_response( {"error": "Ungueltiges JSON"}, status=400 ) success = await library_service.update_path( path_id, name=data.get("name"), path=data.get("path"), media_type=data.get("media_type"), enabled=data.get("enabled"), ) if success: return web.json_response({"message": "Pfad aktualisiert"}) return web.json_response( {"error": "Pfad nicht gefunden"}, status=404 ) async def delete_path(request: web.Request) -> web.Response: """DELETE /api/library/paths/{path_id}""" path_id = int(request.match_info["path_id"]) success = await library_service.remove_path(path_id) if success: return web.json_response({"message": "Pfad entfernt"}) return web.json_response( {"error": "Pfad nicht gefunden"}, status=404 ) # === Scanning === async def post_scan_all(request: web.Request) -> web.Response: """POST /api/library/scan - Alle Pfade scannen""" asyncio.create_task(_run_scan_all()) return web.json_response({"message": "Scan gestartet"}) async def _run_scan_all(): result = await library_service.scan_all() logging.info(f"Komplett-Scan Ergebnis: {result}") async def post_scan_single(request: web.Request) -> web.Response: """POST /api/library/scan/{path_id}""" path_id = int(request.match_info["path_id"]) asyncio.create_task(_run_scan_single(path_id)) return web.json_response({"message": "Scan gestartet"}) async def _run_scan_single(path_id: int): result = await library_service.scan_single_path(path_id) logging.info(f"Einzel-Scan Ergebnis: {result}") # === Videos abfragen === async def get_videos(request: web.Request) -> web.Response: """GET /api/library/videos?filter-params...""" filters = {} for key in ("library_path_id", "media_type", "series_id", "video_codec", "min_width", "max_width", "container", "audio_lang", "audio_channels", "has_subtitle", "is_10bit", "sort", "order", "search"): val = request.query.get(key) if val: filters[key] = val page = int(request.query.get("page", 1)) limit = int(request.query.get("limit", 50)) result = await library_service.get_videos(filters, page, limit) return web.json_response(result) async def get_movies(request: web.Request) -> web.Response: """GET /api/library/movies - Nur Filme (keine Serien)""" filters = {} for key in ("video_codec", "min_width", "max_width", "container", "audio_lang", "audio_channels", "is_10bit", "sort", "order", "search"): val = request.query.get(key) if val: filters[key] = val page = int(request.query.get("page", 1)) limit = int(request.query.get("limit", 50)) result = await library_service.get_movies(filters, page, limit) return web.json_response(result) # === Serien === async def get_series(request: web.Request) -> web.Response: """GET /api/library/series""" path_id = request.query.get("path_id") if path_id: path_id = int(path_id) series = await library_service.get_series_list(path_id) return web.json_response({"series": series}) async def get_series_detail(request: web.Request) -> web.Response: """GET /api/library/series/{series_id}""" series_id = int(request.match_info["series_id"]) detail = await library_service.get_series_detail(series_id) if detail: return web.json_response(detail) return web.json_response( {"error": "Serie nicht gefunden"}, status=404 ) async def delete_series(request: web.Request) -> web.Response: """DELETE /api/library/series/{series_id}?delete_files=1""" series_id = int(request.match_info["series_id"]) delete_files = request.query.get("delete_files") == "1" result = await library_service.delete_series( series_id, delete_files=delete_files ) if result.get("error"): return web.json_response(result, status=404) return web.json_response(result) async def get_missing_episodes(request: web.Request) -> web.Response: """GET /api/library/series/{series_id}/missing""" series_id = int(request.match_info["series_id"]) missing = await library_service.get_missing_episodes(series_id) return web.json_response({"missing": missing}) # === TVDB === async def post_tvdb_match(request: web.Request) -> web.Response: """POST /api/library/series/{series_id}/tvdb-match""" series_id = int(request.match_info["series_id"]) try: data = await request.json() except Exception: return web.json_response( {"error": "Ungueltiges JSON"}, status=400 ) tvdb_id = data.get("tvdb_id") if not tvdb_id: return web.json_response( {"error": "tvdb_id erforderlich"}, status=400 ) result = await tvdb_service.match_and_update_series( series_id, int(tvdb_id), library_service ) if result.get("error"): return web.json_response(result, status=400) return web.json_response(result) async def delete_tvdb_link(request: web.Request) -> web.Response: """DELETE /api/library/series/{series_id}/tvdb""" series_id = int(request.match_info["series_id"]) success = await library_service.unlink_tvdb(series_id) if success: return web.json_response({"message": "TVDB-Zuordnung geloest"}) return web.json_response( {"error": "Serie nicht gefunden"}, status=404 ) async def post_tvdb_refresh(request: web.Request) -> web.Response: """POST /api/library/series/{series_id}/tvdb-refresh""" series_id = int(request.match_info["series_id"]) # TVDB-ID aus DB holen detail = await library_service.get_series_detail(series_id) if not detail or not detail.get("tvdb_id"): return web.json_response( {"error": "Keine TVDB-Zuordnung vorhanden"}, status=400 ) result = await tvdb_service.match_and_update_series( series_id, detail["tvdb_id"], library_service ) if result.get("error"): return web.json_response(result, status=400) return web.json_response(result) async def get_tvdb_search(request: web.Request) -> web.Response: """GET /api/tvdb/search?q=Breaking+Bad""" query = request.query.get("q", "").strip() if not query: return web.json_response( {"error": "Suchbegriff erforderlich"}, status=400 ) if not tvdb_service.is_configured: return web.json_response( {"error": "TVDB nicht konfiguriert (API Key fehlt)"}, status=400, ) results = await tvdb_service.search_series(query) return web.json_response({"results": results}) # === TVDB Metadaten === async def get_series_cast(request: web.Request) -> web.Response: """GET /api/library/series/{series_id}/cast""" series_id = int(request.match_info["series_id"]) detail = await library_service.get_series_detail(series_id) if not detail or not detail.get("tvdb_id"): return web.json_response({"cast": []}) cast = await tvdb_service.get_series_characters(detail["tvdb_id"]) return web.json_response({"cast": cast}) async def get_series_artworks(request: web.Request) -> web.Response: """GET /api/library/series/{series_id}/artworks""" series_id = int(request.match_info["series_id"]) detail = await library_service.get_series_detail(series_id) if not detail or not detail.get("tvdb_id"): return web.json_response({"artworks": []}) artworks = await tvdb_service.get_series_artworks(detail["tvdb_id"]) return web.json_response({"artworks": artworks}) async def post_metadata_download(request: web.Request) -> web.Response: """POST /api/library/series/{series_id}/metadata-download""" series_id = int(request.match_info["series_id"]) detail = await library_service.get_series_detail(series_id) if not detail: return web.json_response( {"error": "Serie nicht gefunden"}, status=404 ) if not detail.get("tvdb_id"): return web.json_response( {"error": "Keine TVDB-Zuordnung"}, status=400 ) result = await tvdb_service.download_metadata( series_id, detail["tvdb_id"], detail.get("folder_path", "") ) return web.json_response(result) async def post_metadata_download_all(request: web.Request) -> web.Response: """POST /api/library/metadata-download-all""" series_list = await library_service.get_series_list() results = {"success": 0, "skipped": 0, "errors": 0} for s in series_list: if not s.get("tvdb_id"): results["skipped"] += 1 continue try: await tvdb_service.download_metadata( s["id"], s["tvdb_id"], s.get("folder_path", "") ) results["success"] += 1 except Exception: results["errors"] += 1 return web.json_response(results) async def get_metadata_image(request: web.Request) -> web.Response: """GET /api/library/metadata/{series_id}/{filename}""" series_id = int(request.match_info["series_id"]) filename = request.match_info["filename"] detail = await library_service.get_series_detail(series_id) if not detail or not detail.get("folder_path"): return web.json_response( {"error": "Nicht gefunden"}, status=404 ) import os file_path = os.path.join( detail["folder_path"], ".metadata", filename ) if not os.path.isfile(file_path): return web.json_response( {"error": "Datei nicht gefunden"}, status=404 ) return web.FileResponse(file_path) # === Filme === async def get_movies_list(request: web.Request) -> web.Response: """GET /api/library/movies-list?path_id=X""" path_id = request.query.get("path_id") if path_id: path_id = int(path_id) movies = await library_service.get_movie_list(path_id) return web.json_response({"movies": movies}) async def get_movie_detail(request: web.Request) -> web.Response: """GET /api/library/movies/{movie_id}""" movie_id = int(request.match_info["movie_id"]) detail = await library_service.get_movie_detail(movie_id) if detail: return web.json_response(detail) return web.json_response( {"error": "Film nicht gefunden"}, status=404 ) async def delete_movie(request: web.Request) -> web.Response: """DELETE /api/library/movies/{movie_id}?delete_files=1""" movie_id = int(request.match_info["movie_id"]) delete_files = request.query.get("delete_files") == "1" result = await library_service.delete_movie( movie_id, delete_files=delete_files ) if result.get("error"): return web.json_response(result, status=404) return web.json_response(result) async def post_movie_tvdb_match(request: web.Request) -> web.Response: """POST /api/library/movies/{movie_id}/tvdb-match""" movie_id = int(request.match_info["movie_id"]) try: data = await request.json() except Exception: return web.json_response( {"error": "Ungueltiges JSON"}, status=400 ) tvdb_id = data.get("tvdb_id") if not tvdb_id: return web.json_response( {"error": "tvdb_id erforderlich"}, status=400 ) result = await tvdb_service.match_and_update_movie( movie_id, int(tvdb_id), library_service ) if result.get("error"): return web.json_response(result, status=400) return web.json_response(result) async def delete_movie_tvdb_link(request: web.Request) -> web.Response: """DELETE /api/library/movies/{movie_id}/tvdb""" movie_id = int(request.match_info["movie_id"]) success = await library_service.unlink_movie_tvdb(movie_id) if success: return web.json_response({"message": "TVDB-Zuordnung geloest"}) return web.json_response( {"error": "Film nicht gefunden"}, status=404 ) async def get_tvdb_movie_search(request: web.Request) -> web.Response: """GET /api/tvdb/search-movies?q=Inception""" query = request.query.get("q", "").strip() if not query: return web.json_response( {"error": "Suchbegriff erforderlich"}, status=400 ) if not tvdb_service.is_configured: return web.json_response( {"error": "TVDB nicht konfiguriert"}, status=400 ) results = await tvdb_service.search_movies(query) return web.json_response({"results": results}) # === TVDB Auto-Match (Review-Modus) === _auto_match_state = { "active": False, "phase": "", "done": 0, "total": 0, "current": "", "suggestions": None, } async def post_tvdb_auto_match(request: web.Request) -> web.Response: """POST /api/library/tvdb-auto-match?type=series|movies|all Sammelt TVDB-Vorschlaege (matched NICHT automatisch).""" if _auto_match_state["active"]: return web.json_response( {"error": "Suche laeuft bereits"}, status=409 ) if not tvdb_service.is_configured: return web.json_response( {"error": "TVDB nicht konfiguriert"}, status=400 ) match_type = request.query.get("type", "all") _auto_match_state.update({ "active": True, "phase": "starting", "done": 0, "total": 0, "current": "", "suggestions": None, }) async def run_collect(): try: async def progress_cb(done, total, name, count): _auto_match_state.update({ "done": done, "total": total, "current": name, }) all_suggestions = [] if match_type in ("series", "all"): _auto_match_state["phase"] = "series" _auto_match_state["done"] = 0 s = await tvdb_service.collect_suggestions( "series", progress_cb ) all_suggestions.extend(s) if match_type in ("movies", "all"): _auto_match_state["phase"] = "movies" _auto_match_state["done"] = 0 s = await tvdb_service.collect_suggestions( "movies", progress_cb ) all_suggestions.extend(s) _auto_match_state["suggestions"] = all_suggestions _auto_match_state["phase"] = "done" except Exception as e: logging.error(f"TVDB Vorschlaege sammeln fehlgeschlagen: {e}") _auto_match_state["phase"] = "error" _auto_match_state["suggestions"] = [] finally: _auto_match_state["active"] = False asyncio.create_task(run_collect()) return web.json_response({"message": "TVDB-Suche gestartet"}) async def get_tvdb_auto_match_status( request: web.Request ) -> web.Response: """GET /api/library/tvdb-auto-match-status""" # Vorschlaege nur bei "done" mitschicken result = { "active": _auto_match_state["active"], "phase": _auto_match_state["phase"], "done": _auto_match_state["done"], "total": _auto_match_state["total"], "current": _auto_match_state["current"], } if _auto_match_state["phase"] == "done": result["suggestions"] = _auto_match_state["suggestions"] return web.json_response(result) async def post_tvdb_confirm(request: web.Request) -> web.Response: """POST /api/library/tvdb-confirm - Einzelnen Vorschlag bestaetigen. Body: {id, type: 'series'|'movies', tvdb_id}""" try: data = await request.json() except Exception: return web.json_response( {"error": "Ungueltiges JSON"}, status=400 ) item_id = data.get("id") media_type = data.get("type") tvdb_id = data.get("tvdb_id") if not item_id or not media_type or not tvdb_id: return web.json_response( {"error": "id, type und tvdb_id erforderlich"}, status=400 ) if media_type == "series": result = await tvdb_service.match_and_update_series( int(item_id), int(tvdb_id), library_service ) elif media_type == "movies": result = await tvdb_service.match_and_update_movie( int(item_id), int(tvdb_id), library_service ) else: return web.json_response( {"error": "type muss 'series' oder 'movies' sein"}, status=400, ) if result.get("error"): return web.json_response(result, status=400) return web.json_response(result) # === TVDB Sprache === async def get_tvdb_language(request: web.Request) -> web.Response: """GET /api/tvdb/language""" lang = config.settings.get("library", {}).get( "tvdb_language", "deu" ) return web.json_response({"language": lang}) async def put_tvdb_language(request: web.Request) -> web.Response: """PUT /api/tvdb/language - TVDB-Sprache aendern""" try: data = await request.json() except Exception: return web.json_response( {"error": "Ungueltiges JSON"}, status=400 ) lang = data.get("language", "").strip() if not lang or len(lang) != 3: return web.json_response( {"error": "Sprache muss 3-Buchstaben-Code sein (z.B. deu)"}, status=400, ) # In Config speichern if "library" not in config.settings: config.settings["library"] = {} config.settings["library"]["tvdb_language"] = lang config.save_settings() return web.json_response( {"message": f"TVDB-Sprache auf '{lang}' gesetzt"} ) async def post_tvdb_refresh_all_episodes( request: web.Request, ) -> web.Response: """POST /api/library/tvdb-refresh-episodes Laedt alle Episoden-Caches neu (z.B. nach Sprachswitch).""" if not tvdb_service.is_configured: return web.json_response( {"error": "TVDB nicht konfiguriert"}, status=400 ) series_list = await library_service.get_series_list() refreshed = 0 for s in series_list: if not s.get("tvdb_id"): continue try: await tvdb_service.fetch_episodes(s["tvdb_id"]) await tvdb_service._update_episode_titles( s["id"], s["tvdb_id"] ) refreshed += 1 except Exception: pass return web.json_response({ "message": f"{refreshed} Serien-Episoden aktualisiert" }) # === Ordner-Ansicht === async def get_browse(request: web.Request) -> web.Response: """GET /api/library/browse?path=...""" path = request.query.get("path") result = await library_service.browse_path(path or None) return web.json_response(result) # === Duplikate === async def get_duplicates(request: web.Request) -> web.Response: """GET /api/library/duplicates""" dupes = await library_service.find_duplicates() return web.json_response({"duplicates": dupes}) # === Konvertierung aus Bibliothek === async def post_convert_video(request: web.Request) -> web.Response: """POST /api/library/videos/{video_id}/convert""" video_id = int(request.match_info["video_id"]) pool = await library_service._get_pool() if not pool: return web.json_response( {"error": "Keine DB-Verbindung"}, status=500 ) try: async with pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute( "SELECT file_path FROM library_videos WHERE id = %s", (video_id,) ) row = await cur.fetchone() if not row: return web.json_response( {"error": "Video nicht gefunden"}, status=404 ) except Exception as e: return web.json_response({"error": str(e)}, status=500) file_path = row[0] preset = None try: data = await request.json() preset = data.get("preset") except Exception: pass jobs = await queue_service.add_paths([file_path], preset) if jobs: return web.json_response({ "message": "Konvertierung gestartet", "job_id": jobs[0].id, }) return web.json_response( {"error": "Job konnte nicht erstellt werden"}, status=500 ) # === Statistiken === async def get_library_stats(request: web.Request) -> web.Response: """GET /api/library/stats""" stats = await library_service.get_stats() return web.json_response(stats) # === Scan-Status === async def get_scan_status(request: web.Request) -> web.Response: """GET /api/library/scan-status""" return web.json_response(library_service._scan_progress) # === Clean-Funktion === async def get_clean_scan(request: web.Request) -> web.Response: """GET /api/library/clean/scan?path_id=""" if not cleaner_service: return web.json_response( {"error": "Clean-Service nicht verfuegbar"}, status=500 ) path_id = request.query.get("path_id") result = await cleaner_service.scan_for_junk( int(path_id) if path_id else None ) return web.json_response(result) async def post_clean_delete(request: web.Request) -> web.Response: """POST /api/library/clean/delete""" if not cleaner_service: return web.json_response( {"error": "Clean-Service nicht verfuegbar"}, status=500 ) try: data = await request.json() except Exception: return web.json_response( {"error": "Ungueltiges JSON"}, status=400 ) files = data.get("files", []) if not files: return web.json_response( {"error": "Keine Dateien angegeben"}, status=400 ) result = await cleaner_service.delete_files(files) return web.json_response(result) async def post_clean_empty_dirs(request: web.Request) -> web.Response: """POST /api/library/clean/empty-dirs""" if not cleaner_service: return web.json_response( {"error": "Clean-Service nicht verfuegbar"}, status=500 ) try: data = await request.json() except Exception: data = {} path_id = data.get("path_id") count = await cleaner_service.delete_empty_dirs( int(path_id) if path_id else None ) return web.json_response({"deleted_dirs": count}) # === Filesystem-Browser (fuer Import) === async def get_browse_fs(request: web.Request) -> web.Response: """GET /api/library/browse-fs?path=... - Echten Filesystem-Browser""" import os from app.services.library import VIDEO_EXTENSIONS path = request.query.get("path", "/mnt") # Sicherheits-Check: Nur unter /mnt erlauben real = os.path.realpath(path) if not real.startswith("/mnt"): return web.json_response( {"error": "Zugriff nur auf /mnt erlaubt"}, status=403 ) if not os.path.isdir(real): return web.json_response( {"error": "Ordner nicht gefunden"}, status=404 ) folders = [] video_count = 0 video_size = 0 try: entries = sorted(os.scandir(real), key=lambda e: e.name.lower()) for entry in entries: if entry.name.startswith("."): continue if entry.is_dir(follow_symlinks=True): # Schnelle Zaehlung: Videos im Unterordner sub_vids = 0 try: for sub in os.scandir(entry.path): if sub.is_file(): ext = os.path.splitext(sub.name)[1].lower() if ext in VIDEO_EXTENSIONS: sub_vids += 1 except PermissionError: pass folders.append({ "name": entry.name, "path": entry.path, "video_count": sub_vids, }) elif entry.is_file(): ext = os.path.splitext(entry.name)[1].lower() if ext in VIDEO_EXTENSIONS: video_count += 1 try: video_size += entry.stat().st_size except OSError: pass except PermissionError: return web.json_response( {"error": "Keine Berechtigung"}, status=403 ) # Breadcrumb parts = real.split("/") breadcrumb = [] for i in range(1, len(parts)): crumb_path = "/".join(parts[:i + 1]) or "/" breadcrumb.append({ "name": parts[i], "path": crumb_path, }) return web.json_response({ "current_path": real, "folders": folders, "video_count": video_count, "video_size": video_size, "breadcrumb": breadcrumb, }) # === Import-Funktion === async def post_create_import(request: web.Request) -> web.Response: """POST /api/library/import""" if not importer_service: return web.json_response( {"error": "Import-Service nicht verfuegbar"}, status=500 ) try: data = await request.json() except Exception: return web.json_response( {"error": "Ungueltiges JSON"}, status=400 ) source = data.get("source_path", "").strip() target_id = data.get("target_library_id") mode = data.get("mode", "copy") if not source or not target_id: return web.json_response( {"error": "source_path und target_library_id erforderlich"}, status=400, ) job_id = await importer_service.create_job( source, int(target_id), mode ) if job_id: return web.json_response( {"message": "Import-Job erstellt", "job_id": job_id} ) return web.json_response( {"error": "Keine Videos gefunden oder Fehler"}, status=400 ) async def post_analyze_import(request: web.Request) -> web.Response: """POST /api/library/import/{job_id}/analyze""" if not importer_service: return web.json_response( {"error": "Import-Service nicht verfuegbar"}, status=500 ) job_id = int(request.match_info["job_id"]) result = await importer_service.analyze_job(job_id) return web.json_response(result) async def get_import_status(request: web.Request) -> web.Response: """GET /api/library/import/{job_id}""" if not importer_service: return web.json_response( {"error": "Import-Service nicht verfuegbar"}, status=500 ) job_id = int(request.match_info["job_id"]) result = await importer_service.get_job_status(job_id) return web.json_response(result) async def post_execute_import(request: web.Request) -> web.Response: """POST /api/library/import/{job_id}/execute""" if not importer_service: return web.json_response( {"error": "Import-Service nicht verfuegbar"}, status=500 ) job_id = int(request.match_info["job_id"]) result = await importer_service.execute_import(job_id) return web.json_response(result) async def put_import_item(request: web.Request) -> web.Response: """PUT /api/library/import/items/{item_id}""" if not importer_service: return web.json_response( {"error": "Import-Service nicht verfuegbar"}, status=500 ) item_id = int(request.match_info["item_id"]) try: data = await request.json() except Exception: return web.json_response( {"error": "Ungueltiges JSON"}, status=400 ) success = await importer_service.update_item(item_id, **data) if success: return web.json_response({"message": "Item aktualisiert"}) return web.json_response( {"error": "Aktualisierung fehlgeschlagen"}, status=400 ) async def put_resolve_conflict(request: web.Request) -> web.Response: """PUT /api/library/import/items/{item_id}/resolve""" if not importer_service: return web.json_response( {"error": "Import-Service nicht verfuegbar"}, status=500 ) item_id = int(request.match_info["item_id"]) try: data = await request.json() except Exception: return web.json_response( {"error": "Ungueltiges JSON"}, status=400 ) action = data.get("action", "") success = await importer_service.resolve_conflict(item_id, action) if success: return web.json_response({"message": "Konflikt geloest"}) return web.json_response( {"error": "Ungueltige Aktion"}, status=400 ) # === Routes registrieren === # Pfade app.router.add_get("/api/library/paths", get_paths) app.router.add_post("/api/library/paths", post_path) app.router.add_put("/api/library/paths/{path_id}", put_path) app.router.add_delete("/api/library/paths/{path_id}", delete_path) # Scanning app.router.add_post("/api/library/scan", post_scan_all) app.router.add_post("/api/library/scan/{path_id}", post_scan_single) app.router.add_get("/api/library/scan-status", get_scan_status) # Videos / Filme app.router.add_get("/api/library/videos", get_videos) app.router.add_get("/api/library/movies", get_movies) # Serien app.router.add_get("/api/library/series", get_series) app.router.add_get("/api/library/series/{series_id}", get_series_detail) app.router.add_delete( "/api/library/series/{series_id}", delete_series ) app.router.add_get( "/api/library/series/{series_id}/missing", get_missing_episodes ) # TVDB app.router.add_post( "/api/library/series/{series_id}/tvdb-match", post_tvdb_match ) app.router.add_delete( "/api/library/series/{series_id}/tvdb", delete_tvdb_link ) app.router.add_post( "/api/library/series/{series_id}/tvdb-refresh", post_tvdb_refresh ) app.router.add_get("/api/tvdb/search", get_tvdb_search) # TVDB Metadaten app.router.add_get( "/api/library/series/{series_id}/cast", get_series_cast ) app.router.add_get( "/api/library/series/{series_id}/artworks", get_series_artworks ) app.router.add_post( "/api/library/series/{series_id}/metadata-download", post_metadata_download, ) app.router.add_post( "/api/library/metadata-download-all", post_metadata_download_all ) app.router.add_get( "/api/library/metadata/{series_id}/{filename}", get_metadata_image ) # Filme app.router.add_get("/api/library/movies-list", get_movies_list) app.router.add_get("/api/library/movies/{movie_id}", get_movie_detail) app.router.add_delete("/api/library/movies/{movie_id}", delete_movie) app.router.add_post( "/api/library/movies/{movie_id}/tvdb-match", post_movie_tvdb_match ) app.router.add_delete( "/api/library/movies/{movie_id}/tvdb", delete_movie_tvdb_link ) app.router.add_get("/api/tvdb/search-movies", get_tvdb_movie_search) # Browse / Duplikate app.router.add_get("/api/library/browse", get_browse) app.router.add_get("/api/library/duplicates", get_duplicates) # Konvertierung app.router.add_post( "/api/library/videos/{video_id}/convert", post_convert_video ) # Statistiken app.router.add_get("/api/library/stats", get_library_stats) # Clean app.router.add_get("/api/library/clean/scan", get_clean_scan) app.router.add_post("/api/library/clean/delete", post_clean_delete) app.router.add_post( "/api/library/clean/empty-dirs", post_clean_empty_dirs ) # Filesystem-Browser app.router.add_get("/api/library/browse-fs", get_browse_fs) # Import app.router.add_post("/api/library/import", post_create_import) app.router.add_post( "/api/library/import/{job_id}/analyze", post_analyze_import ) app.router.add_get( "/api/library/import/{job_id}", get_import_status ) app.router.add_post( "/api/library/import/{job_id}/execute", post_execute_import ) app.router.add_put( "/api/library/import/items/{item_id}", put_import_item ) app.router.add_put( "/api/library/import/items/{item_id}/resolve", put_resolve_conflict ) # TVDB Auto-Match (Review-Modus) app.router.add_post( "/api/library/tvdb-auto-match", post_tvdb_auto_match ) app.router.add_get( "/api/library/tvdb-auto-match-status", get_tvdb_auto_match_status ) app.router.add_post( "/api/library/tvdb-confirm", post_tvdb_confirm ) # TVDB Sprache app.router.add_get("/api/tvdb/language", get_tvdb_language) app.router.add_put("/api/tvdb/language", put_tvdb_language) app.router.add_post( "/api/library/tvdb-refresh-episodes", post_tvdb_refresh_all_episodes, )