Initial commit
This commit is contained in:
596
backend/app/services/snapshot.py
Normal file
596
backend/app/services/snapshot.py
Normal file
@@ -0,0 +1,596 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote
|
||||
|
||||
from ..clients.jellyseerr import JellyseerrClient
|
||||
from ..clients.jellyfin import JellyfinClient
|
||||
from ..clients.sonarr import SonarrClient
|
||||
from ..clients.radarr import RadarrClient
|
||||
from ..clients.prowlarr import ProwlarrClient
|
||||
from ..clients.qbittorrent import QBittorrentClient
|
||||
from ..runtime import get_runtime_settings
|
||||
from ..db import save_snapshot, get_request_cache_payload
|
||||
from ..models import ActionOption, NormalizedState, RequestType, Snapshot, TimelineHop
|
||||
|
||||
|
||||
STATUS_LABELS = {
|
||||
1: "Waiting for approval",
|
||||
2: "Approved",
|
||||
3: "Declined",
|
||||
4: "Ready to watch",
|
||||
5: "Working on it",
|
||||
6: "Partially ready",
|
||||
}
|
||||
|
||||
|
||||
def _status_label(value: Any) -> str:
|
||||
try:
|
||||
numeric = int(value)
|
||||
return STATUS_LABELS.get(numeric, f"Status {numeric}")
|
||||
except (TypeError, ValueError):
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def _pick_first(value: Any) -> Optional[Dict[str, Any]]:
|
||||
if isinstance(value, list):
|
||||
return value[0] if value else None
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _queue_records(queue: Any) -> List[Dict[str, Any]]:
|
||||
if isinstance(queue, dict):
|
||||
records = queue.get("records")
|
||||
if isinstance(records, list):
|
||||
return records
|
||||
if isinstance(queue, list):
|
||||
return queue
|
||||
return []
|
||||
|
||||
|
||||
def _filter_queue(queue: Any, item_id: Optional[int], request_type: RequestType) -> Any:
|
||||
if not item_id:
|
||||
return queue
|
||||
records = _queue_records(queue)
|
||||
if not records:
|
||||
return queue
|
||||
key = "seriesId" if request_type == RequestType.tv else "movieId"
|
||||
filtered = [record for record in records if record.get(key) == item_id]
|
||||
if isinstance(queue, dict):
|
||||
filtered_queue = dict(queue)
|
||||
filtered_queue["records"] = filtered
|
||||
filtered_queue["totalRecords"] = len(filtered)
|
||||
return filtered_queue
|
||||
return filtered
|
||||
|
||||
|
||||
def _download_ids(records: List[Dict[str, Any]]) -> List[str]:
|
||||
ids = []
|
||||
for record in records:
|
||||
download_id = record.get("downloadId") or record.get("download_id")
|
||||
if isinstance(download_id, str) and download_id:
|
||||
ids.append(download_id)
|
||||
return ids
|
||||
|
||||
|
||||
def _missing_episode_numbers_by_season(episodes: Any) -> Dict[int, List[int]]:
|
||||
if not isinstance(episodes, list):
|
||||
return {}
|
||||
grouped: Dict[int, List[int]] = {}
|
||||
now = datetime.now(timezone.utc)
|
||||
for episode in episodes:
|
||||
if not isinstance(episode, dict):
|
||||
continue
|
||||
if not episode.get("monitored", True):
|
||||
continue
|
||||
if episode.get("hasFile"):
|
||||
continue
|
||||
air_date = episode.get("airDateUtc")
|
||||
if isinstance(air_date, str):
|
||||
try:
|
||||
aired_at = datetime.fromisoformat(air_date.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
aired_at = None
|
||||
if aired_at and aired_at > now:
|
||||
continue
|
||||
season_number = episode.get("seasonNumber")
|
||||
episode_number = episode.get("episodeNumber")
|
||||
if not isinstance(episode_number, int):
|
||||
episode_number = episode.get("absoluteEpisodeNumber")
|
||||
if isinstance(season_number, int) and isinstance(episode_number, int):
|
||||
grouped.setdefault(season_number, []).append(episode_number)
|
||||
for season_number in list(grouped.keys()):
|
||||
grouped[season_number] = sorted(set(grouped[season_number]))
|
||||
return grouped
|
||||
|
||||
|
||||
def _summarize_qbit(torrents: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
if not torrents:
|
||||
return {"state": "idle", "message": "0 active downloads."}
|
||||
|
||||
downloading_states = {"downloading", "stalleddl", "queueddl", "checkingdl", "forceddl"}
|
||||
paused_states = {"pauseddl", "pausedup"}
|
||||
completed_states = {"uploading", "stalledup", "queuedup", "checkingup", "forcedup", "stoppedup"}
|
||||
|
||||
downloading = [t for t in torrents if str(t.get("state", "")).lower() in downloading_states]
|
||||
paused = [t for t in torrents if str(t.get("state", "")).lower() in paused_states]
|
||||
completed = [t for t in torrents if str(t.get("state", "")).lower() in completed_states]
|
||||
|
||||
if downloading:
|
||||
return {
|
||||
"state": "downloading",
|
||||
"message": f"Downloading ({len(downloading)} active).",
|
||||
}
|
||||
if paused:
|
||||
return {
|
||||
"state": "paused",
|
||||
"message": f"Paused ({len(paused)} paused).",
|
||||
}
|
||||
if completed:
|
||||
return {
|
||||
"state": "completed",
|
||||
"message": f"Completed/seeding ({len(completed)} seeding).",
|
||||
}
|
||||
|
||||
return {
|
||||
"state": "idle",
|
||||
"message": "0 active downloads.",
|
||||
}
|
||||
|
||||
|
||||
def _artwork_url(path: Optional[str], size: str, cache_mode: str) -> Optional[str]:
|
||||
if not path:
|
||||
return None
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
if cache_mode == "cache":
|
||||
return f"/images/tmdb?path={quote(path)}&size={size}"
|
||||
return f"https://image.tmdb.org/t/p/{size}{path}"
|
||||
|
||||
|
||||
async def build_snapshot(request_id: str) -> Snapshot:
|
||||
timeline = []
|
||||
runtime = get_runtime_settings()
|
||||
|
||||
jellyseerr = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
|
||||
jellyfin = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key)
|
||||
sonarr = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key)
|
||||
radarr = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
|
||||
prowlarr = ProwlarrClient(runtime.prowlarr_base_url, runtime.prowlarr_api_key)
|
||||
qbittorrent = QBittorrentClient(
|
||||
runtime.qbittorrent_base_url,
|
||||
runtime.qbittorrent_username,
|
||||
runtime.qbittorrent_password,
|
||||
)
|
||||
|
||||
snapshot = Snapshot(
|
||||
request_id=request_id,
|
||||
title="Unknown",
|
||||
state=NormalizedState.unknown,
|
||||
state_reason="Awaiting configuration",
|
||||
)
|
||||
|
||||
cached_request = None
|
||||
mode = (runtime.requests_data_source or "prefer_cache").lower()
|
||||
if mode != "always_js" and request_id.isdigit():
|
||||
cached_request = get_request_cache_payload(int(request_id))
|
||||
if cached_request is not None:
|
||||
logging.getLogger(__name__).debug(
|
||||
"snapshot cache hit: request_id=%s mode=%s", request_id, mode
|
||||
)
|
||||
else:
|
||||
logging.getLogger(__name__).debug(
|
||||
"snapshot cache miss: request_id=%s mode=%s", request_id, mode
|
||||
)
|
||||
|
||||
if not jellyseerr.configured() and not cached_request:
|
||||
timeline.append(TimelineHop(service="Jellyseerr", status="not_configured"))
|
||||
timeline.append(TimelineHop(service="Sonarr/Radarr", status="not_configured"))
|
||||
timeline.append(TimelineHop(service="Prowlarr", status="not_configured"))
|
||||
timeline.append(TimelineHop(service="qBittorrent", status="not_configured"))
|
||||
snapshot.timeline = timeline
|
||||
return snapshot
|
||||
|
||||
jelly_request = cached_request
|
||||
if (jelly_request is None or mode == "always_js") and jellyseerr.configured():
|
||||
try:
|
||||
jelly_request = await jellyseerr.get_request(request_id)
|
||||
logging.getLogger(__name__).debug(
|
||||
"snapshot jellyseerr fetch: request_id=%s mode=%s", request_id, mode
|
||||
)
|
||||
except Exception as exc:
|
||||
timeline.append(TimelineHop(service="Jellyseerr", status="error", details={"error": str(exc)}))
|
||||
snapshot.timeline = timeline
|
||||
snapshot.state = NormalizedState.failed
|
||||
snapshot.state_reason = "Failed to reach Jellyseerr"
|
||||
return snapshot
|
||||
|
||||
if not jelly_request:
|
||||
timeline.append(TimelineHop(service="Jellyseerr", status="not_found"))
|
||||
snapshot.timeline = timeline
|
||||
snapshot.state = NormalizedState.unknown
|
||||
snapshot.state_reason = "Request not found in Jellyseerr"
|
||||
return snapshot
|
||||
|
||||
jelly_status = jelly_request.get("status", "unknown")
|
||||
jelly_status_label = _status_label(jelly_status)
|
||||
jelly_type = jelly_request.get("type") or "unknown"
|
||||
snapshot.title = jelly_request.get("media", {}).get("title", "Unknown")
|
||||
snapshot.year = jelly_request.get("media", {}).get("year")
|
||||
snapshot.request_type = RequestType(jelly_type) if jelly_type in {"movie", "tv"} else RequestType.unknown
|
||||
media = jelly_request.get("media", {}) if isinstance(jelly_request, dict) else {}
|
||||
poster_path = None
|
||||
backdrop_path = None
|
||||
if isinstance(media, dict):
|
||||
poster_path = media.get("posterPath") or media.get("poster_path")
|
||||
backdrop_path = media.get("backdropPath") or media.get("backdrop_path")
|
||||
|
||||
if snapshot.title in {None, "", "Unknown"} and jellyseerr.configured():
|
||||
tmdb_id = jelly_request.get("media", {}).get("tmdbId")
|
||||
if tmdb_id:
|
||||
try:
|
||||
if snapshot.request_type == RequestType.movie:
|
||||
details = await jellyseerr.get_movie(int(tmdb_id))
|
||||
if isinstance(details, dict):
|
||||
snapshot.title = details.get("title") or snapshot.title
|
||||
release_date = details.get("releaseDate")
|
||||
snapshot.year = int(release_date[:4]) if release_date else snapshot.year
|
||||
poster_path = poster_path or details.get("posterPath") or details.get("poster_path")
|
||||
backdrop_path = (
|
||||
backdrop_path
|
||||
or details.get("backdropPath")
|
||||
or details.get("backdrop_path")
|
||||
)
|
||||
elif snapshot.request_type == RequestType.tv:
|
||||
details = await jellyseerr.get_tv(int(tmdb_id))
|
||||
if isinstance(details, dict):
|
||||
snapshot.title = details.get("name") or details.get("title") or snapshot.title
|
||||
first_air = details.get("firstAirDate")
|
||||
snapshot.year = int(first_air[:4]) if first_air else snapshot.year
|
||||
poster_path = poster_path or details.get("posterPath") or details.get("poster_path")
|
||||
backdrop_path = (
|
||||
backdrop_path
|
||||
or details.get("backdropPath")
|
||||
or details.get("backdrop_path")
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
cache_mode = (runtime.artwork_cache_mode or "remote").lower()
|
||||
snapshot.artwork = {
|
||||
"poster_path": poster_path,
|
||||
"backdrop_path": backdrop_path,
|
||||
"poster_url": _artwork_url(poster_path, "w342", cache_mode),
|
||||
"backdrop_url": _artwork_url(backdrop_path, "w780", cache_mode),
|
||||
}
|
||||
|
||||
timeline.append(
|
||||
TimelineHop(
|
||||
service="Jellyseerr",
|
||||
status=jelly_status_label,
|
||||
details={
|
||||
"requestedBy": jelly_request.get("requestedBy", {}).get("displayName")
|
||||
or jelly_request.get("requestedBy", {}).get("username")
|
||||
or jelly_request.get("requestedBy", {}).get("jellyfinUsername")
|
||||
or jelly_request.get("requestedBy", {}).get("email"),
|
||||
"createdAt": jelly_request.get("createdAt"),
|
||||
"updatedAt": jelly_request.get("updatedAt"),
|
||||
"approved": jelly_request.get("isApproved"),
|
||||
"statusCode": jelly_status,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
arr_state = None
|
||||
arr_details: Dict[str, Any] = {}
|
||||
arr_item = None
|
||||
arr_queue = None
|
||||
media_status = jelly_request.get("media", {}).get("status")
|
||||
try:
|
||||
media_status_code = int(media_status) if media_status is not None else None
|
||||
except (TypeError, ValueError):
|
||||
media_status_code = None
|
||||
if snapshot.request_type == RequestType.tv:
|
||||
tvdb_id = jelly_request.get("media", {}).get("tvdbId")
|
||||
if tvdb_id:
|
||||
try:
|
||||
series = await sonarr.get_series_by_tvdb_id(int(tvdb_id))
|
||||
arr_item = _pick_first(series)
|
||||
arr_details["series"] = arr_item
|
||||
arr_state = "added" if arr_item else "missing"
|
||||
if arr_item:
|
||||
stats = arr_item.get("statistics") if isinstance(arr_item, dict) else None
|
||||
if isinstance(stats, dict):
|
||||
file_count = stats.get("episodeFileCount")
|
||||
total_count = (
|
||||
stats.get("totalEpisodeCount")
|
||||
if isinstance(stats.get("totalEpisodeCount"), int)
|
||||
else stats.get("episodeCount")
|
||||
)
|
||||
if (
|
||||
isinstance(file_count, int)
|
||||
and isinstance(total_count, int)
|
||||
and total_count > 0
|
||||
and file_count >= total_count
|
||||
):
|
||||
arr_state = "available"
|
||||
if arr_item and isinstance(arr_item.get("id"), int):
|
||||
series_id = int(arr_item["id"])
|
||||
arr_queue = await sonarr.get_queue(series_id)
|
||||
arr_queue = _filter_queue(arr_queue, series_id, RequestType.tv)
|
||||
arr_details["queue"] = arr_queue
|
||||
episodes = await sonarr.get_episodes(series_id)
|
||||
missing_by_season = _missing_episode_numbers_by_season(episodes)
|
||||
if missing_by_season:
|
||||
arr_details["missingEpisodes"] = missing_by_season
|
||||
except Exception as exc:
|
||||
arr_state = "error"
|
||||
arr_details["error"] = str(exc)
|
||||
elif snapshot.request_type == RequestType.movie:
|
||||
tmdb_id = jelly_request.get("media", {}).get("tmdbId")
|
||||
if tmdb_id:
|
||||
try:
|
||||
movie = await radarr.get_movie_by_tmdb_id(int(tmdb_id))
|
||||
arr_item = _pick_first(movie)
|
||||
if not arr_item:
|
||||
title_hint = (
|
||||
jelly_request.get("media", {}).get("title")
|
||||
or jelly_request.get("title")
|
||||
or snapshot.title
|
||||
)
|
||||
year_hint = (
|
||||
jelly_request.get("media", {}).get("year")
|
||||
or jelly_request.get("year")
|
||||
or snapshot.year
|
||||
)
|
||||
try:
|
||||
all_movies = await radarr.get_movies()
|
||||
except Exception:
|
||||
all_movies = None
|
||||
if isinstance(all_movies, list):
|
||||
for candidate in all_movies:
|
||||
if not isinstance(candidate, dict):
|
||||
continue
|
||||
if tmdb_id and candidate.get("tmdbId") == int(tmdb_id):
|
||||
arr_item = candidate
|
||||
break
|
||||
if title_hint and candidate.get("title") == title_hint:
|
||||
if not year_hint or candidate.get("year") == year_hint:
|
||||
arr_item = candidate
|
||||
break
|
||||
arr_details["movie"] = arr_item
|
||||
if arr_item:
|
||||
if arr_item.get("hasFile"):
|
||||
arr_state = "available"
|
||||
elif arr_item.get("isAvailable"):
|
||||
arr_state = "searching"
|
||||
else:
|
||||
arr_state = "added"
|
||||
else:
|
||||
arr_state = "missing"
|
||||
if arr_item and isinstance(arr_item.get("id"), int):
|
||||
arr_queue = await radarr.get_queue(int(arr_item["id"]))
|
||||
arr_queue = _filter_queue(arr_queue, int(arr_item["id"]), RequestType.movie)
|
||||
arr_details["queue"] = arr_queue
|
||||
except Exception as exc:
|
||||
arr_state = "error"
|
||||
arr_details["error"] = str(exc)
|
||||
|
||||
if arr_state is None:
|
||||
arr_state = "unknown"
|
||||
if arr_state == "missing" and media_status_code in {4}:
|
||||
arr_state = "available"
|
||||
elif arr_state == "missing" and media_status_code in {6}:
|
||||
arr_state = "added"
|
||||
|
||||
timeline.append(TimelineHop(service="Sonarr/Radarr", status=arr_state, details=arr_details))
|
||||
|
||||
try:
|
||||
prowlarr_health = await prowlarr.get_health()
|
||||
if isinstance(prowlarr_health, list) and len(prowlarr_health) > 0:
|
||||
timeline.append(TimelineHop(service="Prowlarr", status="issues", details={"health": prowlarr_health}))
|
||||
else:
|
||||
timeline.append(TimelineHop(service="Prowlarr", status="ok"))
|
||||
except Exception as exc:
|
||||
timeline.append(TimelineHop(service="Prowlarr", status="error", details={"error": str(exc)}))
|
||||
|
||||
jellyfin_available = False
|
||||
jellyfin_item = None
|
||||
if jellyfin.configured() and snapshot.title:
|
||||
types = ["Movie"] if snapshot.request_type == RequestType.movie else ["Series"]
|
||||
try:
|
||||
search = await jellyfin.search_items(snapshot.title, types)
|
||||
except Exception:
|
||||
search = None
|
||||
if isinstance(search, dict):
|
||||
items = search.get("Items") or search.get("items") or []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = item.get("Name") or item.get("title")
|
||||
year = item.get("ProductionYear") or item.get("Year")
|
||||
if name and name.strip().lower() == (snapshot.title or "").strip().lower():
|
||||
if snapshot.year and year and int(year) != int(snapshot.year):
|
||||
continue
|
||||
jellyfin_available = True
|
||||
jellyfin_item = item
|
||||
break
|
||||
|
||||
if jellyfin_available and arr_state == "missing" and runtime.jellyfin_sync_to_arr:
|
||||
arr_details["note"] = "Found in Jellyfin but not tracked in Sonarr/Radarr."
|
||||
if snapshot.request_type == RequestType.movie:
|
||||
if runtime.radarr_quality_profile_id and runtime.radarr_root_folder:
|
||||
radarr_client = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
|
||||
if radarr_client.configured():
|
||||
root_folder = await _resolve_root_folder_path(
|
||||
radarr_client, runtime.radarr_root_folder, "Radarr"
|
||||
)
|
||||
tmdb_id = jelly_request.get("media", {}).get("tmdbId")
|
||||
if tmdb_id:
|
||||
try:
|
||||
await radarr_client.add_movie(
|
||||
int(tmdb_id),
|
||||
runtime.radarr_quality_profile_id,
|
||||
root_folder,
|
||||
monitored=False,
|
||||
search_for_movie=False,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if snapshot.request_type == RequestType.tv:
|
||||
if runtime.sonarr_quality_profile_id and runtime.sonarr_root_folder:
|
||||
sonarr_client = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key)
|
||||
if sonarr_client.configured():
|
||||
root_folder = await _resolve_root_folder_path(
|
||||
sonarr_client, runtime.sonarr_root_folder, "Sonarr"
|
||||
)
|
||||
tvdb_id = jelly_request.get("media", {}).get("tvdbId")
|
||||
if tvdb_id:
|
||||
try:
|
||||
await sonarr_client.add_series(
|
||||
int(tvdb_id),
|
||||
runtime.sonarr_quality_profile_id,
|
||||
root_folder,
|
||||
monitored=False,
|
||||
search_missing=False,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
qbit_state = None
|
||||
qbit_message = None
|
||||
try:
|
||||
download_ids = _download_ids(_queue_records(arr_queue))
|
||||
torrent_list: List[Dict[str, Any]] = []
|
||||
if download_ids and qbittorrent.configured():
|
||||
torrents = await qbittorrent.get_torrents_by_hashes("|".join(download_ids))
|
||||
torrent_list = torrents if isinstance(torrents, list) else []
|
||||
summary = _summarize_qbit(torrent_list)
|
||||
qbit_state = summary.get("state")
|
||||
qbit_message = summary.get("message")
|
||||
timeline.append(
|
||||
TimelineHop(
|
||||
service="qBittorrent",
|
||||
status=summary["state"],
|
||||
details={
|
||||
"summary": summary["message"],
|
||||
"torrents": torrent_list,
|
||||
},
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
timeline.append(TimelineHop(service="qBittorrent", status="error", details={"error": str(exc)}))
|
||||
|
||||
status_code = None
|
||||
try:
|
||||
status_code = int(jelly_status)
|
||||
except (TypeError, ValueError):
|
||||
status_code = None
|
||||
|
||||
derived_approved = bool(jelly_request.get("isApproved")) or status_code in {2, 4, 5, 6}
|
||||
|
||||
if derived_approved:
|
||||
snapshot.state = NormalizedState.approved
|
||||
snapshot.state_reason = "Approved and queued for processing."
|
||||
else:
|
||||
snapshot.state = NormalizedState.requested
|
||||
snapshot.state_reason = "Waiting for approval before we can search."
|
||||
|
||||
queue_records = _queue_records(arr_queue)
|
||||
if qbit_state in {"downloading", "paused"}:
|
||||
snapshot.state = NormalizedState.downloading
|
||||
snapshot.state_reason = "Downloading in qBittorrent."
|
||||
if qbit_message:
|
||||
snapshot.state_reason = qbit_message
|
||||
elif qbit_state == "completed":
|
||||
if arr_state == "available":
|
||||
snapshot.state = NormalizedState.completed
|
||||
snapshot.state_reason = "In your library and ready to watch."
|
||||
else:
|
||||
snapshot.state = NormalizedState.importing
|
||||
snapshot.state_reason = "Download finished. Waiting for library import."
|
||||
elif queue_records:
|
||||
if arr_state == "missing":
|
||||
snapshot.state_reason = "Queue shows a download, but qBittorrent has no active torrent."
|
||||
else:
|
||||
snapshot.state_reason = "Waiting for download to start in qBittorrent."
|
||||
elif arr_state == "missing" and derived_approved:
|
||||
snapshot.state = NormalizedState.needs_add
|
||||
snapshot.state_reason = "Approved, but not added to the library yet."
|
||||
elif arr_state == "searching":
|
||||
snapshot.state = NormalizedState.searching
|
||||
snapshot.state_reason = "Searching for a matching release."
|
||||
elif arr_state == "available":
|
||||
snapshot.state = NormalizedState.completed
|
||||
snapshot.state_reason = "In your library and ready to watch."
|
||||
elif arr_state == "added" and snapshot.state == NormalizedState.approved:
|
||||
snapshot.state = NormalizedState.added_to_arr
|
||||
snapshot.state_reason = "Item is present in Sonarr/Radarr"
|
||||
|
||||
if jellyfin_available and snapshot.state not in {
|
||||
NormalizedState.downloading,
|
||||
NormalizedState.importing,
|
||||
}:
|
||||
snapshot.state = NormalizedState.completed
|
||||
snapshot.state_reason = "Ready to watch in Jellyfin."
|
||||
|
||||
snapshot.timeline = timeline
|
||||
actions: List[ActionOption] = []
|
||||
if arr_state == "missing":
|
||||
actions.append(
|
||||
ActionOption(
|
||||
id="readd_to_arr",
|
||||
label="Add to the library queue (Sonarr/Radarr)",
|
||||
risk="medium",
|
||||
)
|
||||
)
|
||||
elif arr_item and arr_state != "available":
|
||||
actions.append(
|
||||
ActionOption(
|
||||
id="search",
|
||||
label="Search again for releases",
|
||||
risk="low",
|
||||
)
|
||||
)
|
||||
|
||||
download_ids = _download_ids(_queue_records(arr_queue))
|
||||
if download_ids and qbittorrent.configured():
|
||||
actions.append(
|
||||
ActionOption(
|
||||
id="resume_torrent",
|
||||
label="Resume the download",
|
||||
risk="low",
|
||||
)
|
||||
)
|
||||
|
||||
snapshot.actions = actions
|
||||
jellyfin_link = None
|
||||
if runtime.jellyfin_public_url and snapshot.state in {
|
||||
NormalizedState.available,
|
||||
NormalizedState.completed,
|
||||
}:
|
||||
base_url = runtime.jellyfin_public_url.rstrip("/")
|
||||
query = quote(snapshot.title or "")
|
||||
jellyfin_link = f"{base_url}/web/index.html#!/search?query={query}"
|
||||
snapshot.raw = {
|
||||
"jellyseerr": jelly_request,
|
||||
"arr": {
|
||||
"item": arr_item,
|
||||
"queue": arr_queue,
|
||||
},
|
||||
"jellyfin": {
|
||||
"publicUrl": runtime.jellyfin_public_url,
|
||||
"available": snapshot.state in {
|
||||
NormalizedState.available,
|
||||
NormalizedState.completed,
|
||||
},
|
||||
"link": jellyfin_link,
|
||||
"item": jellyfin_item,
|
||||
},
|
||||
}
|
||||
|
||||
await asyncio.to_thread(save_snapshot, snapshot)
|
||||
return snapshot
|
||||
Reference in New Issue
Block a user