Compare commits
2 Commits
preview-me
...
fix-user-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4933748f2b | ||
|
|
2da404953b |
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import availabilitySync from '@server/lib/availabilitySync';
|
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import type { AxiosInstance } from 'axios';
|
import type { AxiosInstance } from 'axios';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@@ -242,9 +241,7 @@ class JellyfinAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getItemData(
|
public async getItemData(id: string): Promise<JellyfinLibraryItemExtended> {
|
||||||
id: string
|
|
||||||
): Promise<JellyfinLibraryItemExtended | undefined> {
|
|
||||||
try {
|
try {
|
||||||
const contents = await this.axios.get<any>(
|
const contents = await this.axios.get<any>(
|
||||||
`/Users/${this.userId}/Items/${id}`
|
`/Users/${this.userId}/Items/${id}`
|
||||||
@@ -252,11 +249,6 @@ class JellyfinAPI {
|
|||||||
|
|
||||||
return contents.data;
|
return contents.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (availabilitySync.running) {
|
|
||||||
if (e.response && e.response.status === 500) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ interface SyncStatus {
|
|||||||
libraries: Library[];
|
libraries: Library[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class JellyfinScanner {
|
class JobJellyfinSync {
|
||||||
private sessionId: string;
|
private sessionId: string;
|
||||||
private tmdb: TheMovieDb;
|
private tmdb: TheMovieDb;
|
||||||
private jfClient: JellyfinAPI;
|
private jfClient: JellyfinAPI;
|
||||||
@@ -62,7 +62,7 @@ class JellyfinScanner {
|
|||||||
const metadata = await this.jfClient.getItemData(jellyfinitem.Id);
|
const metadata = await this.jfClient.getItemData(jellyfinitem.Id);
|
||||||
const newMedia = new Media();
|
const newMedia = new Media();
|
||||||
|
|
||||||
if (!metadata?.Id) {
|
if (!metadata.Id) {
|
||||||
logger.debug('No Id metadata for this title. Skipping', {
|
logger.debug('No Id metadata for this title. Skipping', {
|
||||||
label: 'Plex Sync',
|
label: 'Plex Sync',
|
||||||
ratingKey: jellyfinitem.Id,
|
ratingKey: jellyfinitem.Id,
|
||||||
@@ -197,14 +197,6 @@ class JellyfinScanner {
|
|||||||
jellyfinitem.SeriesId ?? jellyfinitem.SeasonId ?? jellyfinitem.Id;
|
jellyfinitem.SeriesId ?? jellyfinitem.SeasonId ?? jellyfinitem.Id;
|
||||||
const metadata = await this.jfClient.getItemData(Id);
|
const metadata = await this.jfClient.getItemData(Id);
|
||||||
|
|
||||||
if (!metadata?.Id) {
|
|
||||||
logger.debug('No Id metadata for this title. Skipping', {
|
|
||||||
label: 'Plex Sync',
|
|
||||||
ratingKey: jellyfinitem.Id,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.ProviderIds.Tvdb) {
|
if (metadata.ProviderIds.Tvdb) {
|
||||||
tvShow = await this.tmdb.getShowByTvdbId({
|
tvShow = await this.tmdb.getShowByTvdbId({
|
||||||
tvdbId: Number(metadata.ProviderIds.Tvdb),
|
tvdbId: Number(metadata.ProviderIds.Tvdb),
|
||||||
@@ -283,7 +275,7 @@ class JellyfinScanner {
|
|||||||
episode.Id
|
episode.Id
|
||||||
);
|
);
|
||||||
|
|
||||||
ExtendedEpisodeData?.MediaSources?.some((MediaSource) => {
|
ExtendedEpisodeData.MediaSources?.some((MediaSource) => {
|
||||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||||
if (MediaStream.Type === 'Video') {
|
if (MediaStream.Type === 'Video') {
|
||||||
if ((MediaStream.Width ?? 0) >= 2000) {
|
if ((MediaStream.Width ?? 0) >= 2000) {
|
||||||
@@ -683,7 +675,7 @@ class JellyfinScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jellyfinFullScanner = new JellyfinScanner();
|
export const jobJellyfinFullSync = new JobJellyfinSync();
|
||||||
export const jellyfinRecentScanner = new JellyfinScanner({
|
export const jobJellyfinRecentSync = new JobJellyfinSync({
|
||||||
isRecentOnly: true,
|
isRecentOnly: true,
|
||||||
});
|
});
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import availabilitySync from '@server/lib/availabilitySync';
|
|
||||||
import downloadTracker from '@server/lib/downloadtracker';
|
import downloadTracker from '@server/lib/downloadtracker';
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
import ImageProxy from '@server/lib/imageproxy';
|
||||||
import {
|
|
||||||
jellyfinFullScanner,
|
|
||||||
jellyfinRecentScanner,
|
|
||||||
} from '@server/lib/scanners/jellyfin';
|
|
||||||
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
|
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
|
||||||
import { radarrScanner } from '@server/lib/scanners/radarr';
|
import { radarrScanner } from '@server/lib/scanners/radarr';
|
||||||
import { sonarrScanner } from '@server/lib/scanners/sonarr';
|
import { sonarrScanner } from '@server/lib/scanners/sonarr';
|
||||||
@@ -15,6 +10,7 @@ import watchlistSync from '@server/lib/watchlistsync';
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import random from 'lodash/random';
|
import random from 'lodash/random';
|
||||||
import schedule from 'node-schedule';
|
import schedule from 'node-schedule';
|
||||||
|
import { jobJellyfinFullSync, jobJellyfinRecentSync } from './jellyfinsync';
|
||||||
|
|
||||||
interface ScheduledJob {
|
interface ScheduledJob {
|
||||||
id: JobId;
|
id: JobId;
|
||||||
@@ -77,38 +73,38 @@ export const startJobs = (): void => {
|
|||||||
// Run recently added jellyfin sync every 5 minutes
|
// Run recently added jellyfin sync every 5 minutes
|
||||||
scheduledJobs.push({
|
scheduledJobs.push({
|
||||||
id: 'jellyfin-recently-added-scan',
|
id: 'jellyfin-recently-added-scan',
|
||||||
name: 'Jellyfin Recently Added Scan',
|
name: 'Jellyfin Recently Added Sync',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'minutes',
|
interval: 'minutes',
|
||||||
cronSchedule: jobs['jellyfin-recently-added-scan'].schedule,
|
cronSchedule: jobs['jellyfin-recently-added-scan'].schedule,
|
||||||
job: schedule.scheduleJob(
|
job: schedule.scheduleJob(
|
||||||
jobs['jellyfin-recently-added-scan'].schedule,
|
jobs['jellyfin-recently-added-scan'].schedule,
|
||||||
() => {
|
() => {
|
||||||
logger.info('Starting scheduled job: Jellyfin Recently Added Scan', {
|
logger.info('Starting scheduled job: Jellyfin Recently Added Sync', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
});
|
});
|
||||||
jellyfinRecentScanner.run();
|
jobJellyfinRecentSync.run();
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
running: () => jellyfinRecentScanner.status().running,
|
running: () => jobJellyfinRecentSync.status().running,
|
||||||
cancelFn: () => jellyfinRecentScanner.cancel(),
|
cancelFn: () => jobJellyfinRecentSync.cancel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run full jellyfin sync every 24 hours
|
// Run full jellyfin sync every 24 hours
|
||||||
scheduledJobs.push({
|
scheduledJobs.push({
|
||||||
id: 'jellyfin-full-scan',
|
id: 'jellyfin-full-scan',
|
||||||
name: 'Jellyfin Full Library Scan',
|
name: 'Jellyfin Full Library Sync',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'hours',
|
interval: 'hours',
|
||||||
cronSchedule: jobs['jellyfin-full-scan'].schedule,
|
cronSchedule: jobs['jellyfin-full-scan'].schedule,
|
||||||
job: schedule.scheduleJob(jobs['jellyfin-full-scan'].schedule, () => {
|
job: schedule.scheduleJob(jobs['jellyfin-full-scan'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Jellyfin Full Scan', {
|
logger.info('Starting scheduled job: Jellyfin Full Sync', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
});
|
});
|
||||||
jellyfinFullScanner.run();
|
jobJellyfinFullSync.run();
|
||||||
}),
|
}),
|
||||||
running: () => jellyfinFullScanner.status().running,
|
running: () => jobJellyfinFullSync.status().running,
|
||||||
cancelFn: () => jellyfinFullScanner.cancel(),
|
cancelFn: () => jobJellyfinFullSync.cancel(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +164,7 @@ export const startJobs = (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Checks if media is still available in plex/sonarr/radarr libs
|
// Checks if media is still available in plex/sonarr/radarr libs
|
||||||
scheduledJobs.push({
|
/* scheduledJobs.push({
|
||||||
id: 'availability-sync',
|
id: 'availability-sync',
|
||||||
name: 'Media Availability Sync',
|
name: 'Media Availability Sync',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
@@ -183,6 +179,7 @@ export const startJobs = (): void => {
|
|||||||
running: () => availabilitySync.running,
|
running: () => availabilitySync.running,
|
||||||
cancelFn: () => availabilitySync.cancel(),
|
cancelFn: () => availabilitySync.cancel(),
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
// Run download sync every minute
|
// Run download sync every minute
|
||||||
scheduledJobs.push({
|
scheduledJobs.push({
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import type { JellyfinLibraryItem } from '@server/api/jellyfin';
|
|
||||||
import JellyfinAPI from '@server/api/jellyfin';
|
|
||||||
import type { PlexMetadata } from '@server/api/plexapi';
|
import type { PlexMetadata } from '@server/api/plexapi';
|
||||||
import PlexAPI from '@server/api/plexapi';
|
import PlexAPI from '@server/api/plexapi';
|
||||||
import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr';
|
import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr';
|
||||||
import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr';
|
import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr';
|
||||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
import MediaRequest from '@server/entity/MediaRequest';
|
import MediaRequest from '@server/entity/MediaRequest';
|
||||||
@@ -21,20 +18,14 @@ class AvailabilitySync {
|
|||||||
public running = false;
|
public running = false;
|
||||||
private plexClient: PlexAPI;
|
private plexClient: PlexAPI;
|
||||||
private plexSeasonsCache: Record<string, PlexMetadata[]>;
|
private plexSeasonsCache: Record<string, PlexMetadata[]>;
|
||||||
|
|
||||||
private jellyfinClient: JellyfinAPI;
|
|
||||||
private jellyfinSeasonsCache: Record<string, JellyfinLibraryItem[]>;
|
|
||||||
|
|
||||||
private sonarrSeasonsCache: Record<string, SonarrSeason[]>;
|
private sonarrSeasonsCache: Record<string, SonarrSeason[]>;
|
||||||
private radarrServers: RadarrSettings[];
|
private radarrServers: RadarrSettings[];
|
||||||
private sonarrServers: SonarrSettings[];
|
private sonarrServers: SonarrSettings[];
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const mediaServerType = getSettings().main.mediaServerType;
|
|
||||||
this.running = true;
|
this.running = true;
|
||||||
this.plexSeasonsCache = {};
|
this.plexSeasonsCache = {};
|
||||||
this.jellyfinSeasonsCache = {};
|
|
||||||
this.sonarrSeasonsCache = {};
|
this.sonarrSeasonsCache = {};
|
||||||
this.radarrServers = settings.radarr.filter((server) => server.syncEnabled);
|
this.radarrServers = settings.radarr.filter((server) => server.syncEnabled);
|
||||||
this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled);
|
this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled);
|
||||||
@@ -46,53 +37,13 @@ class AvailabilitySync {
|
|||||||
const pageSize = 50;
|
const pageSize = 50;
|
||||||
|
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
|
const admin = await userRepository.findOne({
|
||||||
|
select: { id: true, plexToken: true },
|
||||||
|
where: { id: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
// If it is plex admin is selected using plexToken if jellyfin admin is selected using jellyfinUserID
|
if (admin) {
|
||||||
|
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
|
||||||
let admin = null;
|
|
||||||
|
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
|
||||||
admin = await userRepository.findOne({
|
|
||||||
select: { id: true, plexToken: true },
|
|
||||||
where: { id: 1 },
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
mediaServerType === MediaServerType.JELLYFIN ||
|
|
||||||
mediaServerType === MediaServerType.EMBY
|
|
||||||
) {
|
|
||||||
admin = await userRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
select: [
|
|
||||||
'id',
|
|
||||||
'jellyfinAuthToken',
|
|
||||||
'jellyfinUserId',
|
|
||||||
'jellyfinDeviceId',
|
|
||||||
],
|
|
||||||
order: { id: 'ASC' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
|
||||||
if (admin && admin.plexToken) {
|
|
||||||
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
|
|
||||||
} else {
|
|
||||||
logger.error('Plex admin is not configured.');
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
mediaServerType === MediaServerType.JELLYFIN ||
|
|
||||||
mediaServerType === MediaServerType.EMBY
|
|
||||||
) {
|
|
||||||
if (admin) {
|
|
||||||
this.jellyfinClient = new JellyfinAPI(
|
|
||||||
settings.jellyfin.hostname ?? '',
|
|
||||||
admin.jellyfinAuthToken,
|
|
||||||
admin.jellyfinDeviceId
|
|
||||||
);
|
|
||||||
|
|
||||||
this.jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
|
||||||
} else {
|
|
||||||
logger.error('Jellyfin admin is not configured.');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.error('An admin is not configured.');
|
logger.error('An admin is not configured.');
|
||||||
}
|
}
|
||||||
@@ -109,84 +60,41 @@ class AvailabilitySync {
|
|||||||
let movieExists = false;
|
let movieExists = false;
|
||||||
let movieExists4k = false;
|
let movieExists4k = false;
|
||||||
|
|
||||||
// if (mediaServerType === MediaServerType.PLEX) {
|
const { existsInPlex } = await this.mediaExistsInPlex(media, false);
|
||||||
// await this.mediaExistsInPlex(media, false);
|
const { existsInPlex: existsInPlex4k } = await this.mediaExistsInPlex(
|
||||||
// } else if (
|
media,
|
||||||
// mediaServerType === MediaServerType.JELLYFIN ||
|
true
|
||||||
// mediaServerType === MediaServerType.EMBY
|
);
|
||||||
// ) {
|
|
||||||
// await this.mediaExistsInJellyfin(media, false);
|
|
||||||
// }
|
|
||||||
|
|
||||||
const existsInRadarr = await this.mediaExistsInRadarr(media, false);
|
const existsInRadarr = await this.mediaExistsInRadarr(media, false);
|
||||||
const existsInRadarr4k = await this.mediaExistsInRadarr(media, true);
|
const existsInRadarr4k = await this.mediaExistsInRadarr(media, true);
|
||||||
|
|
||||||
// plex
|
if (existsInPlex || existsInRadarr) {
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
movieExists = true;
|
||||||
const { existsInPlex } = await this.mediaExistsInPlex(media, false);
|
logger.info(
|
||||||
const { existsInPlex: existsInPlex4k } =
|
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||||
await this.mediaExistsInPlex(media, true);
|
{
|
||||||
|
label: 'AvailabilitySync',
|
||||||
if (existsInPlex || existsInRadarr) {
|
}
|
||||||
movieExists = true;
|
);
|
||||||
logger.info(
|
|
||||||
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
|
||||||
{
|
|
||||||
label: 'AvailabilitySync',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existsInPlex4k || existsInRadarr4k) {
|
|
||||||
movieExists4k = true;
|
|
||||||
logger.info(
|
|
||||||
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
|
||||||
{
|
|
||||||
label: 'AvailabilitySync',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//jellyfin
|
if (existsInPlex4k || existsInRadarr4k) {
|
||||||
if (
|
movieExists4k = true;
|
||||||
mediaServerType === MediaServerType.JELLYFIN ||
|
logger.info(
|
||||||
mediaServerType === MediaServerType.EMBY
|
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||||
) {
|
{
|
||||||
const { existsInJellyfin } = await this.mediaExistsInJellyfin(
|
label: 'AvailabilitySync',
|
||||||
media,
|
}
|
||||||
false
|
|
||||||
);
|
);
|
||||||
const { existsInJellyfin: existsInJellyfin4k } =
|
|
||||||
await this.mediaExistsInJellyfin(media, true);
|
|
||||||
|
|
||||||
if (existsInJellyfin || existsInRadarr) {
|
|
||||||
movieExists = true;
|
|
||||||
logger.info(
|
|
||||||
`The non-4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
|
||||||
{
|
|
||||||
label: 'AvailabilitySync',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existsInJellyfin4k || existsInRadarr4k) {
|
|
||||||
movieExists4k = true;
|
|
||||||
logger.info(
|
|
||||||
`The 4K movie [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
|
||||||
{
|
|
||||||
label: 'AvailabilitySync',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!movieExists && media.status === MediaStatus.AVAILABLE) {
|
if (!movieExists && media.status === MediaStatus.AVAILABLE) {
|
||||||
await this.mediaUpdater(media, false, mediaServerType);
|
await this.mediaUpdater(media, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!movieExists4k && media.status4k === MediaStatus.AVAILABLE) {
|
if (!movieExists4k && media.status4k === MediaStatus.AVAILABLE) {
|
||||||
await this.mediaUpdater(media, true, mediaServerType);
|
await this.mediaUpdater(media, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,8 +104,6 @@ class AvailabilitySync {
|
|||||||
let showExists = false;
|
let showExists = false;
|
||||||
let showExists4k = false;
|
let showExists4k = false;
|
||||||
|
|
||||||
//plex
|
|
||||||
|
|
||||||
const { existsInPlex, seasonsMap: plexSeasonsMap = new Map() } =
|
const { existsInPlex, seasonsMap: plexSeasonsMap = new Map() } =
|
||||||
await this.mediaExistsInPlex(media, false);
|
await this.mediaExistsInPlex(media, false);
|
||||||
const {
|
const {
|
||||||
@@ -205,16 +111,6 @@ class AvailabilitySync {
|
|||||||
seasonsMap: plexSeasonsMap4k = new Map(),
|
seasonsMap: plexSeasonsMap4k = new Map(),
|
||||||
} = await this.mediaExistsInPlex(media, true);
|
} = await this.mediaExistsInPlex(media, true);
|
||||||
|
|
||||||
//jellyfin
|
|
||||||
const {
|
|
||||||
existsInJellyfin,
|
|
||||||
seasonsMap: jellyfinSeasonsMap = new Map(),
|
|
||||||
} = await this.mediaExistsInJellyfin(media, false);
|
|
||||||
const {
|
|
||||||
existsInJellyfin: existsInJellyfin4k,
|
|
||||||
seasonsMap: jellyfinSeasonsMap4k = new Map(),
|
|
||||||
} = await this.mediaExistsInJellyfin(media, true);
|
|
||||||
|
|
||||||
const { existsInSonarr, seasonsMap: sonarrSeasonsMap } =
|
const { existsInSonarr, seasonsMap: sonarrSeasonsMap } =
|
||||||
await this.mediaExistsInSonarr(media, false);
|
await this.mediaExistsInSonarr(media, false);
|
||||||
const {
|
const {
|
||||||
@@ -222,60 +118,24 @@ class AvailabilitySync {
|
|||||||
seasonsMap: sonarrSeasonsMap4k,
|
seasonsMap: sonarrSeasonsMap4k,
|
||||||
} = await this.mediaExistsInSonarr(media, true);
|
} = await this.mediaExistsInSonarr(media, true);
|
||||||
|
|
||||||
//plex
|
if (existsInPlex || existsInSonarr) {
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
showExists = true;
|
||||||
if (existsInPlex || existsInSonarr) {
|
logger.info(
|
||||||
showExists = true;
|
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||||
logger.info(
|
{
|
||||||
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
label: 'AvailabilitySync',
|
||||||
{
|
}
|
||||||
label: 'AvailabilitySync',
|
);
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
if (existsInPlex4k || existsInSonarr4k) {
|
||||||
if (existsInPlex4k || existsInSonarr4k) {
|
showExists4k = true;
|
||||||
showExists4k = true;
|
logger.info(
|
||||||
logger.info(
|
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
||||||
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
{
|
||||||
{
|
label: 'AvailabilitySync',
|
||||||
label: 'AvailabilitySync',
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//jellyfin
|
|
||||||
if (
|
|
||||||
mediaServerType === MediaServerType.JELLYFIN ||
|
|
||||||
mediaServerType === MediaServerType.EMBY
|
|
||||||
) {
|
|
||||||
if (existsInJellyfin || existsInSonarr) {
|
|
||||||
showExists = true;
|
|
||||||
logger.info(
|
|
||||||
`The non-4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
|
||||||
{
|
|
||||||
label: 'AvailabilitySync',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
mediaServerType === MediaServerType.JELLYFIN ||
|
|
||||||
mediaServerType === MediaServerType.EMBY
|
|
||||||
) {
|
|
||||||
if (existsInJellyfin4k || existsInSonarr4k) {
|
|
||||||
showExists4k = true;
|
|
||||||
logger.info(
|
|
||||||
`The 4K show [TMDB ID ${media.tmdbId}] still exists. Preventing removal.`,
|
|
||||||
{
|
|
||||||
label: 'AvailabilitySync',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here we will create a final map that will cross compare
|
// Here we will create a final map that will cross compare
|
||||||
@@ -295,48 +155,11 @@ class AvailabilitySync {
|
|||||||
filteredSeasonsMap.set(season.seasonNumber, false)
|
filteredSeasonsMap.set(season.seasonNumber, false)
|
||||||
);
|
);
|
||||||
|
|
||||||
// non-4k
|
const finalSeasons = new Map([
|
||||||
const finalSeasons: Map<number, boolean> = new Map();
|
...filteredSeasonsMap,
|
||||||
|
...plexSeasonsMap,
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
...sonarrSeasonsMap,
|
||||||
const plexMap = new Map([
|
]);
|
||||||
...plexSeasonsMap,
|
|
||||||
...filteredSeasonsMap,
|
|
||||||
...sonarrSeasonsMap,
|
|
||||||
]);
|
|
||||||
plexMap.forEach((value, key) => {
|
|
||||||
finalSeasons.set(key, value);
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
mediaServerType === MediaServerType.JELLYFIN ||
|
|
||||||
mediaServerType === MediaServerType.EMBY
|
|
||||||
) {
|
|
||||||
// Adding values from jellyfinSeasonsMap
|
|
||||||
jellyfinSeasonsMap.forEach((value, key) => {
|
|
||||||
finalSeasons.set(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Adding values from filteredSeasonsMap and handling missing keys
|
|
||||||
filteredSeasonsMap.forEach((value, key) => {
|
|
||||||
// Check if the key is missing in jellyfinSeasonsMap
|
|
||||||
if (!finalSeasons.has(key)) {
|
|
||||||
finalSeasons.set(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Adding values from sonarrSeasonsMap and handling missing keys
|
|
||||||
sonarrSeasonsMap.forEach((value, key) => {
|
|
||||||
// Check if the key is missing in jellyfinSeasonsMap and filteredSeasonsMap
|
|
||||||
if (!finalSeasons.has(key)) {
|
|
||||||
finalSeasons.set(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...(mediaServerType === MediaServerType.PLEX ? plexSeasonsMap : []),
|
|
||||||
// ...(mediaServerType === MediaServerType.JELLYFIN ||
|
|
||||||
// mediaServerType === MediaServerType.EMBY ? jellyfinSeasonsMap
|
|
||||||
// : []),
|
|
||||||
|
|
||||||
const filteredSeasonsMap4k: Map<number, boolean> = new Map();
|
const filteredSeasonsMap4k: Map<number, boolean> = new Map();
|
||||||
|
|
||||||
@@ -350,74 +173,18 @@ class AvailabilitySync {
|
|||||||
filteredSeasonsMap4k.set(season.seasonNumber, false)
|
filteredSeasonsMap4k.set(season.seasonNumber, false)
|
||||||
);
|
);
|
||||||
|
|
||||||
// const finalSeasons4k: Map<any, any> = new Map<any, any>([
|
const finalSeasons4k = new Map([
|
||||||
// ...(mediaServerType === MediaServerType.PLEX
|
...filteredSeasonsMap4k,
|
||||||
// ? plexSeasonsMap4k
|
...plexSeasonsMap4k,
|
||||||
// : []),
|
...sonarrSeasonsMap4k,
|
||||||
// ...(mediaServerType === MediaServerType.JELLYFIN ||
|
]);
|
||||||
// mediaServerType === MediaServerType.EMBY
|
|
||||||
// ? jellyfinSeasonsMap4k
|
|
||||||
// : []),
|
|
||||||
// ...filteredSeasonsMap4k,
|
|
||||||
// ...sonarrSeasonsMap4k,
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
// 4k
|
|
||||||
const finalSeasons4k: Map<number, boolean> = new Map();
|
|
||||||
|
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
|
||||||
const plexMap4k = new Map([
|
|
||||||
...plexSeasonsMap4k,
|
|
||||||
...filteredSeasonsMap4k,
|
|
||||||
...sonarrSeasonsMap4k,
|
|
||||||
]);
|
|
||||||
plexMap4k.forEach((value, key) => {
|
|
||||||
finalSeasons4k.set(key, value);
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
mediaServerType === MediaServerType.JELLYFIN ||
|
|
||||||
mediaServerType === MediaServerType.EMBY
|
|
||||||
) {
|
|
||||||
// Adding values from jellyfinSeasonsMap
|
|
||||||
jellyfinSeasonsMap4k.forEach((value, key) => {
|
|
||||||
finalSeasons4k.set(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Adding values from filteredSeasonsMap and handling missing keys
|
|
||||||
filteredSeasonsMap4k.forEach((value, key) => {
|
|
||||||
// Check if the key is missing in jellyfinSeasonsMap
|
|
||||||
if (!finalSeasons4k.has(key)) {
|
|
||||||
finalSeasons4k.set(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Adding values from sonarrSeasonsMap and handling missing keys
|
|
||||||
sonarrSeasonsMap4k.forEach((value, key) => {
|
|
||||||
// Check if the key is missing in jellyfinSeasonsMap and filteredSeasonsMap
|
|
||||||
if (!finalSeasons4k.has(key)) {
|
|
||||||
finalSeasons4k.set(key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Figure out how to run seasonUpdater for each season
|
|
||||||
|
|
||||||
if ([...finalSeasons.values()].includes(false)) {
|
if ([...finalSeasons.values()].includes(false)) {
|
||||||
await this.seasonUpdater(
|
await this.seasonUpdater(media, finalSeasons, false);
|
||||||
media,
|
|
||||||
finalSeasons,
|
|
||||||
false,
|
|
||||||
mediaServerType
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([...finalSeasons4k.values()].includes(false)) {
|
if ([...finalSeasons4k.values()].includes(false)) {
|
||||||
await this.seasonUpdater(
|
await this.seasonUpdater(media, finalSeasons4k, true);
|
||||||
media,
|
|
||||||
finalSeasons4k,
|
|
||||||
true,
|
|
||||||
mediaServerType
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -425,7 +192,7 @@ class AvailabilitySync {
|
|||||||
(media.status === MediaStatus.AVAILABLE ||
|
(media.status === MediaStatus.AVAILABLE ||
|
||||||
media.status === MediaStatus.PARTIALLY_AVAILABLE)
|
media.status === MediaStatus.PARTIALLY_AVAILABLE)
|
||||||
) {
|
) {
|
||||||
await this.mediaUpdater(media, false, mediaServerType);
|
await this.mediaUpdater(media, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -433,7 +200,7 @@ class AvailabilitySync {
|
|||||||
(media.status4k === MediaStatus.AVAILABLE ||
|
(media.status4k === MediaStatus.AVAILABLE ||
|
||||||
media.status4k === MediaStatus.PARTIALLY_AVAILABLE)
|
media.status4k === MediaStatus.PARTIALLY_AVAILABLE)
|
||||||
) {
|
) {
|
||||||
await this.mediaUpdater(media, true, mediaServerType);
|
await this.mediaUpdater(media, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,11 +272,7 @@ class AvailabilitySync {
|
|||||||
return mediaStatus;
|
return mediaStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async mediaUpdater(
|
private async mediaUpdater(media: Media, is4k: boolean): Promise<void> {
|
||||||
media: Media,
|
|
||||||
is4k: boolean,
|
|
||||||
mediaServerType: MediaServerType
|
|
||||||
): Promise<void> {
|
|
||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
@@ -557,32 +320,17 @@ class AvailabilitySync {
|
|||||||
mediaStatus === MediaStatus.PROCESSING
|
mediaStatus === MediaStatus.PROCESSING
|
||||||
? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
|
? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
|
||||||
: null;
|
: null;
|
||||||
if (mediaServerType === MediaServerType.PLEX) {
|
media[is4k ? 'ratingKey4k' : 'ratingKey'] =
|
||||||
media[is4k ? 'ratingKey4k' : 'ratingKey'] =
|
mediaStatus === MediaStatus.PROCESSING
|
||||||
mediaStatus === MediaStatus.PROCESSING
|
? media[is4k ? 'ratingKey4k' : 'ratingKey']
|
||||||
? media[is4k ? 'ratingKey4k' : 'ratingKey']
|
: null;
|
||||||
: undefined;
|
|
||||||
} else if (
|
|
||||||
mediaServerType === MediaServerType.JELLYFIN ||
|
|
||||||
mediaServerType === MediaServerType.EMBY
|
|
||||||
) {
|
|
||||||
media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId'] =
|
|
||||||
mediaStatus === MediaStatus.PROCESSING
|
|
||||||
? media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId']
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`The ${is4k ? '4K' : 'non-4K'} ${
|
`The ${is4k ? '4K' : 'non-4K'} ${
|
||||||
media.mediaType === 'movie' ? 'movie' : 'show'
|
media.mediaType === 'movie' ? 'movie' : 'show'
|
||||||
} [TMDB ID ${media.tmdbId}] was not found in any ${
|
} [TMDB ID ${media.tmdbId}] was not found in any ${
|
||||||
media.mediaType === 'movie' ? 'Radarr' : 'Sonarr'
|
media.mediaType === 'movie' ? 'Radarr' : 'Sonarr'
|
||||||
} and ${
|
} and Plex instance. Status will be changed to unknown.`,
|
||||||
mediaServerType === MediaServerType.PLEX
|
|
||||||
? 'plex'
|
|
||||||
: mediaServerType === MediaServerType.JELLYFIN
|
|
||||||
? 'jellyfin'
|
|
||||||
: 'emby'
|
|
||||||
} instance. Status will be changed to unknown.`,
|
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -610,8 +358,7 @@ class AvailabilitySync {
|
|||||||
private async seasonUpdater(
|
private async seasonUpdater(
|
||||||
media: Media,
|
media: Media,
|
||||||
seasons: Map<number, boolean>,
|
seasons: Map<number, boolean>,
|
||||||
is4k: boolean,
|
is4k: boolean
|
||||||
mediaServerType: MediaServerType
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
const seasonRequestRepository = getRepository(SeasonRequest);
|
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||||
@@ -623,8 +370,6 @@ class AvailabilitySync {
|
|||||||
);
|
);
|
||||||
const seasonKeys = [...seasonsPendingRemoval.keys()];
|
const seasonKeys = [...seasonsPendingRemoval.keys()];
|
||||||
|
|
||||||
// let isSeasonRemoved = false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Need to check and see if there are any related season
|
// Need to check and see if there are any related season
|
||||||
// requests. If they are, we will need to delete them.
|
// requests. If they are, we will need to delete them.
|
||||||
@@ -675,13 +420,7 @@ class AvailabilitySync {
|
|||||||
media.tmdbId
|
media.tmdbId
|
||||||
}] was not found in any ${
|
}] was not found in any ${
|
||||||
media.mediaType === 'tv' ? 'Sonarr' : 'Radarr'
|
media.mediaType === 'tv' ? 'Sonarr' : 'Radarr'
|
||||||
} and ${
|
} and Plex instance. Status will be changed to unknown.`,
|
||||||
mediaServerType === MediaServerType.PLEX
|
|
||||||
? 'plex'
|
|
||||||
: mediaServerType === MediaServerType.JELLYFIN
|
|
||||||
? 'jellyfin'
|
|
||||||
: 'emby'
|
|
||||||
} instance. Status will be changed to unknown.`,
|
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -865,7 +604,6 @@ class AvailabilitySync {
|
|||||||
return seasonExists;
|
return seasonExists;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plex
|
|
||||||
private async mediaExistsInPlex(
|
private async mediaExistsInPlex(
|
||||||
media: Media,
|
media: Media,
|
||||||
is4k: boolean
|
is4k: boolean
|
||||||
@@ -981,123 +719,6 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
return seasonExistsInPlex;
|
return seasonExistsInPlex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jellyfin
|
|
||||||
private async mediaExistsInJellyfin(
|
|
||||||
media: Media,
|
|
||||||
is4k: boolean
|
|
||||||
): Promise<{ existsInJellyfin: boolean; seasonsMap?: Map<number, boolean> }> {
|
|
||||||
const ratingKey = media.jellyfinMediaId;
|
|
||||||
const ratingKey4k = media.jellyfinMediaId4k;
|
|
||||||
let existsInJellyfin = false;
|
|
||||||
let preventSeasonSearch = false;
|
|
||||||
|
|
||||||
// Check each jellyfin instance to see if the media still exists
|
|
||||||
// If found, we will assume the media exists and prevent removal
|
|
||||||
// We can use the cache we built when we fetched the series with mediaExistsInJellyfin
|
|
||||||
try {
|
|
||||||
let jellyfinMedia: JellyfinLibraryItem | undefined;
|
|
||||||
|
|
||||||
if (ratingKey && !is4k) {
|
|
||||||
jellyfinMedia = await this.jellyfinClient?.getItemData(ratingKey);
|
|
||||||
|
|
||||||
if (media.mediaType === 'tv' && jellyfinMedia !== undefined) {
|
|
||||||
this.jellyfinSeasonsCache[ratingKey] =
|
|
||||||
await this.jellyfinClient?.getSeasons(ratingKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ratingKey4k && is4k) {
|
|
||||||
jellyfinMedia = await this.jellyfinClient?.getItemData(ratingKey4k);
|
|
||||||
|
|
||||||
if (media.mediaType === 'tv' && jellyfinMedia !== undefined) {
|
|
||||||
this.jellyfinSeasonsCache[ratingKey4k] =
|
|
||||||
await this.jellyfinClient?.getSeasons(ratingKey4k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jellyfinMedia) {
|
|
||||||
existsInJellyfin = true;
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
if (!ex.message.includes('404' || '500')) {
|
|
||||||
existsInJellyfin = false;
|
|
||||||
preventSeasonSearch = true;
|
|
||||||
logger.debug(
|
|
||||||
`Failure retrieving the ${is4k ? '4K' : 'non-4K'} ${
|
|
||||||
media.mediaType === 'tv' ? 'show' : 'movie'
|
|
||||||
} [TMDB ID ${media.tmdbId}] from Jellyfin.`,
|
|
||||||
{
|
|
||||||
errorMessage: ex.message,
|
|
||||||
label: 'AvailabilitySync',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here we check each season in jellyfin for availability
|
|
||||||
// If the API returns an error other than a 404,
|
|
||||||
// we will have to prevent the season check from happening
|
|
||||||
if (media.mediaType === 'tv') {
|
|
||||||
const seasonsMap: Map<number, boolean> = new Map();
|
|
||||||
|
|
||||||
if (!preventSeasonSearch) {
|
|
||||||
const filteredSeasons = media.seasons.filter(
|
|
||||||
(season) =>
|
|
||||||
season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE ||
|
|
||||||
season[is4k ? 'status4k' : 'status'] ===
|
|
||||||
MediaStatus.PARTIALLY_AVAILABLE
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const season of filteredSeasons) {
|
|
||||||
const seasonExists = await this.seasonExistsInJellyfin(
|
|
||||||
media,
|
|
||||||
season,
|
|
||||||
is4k
|
|
||||||
);
|
|
||||||
|
|
||||||
if (seasonExists) {
|
|
||||||
seasonsMap.set(season.seasonNumber, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { existsInJellyfin, seasonsMap };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { existsInJellyfin };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async seasonExistsInJellyfin(
|
|
||||||
media: Media,
|
|
||||||
season: Season,
|
|
||||||
is4k: boolean
|
|
||||||
): Promise<boolean> {
|
|
||||||
const ratingKey = media.jellyfinMediaId;
|
|
||||||
const ratingKey4k = media.jellyfinMediaId4k;
|
|
||||||
let seasonExistsInJellyfin = false;
|
|
||||||
|
|
||||||
// Check each jellyfin instance to see if the season exists
|
|
||||||
let jellyfinSeasons: JellyfinLibraryItem[] | undefined;
|
|
||||||
|
|
||||||
if (ratingKey && !is4k) {
|
|
||||||
jellyfinSeasons = this.jellyfinSeasonsCache[ratingKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ratingKey4k && is4k) {
|
|
||||||
jellyfinSeasons = this.jellyfinSeasonsCache[ratingKey4k];
|
|
||||||
}
|
|
||||||
|
|
||||||
const seasonIsAvailable = jellyfinSeasons?.find(
|
|
||||||
(jellyfinSeason) => jellyfinSeason.IndexNumber === season.seasonNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
if (seasonIsAvailable) {
|
|
||||||
seasonExistsInJellyfin = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return seasonExistsInJellyfin;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const availabilitySync = new AvailabilitySync();
|
const availabilitySync = new AvailabilitySync();
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import type {
|
|||||||
LogsResultsResponse,
|
LogsResultsResponse,
|
||||||
SettingsAboutResponse,
|
SettingsAboutResponse,
|
||||||
} from '@server/interfaces/api/settingsInterfaces';
|
} from '@server/interfaces/api/settingsInterfaces';
|
||||||
|
import { jobJellyfinFullSync } from '@server/job/jellyfinsync';
|
||||||
import { scheduledJobs } from '@server/job/schedule';
|
import { scheduledJobs } from '@server/job/schedule';
|
||||||
import type { AvailableCacheIds } from '@server/lib/cache';
|
import type { AvailableCacheIds } from '@server/lib/cache';
|
||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
import ImageProxy from '@server/lib/imageproxy';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { jellyfinFullScanner } from '@server/lib/scanners/jellyfin';
|
|
||||||
import { plexFullScanner } from '@server/lib/scanners/plex';
|
import { plexFullScanner } from '@server/lib/scanners/plex';
|
||||||
import type { JobId, Library, MainSettings } from '@server/lib/settings';
|
import type { JobId, Library, MainSettings } from '@server/lib/settings';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
@@ -345,16 +345,16 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.get('/jellyfin/sync', (_req, res) => {
|
settingsRoutes.get('/jellyfin/sync', (_req, res) => {
|
||||||
return res.status(200).json(jellyfinFullScanner.status());
|
return res.status(200).json(jobJellyfinFullSync.status());
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.post('/jellyfin/sync', (req, res) => {
|
settingsRoutes.post('/jellyfin/sync', (req, res) => {
|
||||||
if (req.body.cancel) {
|
if (req.body.cancel) {
|
||||||
jellyfinFullScanner.cancel();
|
jobJellyfinFullSync.cancel();
|
||||||
} else if (req.body.start) {
|
} else if (req.body.start) {
|
||||||
jellyfinFullScanner.run();
|
jobJellyfinFullSync.run();
|
||||||
}
|
}
|
||||||
return res.status(200).json(jellyfinFullScanner.status());
|
return res.status(200).json(jobJellyfinFullSync.status());
|
||||||
});
|
});
|
||||||
settingsRoutes.get('/tautulli', (_req, res) => {
|
settingsRoutes.get('/tautulli', (_req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|||||||
@@ -182,21 +182,25 @@ router.post<
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get<{ id: string }>('/:id', async (req, res, next) => {
|
router.get<{ id: string }>(
|
||||||
try {
|
'/:id',
|
||||||
const userRepository = getRepository(User);
|
isAuthenticated([Permission.MANAGE_USERS, Permission.WATCHLIST_VIEW]),
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
|
||||||
const user = await userRepository.findOneOrFail({
|
const user = await userRepository.findOneOrFail({
|
||||||
where: { id: Number(req.params.id) },
|
where: { id: Number(req.params.id) },
|
||||||
});
|
});
|
||||||
|
|
||||||
return res
|
return res
|
||||||
.status(200)
|
.status(200)
|
||||||
.json(user.filter(req.user?.hasPermission(Permission.MANAGE_USERS)));
|
.json(user.filter(req.user?.hasPermission(Permission.MANAGE_USERS)));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({ status: 404, message: 'User not found.' });
|
next({ status: 404, message: 'User not found.' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
router.use('/:id/settings', userSettingsRoutes);
|
router.use('/:id/settings', userSettingsRoutes);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ type ListViewProps = {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
isReachingEnd?: boolean;
|
isReachingEnd?: boolean;
|
||||||
onScrollBottom: () => void;
|
onScrollBottom: () => void;
|
||||||
mutateParent?: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListView = ({
|
const ListView = ({
|
||||||
@@ -29,7 +28,6 @@ const ListView = ({
|
|||||||
onScrollBottom,
|
onScrollBottom,
|
||||||
isReachingEnd,
|
isReachingEnd,
|
||||||
plexItems,
|
plexItems,
|
||||||
mutateParent,
|
|
||||||
}: ListViewProps) => {
|
}: ListViewProps) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd);
|
useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd);
|
||||||
@@ -50,7 +48,6 @@ const ListView = ({
|
|||||||
type={title.mediaType}
|
type={title.mediaType}
|
||||||
isAddedToWatchlist={true}
|
isAddedToWatchlist={true}
|
||||||
canExpand
|
canExpand
|
||||||
mutateParent={mutateParent}
|
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const DiscoverWatchlist = () => {
|
|||||||
titles,
|
titles,
|
||||||
fetchMore,
|
fetchMore,
|
||||||
error,
|
error,
|
||||||
mutate,
|
|
||||||
} = useDiscover<WatchlistItem>(
|
} = useDiscover<WatchlistItem>(
|
||||||
`/api/v1/${
|
`/api/v1/${
|
||||||
router.pathname.startsWith('/profile')
|
router.pathname.startsWith('/profile')
|
||||||
@@ -77,7 +76,6 @@ const DiscoverWatchlist = () => {
|
|||||||
}
|
}
|
||||||
isReachingEnd={isReachingEnd}
|
isReachingEnd={isReachingEnd}
|
||||||
onScrollBottom={fetchMore}
|
onScrollBottom={fetchMore}
|
||||||
mutateParent={mutate}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import Modal from '@app/components/Common/Modal';
|
import Modal from '@app/components/Common/Modal';
|
||||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||||
import useSettings from '@app/hooks/useSettings';
|
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import type { SonarrSettings } from '@server/lib/settings';
|
||||||
import { type SonarrSettings } from '@server/lib/settings';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Formik } from 'formik';
|
import { Field, Formik } from 'formik';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
@@ -111,7 +109,6 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
|
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
const settings = useSettings();
|
|
||||||
const [testResponse, setTestResponse] = useState<TestResponse>({
|
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||||
profiles: [],
|
profiles: [],
|
||||||
rootFolders: [],
|
rootFolders: [],
|
||||||
@@ -258,9 +255,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
animeTags: sonarr?.animeTags ?? [],
|
animeTags: sonarr?.animeTags ?? [],
|
||||||
isDefault: sonarr?.isDefault ?? false,
|
isDefault: sonarr?.isDefault ?? false,
|
||||||
is4k: sonarr?.is4k ?? false,
|
is4k: sonarr?.is4k ?? false,
|
||||||
enableSeasonFolders:
|
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
|
||||||
sonarr?.enableSeasonFolders ??
|
|
||||||
settings.currentSettings.mediaServerType !== MediaServerType.PLEX,
|
|
||||||
externalUrl: sonarr?.externalUrl,
|
externalUrl: sonarr?.externalUrl,
|
||||||
syncEnabled: sonarr?.syncEnabled ?? false,
|
syncEnabled: sonarr?.syncEnabled ?? false,
|
||||||
enableSearch: !sonarr?.preventSearch,
|
enableSearch: !sonarr?.preventSearch,
|
||||||
@@ -966,24 +961,11 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
>
|
>
|
||||||
{intl.formatMessage(messages.seasonfolders)}
|
{intl.formatMessage(messages.seasonfolders)}
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div className="form-input-area">
|
||||||
className={`form-input-area ${
|
|
||||||
settings.currentSettings.mediaServerType ===
|
|
||||||
MediaServerType.JELLYFIN ||
|
|
||||||
settings.currentSettings.mediaServerType ===
|
|
||||||
MediaServerType.EMBY
|
|
||||||
? 'opacity-50'
|
|
||||||
: 'opacity-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Field
|
<Field
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="enableSeasonFolders"
|
id="enableSeasonFolders"
|
||||||
name="enableSeasonFolders"
|
name="enableSeasonFolders"
|
||||||
disabled={
|
|
||||||
settings.currentSettings.mediaServerType !==
|
|
||||||
MediaServerType.PLEX
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export interface TmdbTitleCardProps {
|
|||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv';
|
||||||
canExpand?: boolean;
|
canExpand?: boolean;
|
||||||
isAddedToWatchlist?: boolean;
|
isAddedToWatchlist?: boolean;
|
||||||
mutateParent?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
|
||||||
@@ -26,7 +25,6 @@ const TmdbTitleCard = ({
|
|||||||
type,
|
type,
|
||||||
canExpand,
|
canExpand,
|
||||||
isAddedToWatchlist = false,
|
isAddedToWatchlist = false,
|
||||||
mutateParent,
|
|
||||||
}: TmdbTitleCardProps) => {
|
}: TmdbTitleCardProps) => {
|
||||||
const { hasPermission } = useUser();
|
const { hasPermission } = useUser();
|
||||||
|
|
||||||
@@ -73,7 +71,6 @@ const TmdbTitleCard = ({
|
|||||||
year={title.releaseDate}
|
year={title.releaseDate}
|
||||||
mediaType={'movie'}
|
mediaType={'movie'}
|
||||||
canExpand={canExpand}
|
canExpand={canExpand}
|
||||||
mutateParent={mutateParent}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TitleCard
|
<TitleCard
|
||||||
@@ -90,7 +87,6 @@ const TmdbTitleCard = ({
|
|||||||
year={title.firstAirDate}
|
year={title.firstAirDate}
|
||||||
mediaType={'tv'}
|
mediaType={'tv'}
|
||||||
canExpand={canExpand}
|
canExpand={canExpand}
|
||||||
mutateParent={mutateParent}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ interface TitleCardProps {
|
|||||||
canExpand?: boolean;
|
canExpand?: boolean;
|
||||||
inProgress?: boolean;
|
inProgress?: boolean;
|
||||||
isAddedToWatchlist?: number | boolean;
|
isAddedToWatchlist?: number | boolean;
|
||||||
mutateParent?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -62,7 +61,6 @@ const TitleCard = ({
|
|||||||
isAddedToWatchlist = false,
|
isAddedToWatchlist = false,
|
||||||
inProgress = false,
|
inProgress = false,
|
||||||
canExpand = false,
|
canExpand = false,
|
||||||
mutateParent,
|
|
||||||
}: TitleCardProps) => {
|
}: TitleCardProps) => {
|
||||||
const isTouch = useIsTouch();
|
const isTouch = useIsTouch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -150,9 +148,6 @@ const TitleCard = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
mutate('/api/v1/discover/watchlist');
|
mutate('/api/v1/discover/watchlist');
|
||||||
if (mutateParent) {
|
|
||||||
mutateParent();
|
|
||||||
}
|
|
||||||
setToggleWatchlist((prevState) => !prevState);
|
setToggleWatchlist((prevState) => !prevState);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ interface DiscoverResult<T, S> {
|
|||||||
error: unknown;
|
error: unknown;
|
||||||
titles: T[];
|
titles: T[];
|
||||||
firstResultData?: BaseSearchResult<T> & S;
|
firstResultData?: BaseSearchResult<T> & S;
|
||||||
mutate?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraEncodes: [RegExp, string][] = [
|
const extraEncodes: [RegExp, string][] = [
|
||||||
@@ -55,7 +54,7 @@ const useDiscover = <
|
|||||||
{ hideAvailable = true } = {}
|
{ hideAvailable = true } = {}
|
||||||
): DiscoverResult<T, S> => {
|
): DiscoverResult<T, S> => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const { data, error, size, setSize, isValidating, mutate } = useSWRInfinite<
|
const { data, error, size, setSize, isValidating } = useSWRInfinite<
|
||||||
BaseSearchResult<T> & S
|
BaseSearchResult<T> & S
|
||||||
>(
|
>(
|
||||||
(pageIndex: number, previousPageData) => {
|
(pageIndex: number, previousPageData) => {
|
||||||
@@ -120,7 +119,6 @@ const useDiscover = <
|
|||||||
error,
|
error,
|
||||||
titles,
|
titles,
|
||||||
firstResultData: data?.[0],
|
firstResultData: data?.[0],
|
||||||
mutate,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const defaultTheme = require('tailwindcss/defaultTheme');
|
|||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
important: true,
|
||||||
mode: 'jit',
|
mode: 'jit',
|
||||||
content: [
|
content: [
|
||||||
'./node_modules/react-tailwindcss-datepicker-sct/dist/index.esm.js',
|
'./node_modules/react-tailwindcss-datepicker-sct/dist/index.esm.js',
|
||||||
|
|||||||
Reference in New Issue
Block a user