From 8b3e2fce3f07a6055741b5288390f5df1f60cbb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:01:59 +0000 Subject: [PATCH 1/3] Initial plan From 9eada905add5dc1e1b84065f637271ad2531b91f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:10:48 +0000 Subject: [PATCH 2/3] Fix looping error when movie/series is deleted from another device When a movie/TV show is deleted from another device while its detail view is open, onBecomeActive triggers reload() which calls the API. The API returns a 404 error, showing an error alert. On macOS, dismissing the alert causes the window to become active again (appearsActive changes), triggering onBecomeActive again, creating an infinite loop. Fix: - Add isNotFound property to API.Error to detect 404 status codes - Remove items from local cache when API returns 404 during get operations - Navigate back from detail views when reload detects a 404 error Co-authored-by: tillkruss <665029+tillkruss@users.noreply.github.com> --- Ruddarr/Dependencies/API/API+Error.swift | 11 +++++++++++ Ruddarr/Models/Movies/Movies.swift | 13 +++++++++---- Ruddarr/Models/Series/SeriesModel.swift | 13 +++++++++---- Ruddarr/Views/Movies/MovieView.swift | 10 +++++++++- Ruddarr/Views/Series/SeasonView.swift | 10 +++++++++- Ruddarr/Views/Series/SeriesItemView.swift | 10 +++++++++- 6 files changed, 56 insertions(+), 11 deletions(-) diff --git a/Ruddarr/Dependencies/API/API+Error.swift b/Ruddarr/Dependencies/API/API+Error.swift index 24ddfbf1..33448870 100644 --- a/Ruddarr/Dependencies/API/API+Error.swift +++ b/Ruddarr/Dependencies/API/API+Error.swift @@ -107,6 +107,17 @@ extension API.Error: LocalizedError { } } +extension API.Error { + var isNotFound: Bool { + switch self { + case .badStatusCode(code: 404), .errorResponse(code: 404, _): + true + default: + false + } + } +} + extension DecodingError { var context: DecodingError.Context { switch self { diff --git a/Ruddarr/Models/Movies/Movies.swift b/Ruddarr/Models/Movies/Movies.swift index a74e4a8c..4c7172ee 100644 --- a/Ruddarr/Models/Movies/Movies.swift +++ b/Ruddarr/Models/Movies/Movies.swift @@ -136,10 +136,15 @@ class Movies { case .get(let movie): if let index = items.firstIndex(where: { $0.id == movie.id }) { - let item = try await dependencies.api.getMovie(movie.id, instance) - - if items[index] != item { - items[index] = item + do { + let item = try await dependencies.api.getMovie(movie.id, instance) + + if items[index] != item { + items[index] = item + } + } catch let apiError as API.Error where apiError.isNotFound { + items.remove(at: index) + throw apiError } } diff --git a/Ruddarr/Models/Series/SeriesModel.swift b/Ruddarr/Models/Series/SeriesModel.swift index 0ecd5cc3..31c388b4 100644 --- a/Ruddarr/Models/Series/SeriesModel.swift +++ b/Ruddarr/Models/Series/SeriesModel.swift @@ -153,10 +153,15 @@ class SeriesModel { case .get(let series): if let index = items.firstIndex(where: { $0.id == series.id }) { - let item = try await dependencies.api.getSeries(series.id, instance) - - if items[index] != item { - items[index] = item + do { + let item = try await dependencies.api.getSeries(series.id, instance) + + if items[index] != item { + items[index] = item + } + } catch let apiError as API.Error where apiError.isNotFound { + items.remove(at: index) + throw apiError } } diff --git a/Ruddarr/Views/Movies/MovieView.swift b/Ruddarr/Views/Movies/MovieView.swift index 200cd051..ca513c2c 100644 --- a/Ruddarr/Views/Movies/MovieView.swift +++ b/Ruddarr/Views/Movies/MovieView.swift @@ -149,7 +149,15 @@ extension MovieView { } func reload() async { - _ = await instance.movies.get(movie) + guard await instance.movies.get(movie) else { + if instance.movies.error?.isNotFound == true { + instance.movies.error = nil + if !dependencies.router.moviesPath.isEmpty { + dependencies.router.moviesPath.removeLast() + } + } + return + } } func refresh() async { diff --git a/Ruddarr/Views/Series/SeasonView.swift b/Ruddarr/Views/Series/SeasonView.swift index 0da57d1d..b2ca8312 100644 --- a/Ruddarr/Views/Series/SeasonView.swift +++ b/Ruddarr/Views/Series/SeasonView.swift @@ -265,7 +265,15 @@ extension SeasonView { } func reload() async { - _ = await instance.series.get(series) + guard await instance.series.get(series) else { + if instance.series.error?.isNotFound == true { + instance.series.error = nil + if !dependencies.router.seriesPath.isEmpty { + dependencies.router.seriesPath.removeLast() + } + } + return + } await instance.episodes.fetch(series) await instance.files.fetch(series) } diff --git a/Ruddarr/Views/Series/SeriesItemView.swift b/Ruddarr/Views/Series/SeriesItemView.swift index e7d0393e..91a149c1 100644 --- a/Ruddarr/Views/Series/SeriesItemView.swift +++ b/Ruddarr/Views/Series/SeriesItemView.swift @@ -154,7 +154,15 @@ extension SeriesDetailView { } func reload() async { - _ = await instance.series.get(series) + guard await instance.series.get(series) else { + if instance.series.error?.isNotFound == true { + instance.series.error = nil + if !dependencies.router.seriesPath.isEmpty { + dependencies.router.seriesPath.removeLast() + } + } + return + } await instance.episodes.fetch(series) await instance.files.fetch(series) } From aa7b18d56a4cb9b9f3b6c499e25c0b32237752bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:12:19 +0000 Subject: [PATCH 3/3] Navigate to root path instead of just popping one level on 404 Co-authored-by: tillkruss <665029+tillkruss@users.noreply.github.com> --- Ruddarr/Views/Movies/MovieView.swift | 4 +--- Ruddarr/Views/Series/SeasonView.swift | 4 +--- Ruddarr/Views/Series/SeriesItemView.swift | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Ruddarr/Views/Movies/MovieView.swift b/Ruddarr/Views/Movies/MovieView.swift index ca513c2c..6d6c90c8 100644 --- a/Ruddarr/Views/Movies/MovieView.swift +++ b/Ruddarr/Views/Movies/MovieView.swift @@ -152,9 +152,7 @@ extension MovieView { guard await instance.movies.get(movie) else { if instance.movies.error?.isNotFound == true { instance.movies.error = nil - if !dependencies.router.moviesPath.isEmpty { - dependencies.router.moviesPath.removeLast() - } + dependencies.router.moviesPath = .init() } return } diff --git a/Ruddarr/Views/Series/SeasonView.swift b/Ruddarr/Views/Series/SeasonView.swift index b2ca8312..074d9688 100644 --- a/Ruddarr/Views/Series/SeasonView.swift +++ b/Ruddarr/Views/Series/SeasonView.swift @@ -268,9 +268,7 @@ extension SeasonView { guard await instance.series.get(series) else { if instance.series.error?.isNotFound == true { instance.series.error = nil - if !dependencies.router.seriesPath.isEmpty { - dependencies.router.seriesPath.removeLast() - } + dependencies.router.seriesPath = .init() } return } diff --git a/Ruddarr/Views/Series/SeriesItemView.swift b/Ruddarr/Views/Series/SeriesItemView.swift index 91a149c1..d6a48c74 100644 --- a/Ruddarr/Views/Series/SeriesItemView.swift +++ b/Ruddarr/Views/Series/SeriesItemView.swift @@ -157,9 +157,7 @@ extension SeriesDetailView { guard await instance.series.get(series) else { if instance.series.error?.isNotFound == true { instance.series.error = nil - if !dependencies.router.seriesPath.isEmpty { - dependencies.router.seriesPath.removeLast() - } + dependencies.router.seriesPath = .init() } return }