Skip to content

Commit

Permalink
feat: added slider to allow end users to determine if new seasons sho…
Browse files Browse the repository at this point in the history
…uld be monitored in Sonarr

feat Fallenbagel#1047
  • Loading branch information
Gavin Fuller committed Dec 20, 2024
1 parent 66a5ab4 commit 2b4effa
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 25 deletions.
5 changes: 3 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 29 additions & 21 deletions server/api/servarr/sonarr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ export interface SonarrSeason {
percentOfEpisodes: number;
};
}

interface EpisodeResult {
seriesId: number;
episodeId: number;
episode: EpisodeResult;
episodeFileId: number;
seasonNumber: number;
episodeNumber: number;
Expand Down Expand Up @@ -99,6 +102,7 @@ export interface AddSeriesOptions {
seriesType: SonarrSeries['seriesType'];
monitored?: boolean;
searchNow?: boolean;
autoRequestNewSeasons?: boolean;
}

export interface LanguageProfile {
Expand Down Expand Up @@ -185,7 +189,11 @@ class SonarrAPI extends ServarrBase<{
if (series.id) {
series.monitored = options.monitored ?? series.monitored;
series.tags = options.tags ?? series.tags;
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
series.seasons = this.buildSeasonList(
options.seasons,
series.seasons,
options.autoRequestNewSeasons
);

const newSeriesData = await this.put<SonarrSeries>(
'/series',
Expand Down Expand Up @@ -226,17 +234,17 @@ class SonarrAPI extends ServarrBase<{
options.seasons,
series.seasons.map((season) => ({
seasonNumber: season.seasonNumber,
// We force all seasons to false if its the first request
monitored: false,
}))
monitored: false, // Initialize all seasons as unmonitored
})),
options.autoRequestNewSeasons
),
tags: options.tags,
seasonFolder: options.seasonFolder,
monitored: options.monitored,
rootFolderPath: options.rootFolderPath,
seriesType: options.seriesType,
addOptions: {
ignoreEpisodesWithFiles: true,
monitor: options.autoRequestNewSeasons ? 'future' : 'none',
searchForMissingEpisodes: options.searchNow,
},
} as Partial<SonarrSeries>);
Expand All @@ -248,7 +256,7 @@ class SonarrAPI extends ServarrBase<{
movie: createdSeriesData,
});
} else {
logger.error('Failed to add movie to Sonarr', {
logger.error('Failed to add series to Sonarr', {
label: 'Sonarr',
options,
});
Expand Down Expand Up @@ -318,39 +326,39 @@ class SonarrAPI extends ServarrBase<{

private buildSeasonList(
seasons: number[],
existingSeasons?: SonarrSeason[]
existingSeasons?: SonarrSeason[],
autoRequestNewSeasons?: boolean
): SonarrSeason[] {
if (existingSeasons) {
const newSeasons = existingSeasons.map((season) => {
return existingSeasons.map((season) => {
// Monitor requested seasons
if (seasons.includes(season.seasonNumber)) {
season.monitored = true;
} else {
// Set future seasons' monitoring based on autoRequestNewSeasons
season.monitored = autoRequestNewSeasons !== false;
}
return season;
});

return newSeasons;
}

const newSeasons = seasons.map(
(seasonNumber): SonarrSeason => ({
seasonNumber,
monitored: true,
})
);

return newSeasons;
// If no existing seasons, monitor only the requested seasons
return seasons.map((seasonNumber) => ({
seasonNumber,
monitored: true,
}));
}

public removeSerie = async (serieId: number): Promise<void> => {
public removeSeries = async (serieId: number): Promise<void> => {
try {
const { id, title } = await this.getSeriesByTvdbId(serieId);
await this.delete(`/series/${id}`, {
deleteFiles: 'true',
addImportExclusion: 'false',
});
logger.info(`[Radarr] Removed serie ${title}`);
logger.info(`[Sonarr] Removed series ${title}`);
} catch (e) {
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
throw new Error(`[Sonarr] Failed to remove series: ${e.message}`);
}
};
}
Expand Down
11 changes: 11 additions & 0 deletions server/entity/MediaRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ export class MediaRequest {
}
}

const autoRequestNewSeasonsValue =
typeof requestBody.autoRequestNewSeasons === 'boolean'
? requestBody.autoRequestNewSeasons
: true;

if (requestBody.mediaType === MediaType.MOVIE) {
await mediaRepository.save(media);

Expand Down Expand Up @@ -247,6 +252,7 @@ export class MediaRequest {
rootFolder: requestBody.rootFolder,
tags: requestBody.tags,
isAutoRequest: options.isAutoRequest ?? false,
autoRequestNewSeasons: autoRequestNewSeasonsValue,
});

await requestRepository.save(request);
Expand Down Expand Up @@ -369,6 +375,7 @@ export class MediaRequest {
})
),
isAutoRequest: options.isAutoRequest ?? false,
autoRequestNewSeasons: autoRequestNewSeasonsValue,
});

await requestRepository.save(request);
Expand Down Expand Up @@ -470,6 +477,9 @@ export class MediaRequest {
@Column({ default: false })
public isAutoRequest: boolean;

@Column({ nullable: true, default: true })
public autoRequestNewSeasons?: boolean;

constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init);
}
Expand Down Expand Up @@ -1112,6 +1122,7 @@ export class MediaRequest {
tags,
monitored: true,
searchNow: !sonarrSettings.preventSearch,
autoRequestNewSeasons: this.autoRequestNewSeasons,
};

