Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 'Request New Seasons' option added to request dialog #1167

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
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
33 changes: 33 additions & 0 deletions 1734669224886-AddAutoRequestNewSeasonsToMediaRequest.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should go in the server/migration/sqlite folder.

And you should generate the PostgreSQL migration too. I'll write some documentation on how to generate properly these migrations this week-end.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class AddAutoRequestNewSeasonsToMediaRequest1734669224886
implements MigrationInterface
{
name = 'AddAutoRequestNewSeasonsToMediaRequest1734669224886';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), "mediaId" integer NOT NULL, "requestedById" integer, "modifiedById" integer, "autoRequestNewSeasons" boolean DEFAULT (1), CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest", "mediaId", "requestedById", "modifiedById" FROM "media_request"`
);
await queryRunner.query(`DROP TABLE "media_request"`);
await queryRunner.query(
`ALTER TABLE "temporary_media_request" RENAME TO "media_request"`
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "media_request" RENAME TO "temporary_media_request"`
);
await queryRunner.query(
`CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), "mediaId" integer NOT NULL, "requestedById" integer, "modifiedById" integer, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest", "mediaId", "requestedById", "modifiedById" FROM "temporary_media_request"`
);
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
}
}
5 changes: 3 additions & 2 deletions pnpm-lock.yaml
Copy link
Collaborator

Choose a reason for hiding this comment

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

There is no reason to have changes in this file.

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}`);
gauthier-th marked this conversation as resolved.
Show resolved Hide resolved
} 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.',
Comment on lines +37 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is useless for this PR.

});

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
6 changes: 5 additions & 1 deletion src/i18n/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,8 @@
"components.RequestModal.SearchByNameModal.notvdbiddescription": "We were unable to automatically match this series. Please select the correct match below.",
"components.RequestModal.alreadyrequested": "Already Requested",
"components.RequestModal.approve": "Approve Request",
"components.RequestModal.autoRequestNewSeasons": "Automatically Request New Seasons",
"components.RequestModal.autoRequestNewSeasonsDescription": "New seasons will be requested automatically when they become available",
"components.RequestModal.autoapproval": "Automatic Approval",
"components.RequestModal.cancel": "Cancel Request",
"components.RequestModal.edit": "Edit Request",
Expand All @@ -559,6 +561,8 @@
"components.RequestModal.requestseasons4k": "Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}} in 4K",
"components.RequestModal.requestseries4ktitle": "Request Series in 4K",
"components.RequestModal.requestseriestitle": "Request Series",
"components.RequestModal.searchAutomatically": "Search Automatically",
"components.RequestModal.searchAutomaticallyDescription": "Automatically search for this movie in Radarr after the request is approved.",
"components.RequestModal.season": "Season",
"components.RequestModal.seasonnumber": "Season {number}",
"components.RequestModal.selectmovies": "Select Movie(s)",
Expand Down Expand Up @@ -1100,7 +1104,7 @@
"components.Setup.finishing": "Finishing…",
"components.Setup.servertype": "Choose Server Type",
"components.Setup.setup": "Setup",
"components.Setup.signin": "Sign In",
"components.Setup.signin": "Sign in to your account",
"components.Setup.signinMessage": "Get started by signing in",
"components.Setup.signinWithEmby": "Enter your Emby details",
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
Expand Down
Loading