Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions server/api/servarr/radarr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,17 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
public removeMovie = async (movieId: number): Promise<void> => {
try {
const { id, title } = await this.getMovieByTmdbId(movieId);
if (!id) {
// Movie is not in the Radarr library (e.g. already removed). Treat the
// desired end-state as reached so retries remain idempotent.
logger.info(
'[Radarr] Movie not present in library; nothing to remove',
{
tmdbId: movieId,
}
);
return;
}
await this.axios.delete(`/movie/${id}`, {
params: {
deleteFiles: true,
Expand All @@ -284,6 +295,26 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
}
};

public removeTagFromMovie = async (
tmdbId: number,
tagId: number
): Promise<void> => {
try {
const movie = await this.getMovieByTmdbId(tmdbId);
const updatedTags = movie.tags.filter((t) => t !== tagId);
await this.axios.put(`/movie`, {
...movie,
tags: updatedTags,
});
logger.info(`[Radarr] Removed tag ${tagId} from movie ${movie.title}`);
Comment on lines +303 to +309
} catch (e) {
throw new Error(
`[Radarr] Failed to remove tag from movie: ${e.message}`,
{ cause: e }
);
}
};

public clearCache = ({
tmdbId,
externalId,
Expand Down
87 changes: 87 additions & 0 deletions server/api/servarr/sonarr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,15 @@ class SonarrAPI extends ServarrBase<{
public removeSeries = async (serieId: number): Promise<void> => {
try {
const { id, title } = await this.getSeriesByTvdbId(serieId);
if (!id) {
// Series is not in the Sonarr library (e.g. already removed). Treat the
// desired end-state as reached so retries remain idempotent.
logger.info(
'[Sonarr] Series not present in library; nothing to remove',
{ tvdbId: serieId }
);
return;
}
await this.axios.delete(`/series/${id}`, {
params: {
deleteFiles: true,
Expand All @@ -427,6 +436,84 @@ class SonarrAPI extends ServarrBase<{
}
};

public removeTagFromSeries = async (
tvdbId: number,
tagId: number
): Promise<void> => {
try {
const series = await this.getSeriesByTvdbId(tvdbId);
if (!series.id) {
throw new Error('Series not found in Sonarr');
}
const updatedTags = series.tags.filter((t) => t !== tagId);
await this.axios.put(`/series/${series.id}`, {
...series,
tags: updatedTags,
});
Comment on lines +444 to +452
logger.info(`[Sonarr] Removed tag ${tagId} from series ${series.title}`);
} catch (e) {
throw new Error(
`[Sonarr] Failed to remove tag from series: ${e.message}`,
{ cause: e }
);
}
};

public removeSeasonFiles = async (
tvdbId: number,
seasonNumbers: number[]
): Promise<void> => {
try {
const series = await this.getSeriesByTvdbId(tvdbId);
if (!series.id) {
// Series is not in the Sonarr library (e.g. already removed). Treat the
// desired end-state as reached so retries remain idempotent.
logger.info(
'[Sonarr] Series not present in library; nothing to remove',
{ tvdbId }
);
return;
}
Comment on lines +462 to +476

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same idempotency issue in removeSeasonFiles.

The !series.id guard at line 468 won't execute when the series is absent because getSeriesByTvdbId throws first. Apply the nested try-catch pattern here as well.

🔧 Proposed fix
 public removeSeasonFiles = async (
   tvdbId: number,
   seasonNumbers: number[]
 ): Promise<void> => {
   try {
-    const series = await this.getSeriesByTvdbId(tvdbId);
-    if (!series.id) {
-      // Series is not in the Sonarr library (e.g. already removed). Treat the
-      // desired end-state as reached so retries remain idempotent.
+    let series;
+    try {
+      series = await this.getSeriesByTvdbId(tvdbId);
+    } catch {
       logger.info(
         '[Sonarr] Series not present in library; nothing to remove',
         { tvdbId }
       );
       return;
     }
+    if (!series.id) {
+      logger.info(
+        '[Sonarr] Series not present in library; nothing to remove',
+        { tvdbId }
+      );
+      return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public removeSeasonFiles = async (
tvdbId: number,
seasonNumbers: number[]
): Promise<void> => {
try {
const series = await this.getSeriesByTvdbId(tvdbId);
if (!series.id) {
// Series is not in the Sonarr library (e.g. already removed). Treat the
// desired end-state as reached so retries remain idempotent.
logger.info(
'[Sonarr] Series not present in library; nothing to remove',
{ tvdbId }
);
return;
}
public removeSeasonFiles = async (
tvdbId: number,
seasonNumbers: number[]
): Promise<void> => {
try {
let series;
try {
series = await this.getSeriesByTvdbId(tvdbId);
} catch {
logger.info(
'[Sonarr] Series not present in library; nothing to remove',
{ tvdbId }
);
return;
}
if (!series.id) {
logger.info(
'[Sonarr] Series not present in library; nothing to remove',
{ tvdbId }
);
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/servarr/sonarr.ts` around lines 462 - 476, The removeSeasonFiles
method has an idempotency issue where the guard check for series.id never
executes because getSeriesByTvdbId throws an error when the series is not found,
rather than returning a series object without an id. Wrap the call to
getSeriesByTvdbId in a nested try-catch block within the removeSeasonFiles
method to catch the error when the series is absent, log an appropriate message
indicating the series is not present in the library, and return early to treat
this as an idempotent success case.


const episodes = await this.getEpisodes(series.id);
const targetEpisodes = episodes.filter((ep) =>
seasonNumbers.includes(ep.seasonNumber)
);
const episodeFileIds = targetEpisodes
.filter((ep) => ep.hasFile && ep.episodeFileId > 0)
.map((ep) => ep.episodeFileId);

// Unmonitor the affected episodes before deleting files
const episodeIds = targetEpisodes.map((ep) => ep.id);
if (episodeIds.length > 0) {
await this.axios.put('/episode/monitor', {
episodeIds,
monitored: false,
});
}

// Delete episode files
for (const fileId of [...new Set(episodeFileIds)]) {
await this.axios.delete(`/episodefile/${fileId}`);
}

// Unmonitor the seasons
series.seasons = series.seasons.map((s) => ({
...s,
monitored: seasonNumbers.includes(s.seasonNumber) ? false : s.monitored,
}));
await this.axios.put(`/series/${series.id}`, series);

Comment on lines +500 to +506
logger.info(
`[Sonarr] Removed files for seasons ${seasonNumbers.join(', ')} of ${series.title}`
);
} catch (e) {
throw new Error(`[Sonarr] Failed to remove season files: ${e.message}`, {
cause: e,
});
}
};

public clearCache = ({
tvdbId,
externalId,
Expand Down