// Run this asynchronously so we don't wait for it on the UI side
Expand Down
1 change: 1 addition & 0 deletions server/interfaces/api/requestInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export type MediaRequestBody = {
languageProfileId?: number;
userId?: number;
tags?: number[];
autoRequestNewSeasons?: boolean;
};
2 changes: 1 addition & 1 deletion server/routes/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ mediaRoutes.delete(
if (!tvdbId) {
throw new Error('TVDB ID not found');
}
await (service as SonarrAPI).removeSerie(tvdbId);
await (service as SonarrAPI).removeSeries(tvdbId);
}

return res.status(204).send();
Expand Down
2 changes: 1 addition & 1 deletion src/components/Common/SlideCheckbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const SlideCheckbox = ({ onClick, checked = false }: SlideCheckboxProps) => {
<span
role="checkbox"
tabIndex={0}
aria-checked={false}
aria-checked={checked}
onClick={() => {
onClick();
}}
Expand Down
3 changes: 3 additions & 0 deletions src/components/RequestModal/MovieRequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const messages = defineMessages('components.RequestModal', {
requestApproved: 'Request for <strong>{title}</strong> approved!',
requesterror: 'Something went wrong while submitting the request.',
pendingapproval: 'Your request is pending approval.',
searchAutomatically: 'Search Automatically',
searchAutomaticallyDescription:
'Automatically search for this movie in Radarr after the request is approved.',
});

interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
Expand Down
26 changes: 26 additions & 0 deletions src/components/RequestModal/TvRequestModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Alert from '@app/components/Common/Alert';
import Badge from '@app/components/Common/Badge';
import Modal from '@app/components/Common/Modal';
import SlideCheckbox from '@app/components/Common/SlideCheckbox';
import type { RequestOverrides } from '@app/components/RequestModal/AdvancedRequester';
import AdvancedRequester from '@app/components/RequestModal/AdvancedRequester';
import QuotaDisplay from '@app/components/RequestModal/QuotaDisplay';
Expand Down Expand Up @@ -49,6 +50,9 @@ const messages = defineMessages('components.RequestModal', {
autoapproval: 'Automatic Approval',
requesterror: 'Something went wrong while submitting the request.',
pendingapproval: 'Your request is pending approval.',
autoRequestNewSeasons: 'Automatically Request New Seasons',
autoRequestNewSeasonsDescription:
'New seasons will be requested automatically when they become available',
});

interface RequestModalProps extends React.HTMLAttributes<HTMLDivElement> {
Expand All @@ -70,6 +74,7 @@ const TvRequestModal = ({
}: RequestModalProps) => {
const settings = useSettings();
const { addToast } = useToasts();
const [autoRequestNewSeasons, setAutoRequestNewSeasons] = useState(true);
const editingSeasons: number[] = (editRequest?.seasons ?? []).map(
(season) => season.seasonNumber
);
Expand Down Expand Up @@ -124,6 +129,7 @@ const TvRequestModal = ({
userId: requestOverrides?.user?.id,
tags: requestOverrides?.tags,
seasons: selectedSeasons,
autoRequestNewSeasons,
}),
});
if (!res.ok) throw new Error();
Expand Down Expand Up @@ -213,6 +219,7 @@ const TvRequestModal = ({
tvdbId: tvdbId ?? data?.externalIds.tvdbId,
mediaType: 'tv',
is4k,
autoRequestNewSeasons,
seasons: settings.currentSettings.partialRequestsEnabled
? selectedSeasons
: getAllSeasons().filter(
Expand Down Expand Up @@ -710,6 +717,25 @@ const TvRequestModal = ({
</div>
</div>
</div>

{/* Add auto-request checkbox after the seasons table */}
{!editRequest && (
<div className="mb-6 mt-4 flex items-center space-x-2">
<SlideCheckbox
checked={autoRequestNewSeasons}
onClick={() => setAutoRequestNewSeasons(!autoRequestNewSeasons)}
/>
<div className="flex flex-col">
<span className="text-gray-100">
{intl.formatMessage(messages.autoRequestNewSeasons)}
</span>
<span className="text-sm text-gray-400">
{intl.formatMessage(messages.autoRequestNewSeasonsDescription)}
</span>
</div>
</div>
)}

{(hasPermission(Permission.REQUEST_ADVANCED) ||
hasPermission(Permission.MANAGE_REQUESTS)) && (
<AdvancedRequester
Expand Down

0 comments on commit 2b4effa

Please sign in to comment.