Compare commits

..

2 Commits

Author SHA1 Message Date
0xsysr3ll
ed3a9b1c8d fix(availability-sync): added some diagnostic logging
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
2026-01-20 23:10:24 +01:00
0xsysr3ll
8431ef3f3b fix(availability-sync): remove incorrect existence flags for media in Radarr, Sonarr, and Plex
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
2026-01-20 20:07:16 +01:00
6 changed files with 129 additions and 162 deletions

View File

@@ -41,7 +41,6 @@
"@formatjs/swc-plugin-experimental": "^0.4.0",
"@headlessui/react": "1.7.12",
"@heroicons/react": "2.2.0",
"@seerr-team/react-tailwindcss-datepicker": "^1.3.4",
"@supercharge/request-ip": "1.2.0",
"@svgr/webpack": "6.5.1",
"@tanem/react-nprogress": "5.0.56",
@@ -91,6 +90,7 @@
"react-popper-tooltip": "4.4.2",
"react-select": "5.10.2",
"react-spring": "9.7.1",
"react-tailwindcss-datepicker-sct": "1.3.4",
"react-toast-notifications": "2.5.1",
"react-transition-group": "^4.4.5",
"react-truncate-markup": "5.1.2",

28
pnpm-lock.yaml generated
View File

@@ -32,9 +32,6 @@ importers:
'@heroicons/react':
specifier: 2.2.0
version: 2.2.0(react@18.3.1)
'@seerr-team/react-tailwindcss-datepicker':
specifier: ^1.3.4
version: 1.3.4(dayjs@1.11.19)(react@18.3.1)
'@supercharge/request-ip':
specifier: 1.2.0
version: 1.2.0
@@ -182,6 +179,9 @@ importers:
react-spring:
specifier: 9.7.1
version: 9.7.1(@react-three/fiber@8.16.8(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)(three@0.165.0))(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.10(@types/react@18.3.3)(konva@9.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(encoding@0.1.13)(react@18.3.1))(react-zdog@1.2.2)(react@18.3.1)(three@0.165.0)(zdog@1.1.3)
react-tailwindcss-datepicker-sct:
specifier: 1.3.4
version: 1.3.4(dayjs@1.11.19)(react@18.3.1)
react-toast-notifications:
specifier: 2.5.1
version: 2.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -2992,12 +2992,6 @@ packages:
'@rushstack/eslint-patch@1.10.3':
resolution: {integrity: sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==}
'@seerr-team/react-tailwindcss-datepicker@1.3.4':
resolution: {integrity: sha512-KZrnl6WL1lvUnAG4RZIkReJ+E0vSpOtMEuatobMqiWAa5Y+Z3d0ZcOOJWHoeRNtF19sIzzBkuLyhFNFlNtXg3A==}
peerDependencies:
dayjs: ^1.11.6
react: ^17.0.2 || ^18.2.0
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
@@ -7955,6 +7949,12 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react-tailwindcss-datepicker-sct@1.3.4:
resolution: {integrity: sha512-QlLekGZDbmW2DPGS33c4gfIxkk4gcgu4sRzBIm4/mZxfHuo7J+GR6SBVNIb5Xh8aCLlGtgyLqD+o0UmOVFIc4w==}
peerDependencies:
dayjs: ^1.11.6
react: ^17.0.2 || ^18.2.0
react-toast-notifications@2.5.1:
resolution: {integrity: sha512-eYuuiSPGLyuMHojRH2U7CbENvFHsvNia39pLM/s10KipIoNs14T7RIJk4aU2N+l++OsSgtJqnFObx9bpwLMU5A==}
peerDependencies:
@@ -13179,11 +13179,6 @@ snapshots:
'@rushstack/eslint-patch@1.10.3': {}
'@seerr-team/react-tailwindcss-datepicker@1.3.4(dayjs@1.11.19)(react@18.3.1)':
dependencies:
dayjs: 1.11.19
react: 18.3.1
'@selderee/plugin-htmlparser2@0.11.0':
dependencies:
domhandler: 5.0.3
@@ -19190,6 +19185,11 @@ snapshots:
- three
- zdog
react-tailwindcss-datepicker-sct@1.3.4(dayjs@1.11.19)(react@18.3.1):
dependencies:
dayjs: 1.11.19
react: 18.3.1
react-toast-notifications@2.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@emotion/core': 10.3.1(react@18.3.1)

View File

@@ -332,16 +332,9 @@ export class MediaRequest {
if (requestBody.mediaType === MediaType.MOVIE) {
await mediaRepository.save(media);
if (!media.id) {
throw new Error(
`Failed to save media before creating request. Media type: ${requestBody.mediaType}, TMDB ID: ${requestBody.mediaId}, persisted media id: ${media.id}`
);
}
const request = new MediaRequest({
type: MediaType.MOVIE,
media,
mediaId: media.id,
requestedBy: requestUser,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: user.hasPermission(
@@ -449,16 +442,9 @@ export class MediaRequest {
await mediaRepository.save(media);
if (!media.id) {
throw new Error(
`Failed to save media before creating request. Media type: TV, TMDB ID: ${requestBody.mediaId}, is4k: ${requestBody.is4k}`
);
}
const request = new MediaRequest({
type: MediaType.TV,
media,
mediaId: media.id,
requestedBy: requestUser,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: user.hasPermission(
@@ -535,9 +521,6 @@ export class MediaRequest {
})
public media: Media;
@Column({ name: 'mediaId', nullable: true })
public mediaId: number;
@ManyToOne(() => User, (user) => user.requests, {
eager: true,
onDelete: 'CASCADE',

View File

@@ -143,7 +143,9 @@ class AvailabilitySync {
const { existsInPlex: existsInPlex4k } =
await this.mediaExistsInPlex(media, true);
if (existsInPlex || existsInRadarr) {
// Media must exist in Plex to be considered available
// If it exists in Radarr but not in Plex, it should be marked as deleted
if (existsInPlex) {
movieExists = true;
logger.info(
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
@@ -153,7 +155,7 @@ class AvailabilitySync {
);
}
if (existsInPlex4k || existsInRadarr4k) {
if (existsInPlex4k) {
movieExists4k = true;
logger.info(
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
@@ -240,7 +242,9 @@ class AvailabilitySync {
//plex
if (mediaServerType === MediaServerType.PLEX) {
if (existsInPlex || existsInSonarr) {
// Media must exist in Plex to be considered available
// If it exists in Sonarr but not in Plex, it should be marked as deleted
if (existsInPlex) {
showExists = true;
logger.info(
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
@@ -252,7 +256,7 @@ class AvailabilitySync {
}
if (mediaServerType === MediaServerType.PLEX) {
if (existsInPlex4k || existsInSonarr4k) {
if (existsInPlex4k) {
showExists4k = true;
logger.info(
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
@@ -611,24 +615,21 @@ class AvailabilitySync {
is4k: boolean
): Promise<boolean> {
let existsInRadarr = false;
const externalServiceId = is4k
? media.externalServiceId4k
: media.externalServiceId;
const has4kServer = this.radarrServers.some((s) => s.is4k);
const hasNon4kServer = this.radarrServers.some((s) => !s.is4k);
logger.debug(
`Checking Radarr for ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
media.tmdbId
}]`,
{
label: 'AvailabilitySync',
has4kServer,
hasNon4kServer,
externalServiceId: media.externalServiceId,
externalServiceId4k: media.externalServiceId4k,
serversToCheck: this.radarrServers.filter((s) => s.is4k === is4k)
.length,
}
);
if (!externalServiceId) {
logger.debug(
`Skipping Radarr check for ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
media.tmdbId
}] - no externalServiceId available`,
{
label: 'Availability Sync',
}
);
return false;
}
// Check for availability in all of the available radarr servers
// If any find the media, we will assume the media exists
@@ -655,70 +656,63 @@ class AvailabilitySync {
});
}
if (radarr && radarr.hasFile) {
const resolution =
radarr?.movieFile?.mediaInfo?.resolution?.split('x');
const is4kMovie =
resolution?.length === 2 && Number(resolution[0]) >= 2000;
logger.debug(
`Radarr file found for movie [TMDB ID ${media.tmdbId}]`,
{
label: 'AvailabilitySync',
serverId: server.id,
serverIs4k: server.is4k,
hasFile: radarr.hasFile,
resolution: radarr?.movieFile?.mediaInfo?.resolution,
parsedWidth: resolution?.[0],
is4kMovie,
checkingFor: is4k ? '4K' : 'non-4K',
if (radarr) {
if (radarr.hasFile) {
const resolution =
radarr?.movieFile?.mediaInfo?.resolution?.split('x');
const is4kMovie =
resolution?.length === 2 && Number(resolution[0]) >= 2000;
const matches4k = is4k ? is4kMovie : !is4kMovie;
if (matches4k) {
existsInRadarr = true;
logger.debug(
`Found ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
media.tmdbId
}] in Radarr`,
{
radarrId: radarr.id,
radarrTitle: radarr.title,
hasFile: radarr.hasFile,
externalServiceId: externalServiceId,
label: 'Availability Sync',
}
);
} else {
logger.debug(
`Movie [TMDB ID ${media.tmdbId}] found in Radarr but resolution doesn't match (is4k: ${is4k}, movie resolution: ${radarr?.movieFile?.mediaInfo?.resolution})`,
{
label: 'Availability Sync',
}
);
}
);
if (has4kServer && hasNon4kServer) {
// User has both server types so use resolution to distinguish
// This handles the case where same content exists in both qualities
existsInRadarr = is4k ? is4kMovie : !is4kMovie;
logger.debug(
`Dual-server setup: using resolution check for movie [TMDB ID ${media.tmdbId}]`,
{
label: 'AvailabilitySync',
is4kMovie,
is4kCheck: is4k,
existsInRadarr,
}
);
} else {
// User only has one server type so if file exists, count it
// Don't penalize users whose Radarr upgrades to 4K on a non-4K server
existsInRadarr = true;
logger.debug(
`Single-server setup: file exists, marking as available for movie [TMDB ID ${media.tmdbId}]`,
`Movie [TMDB ID ${media.tmdbId}] found in Radarr but has no file`,
{
label: 'AvailabilitySync',
is4kMovie,
is4kCheck: is4k,
existsInRadarr,
radarrId: radarr.id,
radarrTitle: radarr.title,
externalServiceId: externalServiceId,
label: 'Availability Sync',
}
);
}
} else {
logger.debug(`Radarr response for movie [TMDB ID ${media.tmdbId}]`, {
label: 'AvailabilitySync',
serverId: server.id,
found: !!radarr,
hasFile: radarr?.hasFile ?? false,
});
}
} catch (ex) {
if (!ex.message.includes('404')) {
existsInRadarr = true;
if (ex.message.includes('404')) {
logger.debug(
`Movie [TMDB ID ${media.tmdbId}] not found in Radarr (404) - externalServiceId may be stale: ${externalServiceId}`,
{
label: 'Availability Sync',
}
);
} else {
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${
media.tmdbId
}] from Radarr.`,
{
errorMessage: ex.message,
externalServiceId: externalServiceId,
label: 'Availability Sync',
}
);
@@ -766,7 +760,6 @@ class AvailabilitySync {
}
} catch (ex) {
if (!ex.message.includes('404')) {
existsInSonarr = true;
preventSeasonSearch = true;
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${
@@ -865,75 +858,66 @@ class AvailabilitySync {
// We can use the cache we built when we fetched the series with mediaExistsInPlex
try {
let plexMedia: PlexMetadata | undefined;
const currentRatingKey = is4k ? ratingKey4k : ratingKey;
if (ratingKey && !is4k) {
plexMedia = await this.plexClient?.getMetadata(ratingKey);
if (!currentRatingKey) {
logger.debug(
`Skipping Plex check for ${is4k ? '4K' : 'non-4K'} ${
media.mediaType === 'tv' ? 'show' : 'movie'
} [TMDB ID ${media.tmdbId}] - no ratingKey available`,
{
label: 'Availability Sync',
}
);
} else {
if (ratingKey && !is4k) {
plexMedia = await this.plexClient?.getMetadata(ratingKey);
if (media.mediaType === 'tv') {
this.plexSeasonsCache[ratingKey] =
await this.plexClient?.getChildrenMetadata(ratingKey);
if (media.mediaType === 'tv') {
this.plexSeasonsCache[ratingKey] =
await this.plexClient?.getChildrenMetadata(ratingKey);
}
}
}
if (ratingKey4k && is4k) {
plexMedia = await this.plexClient?.getMetadata(ratingKey4k);
if (ratingKey4k && is4k) {
plexMedia = await this.plexClient?.getMetadata(ratingKey4k);
if (media.mediaType === 'tv') {
this.plexSeasonsCache[ratingKey4k] =
await this.plexClient?.getChildrenMetadata(ratingKey4k);
if (media.mediaType === 'tv') {
this.plexSeasonsCache[ratingKey4k] =
await this.plexClient?.getChildrenMetadata(ratingKey4k);
}
}
if (plexMedia) {
if (ratingKey === ratingKey4k) {
plexMedia = undefined;
}
if (
plexMedia &&
media.mediaType === 'movie' &&
!plexMedia.Media?.some(
(mediaItem) => (mediaItem.width ?? 0) >= 2000
)
) {
plexMedia = undefined;
}
if (plexMedia && media.mediaType === 'tv') {
const cachedSeasons = this.plexSeasonsCache[ratingKey4k];
if (cachedSeasons?.length) {
let has4kInAnySeason = false;
for (const season of cachedSeasons) {
try {
const episodes = await this.plexClient?.getChildrenMetadata(
season.ratingKey
);
const has4kEpisode = episodes?.some((episode) =>
episode.Media?.some(
(mediaItem) => (mediaItem.width ?? 0) >= 2000
)
);
if (has4kEpisode) {
has4kInAnySeason = true;
break;
}
} catch {
// If we can't fetch episodes for a season, continue checking other seasons
}
}
if (!has4kInAnySeason) {
plexMedia = undefined;
}
existsInPlex = true;
logger.debug(
`Found ${is4k ? '4K' : 'non-4K'} ${
media.mediaType === 'tv' ? 'show' : 'movie'
} [TMDB ID ${media.tmdbId}] in Plex`,
{
ratingKey: is4k ? ratingKey4k : ratingKey,
plexTitle: plexMedia.title,
plexRatingKey: plexMedia.ratingKey,
plexGuid: plexMedia.guid,
label: 'Availability Sync',
}
}
);
}
}
if (plexMedia) {
existsInPlex = true;
}
} catch (ex) {
if (!ex.message.includes('404')) {
existsInPlex = true;
if (ex.message.includes('404')) {
logger.debug(
`Media ${is4k ? '4K' : 'non-4K'} ${
media.mediaType === 'tv' ? 'show' : 'movie'
} [TMDB ID ${
media.tmdbId
}] not found in Plex (404) - ratingKey may be stale`,
{
ratingKey: is4k ? ratingKey4k : ratingKey,
label: 'Availability Sync',
}
);
} else {
preventSeasonSearch = true;
logger.debug(
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${

View File

@@ -19,8 +19,8 @@ import {
} from '@app/hooks/useUpdateQueryParams';
import defineMessages from '@app/utils/defineMessages';
import { XCircleIcon } from '@heroicons/react/24/outline';
import Datepicker from '@seerr-team/react-tailwindcss-datepicker';
import { useIntl } from 'react-intl';
import Datepicker from 'react-tailwindcss-datepicker-sct';
const messages = defineMessages('components.Discover.FilterSlideover', {
filters: 'Filters',

View File

@@ -5,7 +5,7 @@ const defaultTheme = require('tailwindcss/defaultTheme');
module.exports = {
mode: 'jit',
content: [
'./node_modules/@seerr-team/react-tailwindcss-datepicker/dist/index.esm.js',
'./node_modules/react-tailwindcss-datepicker-sct/dist/index.esm.js',
'./src/pages/**/*.{ts,tsx}',
'./src/components/**/*.{ts,tsx}',
],