feat: add support for requesting "Specials" for TV Shows (#3724)

* feat: add support for requesting "Specials" for TV Shows

This commit is responsible for adding support in Overseerr for requesting "Special" episodes for TV
Shows. This request has become especially pertinent when you consider shows like "Doctor Who". These
shows have Specials that are critical to understanding the plot of a TV show.

fix #779

* chore(yarn.lock): undo inappropriate changes to yarn.lock

I was informed by @sct in a comment on the #3724 PR that it was not appropriate to commit the
changes that ended up being made to the yarn.lock file. This commit is responsible, then, for
undoing the changes to the yarn.lock file that ended up being submitted.

* refactor: change loose equality to strict equality

I received a comment from OwsleyJr pointing out that we are using loose equality when we could
alternatively just be using strict equality to increase the robustness of our code. This commit
does exactly that by squashing out previous usages of loose equality in my commits and replacing
them with strict equality

* refactor: move 'Specials' string to a global message

Owsley pointed out that we are redefining the 'Specials' string multiple times throughout this PR.
Instead, we can just move it as a global message. This commit does exactly that. It squashes out and
previous declarations of the 'Specials' string inside the src files, and moves it directly to the
global messages file.
pull/3964/head
Ahmed Siddiqui 3 months ago committed by GitHub
parent a2c25d5e4b
commit c2d4c61fae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5018,7 +5018,7 @@ paths:
- type: array - type: array
items: items:
type: number type: number
minimum: 1 minimum: 0
- type: string - type: string
enum: [all] enum: [all]
is4k: is4k:
@ -5124,7 +5124,7 @@ paths:
type: array type: array
items: items:
type: number type: number
minimum: 1 minimum: 0
is4k: is4k:
type: boolean type: boolean
example: false example: false

@ -246,9 +246,7 @@ export class MediaRequest {
>; >;
const requestedSeasons = const requestedSeasons =
requestBody.seasons === 'all' requestBody.seasons === 'all'
? tmdbMediaShow.seasons ? tmdbMediaShow.seasons.map((season) => season.season_number)
.map((season) => season.season_number)
.filter((sn) => sn > 0)
: (requestBody.seasons as number[]); : (requestBody.seasons as number[]);
let existingSeasons: number[] = []; let existingSeasons: number[] = [];

@ -278,9 +278,7 @@ class PlexScanner
const seasons = tvShow.seasons; const seasons = tvShow.seasons;
const processableSeasons: ProcessableSeason[] = []; const processableSeasons: ProcessableSeason[] = [];
const filteredSeasons = seasons.filter((sn) => sn.season_number !== 0); for (const season of seasons) {
for (const season of filteredSeasons) {
const matchedPlexSeason = metadata.Children?.Metadata.find( const matchedPlexSeason = metadata.Children?.Metadata.find(
(md) => Number(md.index) === season.season_number (md) => Number(md.index) === season.season_number
); );

@ -103,10 +103,8 @@ class SonarrScanner
const tmdbId = tvShow.id; const tmdbId = tvShow.id;
const filteredSeasons = sonarrSeries.seasons.filter( const filteredSeasons = sonarrSeries.seasons.filter((sn) =>
(sn) => tvShow.seasons.find((s) => s.season_number === sn.seasonNumber)
sn.seasonNumber !== 0 &&
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber)
); );
for (const season of filteredSeasons) { for (const season of filteredSeasons) {

@ -243,7 +243,11 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
key={`season-${season.id}`} key={`season-${season.id}`}
className="mb-1 mr-2 inline-block" className="mb-1 mr-2 inline-block"
> >
<Badge>{season.seasonNumber}</Badge> <Badge>
{season.seasonNumber === 0
? intl.formatMessage(globalMessages.specials)
: season.seasonNumber}
</Badge>
</span> </span>
))} ))}
</div> </div>

@ -381,8 +381,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
<span className="mr-2 font-bold "> <span className="mr-2 font-bold ">
{intl.formatMessage(messages.seasons, { {intl.formatMessage(messages.seasons, {
seasonCount: seasonCount:
title.seasons.filter((season) => season.seasonNumber !== 0) title.seasons.length === request.seasons.length
.length === request.seasons.length
? 0 ? 0
: request.seasons.length, : request.seasons.length,
})} })}
@ -390,7 +389,11 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
<div className="hide-scrollbar overflow-x-scroll"> <div className="hide-scrollbar overflow-x-scroll">
{request.seasons.map((season) => ( {request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2"> <span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge> <Badge>
{season.seasonNumber === 0
? intl.formatMessage(globalMessages.specials)
: season.seasonNumber}
</Badge>
</span> </span>
))} ))}
</div> </div>

@ -440,9 +440,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
<span className="card-field-name"> <span className="card-field-name">
{intl.formatMessage(messages.seasons, { {intl.formatMessage(messages.seasons, {
seasonCount: seasonCount:
title.seasons.filter( title.seasons.length === request.seasons.length
(season) => season.seasonNumber !== 0
).length === request.seasons.length
? 0 ? 0
: request.seasons.length, : request.seasons.length,
})} })}
@ -450,7 +448,11 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll"> <div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">
{request.seasons.map((season) => ( {request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2"> <span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge> <Badge>
{season.seasonNumber === 0
? intl.formatMessage(globalMessages.specials)
: season.seasonNumber}
</Badge>
</span> </span>
))} ))}
</div> </div>

