Automate repair completion confirmation
Magent CI/CD / verify (push) Canceled after 4m31s
Magent CI/CD / deploy-prod (push) Canceled after 0s
Magent CI/CD / deploy-beta (push) Canceled after 0s

This commit is contained in:
2026-09-01 22:37:23 +12:00
parent ded794a819
commit 0b59289a2e
4 changed files with 413 additions and 6 deletions
+99 -3
View File
@@ -1803,6 +1803,16 @@ def _replacement_quality_name(file_data: Dict[str, Any]) -> Optional[str]:
return str(value).strip() if value is not None and str(value).strip() else None
def _jellyfin_media_signature(item: Any) -> Dict[str, Any]:
if not isinstance(item, dict):
return {}
return {
key: item.get(key)
for key in ("Id", "Etag", "Path", "DateCreated", "MediaSources")
if item.get(key) is not None
}
def _replacement_file_payload(
file_data: Dict[str, Any],
*,
@@ -1850,6 +1860,7 @@ def _record_replacement_activity(
user: Dict[str, str],
event_type: str,
message: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
if not issue:
return
@@ -1871,6 +1882,11 @@ def _record_replacement_activity(
actor_username=str(user.get("username") or "unknown"),
actor_role=str(user.get("role") or "user"),
message=message,
metadata_json=(
json.dumps(metadata, separators=(",", ":"), sort_keys=True)
if metadata
else None
),
)
@@ -2164,6 +2180,9 @@ async def action_replace_media(
collector = "Sonarr" if snapshot.request_type == RequestType.tv else "Radarr"
target_names: List[str] = []
collector_id: Optional[int] = None
target_episodes: List[Dict[str, int]] = []
jellyfin_baseline: List[Dict[str, Any]] = []
try:
if snapshot.request_type == RequestType.movie:
movie_id = arr_item.get("id")
@@ -2172,6 +2191,11 @@ async def action_replace_media(
raise HTTPException(status_code=409, detail="Radarr does not report a replaceable movie file")
if len(file_ids) != 1 or movie_file.get("id") != file_ids[0]:
raise HTTPException(status_code=409, detail="The selected movie file is no longer current")
collector_id = movie_id
jellyfin_baseline = [
_jellyfin_media_signature(snapshot.raw.get("jellyfin", {}).get("item"))
]
jellyfin_baseline = [item for item in jellyfin_baseline if item]
target_names = [_replacement_file_name(movie_file)]
radarr = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
if not radarr.configured():
@@ -2197,15 +2221,46 @@ async def action_replace_media(
] if isinstance(episode_files, list) else []
if len(selected_files) != len(file_ids):
raise HTTPException(status_code=409, detail="One or more selected episode files are no longer current")
episode_ids = [
episode.get("id")
target_episodes = [
{
"id": int(episode["id"]),
"seasonNumber": int(episode["seasonNumber"]),
"episodeNumber": int(episode["episodeNumber"]),
}
for episode in episodes
if isinstance(episode, dict)
and episode.get("episodeFileId") in file_ids
and isinstance(episode.get("id"), int)
and isinstance(episode.get("seasonNumber"), int)
and isinstance(episode.get("episodeNumber"), int)
] if isinstance(episodes, list) else []
episode_ids = [episode["id"] for episode in target_episodes]
if not episode_ids:
raise HTTPException(status_code=409, detail="Sonarr could not match episodes to this file")
collector_id = series_id
jellyfin_series = snapshot.raw.get("jellyfin", {}).get("item")
jellyfin_series_id = jellyfin_series.get("Id") if isinstance(jellyfin_series, dict) else None
if jellyfin_series_id:
try:
jellyfin = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key)
jellyfin_episodes = await jellyfin.get_series_episodes(str(jellyfin_series_id))
target_pairs = {
(episode["seasonNumber"], episode["episodeNumber"])
for episode in target_episodes
}
jellyfin_baseline = [
{
"seasonNumber": int(item["ParentIndexNumber"]),
"episodeNumber": int(item["IndexNumber"]),
**_jellyfin_media_signature(item),
}
for item in jellyfin_episodes
if isinstance(item.get("ParentIndexNumber"), int)
and isinstance(item.get("IndexNumber"), int)
and (item["ParentIndexNumber"], item["IndexNumber"]) in target_pairs
]
except Exception:
logger.warning("Could not capture Jellyfin episode baseline request_id=%s", request_id)
target_names = [_replacement_file_name(file_data) for file_data in selected_files]
await sonarr.monitor_episodes(episode_ids, True)
for selected_file_id in file_ids:
@@ -2262,6 +2317,19 @@ async def action_replace_media(
user=user,
event_type="replacement_started",
message=message,
metadata={
"repairTracking": {
"requestId": request_id,
"actionId": "replace_media",
"mediaType": snapshot.request_type.value,
"collectorId": collector_id,
"originalFileIds": file_ids,
"episodes": target_episodes,
"jellyfinBaseline": jellyfin_baseline,
"jellyfinFoundAtStart": bool(snapshot.raw.get("jellyfin", {}).get("found")),
"startedAt": datetime.now(timezone.utc).isoformat(),
}
},
)
return {
"status": "ok",
@@ -2318,6 +2386,7 @@ async def action_search_missing_media(
if not isinstance(arr_item, dict) or not isinstance(arr_item.get("id"), int):
raise HTTPException(status_code=404, detail="Item not found in Sonarr/Radarr")
collector_id = int(arr_item["id"])
target_episodes: List[Dict[str, int]] = []
try:
if snapshot.request_type == RequestType.movie:
radarr = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
@@ -2358,6 +2427,17 @@ async def action_search_missing_media(
)
]
if searched_ids:
target_episodes = [
{
"id": int(episode_map[episode_id]["id"]),
"seasonNumber": int(episode_map[episode_id]["seasonNumber"]),
"episodeNumber": int(episode_map[episode_id]["episodeNumber"]),
}
for episode_id in searched_ids
if isinstance(episode_map.get(episode_id), dict)
and isinstance(episode_map[episode_id].get("seasonNumber"), int)
and isinstance(episode_map[episode_id].get("episodeNumber"), int)
]
await sonarr.monitor_episodes(searched_ids, True)
await sonarr.search_episodes(searched_ids)
message = f"Sonarr started searching for {len(searched_ids)} selected missing episode(s)."
@@ -2400,7 +2480,23 @@ async def action_search_missing_media(
save_action, request_id, "search_missing", "Search for missing content", "ok", message
)
_record_replacement_activity(
linked_issue, user=user, event_type="missing_search_started", message=message
linked_issue,
user=user,
event_type="missing_search_started",
message=message,
metadata={
"repairTracking": {
"requestId": request_id,
"actionId": "search_missing",
"mediaType": snapshot.request_type.value,
"collectorId": collector_id,
"originalFileIds": [],
"episodes": target_episodes,
"jellyfinBaseline": [],
"jellyfinFoundAtStart": bool(snapshot.raw.get("jellyfin", {}).get("found")),
"startedAt": datetime.now(timezone.utc).isoformat(),
}
},
)
return {"status": "ok", "message": message, "episode_ids": searched_ids}