Compare commits

..

1 Commits

Author SHA1 Message Date
gauthier-th
dcea6e965d fix(proxy): initialize image proxies after the proxy is set up
The ImageProxy for TMDB and TheTVDB were initialized before the proxy settings were set up, so they
were ignoring the proxy settings.

fix #1787
2025-07-20 12:02:18 +02:00
12 changed files with 62 additions and 210 deletions

View File

@@ -141,83 +141,14 @@ components:
UserSettings: UserSettings:
type: object type: object
properties: properties:
username:
type: string
nullable: true
example: 'Mr User'
email:
type: string
example: 'user@example.com'
discordId:
type: string
nullable: true
example: '123456789'
locale: locale:
type: string type: string
nullable: true
example: 'en'
discoverRegion: discoverRegion:
type: string type: string
nullable: true
example: 'US'
streamingRegion:
type: string
nullable: true
example: 'US'
originalLanguage: originalLanguage:
type: string type: string
nullable: true streamingRegion:
example: 'en' type: string
movieQuotaLimit:
type: number
nullable: true
description: 'Maximum number of movie requests allowed'
example: 10
movieQuotaDays:
type: number
nullable: true
description: 'Time period in days for movie quota'
example: 30
tvQuotaLimit:
type: number
nullable: true
description: 'Maximum number of TV requests allowed'
example: 5
tvQuotaDays:
type: number
nullable: true
description: 'Time period in days for TV quota'
example: 14
globalMovieQuotaDays:
type: number
nullable: true
description: 'Global movie quota days setting'
example: 30
globalMovieQuotaLimit:
type: number
nullable: true
description: 'Global movie quota limit setting'
example: 10
globalTvQuotaLimit:
type: number
nullable: true
description: 'Global TV quota limit setting'
example: 5
globalTvQuotaDays:
type: number
nullable: true
description: 'Global TV quota days setting'
example: 14
watchlistSyncMovies:
type: boolean
nullable: true
description: 'Enable watchlist sync for movies'
example: true
watchlistSyncTv:
type: boolean
nullable: true
description: 'Enable watchlist sync for TV'
example: false
MainSettings: MainSettings:
type: object type: object
properties: properties:
@@ -4538,7 +4469,11 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/UserSettings' type: object
properties:
username:
type: string
example: 'Mr User'
post: post:
summary: Update general settings for a user summary: Update general settings for a user
description: Updates and returns general settings for a specific user. Requires `MANAGE_USERS` permission if editing other users. description: Updates and returns general settings for a specific user. Requires `MANAGE_USERS` permission if editing other users.
@@ -4555,14 +4490,22 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/UserSettings' type: object
properties:
username:
type: string
nullable: true
responses: responses:
'200': '200':
description: Updated user general settings returned description: Updated user general settings returned
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/UserSettings' type: object
properties:
username:
type: string
example: 'Mr User'
/user/{userId}/settings/password: /user/{userId}/settings/password:
get: get:
summary: Get password page informatiom summary: Get password page informatiom
@@ -6656,16 +6599,9 @@ paths:
example: '1' example: '1'
schema: schema:
type: string type: string
- in: query
name: is4k
description: Whether to remove from 4K service instance (true) or regular service instance (false)
required: false
example: false
schema:
type: boolean
responses: responses:
'204': '204':
description: Successfully removed media item description: Succesfully removed media item
/media/{mediaId}/{status}: /media/{mediaId}/{status}:
post: post:
summary: Update media status summary: Update media status
@@ -7332,22 +7268,11 @@ paths:
example: 1 example: 1
responses: responses:
'200': '200':
description: Keyword returned (null if not found) description: Keyword returned
content: content:
application/json: application/json:
schema: schema:
nullable: true
$ref: '#/components/schemas/Keyword' $ref: '#/components/schemas/Keyword'
'500':
description: Internal server error
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: 'Unable to retrieve keyword data.'
/watchproviders/regions: /watchproviders/regions:
get: get:
summary: Get watch provider regions summary: Get watch provider regions

View File

