Compare commits

...

5 Commits

Author SHA1 Message Date
Gauthier
2fb9bda1c6 fix(watchlist): handle undefined Guid for Plex watchlist metadata 2025-09-20 23:06:48 +02:00
fallenbagel
9e737576de fix(dnscaching): check dnsCache before init & support forceipv4 with caching (#1910)
* fix: ensure dnsCache is checked for when its enabled before initialization

previously dnsCache was being initialized even if it was disabled because the previous check was
always returning truthy.

fix #1857

* chore: update dns-caching to 0.2.6

This will allow dns-caching to respect forceIpv4 flag.

* chore: update dns-caching to 0.2.7
2025-09-18 16:30:42 +05:00
0xsysr3ll
cd479d0d17 feat(api): add excludeKeywords parameter to discovery queries (#1908)
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
2025-09-17 03:32:39 +08:00
Gauthier
e9f2f4490f fix(api): catch error when watchlist item doesn't exist anymore (#1907) 2025-09-16 15:42:41 +02:00
Gauthier
d5bf17574f fix(prettier): include sw.js file in .prettierignore (#1885) 2025-09-15 10:23:59 +02:00
11 changed files with 351 additions and 269 deletions

View File

@@ -9,7 +9,11 @@ cypress/config/settings.cypress.json
# assets
src/assets/
public/
!public/sw.js
docs/
!/public/
/public/*
!/public/sw.js
# helm charts
**/charts

View File

@@ -5198,6 +5198,12 @@ paths:
schema:
type: string
example: 1,2
- in: query
name: excludeKeywords
schema:
type: string
example: 3,4
description: Comma-separated list of keyword IDs to exclude from results
- in: query
name: sortBy
schema:
@@ -5518,6 +5524,12 @@ paths:
schema:
type: string
example: 1,2
- in: query
name: excludeKeywords
schema:
type: string
example: 3,4
description: Comma-separated list of keyword IDs to exclude from results
- in: query
name: sortBy
schema:

View File

@@ -57,7 +57,7 @@
"cronstrue": "2.23.0",
"date-fns": "2.29.3",
"dayjs": "1.11.7",
"dns-caching": "^0.2.5",
"dns-caching": "^0.2.7",
"email-templates": "12.0.1",
"email-validator": "2.0.4",
"express": "4.21.2",

536
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -113,7 +113,7 @@ interface MetadataResponse {
ratingKey: string;
type: 'movie' | 'show';
title: string;
Guid: {
Guid?: {
id: `imdb://tt${number}` | `tmdb://${number}` | `tvdb://${number}`;
}[];
}[];
@@ -312,19 +312,32 @@ class PlexTvAPI extends ExternalAPI {
const watchlistDetails = await Promise.all(
(cachedWatchlist?.response.MediaContainer.Metadata ?? []).map(
async (watchlistItem) => {
const detailedResponse = await this.getRolling<MetadataResponse>(
`/library/metadata/${watchlistItem.ratingKey}`,
{
baseURL: 'https://discover.provider.plex.tv',
let detailedResponse: MetadataResponse;
try {
detailedResponse = await this.getRolling<MetadataResponse>(
`/library/metadata/${watchlistItem.ratingKey}`,
{
baseURL: 'https://discover.provider.plex.tv',
}
);
} catch (e) {
if (e.response?.status === 404) {
logger.warn(
`Item with ratingKey ${watchlistItem.ratingKey} not found, it may have been removed from the server.`,
{ label: 'Plex.TV Metadata API' }
);
return null;
} else {
throw e;
}
);
}
const metadata = detailedResponse.MediaContainer.Metadata[0];
const tmdbString = metadata.Guid.find((guid) =>
const tmdbString = metadata.Guid?.find((guid) =>
guid.id.startsWith('tmdb')
);
const tvdbString = metadata.Guid.find((guid) =>
const tvdbString = metadata.Guid?.find((guid) =>
guid.id.startsWith('tvdb')
);
@@ -343,7 +356,9 @@ class PlexTvAPI extends ExternalAPI {
)
);
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
const filteredList = watchlistDetails.filter(
(detail) => detail?.tmdbId
) as PlexWatchlistItem[];
return {
offset,

View File

@@ -86,6 +86,7 @@ interface DiscoverMovieOptions {
genre?: string;
studio?: string;
keywords?: string;
excludeKeywords?: string;
sortBy?: SortOptions;
watchRegion?: string;
watchProviders?: string;
@@ -111,6 +112,7 @@ interface DiscoverTvOptions {
genre?: string;
network?: number;
keywords?: string;
excludeKeywords?: string;
sortBy?: SortOptions;
watchRegion?: string;
watchProviders?: string;
@@ -495,6 +497,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
genre,
studio,
keywords,
excludeKeywords,
withRuntimeGte,
withRuntimeLte,
voteAverageGte,
@@ -545,6 +548,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
with_genres: genre,
with_companies: studio,
with_keywords: keywords,
without_keywords: excludeKeywords,
'with_runtime.gte': withRuntimeGte,
'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte,
@@ -577,6 +581,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
genre,
network,
keywords,
excludeKeywords,
withRuntimeGte,
withRuntimeLte,
voteAverageGte,
@@ -628,6 +633,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
with_genres: genre,
with_networks: network,
with_keywords: keywords,
without_keywords: excludeKeywords,
'with_runtime.gte': withRuntimeGte,
'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte,

View File

@@ -82,7 +82,7 @@ app
}
// Add DNS caching
if (settings.network.dnsCache) {
if (settings.network.dnsCache?.enabled) {
initializeDnsCache({
forceMinTtl: settings.network.dnsCache.forceMinTtl,
forceMaxTtl: settings.network.dnsCache.forceMaxTtl,

View File

@@ -61,6 +61,7 @@ const QueryFilterOptions = z.object({
studio: z.coerce.string().optional(),
genre: z.coerce.string().optional(),
keywords: z.coerce.string().optional(),
excludeKeywords: z.coerce.string().optional(),
language: z.coerce.string().optional(),
withRuntimeGte: z.coerce.string().optional(),
withRuntimeLte: z.coerce.string().optional(),
@@ -90,6 +91,7 @@ discoverRoutes.get('/movies', async (req, res, next) => {
try {
const query = ApiQuerySchema.parse(req.query);
const keywords = query.keywords;
const excludeKeywords = query.excludeKeywords;
const data = await tmdb.getDiscoverMovies({
page: Number(query.page),
@@ -105,6 +107,7 @@ discoverRoutes.get('/movies', async (req, res, next) => {
? new Date(query.primaryReleaseDateGte).toISOString().split('T')[0]
: undefined,
keywords,
excludeKeywords,
withRuntimeGte: query.withRuntimeGte,
withRuntimeLte: query.withRuntimeLte,
voteAverageGte: query.voteAverageGte,
@@ -381,6 +384,7 @@ discoverRoutes.get('/tv', async (req, res, next) => {
try {
const query = ApiQuerySchema.parse(req.query);
const keywords = query.keywords;
const excludeKeywords = query.excludeKeywords;
const data = await tmdb.getDiscoverTv({
page: Number(query.page),
sortBy: query.sortBy as SortOptions,
@@ -395,6 +399,7 @@ discoverRoutes.get('/tv', async (req, res, next) => {
: undefined,
originalLanguage: query.language,
keywords,
excludeKeywords,
withRuntimeGte: query.withRuntimeGte,
withRuntimeLte: query.withRuntimeLte,
voteAverageGte: query.voteAverageGte,

View File

@@ -33,6 +33,7 @@ const messages = defineMessages('components.Discover.FilterSlideover', {
studio: 'Studio',
genres: 'Genres',
keywords: 'Keywords',
excludeKeywords: 'Exclude Keywords',
originalLanguage: 'Original Language',
runtimeText: '{minValue}-{maxValue} minute runtime',
ratingText: 'Ratings between {minValue} and {maxValue}',
@@ -181,6 +182,19 @@ const FilterSlideover = ({
updateQueryParams('keywords', value?.map((v) => v.value).join(','));
}}
/>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.excludeKeywords)}
</span>
<KeywordSelector
defaultValue={currentFilters.excludeKeywords}
isMulti
onChange={(value) => {
updateQueryParams(
'excludeKeywords',
value?.map((v) => v.value).join(',')
);
}}
/>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.originalLanguage)}
</span>

View File

@@ -99,6 +99,7 @@ export const QueryFilterOptions = z.object({
studio: z.string().optional(),
genre: z.string().optional(),
keywords: z.string().optional(),
excludeKeywords: z.string().optional(),
language: z.string().optional(),
withRuntimeGte: z.string().optional(),
withRuntimeLte: z.string().optional(),
@@ -161,6 +162,10 @@ export const prepareFilterValues = (
filterValues.keywords = values.keywords;
}
if (values.excludeKeywords) {
filterValues.excludeKeywords = values.excludeKeywords;
}
if (values.language) {
filterValues.language = values.language;
}

View File

@@ -78,6 +78,7 @@
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
"components.Discover.FilterSlideover.certification": "Content Rating",
"components.Discover.FilterSlideover.clearfilters": "Clear Active Filters",
"components.Discover.FilterSlideover.excludeKeywords": "Exclude Keywords",
"components.Discover.FilterSlideover.filters": "Filters",
"components.Discover.FilterSlideover.firstAirDate": "First Air Date",
"components.Discover.FilterSlideover.from": "From",