@ -41,7 +41,6 @@ const messages = defineMessages({
season: 'Season', season: 'Season',
numberofepisodes: '# of Episodes', numberofepisodes: '# of Episodes',
seasonnumber: 'Season {number}', seasonnumber: 'Season {number}',
extras: 'Extras',
errorediting: 'Something went wrong while editing the request.', errorediting: 'Something went wrong while editing the request.',
requestedited: 'Request for <strong>{title}</strong> edited successfully!', requestedited: 'Request for <strong>{title}</strong> edited successfully!',
requestApproved: 'Request for <strong>{title}</strong> approved!', requestApproved: 'Request for <strong>{title}</strong> approved!',
@ -232,9 +231,7 @@ const TvRequestModal = ({
const getAllSeasons = (): number[] => { const getAllSeasons = (): number[] => {
return (data?.seasons ?? []) return (data?.seasons ?? [])
.filter( .filter((season) => season.episodeCount !== 0)
(season) => season.seasonNumber !== 0 && season.episodeCount !== 0
)
.map((season) => season.seasonNumber); .map((season) => season.seasonNumber);
}; };
@ -557,10 +554,7 @@ const TvRequestModal = ({
</thead> </thead>
<tbody className="divide-y divide-gray-700"> <tbody className="divide-y divide-gray-700">
{data?.seasons {data?.seasons
.filter( .filter((season) => season.episodeCount !== 0)
(season) =>
season.seasonNumber !== 0 && season.episodeCount !== 0
)
.map((season) => { .map((season) => {
const seasonRequest = getSeasonRequest( const seasonRequest = getSeasonRequest(
season.seasonNumber season.seasonNumber
@ -637,7 +631,7 @@ const TvRequestModal = ({
</td> </td>
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6"> <td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
{season.seasonNumber === 0 {season.seasonNumber === 0
? intl.formatMessage(messages.extras) ? intl.formatMessage(globalMessages.specials)
: intl.formatMessage(messages.seasonnumber, { : intl.formatMessage(messages.seasonnumber, {
number: season.seasonNumber, number: season.seasonNumber,
})} })}

@ -200,6 +200,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
); );
} }
// Does NOT include "Specials"
const seasonCount = data.seasons.filter( const seasonCount = data.seasons.filter(
(season) => season.seasonNumber !== 0 && season.episodeCount !== 0 (season) => season.seasonNumber !== 0 && season.episodeCount !== 0
).length; ).length;
@ -257,9 +258,17 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
return [...requestedSeasons, ...availableSeasons]; return [...requestedSeasons, ...availableSeasons];
}; };
const isComplete = seasonCount <= getAllRequestedSeasons(false).length; const showHasSpecials = data.seasons.some(
(season) => season.seasonNumber === 0
);
const isComplete =
(showHasSpecials ? seasonCount + 1 : seasonCount) <=
getAllRequestedSeasons(false).length;
const is4kComplete = seasonCount <= getAllRequestedSeasons(true).length; const is4kComplete =
(showHasSpecials ? seasonCount + 1 : seasonCount) <=
getAllRequestedSeasons(true).length;
const streamingProviders = const streamingProviders =
data?.watchProviders?.find((provider) => provider.iso_3166_1 === region) data?.watchProviders?.find((provider) => provider.iso_3166_1 === region)
@ -522,7 +531,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
{data.seasons {data.seasons
.slice() .slice()
.reverse() .reverse()
.filter((season) => season.seasonNumber !== 0)
.map((season) => { .map((season) => {
const show4k = const show4k =
settings.currentSettings.series4kEnabled && settings.currentSettings.series4kEnabled &&
@ -576,9 +584,11 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
> >
<div className="flex flex-1 items-center space-x-2 text-lg"> <div className="flex flex-1 items-center space-x-2 text-lg">
<span> <span>
{intl.formatMessage(messages.seasonnumber, { {season.seasonNumber === 0
seasonNumber: season.seasonNumber, ? intl.formatMessage(globalMessages.specials)
})} : intl.formatMessage(messages.seasonnumber, {
seasonNumber: season.seasonNumber,
})}
</span> </span>
<Badge badgeType="dark"> <Badge badgeType="dark">
{intl.formatMessage(messages.episodeCount, { {intl.formatMessage(messages.episodeCount, {

@ -55,6 +55,7 @@ const globalMessages = defineMessages({
noresults: 'No results.', noresults: 'No results.',
open: 'Open', open: 'Open',
resolved: 'Resolved', resolved: 'Resolved',
specials: 'Specials',
}); });
export default globalMessages; export default globalMessages;

@ -471,7 +471,6 @@
"components.RequestModal.cancel": "Cancel Request", "components.RequestModal.cancel": "Cancel Request",
"components.RequestModal.edit": "Edit Request", "components.RequestModal.edit": "Edit Request",
"components.RequestModal.errorediting": "Something went wrong while editing the request.", "components.RequestModal.errorediting": "Something went wrong while editing the request.",
"components.RequestModal.extras": "Extras",
"components.RequestModal.numberofepisodes": "# of Episodes", "components.RequestModal.numberofepisodes": "# of Episodes",
"components.RequestModal.pending4krequest": "Pending 4K Request", "components.RequestModal.pending4krequest": "Pending 4K Request",
"components.RequestModal.pendingapproval": "Your request is pending approval.", "components.RequestModal.pendingapproval": "Your request is pending approval.",

Loading…
Cancel
Save