@@ -1054,7 +1054,7 @@ class TheMovieDb extends ExternalAPI {
keywordId, keywordId,
}: { }: {
keywordId: number; keywordId: number;
}): Promise<TmdbKeyword | null> { }): Promise<TmdbKeyword> {
try { try {
const data = await this.get<TmdbKeyword>( const data = await this.get<TmdbKeyword>(
`/keyword/${keywordId}`, `/keyword/${keywordId}`,
@@ -1064,9 +1064,6 @@ class TheMovieDb extends ExternalAPI {
return data; return data;
} catch (e) { } catch (e) {
if (e.response?.status === 404) {
return null;
}
throw new Error(`[TMDB] Failed to fetch keyword: ${e.message}`); throw new Error(`[TMDB] Failed to fetch keyword: ${e.message}`);
} }
} }

View File

@@ -72,7 +72,6 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
const blacklistedTagsArr = blacklistedTags.split(','); const blacklistedTagsArr = blacklistedTags.split(',');
const pageLimit = settings.main.blacklistedTagsLimit; const pageLimit = settings.main.blacklistedTagsLimit;
const invalidKeywords = new Set<string>();
if (blacklistedTags.length === 0) { if (blacklistedTags.length === 0) {
return; return;
@@ -88,19 +87,6 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
// Iterate for each tag // Iterate for each tag
for (const tag of blacklistedTagsArr) { for (const tag of blacklistedTagsArr) {
const keywordDetails = await tmdb.getKeywordDetails({
keywordId: Number(tag),
});
if (keywordDetails === null) {
logger.warn('Skipping invalid keyword in blacklisted tags', {
label: 'Blacklisted Tags Processor',
keywordId: tag,
});
invalidKeywords.add(tag);
continue;
}
let queryMax = pageLimit * SortOptionsIterable.length; let queryMax = pageLimit * SortOptionsIterable.length;
let fixedSortMode = false; // Set to true when the page limit allows for getting every page of tag let fixedSortMode = false; // Set to true when the page limit allows for getting every page of tag
@@ -116,51 +102,24 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
throw new AbortTransaction(); throw new AbortTransaction();
} }
try { const response = await getDiscover({
const response = await getDiscover({ page,
page, sortBy,
sortBy, keywords: tag,
keywords: tag, });
}); await this.processResults(response, tag, type, em);
await new Promise((res) => setTimeout(res, TMDB_API_DELAY_MS));
await this.processResults(response, tag, type, em); this.progress++;
await new Promise((res) => setTimeout(res, TMDB_API_DELAY_MS)); if (page === 1 && response.total_pages <= queryMax) {
// We will finish the tag with less queries than expected, move progress accordingly
this.progress++; this.progress += queryMax - response.total_pages;
if (page === 1 && response.total_pages <= queryMax) { fixedSortMode = true;
// We will finish the tag with less queries than expected, move progress accordingly queryMax = response.total_pages;
this.progress += queryMax - response.total_pages;
fixedSortMode = true;
queryMax = response.total_pages;
}
} catch (error) {
logger.error('Error processing keyword in blacklisted tags', {
label: 'Blacklisted Tags Processor',
keywordId: tag,
errorMessage: error.message,
});
} }
} }
} }
} }
if (invalidKeywords.size > 0) {
const currentTags = blacklistedTagsArr.filter(
(tag) => !invalidKeywords.has(tag)
);
const cleanedTags = currentTags.join(',');
if (cleanedTags !== blacklistedTags) {
settings.main.blacklistedTags = cleanedTags;
await settings.save();
logger.info('Cleaned up invalid keywords from settings', {
label: 'Blacklisted Tags Processor',
removedKeywords: Array.from(invalidKeywords),
newBlacklistedTags: cleanedTags,
});
}
}
} }
private async processResults( private async processResults(

View File

@@ -128,15 +128,11 @@ discoverRoutes.get('/movies', async (req, res, next) => {
if (keywords) { if (keywords) {
const splitKeywords = keywords.split(','); const splitKeywords = keywords.split(',');
const keywordResults = await Promise.all( keywordData = await Promise.all(
splitKeywords.map(async (keywordId) => { splitKeywords.map(async (keywordId) => {
return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) }); return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) });
}) })
); );
keywordData = keywordResults.filter(
(keyword): keyword is TmdbKeyword => keyword !== null
);
} }
return res.status(200).json({ return res.status(200).json({
@@ -419,15 +415,11 @@ discoverRoutes.get('/tv', async (req, res, next) => {
if (keywords) { if (keywords) {
const splitKeywords = keywords.split(','); const splitKeywords = keywords.split(',');
const keywordResults = await Promise.all( keywordData = await Promise.all(
splitKeywords.map(async (keywordId) => { splitKeywords.map(async (keywordId) => {
return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) }); return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) });
}) })
); );
keywordData = keywordResults.filter(
(keyword): keyword is TmdbKeyword => keyword !== null
);
} }
return res.status(200).json({ return res.status(200).json({

View File

@@ -197,10 +197,8 @@ mediaRoutes.delete(
const media = await mediaRepository.findOneOrFail({ const media = await mediaRepository.findOneOrFail({
where: { id: Number(req.params.id) }, where: { id: Number(req.params.id) },
}); });
const is4k = media.serviceUrl4k !== undefined;
const is4k = req.query.is4k === 'true';
const isMovie = media.mediaType === MediaType.MOVIE; const isMovie = media.mediaType === MediaType.MOVIE;
let serviceSettings; let serviceSettings;
if (isMovie) { if (isMovie) {
serviceSettings = settings.radarr.find( serviceSettings = settings.radarr.find(
@@ -227,7 +225,6 @@ mediaRoutes.delete(
); );
} }
} }
if (!serviceSettings) { if (!serviceSettings) {
logger.warn( logger.warn(
`There is no default ${ `There is no default ${
@@ -242,7 +239,6 @@ mediaRoutes.delete(
); );
return; return;
} }
let service; let service;
if (isMovie) { if (isMovie) {
service = new RadarrAPI({ service = new RadarrAPI({

View File

@@ -29,10 +29,14 @@ const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => {
const keywordIds = data.blacklistedTags.slice(1, -1).split(','); const keywordIds = data.blacklistedTags.slice(1, -1).split(',');
Promise.all( Promise.all(
keywordIds.map(async (keywordId) => { keywordIds.map(async (keywordId) => {
const { data } = await axios.get<Keyword | null>( try {
`/api/v1/keyword/${keywordId}` const { data } = await axios.get<Keyword>(
); `/api/v1/keyword/${keywordId}`
return data?.name || `[Invalid: ${keywordId}]`; );
return data.name;
} catch (err) {
return '';
}
}) })
).then((keywords) => { ).then((keywords) => {
setTagNamesBlacklistedFor(keywords.join(', ')); setTagNamesBlacklistedFor(keywords.join(', '));

View File

@@ -5,10 +5,7 @@ import { encodeURIExtraParams } from '@app/hooks/useDiscover';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { ArrowDownIcon } from '@heroicons/react/24/solid'; import { ArrowDownIcon } from '@heroicons/react/24/solid';
import type { import type { TmdbKeywordSearchResponse } from '@server/api/themoviedb/interfaces';
TmdbKeyword,
TmdbKeywordSearchResponse,
} from '@server/api/themoviedb/interfaces';
import type { Keyword } from '@server/models/common'; import type { Keyword } from '@server/models/common';
import axios from 'axios'; import axios from 'axios';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
@@ -127,19 +124,15 @@ const ControlledKeywordSelector = ({
const keywords = await Promise.all( const keywords = await Promise.all(
defaultValue.split(',').map(async (keywordId) => { defaultValue.split(',').map(async (keywordId) => {
const { data } = await axios.get<Keyword | null>( const { data } = await axios.get<Keyword>(
`/api/v1/keyword/${keywordId}` `/api/v1/keyword/${keywordId}`
); );
return data; return data;
}) })
); );
const validKeywords: TmdbKeyword[] = keywords.filter(
(keyword): keyword is TmdbKeyword => keyword !== null
);
onChange( onChange(
validKeywords.map((keyword) => ({ keywords.map((keyword) => ({
label: keyword.name, label: keyword.name,
value: keyword.id, value: keyword.id,
})) }))

View File

@@ -77,19 +77,16 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
const keywords = await Promise.all( const keywords = await Promise.all(
slider.data.split(',').map(async (keywordId) => { slider.data.split(',').map(async (keywordId) => {
const keyword = await axios.get<Keyword | null>( const keyword = await axios.get<Keyword>(
`/api/v1/keyword/${keywordId}` `/api/v1/keyword/${keywordId}`
); );
return keyword.data; return keyword.data;
}) })
); );
const validKeywords: Keyword[] = keywords.filter(
(keyword): keyword is Keyword => keyword !== null
);
setDefaultDataValue( setDefaultDataValue(
validKeywords.map((keyword) => ({ keywords.map((keyword) => ({
label: keyword.name, label: keyword.name,
value: keyword.id, value: keyword.id,
})) }))

View File

@@ -118,11 +118,9 @@ const ManageSlideOver = ({
} }
}; };
const deleteMediaFile = async (is4k = false) => { const deleteMediaFile = async () => {
if (data.mediaInfo) { if (data.mediaInfo) {
await axios.delete( await axios.delete(`/api/v1/media/${data.mediaInfo.id}/file`);
`/api/v1/media/${data.mediaInfo.id}/file?is4k=${is4k}`
);
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
revalidate(); revalidate();
onClose(); onClose();
@@ -416,7 +414,7 @@ const ManageSlideOver = ({
isDefaultService() && ( isDefaultService() && (
<div> <div>
<ConfirmButton <ConfirmButton
onClick={() => deleteMediaFile(false)} onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage( confirmText={intl.formatMessage(
globalMessages.areyousure globalMessages.areyousure
)} )}
@@ -575,7 +573,7 @@ const ManageSlideOver = ({
{isDefaultService() && ( {isDefaultService() && (
<div> <div>
<ConfirmButton <ConfirmButton
onClick={() => deleteMediaFile(true)} onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage( confirmText={intl.formatMessage(
globalMessages.areyousure globalMessages.areyousure
)} )}

View File

@@ -343,9 +343,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
const deleteMediaFile = async () => { const deleteMediaFile = async () => {
if (request.media) { if (request.media) {
await axios.delete( await axios.delete(`/api/v1/media/${request.media.id}/file`);
`/api/v1/media/${request.media.id}/file?is4k=${request.is4k}`
);
await axios.delete(`/api/v1/media/${request.media.id}`); await axios.delete(`/api/v1/media/${request.media.id}`);
revalidateList(); revalidateList();
} }

View File

@@ -309,19 +309,16 @@ export const KeywordSelector = ({
const keywords = await Promise.all( const keywords = await Promise.all(
defaultValue.split(',').map(async (keywordId) => { defaultValue.split(',').map(async (keywordId) => {
const keyword = await axios.get<Keyword | null>( const keyword = await axios.get<Keyword>(
`/api/v1/keyword/${keywordId}` `/api/v1/keyword/${keywordId}`
); );
return keyword.data; return keyword.data;
}) })
); );
const validKeywords: Keyword[] = keywords.filter(
(keyword): keyword is Keyword => keyword !== null
);
setDefaultDataValue( setDefaultDataValue(
validKeywords.map((keyword) => ({ keywords.map((keyword) => ({
label: keyword.name, label: keyword.name,
value: keyword.id, value: keyword.id,
})) }))

View File

@@ -113,16 +113,12 @@ const OverrideRuleTiles = ({
.flat() .flat()
.filter((keywordId) => keywordId) .filter((keywordId) => keywordId)
.map(async (keywordId) => { .map(async (keywordId) => {
const response = await axios.get<Keyword | null>( const response = await axios.get(`/api/v1/keyword/${keywordId}`);
`/api/v1/keyword/${keywordId}` const keyword: Keyword = response.data;
); return keyword;
return response.data;
}) })
); );
const validKeywords: Keyword[] = keywords.filter( setKeywords(keywords);
(keyword): keyword is Keyword => keyword !== null
);
setKeywords(validKeywords);
const allUsersFromRules = rules const allUsersFromRules = rules
.map((rule) => rule.users) .map((rule) => rule.users)
.filter((users) => users) .filter((users) => users)