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
13 changes: 13 additions & 0 deletions seerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5595,6 +5595,19 @@ paths:
schema:
type: string
example: 2023-01-01
- in: query
name: releaseType

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
name: releaseType
name: minimumReleaseType

schema:
type: string
enum: ['2', '3', '4', '5', '6']
example: '4'
description: >-
Minimum release availability for movie discovery. The selected type
and everything above it are matched (e.g. 4 = digital also matches 5
= physical and 6 = TV, sent to TMDB as with_release_type=4|5|6).
When set, results are filtered on release_date instead of
primary_release_date. 2 = limited theatrical, 3 = theatrical,
4 = digital, 5 = physical, 6 = TV
Comment on lines +5604 to +5610

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -u

for file in \
  "src/components/Discover/FilterSlideover/index.tsx" \
  "server/routes/discover.ts" \
  "server/api/themoviedb/index.ts"; do
  printf '\n== %s ==\n' "$file"
  rg -n -C 10 \
    'releaseType|primaryReleaseDateGte|primaryReleaseDateLte|release_date|defaultFutureDate|new Date' \
    "$file" || true
done

Repository: seerr-team/seerr

Length of output: 14899


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

route = Path("server/routes/discover.ts").read_text()
client = Path("server/api/themoviedb/index.ts").read_text()
ui = Path("src/components/Discover/FilterSlideover/index.tsx").read_text()

assert "releaseType: query.releaseType" in route
assert "primaryReleaseDateLte: query.primaryReleaseDateLte" in route
assert "primaryReleaseDateGte: query.primaryReleaseDateGte" in route
assert "with_release_type: releaseTypeFilter" in client
assert "if (value && !currentFilters[dateLte])" in ui
assert "[dateLte]: today" in ui

def tmdb_dates(gte=None, lte=None):
    # Mirrors the date-bound logic in getDiscoverMovies.
    past = "1900-01-01"
    future = "<defaultFutureDate>"
    return (
        past if not gte and lte else gte,
        future if not lte and gte else lte,
    )

print("route forwards releaseType and both date fields without adding a default")
print("no dates ->", tmdb_dates())
print("gte only ->", tmdb_dates("2025-01-01"))
print("lte only ->", tmdb_dates(lte="2025-01-01"))
print("UI adds today's dateLte only when the user selects releaseType")
PY

Repository: seerr-team/seerr

Length of output: 406


Apply or document the primaryReleaseDateLte default for releaseType.

The UI sets primaryReleaseDateLte to today, but direct API requests do not. Requests with only releaseType send no release-date bounds, so TMDB does not apply the release-type filter.

🤖 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 `@seerr-api.yml` around lines 5604 - 5610, Ensure the API applies or documents
the UI’s primaryReleaseDateLte=today default when releaseType is provided.
Update the releaseType schema/handling near its description so direct requests
with only releaseType still include the appropriate release-date bound, or
clearly document that callers must supply primaryReleaseDateLte.

