mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
feat: add anidb support to jellyfin scanner (#1533)
* feat: anidb on jellyfin scanner * fix: handle 4k movies if the provider is anidb * fix: add nullish coalescing for anidbId * Update server/lib/scanners/jellyfin/index.ts Co-authored-by: Gauthier <mail@gauthierth.fr> * fix: await getEpisodes call in JellyfinScanner to handle async behavior --------- Co-authored-by: Gauthier <mail@gauthierth.fr>
This commit is contained in:
@@ -48,6 +48,7 @@ export interface AnidbItem {
|
|||||||
tvdbId?: number;
|
tvdbId?: number;
|
||||||
tmdbId?: number;
|
tmdbId?: number;
|
||||||
imdbId?: string;
|
imdbId?: string;
|
||||||
|
tvdbSeason?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnimeListMapping {
|
class AnimeListMapping {
|
||||||
@@ -97,6 +98,7 @@ class AnimeListMapping {
|
|||||||
tvdbId: anime.$.defaulttvdbseason === '0' ? undefined : tvdbId,
|
tvdbId: anime.$.defaulttvdbseason === '0' ? undefined : tvdbId,
|
||||||
tmdbId: tmdbId,
|
tmdbId: tmdbId,
|
||||||
imdbId: imdbIds[0], // this is used for one AniDB -> one imdb movie mapping
|
imdbId: imdbIds[0], // this is used for one AniDB -> one imdb movie mapping
|
||||||
|
tvdbSeason: Number(anime.$.defaulttvdbseason),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tvdbId) {
|
if (tvdbId) {
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
|||||||
Tmdb?: string;
|
Tmdb?: string;
|
||||||
Imdb?: string;
|
Imdb?: string;
|
||||||
Tvdb?: string;
|
Tvdb?: string;
|
||||||
|
AniDB?: string;
|
||||||
};
|
};
|
||||||
MediaSources?: JellyfinMediaSource[];
|
MediaSources?: JellyfinMediaSource[];
|
||||||
Width?: number;
|
Width?: number;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import animeList from '@server/api/animelist';
|
||||||
import type { JellyfinLibraryItem } from '@server/api/jellyfin';
|
import type { JellyfinLibraryItem } from '@server/api/jellyfin';
|
||||||
import JellyfinAPI from '@server/api/jellyfin';
|
import JellyfinAPI from '@server/api/jellyfin';
|
||||||
import { getMetadataProvider } from '@server/api/metadata';
|
import { getMetadataProvider } from '@server/api/metadata';
|
||||||
@@ -45,6 +46,7 @@ class JellyfinScanner {
|
|||||||
private enable4kMovie = false;
|
private enable4kMovie = false;
|
||||||
private enable4kShow = false;
|
private enable4kShow = false;
|
||||||
private asyncLock = new AsyncLock();
|
private asyncLock = new AsyncLock();
|
||||||
|
private processedAnidbSeason: Map<number, Map<number, number>>;
|
||||||
|
|
||||||
constructor({ isRecentOnly }: { isRecentOnly?: boolean } = {}) {
|
constructor({ isRecentOnly }: { isRecentOnly?: boolean } = {}) {
|
||||||
this.tmdb = new TheMovieDb();
|
this.tmdb = new TheMovieDb();
|
||||||
@@ -66,7 +68,7 @@ class JellyfinScanner {
|
|||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const metadata = await this.jfClient.getItemData(jellyfinitem.Id);
|
let metadata = await this.jfClient.getItemData(jellyfinitem.Id);
|
||||||
const newMedia = new Media();
|
const newMedia = new Media();
|
||||||
|
|
||||||
if (!metadata?.Id) {
|
if (!metadata?.Id) {
|
||||||
@@ -77,8 +79,18 @@ class JellyfinScanner {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const anidbId = Number(metadata.ProviderIds.AniDB ?? null);
|
||||||
|
|
||||||
newMedia.tmdbId = Number(metadata.ProviderIds.Tmdb ?? null);
|
newMedia.tmdbId = Number(metadata.ProviderIds.Tmdb ?? null);
|
||||||
newMedia.imdbId = metadata.ProviderIds.Imdb;
|
newMedia.imdbId = metadata.ProviderIds.Imdb;
|
||||||
|
|
||||||
|
// We use anidb only if we have the anidbId and nothing else
|
||||||
|
if (anidbId && !newMedia.imdbId && !newMedia.tmdbId) {
|
||||||
|
const result = animeList.getFromAnidbId(anidbId);
|
||||||
|
newMedia.tmdbId = Number(result?.tmdbId ?? null);
|
||||||
|
newMedia.imdbId = result?.imdbId;
|
||||||
|
}
|
||||||
|
|
||||||
if (newMedia.imdbId && !isNaN(newMedia.tmdbId)) {
|
if (newMedia.imdbId && !isNaN(newMedia.tmdbId)) {
|
||||||
const tmdbMovie = await this.tmdb.getMediaByImdbId({
|
const tmdbMovie = await this.tmdb.getMediaByImdbId({
|
||||||
imdbId: newMedia.imdbId,
|
imdbId: newMedia.imdbId,
|
||||||
@@ -89,6 +101,40 @@ class JellyfinScanner {
|
|||||||
throw new Error('Unable to find TMDb ID');
|
throw new Error('Unable to find TMDb ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With AniDB we can have mixed libraries with movies in a "show" library
|
||||||
|
// We take the first episode of the first season (the movie) and use it to
|
||||||
|
// get more information, like the MediaSource
|
||||||
|
if (anidbId && metadata.Type === 'Series') {
|
||||||
|
const season = (await this.jfClient.getSeasons(jellyfinitem.Id)).find(
|
||||||
|
(md) => {
|
||||||
|
return md.IndexNumber === 1;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!season) {
|
||||||
|
this.log('No season found for anidb movie', 'debug', {
|
||||||
|
jellyfinitem,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const episodes = await this.jfClient.getEpisodes(
|
||||||
|
jellyfinitem.Id,
|
||||||
|
season.Id
|
||||||
|
);
|
||||||
|
if (!episodes[0]) {
|
||||||
|
this.log('No episode found for anidb movie', 'debug', {
|
||||||
|
jellyfinitem,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metadata = await this.jfClient.getItemData(episodes[0].Id);
|
||||||
|
if (!metadata) {
|
||||||
|
this.log('No metadata found for anidb movie', 'debug', {
|
||||||
|
jellyfinitem,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const has4k = metadata.MediaSources?.some((MediaSource) => {
|
const has4k = metadata.MediaSources?.some((MediaSource) => {
|
||||||
return MediaSource.MediaStreams.filter(
|
return MediaSource.MediaStreams.filter(
|
||||||
(MediaStream) => MediaStream.Type === 'Video'
|
(MediaStream) => MediaStream.Type === 'Video'
|
||||||
@@ -106,6 +152,12 @@ class JellyfinScanner {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.asyncLock.dispatch(newMedia.tmdbId, async () => {
|
await this.asyncLock.dispatch(newMedia.tmdbId, async () => {
|
||||||
|
if (!metadata) {
|
||||||
|
// this will never execute, but typescript thinks somebody could reset tvShow from
|
||||||
|
// outer scope back to null before this async gets called
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const existing = await this.getExisting(
|
const existing = await this.getExisting(
|
||||||
newMedia.tmdbId,
|
newMedia.tmdbId,
|
||||||
MediaType.MOVIE
|
MediaType.MOVIE
|
||||||
@@ -274,6 +326,28 @@ class JellyfinScanner {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let tvdbSeasonFromAnidb: number | undefined;
|
||||||
|
if (!tvShow && metadata.ProviderIds.AniDB) {
|
||||||
|
const anidbId = Number(metadata.ProviderIds.AniDB);
|
||||||
|
const result = animeList.getFromAnidbId(anidbId);
|
||||||
|
tvdbSeasonFromAnidb = result?.tvdbSeason;
|
||||||
|
if (result?.tvdbId) {
|
||||||
|
try {
|
||||||
|
tvShow = await this.tmdb.getShowByTvdbId({
|
||||||
|
tvdbId: result.tvdbId,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
this.log('Unable to find AniDB ID for this title.', 'debug', {
|
||||||
|
jellyfinitem,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// With AniDB we can have mixed libraries with movies in a "show" library
|
||||||
|
else if (result?.imdbId || result?.tmdbId) {
|
||||||
|
await this.processMovie(jellyfinitem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (tvShow) {
|
if (tvShow) {
|
||||||
await this.asyncLock.dispatch(tvShow.id, async () => {
|
await this.asyncLock.dispatch(tvShow.id, async () => {
|
||||||
@@ -302,9 +376,20 @@ class JellyfinScanner {
|
|||||||
|
|
||||||
for (const season of seasons) {
|
for (const season of seasons) {
|
||||||
const JellyfinSeasons = await this.jfClient.getSeasons(Id);
|
const JellyfinSeasons = await this.jfClient.getSeasons(Id);
|
||||||
const matchedJellyfinSeason = JellyfinSeasons.find(
|
const matchedJellyfinSeason = JellyfinSeasons.find((md) => {
|
||||||
(md) => Number(md.IndexNumber) === season.season_number
|
if (tvdbSeasonFromAnidb) {
|
||||||
|
// In AniDB we don't have the concept of seasons,
|
||||||
|
// we have multiple shows with only Season 1 (and sometimes a season with index 0 for specials).
|
||||||
|
// We use tvdbSeasonFromAnidb to check if we are on the correct TMDB season and
|
||||||
|
// md.IndexNumber === 1 to be sure to find the correct season on jellyfin
|
||||||
|
return (
|
||||||
|
tvdbSeasonFromAnidb === season.season_number &&
|
||||||
|
md.IndexNumber === 1
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return Number(md.IndexNumber) === season.season_number;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const existingSeason = media?.seasons.find(
|
const existingSeason = media?.seasons.find(
|
||||||
(es) => es.seasonNumber === season.season_number
|
(es) => es.seasonNumber === season.season_number
|
||||||
@@ -357,6 +442,29 @@ class JellyfinScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With AniDB we can have multiple shows for one season, so we need to save
|
||||||
|
// the episode from all the jellyfin entries to get the total
|
||||||
|
if (tvdbSeasonFromAnidb) {
|
||||||
|
if (this.processedAnidbSeason.has(tvShow.id)) {
|
||||||
|
const show = this.processedAnidbSeason.get(tvShow.id)!;
|
||||||
|
if (show.has(season.season_number)) {
|
||||||
|
show.set(
|
||||||
|
season.season_number,
|
||||||
|
show.get(season.season_number)! + totalStandard
|
||||||
|
);
|
||||||
|
|
||||||
|
totalStandard = show.get(season.season_number)!;
|
||||||
|
} else {
|
||||||
|
show.set(season.season_number, totalStandard);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.processedAnidbSeason.set(
|
||||||
|
tvShow.id,
|
||||||
|
new Map([[season.season_number, totalStandard]])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
media &&
|
media &&
|
||||||
(totalStandard > 0 || (total4k > 0 && !this.enable4kShow)) &&
|
(totalStandard > 0 || (total4k > 0 && !this.enable4kShow)) &&
|
||||||
@@ -569,6 +677,7 @@ class JellyfinScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async processItems(slicedItems: JellyfinLibraryItem[]) {
|
private async processItems(slicedItems: JellyfinLibraryItem[]) {
|
||||||
|
this.processedAnidbSeason = new Map();
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
slicedItems.map(async (item) => {
|
slicedItems.map(async (item) => {
|
||||||
if (item.Type === 'Movie') {
|
if (item.Type === 'Movie') {
|
||||||
@@ -666,6 +775,8 @@ class JellyfinScanner {
|
|||||||
(library) => library.enabled
|
(library) => library.enabled
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await animeList.sync();
|
||||||
|
|
||||||
this.enable4kMovie = settings.radarr.some((radarr) => radarr.is4k);
|
this.enable4kMovie = settings.radarr.some((radarr) => radarr.is4k);
|
||||||
if (this.enable4kMovie) {
|
if (this.enable4kMovie) {
|
||||||
this.log(
|
this.log(
|
||||||
|
|||||||
Reference in New Issue
Block a user