- in: query
name: withRuntimeGte
schema:
Expand Down
22 changes: 20 additions & 2 deletions server/api/themoviedb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ interface DiscoverMovieOptions {
keywords?: string;
excludeKeywords?: string;
sortBy?: SortOptions;
releaseType?: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
releaseType?: string;
minimumReleaseType?: string;

watchRegion?: string;
watchProviders?: string;
certification?: string;
Expand Down Expand Up @@ -601,6 +602,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
voteAverageLte,
voteCountGte,
voteCountLte,
releaseType,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
releaseType,
minimumReleaseType,

watchProviders,
watchRegion,
certification,
Expand All @@ -619,6 +621,21 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
.toISOString()
.split('T')[0];

// "Minimum availability" is cumulative: picking Digital (4) should also
// match Physical (5) and TV (6), so expand the selected type up to 6.
// with_release_type is a no-op on TMDB unless it's paired with a
// release_date range, so when it's set we filter on release_date instead
// of primary_release_date (the field the plain Release Date filter uses).
const releaseTypeFilter = releaseType
? Array.from(
{ length: 6 - Number(releaseType) + 1 },
(_, i) => Number(releaseType) + i
).join('|')
: undefined;
const releaseDateField = releaseType
? 'release_date'
: 'primary_release_date';
Comment on lines +629 to +637

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
const releaseTypeFilter = releaseType
? Array.from(
{ length: 6 - Number(releaseType) + 1 },
(_, i) => Number(releaseType) + i
).join('|')
: undefined;
const releaseDateField = releaseType
? 'release_date'
: 'primary_release_date';
const releaseTypeFilter = minimumReleaseType
? Array.from(
{ length: 6 - Number(minimumReleaseType) + 1 },
(_, i) => Number(minimumReleaseType) + i
).join('|')
: undefined;
const releaseDateField = minimumReleaseType
? 'release_date'
: 'primary_release_date';


const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
params: {
sort_by: sortBy,
Expand All @@ -635,14 +652,15 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
: this.originalLanguage,
// Set our release date values, but check if one is set and not the other,
// so we can force a past date or a future date. TMDB Requires both values if one is set!
'primary_release_date.gte':
[`${releaseDateField}.gte`]:
!primaryReleaseDateGte && primaryReleaseDateLte
? defaultPastDate
: primaryReleaseDateGte,
'primary_release_date.lte':
[`${releaseDateField}.lte`]:
!primaryReleaseDateLte && primaryReleaseDateGte
? defaultFutureDate
: primaryReleaseDateLte,
with_release_type: releaseTypeFilter,
with_genres: genre,
with_companies: studio,
with_keywords: keywords,
Expand Down
2 changes: 2 additions & 0 deletions server/routes/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const QueryFilterOptions = z.object({
sortBy: z.coerce.string().optional(),
primaryReleaseDateGte: z.coerce.string().optional(),
primaryReleaseDateLte: z.coerce.string().optional(),
Comment on lines 66 to 67

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Might be worth renaming these to releaseDateGte and releaseDateLte now that it is not necessarily the primary one.

releaseType: z.enum(['2', '3', '4', '5', '6']).optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
releaseType: z.enum(['2', '3', '4', '5', '6']).optional(),
minimumReleaseType: z.enum(['2', '3', '4', '5', '6']).optional(),

firstAirDateGte: z.coerce.string().optional(),
firstAirDateLte: z.coerce.string().optional(),
studio: z.coerce.string().optional(),
Expand Down Expand Up @@ -115,6 +116,7 @@ discoverRoutes.get('/movies', async (req, res, next) => {
primaryReleaseDateGte: query.primaryReleaseDateGte
? new Date(query.primaryReleaseDateGte).toISOString().split('T')[0]
: undefined,
releaseType: query.releaseType,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
releaseType: query.releaseType,
minimumReleaseType: query.minimumReleaseType,

keywords,
excludeKeywords,
withRuntimeGte: query.withRuntimeGte,
Expand Down
50 changes: 50 additions & 0 deletions src/components/Discover/FilterSlideover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ const messages = defineMessages('components.Discover.FilterSlideover', {
activefilters:
'{count, plural, one {# Active Filter} other {# Active Filters}}',
releaseDate: 'Release Date',
minimumRelease: 'Minimum Release',
anyRelease: 'Any',
limitedTheatrical: 'Theatrical (Limited)',
theatrical: 'Theatrical',
digital: 'Digital',
physical: 'Physical',
tvRelease: 'TV',
firstAirDate: 'First Air Date',
from: 'From',
to: 'To',
Expand Down Expand Up @@ -133,6 +140,49 @@ const FilterSlideover = ({
</div>
{type === 'movie' && (
<>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.minimumRelease)}
</span>
Comment on lines +143 to +145

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
<span className="text-lg font-semibold">
{intl.formatMessage(messages.minimumRelease)}
</span>
<label
className="text-lg font-semibold"
htmlFor="minimumReleaseType"
>
{intl.formatMessage(messages.minimumRelease)}
</label>

<select
id="releaseType"
name="releaseType"
value={currentFilters.releaseType ?? ''}
Comment on lines +143 to +149

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Associate the selector with its visible label.

Line 143 renders the control label as a span. The select on lines 146-149 therefore has no accessible name. Use a label with htmlFor="releaseType".

Proposed fix
-            <span className="text-lg font-semibold">
+            <label
+              htmlFor="releaseType"
+              className="text-lg font-semibold"
+            >
               {intl.formatMessage(messages.minimumRelease)}
-            </span>
+            </label>
📝 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
<span className="text-lg font-semibold">
{intl.formatMessage(messages.minimumRelease)}
</span>
<select
id="releaseType"
name="releaseType"
value={currentFilters.releaseType ?? ''}
<label
htmlFor="releaseType"
className="text-lg font-semibold"
>
{intl.formatMessage(messages.minimumRelease)}
</label>
<select
id="releaseType"
name="releaseType"
value={currentFilters.releaseType ?? ''}
🤖 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 `@src/components/Discover/FilterSlideover/index.tsx` around lines 143 - 149,
Replace the visible label span in the release-type selector block with a label
element and associate it using htmlFor="releaseType", preserving the existing
message text and select id.

Comment on lines +147 to +149

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
id="releaseType"
name="releaseType"
value={currentFilters.releaseType ?? ''}
id="minimumReleaseType"
name="minimumReleaseType"
value={currentFilters.minimumReleaseType ?? ''}

onChange={(e) => {
const value = e.target.value || undefined;
// with_release_type only filters when paired with a date range,
// so default the "To" date to today if the user hasn't set one.
if (value && !currentFilters[dateLte]) {
const now = new Date();
const today = `${now.getFullYear()}-${String(
now.getMonth() + 1
).padStart(2, '0')}-${String(now.getDate()).padStart(
2,
'0'
)}`;
Comment on lines +156 to +161

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
const today = `${now.getFullYear()}-${String(
now.getMonth() + 1
).padStart(2, '0')}-${String(now.getDate()).padStart(
2,
'0'
)}`;
// Get today's date in ISO format YYYY-mm-dd
const today = new Date().toISOString().substr(0,10);

batchUpdateQueryParams({
releaseType: value,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
releaseType: value,
minimumReleaseType: value,

[dateLte]: today,
});
} else {
updateQueryParams('releaseType', value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
updateQueryParams('releaseType', value);
updateQueryParams('minimumReleaseType', value);

}
}}
>
<option value="">
{intl.formatMessage(messages.anyRelease)}
</option>
<option value="2">
{intl.formatMessage(messages.limitedTheatrical)}
</option>
<option value="3">
{intl.formatMessage(messages.theatrical)}
</option>
<option value="4">{intl.formatMessage(messages.digital)}</option>
<option value="5">{intl.formatMessage(messages.physical)}</option>
<option value="6">
{intl.formatMessage(messages.tvRelease)}
</option>
Comment on lines +171 to +184

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The Premiere release type is missing. Is that intentional?

Comment on lines +171 to +184

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The mapping between Release Message Name (e.g. pysical) - Release Type Value (e.g. 5) should probably be stored somewhere in a centralized place and not in the HTML.

I would also add a link to the TMDB source https://developer.themoviedb.org/reference/movie-release-dates

</select>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.studio)}
</span>
Expand Down
5 changes: 5 additions & 0 deletions src/components/Discover/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const QueryFilterOptions = z.object({
sortBy: z.string().optional(),
primaryReleaseDateGte: z.string().optional(),
primaryReleaseDateLte: z.string().optional(),
releaseType: z.enum(['2', '3', '4', '5', '6']).optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
releaseType: z.enum(['2', '3', '4', '5', '6']).optional(),
minimumReleaseType: z.enum(['2', '3', '4', '5', '6']).optional(),

firstAirDateGte: z.string().optional(),
firstAirDateLte: z.string().optional(),
studio: z.string().optional(),
Expand Down Expand Up @@ -138,6 +139,10 @@ export const prepareFilterValues = (
filterValues.primaryReleaseDateLte = values.primaryReleaseDateLte;
}

if (values.releaseType) {
filterValues.releaseType = values.releaseType;
}
Comment on lines +142 to +144

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
if (values.releaseType) {
filterValues.releaseType = values.releaseType;
}
if (values.minimumReleaseType) {
filterValues.minimumReleaseType = values.minimumReleaseType;
}


if (values.firstAirDateGte) {
filterValues.firstAirDateGte = values.firstAirDateGte;
}
Expand Down
7 changes: 7 additions & 0 deletions src/i18n/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,32 @@
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Watchlist",
"components.Discover.DiscoverWatchlist.watchlist": "Plex Watchlist",
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
"components.Discover.FilterSlideover.anyRelease": "Any",
"components.Discover.FilterSlideover.certification": "Content Rating",
"components.Discover.FilterSlideover.clearfilters": "Clear Active Filters",
"components.Discover.FilterSlideover.digital": "Digital",
"components.Discover.FilterSlideover.excludeKeywords": "Exclude Keywords",
"components.Discover.FilterSlideover.filters": "Filters",
"components.Discover.FilterSlideover.firstAirDate": "First Air Date",
"components.Discover.FilterSlideover.from": "From",
"components.Discover.FilterSlideover.genres": "Genres",
"components.Discover.FilterSlideover.keywords": "Keywords",
"components.Discover.FilterSlideover.limitedTheatrical": "Theatrical (Limited)",
"components.Discover.FilterSlideover.minimumRelease": "Minimum Release",
"components.Discover.FilterSlideover.originalLanguage": "Original Language",
"components.Discover.FilterSlideover.physical": "Physical",
"components.Discover.FilterSlideover.ratingText": "Ratings between {minValue} and {maxValue}",
"components.Discover.FilterSlideover.releaseDate": "Release Date",
"components.Discover.FilterSlideover.runtime": "Runtime",
"components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} minute runtime",
"components.Discover.FilterSlideover.status": "Status",
"components.Discover.FilterSlideover.streamingservices": "Streaming Services",
"components.Discover.FilterSlideover.studio": "Studio",
"components.Discover.FilterSlideover.theatrical": "Theatrical",
"components.Discover.FilterSlideover.tmdbuserscore": "TMDB User Score",
"components.Discover.FilterSlideover.tmdbuservotecount": "TMDB User Vote Count",
"components.Discover.FilterSlideover.to": "To",
"components.Discover.FilterSlideover.tvRelease": "TV",
"components.Discover.FilterSlideover.voteCount": "Number of votes between {minValue} and {maxValue}",
"components.Discover.MovieGenreList.moviegenres": "Movie Genres",
"components.Discover.MovieGenreSlider.moviegenres": "Movie Genres",
Expand Down