aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/ci-tests.yml2
-rw-r--r--.github/workflows/pull-request-conflict.yml2
-rw-r--r--Directory.Packages.props2
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs3
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs22
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs8
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs30
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_DO.json110
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json8
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs18
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs3
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs18
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs52
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs7
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs13
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs14
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs25
-rw-r--r--Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs2
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixAudioData.cs104
-rw-r--r--Jellyfin.Server/Startup.cs7
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs3
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs3
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs19
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs53
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs3
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs15
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs11
-rw-r--r--MediaBrowser.Controller/Providers/ItemInfo.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs27
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs4
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs165
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs307
-rw-r--r--MediaBrowser.Providers/Movies/MovieMetadataService.cs16
-rw-r--r--MediaBrowser.Providers/Movies/TrailerMetadataService.cs21
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs4
-rw-r--r--MediaBrowser.Providers/Music/AudioMetadataService.cs5
-rw-r--r--MediaBrowser.Providers/Music/MusicVideoMetadataService.cs5
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs21
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs57
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs15
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeriesNfoSeasonParser.cs60
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs5
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/SeriesNfoSeasonProvider.cs89
-rw-r--r--src/Jellyfin.Extensions/StreamExtensions.cs7
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs58
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs4
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs5
55 files changed, 951 insertions, 523 deletions
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 0ccc3307c..a3976a3a4 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
+ uses: github/codeql-action/init@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
+ uses: github/codeql-action/autobuild@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
+ uses: github/codeql-action/analyze@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index a5650c5a4..77adfddee 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -34,7 +34,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@9f1033dc04b18a7dfa51aeefeb18540e8939021f # 5.3.4
+ uses: danielpalme/ReportGenerator-GitHub-Action@fa728091745cdd279fddda1e0e80fb29265d0977 # 5.3.5
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml
index 5823c309c..5d342b7f8 100644
--- a/.github/workflows/pull-request-conflict.yml
+++ b/.github/workflows/pull-request-conflict.yml
@@ -15,7 +15,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
- uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
+ uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f9a436a6a..8d3dd064b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -16,7 +16,7 @@
<PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
- <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.4.3" />
+ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.5.0" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index f0c267627..c06cd8510 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -19,7 +19,8 @@ namespace Emby.Server.Implementations
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
- { SqliteCacheSizeKey, "20000" }
+ { SqliteCacheSizeKey, "20000" },
+ { SqliteDisableSecondLevelCacheKey, bool.FalseString }
};
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 34d753093..a09988fee 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -1298,16 +1298,15 @@ namespace Emby.Server.Implementations.Data
&& type != typeof(Book)
&& type != typeof(LiveTvProgram)
&& type != typeof(AudioBook)
- && type != typeof(Audio)
&& type != typeof(MusicAlbum);
}
private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query)
{
- return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query));
+ return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query), false);
}
- private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
+ private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields, bool skipDeserialization)
{
var typeString = reader.GetString(0);
@@ -1320,7 +1319,7 @@ namespace Emby.Server.Implementations.Data
BaseItem item = null;
- if (TypeRequiresDeserialization(type))
+ if (TypeRequiresDeserialization(type) && !skipDeserialization)
{
try
{
@@ -2562,7 +2561,7 @@ namespace Emby.Server.Implementations.Data
foreach (var row in statement.ExecuteQuery())
{
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
+ var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, query.SkipDeserialization);
if (item is not null)
{
items.Add(item);
@@ -2774,7 +2773,7 @@ namespace Emby.Server.Implementations.Data
foreach (var row in statement.ExecuteQuery())
{
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
+ var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
if (item is not null)
{
list.Add(item);
@@ -5021,7 +5020,7 @@ AND Type = @InternalPersonType)");
foreach (var row in statement.ExecuteQuery())
{
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
+ var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
if (item is not null)
{
var countStartColumn = columns.Count - 1;
@@ -5222,19 +5221,20 @@ AND Type = @InternalPersonType)");
throw new ArgumentNullException(nameof(itemId));
}
- ArgumentNullException.ThrowIfNull(people);
-
CheckDisposed();
using var connection = GetConnection();
using var transaction = connection.BeginTransaction();
- // First delete chapters
+ // Delete all existing people first
using var command = connection.CreateCommand();
command.CommandText = "delete from People where ItemId=@ItemId";
command.TryBind("@ItemId", itemId);
command.ExecuteNonQuery();
- InsertPeople(itemId, people, connection);
+ if (people is not null)
+ {
+ InsertPeople(itemId, people, connection);
+ }
transaction.Commit();
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index cca835e4f..e66f2496a 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2812,8 +2812,10 @@ namespace Emby.Server.Implementations.Library
}
_itemRepository.UpdatePeople(item.Id, people);
-
- await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
+ if (people is not null)
+ {
+ await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
+ }
}
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool removeOnFailure)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 1bdae7f62..f7270bec1 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
+using Emby.Naming.Audio;
using Emby.Naming.Common;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
@@ -85,6 +86,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
+ var albumParser = new AlbumParser(_namingOptions);
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
@@ -100,6 +102,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
}
+ // If the folder is a multi-disc folder, then it is not an artist folder
+ if (albumParser.IsMultiPart(fileSystemInfo.FullName))
+ {
+ return;
+ }
+
// If we contain a music album assume we are an artist folder
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index ff4a88162..733ae2d1a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
IndexNumber = seasonParserResult.SeasonNumber,
SeriesId = series.Id,
- SeriesName = series.Name
+ SeriesName = series.Name,
+ Path = seasonParserResult.IsSeasonFolder ? path : args.Parent.Path
};
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
@@ -78,27 +79,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
}
}
- if (season.IndexNumber.HasValue)
+ if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name))
{
var seasonNumber = season.IndexNumber.Value;
- if (string.IsNullOrEmpty(season.Name))
- {
- var seasonNames = series.GetSeasonNames();
- if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
- {
- season.Name = seasonName;
- }
- else
- {
- season.Name = seasonNumber == 0 ?
- args.LibraryOptions.SeasonZeroDisplayName :
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("NameSeasonNumber"),
- seasonNumber,
- args.LibraryOptions.PreferredMetadataLanguage);
- }
- }
+ season.Name = seasonNumber == 0 ?
+ args.LibraryOptions.SeasonZeroDisplayName :
+ string.Format(
+ CultureInfo.InvariantCulture,
+ _localization.GetLocalizedString("NameSeasonNumber"),
+ seasonNumber,
+ args.LibraryOptions.PreferredMetadataLanguage);
}
return season;
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index b5e2c9b6b..44f3b6596 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -17,7 +17,7 @@
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
- "HeaderFavoriteAlbums": "Favoritalbummer",
+ "HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritkunstnere",
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
"HeaderFavoriteShows": "Yndlingsserier",
@@ -87,21 +87,21 @@
"UserOnlineFromDevice": "{0} er online fra {1}",
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
- "UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
+ "UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1} på {2}",
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
- "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
+ "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er konfigurerede til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins",
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
"TaskCleanLogs": "Ryd Log-mappe",
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
"TaskRefreshLibrary": "Scan Mediebibliotek",
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
- "TaskCleanCache": "Ryd Cache-mappe",
+ "TaskCleanCache": "Ryd cache-mappe",
"TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
@@ -128,5 +128,7 @@
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
"TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
- "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner enheder fra samlinger og afspilningslister der ikke eksisterer længere."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
+ "TaskAudioNormalizationDescription": "Skanner filer for data vedrørende audio-normalisering.",
+ "TaskAudioNormalization": "Audio-normalisering"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index d4151adf3..e9ace71a5 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -11,7 +11,7 @@
"Collections": "Colecciones",
"DeviceOfflineWithName": "{0} se ha desconectado",
"DeviceOnlineWithName": "{0} está conectado",
- "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
+ "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index f9d62d54e..13e007b4c 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -11,7 +11,7 @@
"Collections": "Colecciones",
"DeviceOfflineWithName": "{0} se ha desconectado",
"DeviceOnlineWithName": "{0} está conectado",
- "FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}",
+ "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json
index 0f4c7438f..8cdd06b7c 100644
--- a/Emby.Server.Implementations/Localization/Core/es_DO.json
+++ b/Emby.Server.Implementations/Localization/Core/es_DO.json
@@ -12,14 +12,118 @@
"Application": "Aplicación",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"HeaderContinueWatching": "Continuar Viendo",
- "HeaderAlbumArtists": "Artistas del Álbum",
+ "HeaderAlbumArtists": "Artistas del álbum",
"Genres": "Géneros",
"Folders": "Carpetas",
"Favorites": "Favoritos",
- "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
+ "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
"HeaderFavoriteSongs": "Canciones Favoritas",
"HeaderFavoriteEpisodes": "Episodios Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
"External": "Externo",
- "Default": "Predeterminado"
+ "Default": "Predeterminado",
+ "Movies": "Películas",
+ "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de la configuración ha sido actualizada",
+ "MixedContent": "Contenido mixto",
+ "Music": "Música",
+ "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
+ "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
+ "NotificationOptionVideoPlayback": "Reproducción de video iniciada",
+ "Sync": "Sincronizar",
+ "Shows": "Series",
+ "UserDownloadingItemWithValues": "{0} está descargando {1}",
+ "UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
+ "UserOnlineFromDevice": "{0} está en línea desde {1}",
+ "TasksChannelsCategory": "Canales de Internet",
+ "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
+ "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
+ "TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
+ "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
+ "TaskAudioNormalization": "Normalización de audio",
+ "TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.",
+ "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Remover elementos de colecciones y listas de reproducción que no existen.",
+ "TvShows": "Series de TV",
+ "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
+ "TaskRefreshChannels": "Actualizar canales",
+ "Photos": "Fotos",
+ "HeaderFavoriteShows": "Programas favoritos",
+ "TaskCleanActivityLog": "Limpiar registro de actividades",
+ "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
+ "System": "Sistema",
+ "User": "Usuario",
+ "Forced": "Forzado",
+ "PluginInstalledWithName": "{0} ha sido instalado",
+ "HeaderFavoriteAlbums": "Álbumes favoritos",
+ "TaskUpdatePlugins": "Actualizar Plugins",
+ "Latest": "Recientes",
+ "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
+ "Songs": "Canciones",
+ "NotificationOptionPluginError": "Falla de plugin",
+ "ScheduledTaskStartedWithName": "{0} iniciado",
+ "TasksApplicationCategory": "Aplicación",
+ "UserDeletedWithName": "El usuario {0} ha sido eliminado",
+ "TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
+ "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para plugins que están configurados para actualizarse automáticamente.",
+ "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
+ "NotificationOptionUserLockedOut": "Usuario bloqueado",
+ "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
+ "TaskCleanTranscode": "Limpiar el directorio de transcodificaciones",
+ "NotificationOptionPluginUpdateInstalled": "Actualización de plugin instalada",
+ "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
+ "TasksLibraryCategory": "Biblioteca",
+ "NotificationOptionPluginInstalled": "Plugin instalado",
+ "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
+ "VersionNumber": "Versión {0}",
+ "HeaderNextUp": "A continuación",
+ "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca",
+ "LabelIpAddressValue": "Dirección IP: {0}",
+ "NameSeasonNumber": "Temporada {0}",
+ "NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
+ "Plugin": "Plugin",
+ "NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
+ "NotificationOptionTaskFailed": "Falló la tarea programada",
+ "LabelRunningTimeValue": "Tiempo en ejecución: {0}",
+ "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
+ "TaskRefreshLibrary": "Escanear biblioteca de medios",
+ "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
+ "TasksMaintenanceCategory": "Mantenimiento",
+ "ProviderValue": "Proveedor: {0}",
+ "UserCreatedWithName": "El usuario {0} ha sido creado",
+ "PluginUninstalledWithName": "{0} ha sido desinstalado",
+ "ValueSpecialEpisodeName": "Especial - {0}",
+ "ScheduledTaskFailedWithName": "{0} falló",
+ "TaskCleanLogs": "Limpiar directorio de registros",
+ "NameInstallFailed": "Falló la instalación de {0}",
+ "UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
+ "TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios para encontrar archivos nuevos y actualizar los metadatos.",
+ "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo en un momento.",
+ "Playlists": "Listas de reproducción",
+ "TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
+ "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
+ "TaskRefreshPeople": "Actualizar personas",
+ "NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
+ "HeaderLiveTV": "TV en vivo",
+ "NameSeasonUnknown": "Temporada desconocida",
+ "NotificationOptionInstallationFailed": "Fallo de instalación",
+ "NotificationOptionPluginUninstalled": "Plugin desinstalado",
+ "TaskCleanCache": "Limpiar directorio caché",
+ "TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
+ "Inherit": "Heredar",
+ "HeaderRecordingGroups": "Grupos de grabación",
+ "ItemAddedWithName": "{0} fue agregado a la biblioteca",
+ "TaskOptimizeDatabase": "Optimizar base de datos",
+ "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
+ "HearingImpaired": "Discapacidad auditiva",
+ "HomeVideos": "Videos caseros",
+ "ItemRemovedWithName": "{0} fue removido de la biblioteca",
+ "MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
+ "MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
+ "MusicVideos": "Videos musicales",
+ "NewVersionIsAvailable": "Una nueva versión de Jellyfin está disponible para descargar.",
+ "PluginUpdatedWithName": "{0} ha sido actualizado",
+ "Undefined": "Sin definir",
+ "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
+ "TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
+ "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad."
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 78a443348..b925a482b 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -81,7 +81,7 @@
"Movies": "Film",
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
- "FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
+ "FailedLoginAttemptWithUserName": "Gagal upaya login dari {0}",
"CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus",
"DeviceOnlineWithName": "{0} telah terhubung",
@@ -125,5 +125,9 @@
"External": "Luar",
"HearingImpaired": "Gangguan Pendengaran",
"TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan."
+ "TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan.",
+ "TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
+ "TaskAudioNormalization": "Normalisasi Audio",
+ "TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada."
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
index 04d6ed0f2..7f3a8e291 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
@@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -69,7 +70,7 @@ public partial class AudioNormalizationTask : IScheduledTask
/// <inheritdoc />
public string Key => "AudioNormalization";
- [GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
+ [GeneratedRegex(@"^\s+I:\s+(.*?)\s+LUFS")]
private static partial Regex LUFSRegex();
/// <inheritdoc />
@@ -179,16 +180,17 @@ public partial class AudioNormalizationTask : IScheduledTask
}
using var reader = process.StandardError;
- var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
- cancellationToken.ThrowIfCancellationRequested();
- MatchCollection split = LUFSRegex().Matches(output);
-
- if (split.Count != 0)
+ await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
{
- return float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
+ Match match = LUFSRegex().Match(line);
+
+ if (match.Success)
+ {
+ return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
+ }
}
- _logger.LogError("Failed to find LUFS value in output:\n{Output}", output);
+ _logger.LogError("Failed to find LUFS value in output");
return null;
}
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 10d5b4f97..3dda5fdee 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1202,7 +1202,8 @@ namespace Emby.Server.Implementations.Session
new DtoOptions(false)
{
EnableImages = false
- })
+ },
+ user.DisplayMissingEpisodes)
.Where(i => !i.IsVirtualItem)
.SkipWhile(i => !i.Id.Equals(episode.Id))
.ToList();
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
index 9b4e2182c..e425000cd 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
using MediaBrowser.Common.Configuration;
using Microsoft.AspNetCore.Authorization;
@@ -24,24 +25,31 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
{
+ // Succeed if the startup wizard / first time setup is not complete
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
context.Succeed(requirement);
}
- else if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
+
+ // Succeed if user is admin
+ else if (context.User.IsInRole(UserRoles.Administrator))
{
- context.Fail();
+ context.Succeed(requirement);
}
- else if (!requirement.RequireAdmin && context.User.IsInRole(UserRoles.Guest))
+
+ // Fail if admin is required and user is not admin
+ else if (requirement.RequireAdmin)
{
context.Fail();
}
- else
+
+ // Succeed if admin is not required and user is not guest
+ else if (context.User.IsInRole(UserRoles.User))
{
- // Any user-specific checks are handled in the DefaultAuthorizationHandler.
context.Succeed(requirement);
}
+ // Any user-specific checks are handled in the DefaultAuthorizationHandler.
return Task.CompletedTask;
}
}
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index b4ce343be..4001a6add 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -290,17 +290,35 @@ public class ItemUpdateController : BaseJellyfinApiController
{
foreach (var season in rseries.Children.OfType<Season>())
{
- season.OfficialRating = request.OfficialRating;
+ if (!season.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ season.OfficialRating = request.OfficialRating;
+ }
+
season.CustomRating = request.CustomRating;
- season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!season.LockedFields.Contains(MetadataField.Tags))
+ {
+ season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
season.OnMetadataChanged();
await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
foreach (var ep in season.Children.OfType<Episode>())
{
- ep.OfficialRating = request.OfficialRating;
+ if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ ep.OfficialRating = request.OfficialRating;
+ }
+
ep.CustomRating = request.CustomRating;
- ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!ep.LockedFields.Contains(MetadataField.Tags))
+ {
+ ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
@@ -310,9 +328,18 @@ public class ItemUpdateController : BaseJellyfinApiController
{
foreach (var ep in season.Children.OfType<Episode>())
{
- ep.OfficialRating = request.OfficialRating;
+ if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ ep.OfficialRating = request.OfficialRating;
+ }
+
ep.CustomRating = request.CustomRating;
- ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!ep.LockedFields.Contains(MetadataField.Tags))
+ {
+ ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
@@ -321,9 +348,18 @@ public class ItemUpdateController : BaseJellyfinApiController
{
foreach (BaseItem track in album.Children)
{
- track.OfficialRating = request.OfficialRating;
+ if (!track.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ track.OfficialRating = request.OfficialRating;
+ }
+
track.CustomRating = request.CustomRating;
- track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!track.LockedFields.Contains(MetadataField.Tags))
+ {
+ track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
track.OnMetadataChanged();
await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index fb9f44d46..d268b93a8 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -319,7 +319,7 @@ public class LibraryStructureController : BaseJellyfinApiController
public ActionResult UpdateLibraryOptions(
[FromBody] UpdateLibraryOptionsDto request)
{
- var item = _libraryManager.GetItemById<CollectionFolder>(request.Id, User.GetUserId());
+ var item = _libraryManager.GetItemById<CollectionFolder>(request.Id);
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 68b4b6b8b..426402667 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -231,6 +231,7 @@ public class TvShowsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey();
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
{
@@ -240,7 +241,7 @@ public class TvShowsController : BaseJellyfinApiController
return NotFound("No season exists with Id " + seasonId);
}
- episodes = seasonItem.GetEpisodes(user, dtoOptions);
+ episodes = seasonItem.GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes);
}
else if (season.HasValue) // Season number was supplied. Get episodes by season number
{
@@ -256,7 +257,7 @@ public class TvShowsController : BaseJellyfinApiController
episodes = seasonItem is null ?
new List<BaseItem>()
- : ((Season)seasonItem).GetEpisodes(user, dtoOptions);
+ : ((Season)seasonItem).GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes);
}
else // No season number or season id was supplied. Returning all episodes.
{
@@ -265,7 +266,7 @@ public class TvShowsController : BaseJellyfinApiController
return NotFound("Series not found");
}
- episodes = series.GetEpisodes(user, dtoOptions).ToList();
+ episodes = series.GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes).ToList();
}
// Filter after the fact in case the ui doesn't want them
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 5faa7bc59..212d678a8 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -385,19 +385,6 @@ public class MediaInfoHelper
/// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
{
- // Enforce more restrictive transcoding profile for LiveTV due to compatability reasons
- // Cap the MaxStreamingBitrate to 20Mbps, because we are unable to reliably probe source bitrate,
- // which will cause the client to request extremely high bitrate that may fail the player/encoder
- request.MaxStreamingBitrate = request.MaxStreamingBitrate > 20000000 ? 20000000 : request.MaxStreamingBitrate;
-
- if (request.DeviceProfile is not null)
- {
- // Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues
- // Notably: Some channels won't play on FireFox and LG webOs
- // Some channels from HDHomerun will experience A/V sync issues
- request.DeviceProfile.TranscodingProfiles = request.DeviceProfile.TranscodingProfiles.Where(p => !string.Equals(p.Container, "mp4", StringComparison.OrdinalIgnoreCase)).ToArray();
- }
-
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
var profile = request.DeviceProfile;
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 6cd466da0..af4a9e689 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -142,6 +142,20 @@ public static class StreamingHelpers
}
else
{
+ // Enforce more restrictive transcoding profile for LiveTV due to compatability reasons
+ // Cap the MaxStreamingBitrate to 30Mbps, because we are unable to reliably probe source bitrate,
+ // which will cause the client to request extremely high bitrate that may fail the player/encoder
+ streamingRequest.VideoBitRate = streamingRequest.VideoBitRate > 30000000 ? 30000000 : streamingRequest.VideoBitRate;
+
+ if (streamingRequest.SegmentContainer is not null)
+ {
+ // Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues
+ // Notably: Some channels won't play on FireFox and LG webOS
+ // Some channels from HDHomerun will experience A/V sync issues
+ streamingRequest.SegmentContainer = "ts";
+ streamingRequest.VideoCodec = "h264";
+ }
+
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
mediaSource = liveStreamInfo.Item1;
state.DirectStreamProvider = liveStreamInfo.Item2;
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
index 3d747f2ea..a88989840 100644
--- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -16,21 +16,28 @@ public static class ServiceCollectionExtensions
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
/// </summary>
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
+ /// <param name="disableSecondLevelCache">Whether second level cache disabled..</param>
/// <returns>The updated service collection.</returns>
- public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
+ public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache)
{
- serviceCollection.AddEFSecondLevelCache(options =>
- options.UseMemoryCacheProvider()
- .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
- .UseCacheKeyPrefix("EF_")
- // Don't cache null values. Remove this optional setting if it's not necessary.
- .SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
+ if (!disableSecondLevelCache)
+ {
+ serviceCollection.AddEFSecondLevelCache(options =>
+ options.UseMemoryCacheProvider()
+ .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
+ .UseCacheKeyPrefix("EF_")
+ // Don't cache null values. Remove this optional setting if it's not necessary.
+ .SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
+ }
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
- opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}")
- .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
+ var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
+ if (!disableSecondLevelCache)
+ {
+ dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
+ }
});
return serviceCollection;
diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
index 6b95770ed..858df6728 100644
--- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
@@ -85,6 +85,6 @@ public static class WebHostBuilderExtensions
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
}
})
- .UseStartup(_ => new Startup(appHost));
+ .UseStartup(_ => new Startup(appHost, startupConfig));
}
}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 44aa43044..81fecc9a1 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -44,7 +44,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.FixPlaylistOwner),
typeof(Routines.MigrateRatingLevels),
typeof(Routines.AddDefaultCastReceivers),
- typeof(Routines.UpdateDefaultPluginRepository)
+ typeof(Routines.UpdateDefaultPluginRepository),
+ typeof(Routines.FixAudioData),
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
new file mode 100644
index 000000000..74f7e9c3e
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+ /// <summary>
+ /// Fixes the data column of audio types to be deserializable.
+ /// </summary>
+ internal class FixAudioData : IMigrationRoutine
+ {
+ private const string DbFilename = "library.db";
+ private readonly ILogger<FixAudioData> _logger;
+ private readonly IServerApplicationPaths _applicationPaths;
+ private readonly IItemRepository _itemRepository;
+
+ public FixAudioData(
+ IServerApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IItemRepository itemRepository)
+ {
+ _applicationPaths = applicationPaths;
+ _itemRepository = itemRepository;
+ _logger = loggerFactory.CreateLogger<FixAudioData>();
+ }
+
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("{CF6FABC2-9FBE-4933-84A5-FFE52EF22A58}");
+
+ /// <inheritdoc/>
+ public string Name => "FixAudioData";
+
+ /// <inheritdoc/>
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
+
+ // Back up the database before modifying any entries
+ for (int i = 1; ; i++)
+ {
+ var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
+ if (!File.Exists(bakPath))
+ {
+ try
+ {
+ File.Copy(dbPath, bakPath);
+ _logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
+ throw;
+ }
+ }
+ }
+
+ _logger.LogInformation("Backfilling audio lyrics data to database.");
+ var startIndex = 0;
+ var records = _itemRepository.GetCount(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Audio],
+ });
+
+ while (startIndex < records)
+ {
+ var results = _itemRepository.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Audio],
+ StartIndex = startIndex,
+ Limit = 100,
+ SkipDeserialization = true
+ })
+ .Cast<Audio>()
+ .ToList();
+
+ foreach (var audio in results)
+ {
+ var lyricMediaStreams = audio.GetMediaStreams().Where(s => s.Type == MediaStreamType.Lyric).Select(s => s.Path).ToList();
+ if (lyricMediaStreams.Count > 0)
+ {
+ audio.HasLyrics = true;
+ audio.LyricFiles = lyricMediaStreams;
+ }
+ }
+
+ _itemRepository.SaveItems(results, CancellationToken.None);
+ startIndex += 100;
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index e9fb3e4c2..2ff377403 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -40,15 +40,18 @@ namespace Jellyfin.Server
{
private readonly CoreAppHost _serverApplicationHost;
private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IConfiguration _startupConfig;
/// <summary>
/// Initializes a new instance of the <see cref="Startup" /> class.
/// </summary>
/// <param name="appHost">The server application host.</param>
- public Startup(CoreAppHost appHost)
+ /// <param name="startupConfig">The server startupConfig.</param>
+ public Startup(CoreAppHost appHost, IConfiguration startupConfig)
{
_serverApplicationHost = appHost;
_serverConfigurationManager = appHost.ConfigurationManager;
+ _startupConfig = startupConfig;
}
/// <summary>
@@ -67,7 +70,7 @@ namespace Jellyfin.Server
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
- services.AddJellyfinDbContext();
+ services.AddJellyfinDbContext(_startupConfig.GetSqliteSecondLevelCacheDisabled());
services.AddJellyfinApiSwagger();
// configure custom legacy authentication
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 22793206e..184bb4d68 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -752,9 +752,6 @@ namespace MediaBrowser.Controller.Entities
public virtual bool SupportsAncestors => true;
[JsonIgnore]
- public virtual bool StopRefreshIfLocalMetadataFound => true;
-
- [JsonIgnore]
protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
[JsonIgnore]
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 555dd050c..1461a3680 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -51,6 +51,7 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>();
VideoTypes = Array.Empty<VideoType>();
Years = Array.Empty<int>();
+ SkipDeserialization = false;
}
public InternalItemsQuery(User? user)
@@ -358,6 +359,8 @@ namespace MediaBrowser.Controller.Entities
public string? SeriesTimerId { get; set; }
+ public bool SkipDeserialization { get; set; }
+
public void SetUser(User user)
{
MaxParentalRating = user.MaxParentalAgeRating;
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index 81f6248fa..ede544eec 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -45,9 +45,6 @@ namespace MediaBrowser.Controller.Entities.Movies
set => TmdbCollectionName = value;
}
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
-
public override double GetDefaultPrimaryImageAspectRatio()
{
// hack for tv plugins
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index c29cefc15..083f12746 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -159,7 +159,7 @@ namespace MediaBrowser.Controller.Entities.TV
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
- var items = GetEpisodes(user, query.DtoOptions).Where(filter);
+ var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
return PostFilterAndSort(items, query, false);
}
@@ -169,30 +169,31 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary>
/// <param name="user">The user.</param>
/// <param name="options">The options to use.</param>
+ /// <param name="shouldIncludeMissingEpisodes">If missing episodes should be included.</param>
/// <returns>Set of episodes.</returns>
- public List<BaseItem> GetEpisodes(User user, DtoOptions options)
+ public List<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
- return GetEpisodes(Series, user, options);
+ return GetEpisodes(Series, user, options, shouldIncludeMissingEpisodes);
}
- public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options)
+ public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
- return GetEpisodes(series, user, null, options);
+ return GetEpisodes(series, user, null, options, shouldIncludeMissingEpisodes);
}
- public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options)
+ public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
- return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options);
+ return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
}
public List<BaseItem> GetEpisodes()
{
- return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true));
+ return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
}
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
- return GetEpisodes(user, new DtoOptions(true));
+ return GetEpisodes(user, new DtoOptions(true), true);
}
protected override bool GetBlockUnratedValue(User user)
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index d200721b2..d704208cd 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -25,12 +25,9 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary>
public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer
{
- private readonly Dictionary<int, string> _seasonNames;
-
public Series()
{
AirDays = Array.Empty<DayOfWeek>();
- _seasonNames = new Dictionary<int, string>();
}
public DayOfWeek[] AirDays { get; set; }
@@ -72,9 +69,6 @@ namespace MediaBrowser.Controller.Entities.TV
/// <value>The status.</value>
public SeriesStatus? Status { get; set; }
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
-
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 2;
@@ -212,26 +206,6 @@ namespace MediaBrowser.Controller.Entities.TV
return LibraryManager.GetItemList(query);
}
- public Dictionary<int, string> GetSeasonNames()
- {
- var newSeasons = Children.OfType<Season>()
- .Where(s => s.IndexNumber.HasValue)
- .Where(s => !_seasonNames.ContainsKey(s.IndexNumber.Value))
- .DistinctBy(s => s.IndexNumber);
-
- foreach (var season in newSeasons)
- {
- SetSeasonName(season.IndexNumber.Value, season.Name);
- }
-
- return _seasonNames;
- }
-
- public void SetSeasonName(int index, string name)
- {
- _seasonNames[index] = name;
- }
-
private void SetSeasonQueryOptions(InternalItemsQuery query, User user)
{
var seriesKey = GetUniqueSeriesKey(this);
@@ -276,7 +250,7 @@ namespace MediaBrowser.Controller.Entities.TV
return LibraryManager.GetItemsResult(query);
}
- public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options)
+ public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
var seriesKey = GetUniqueSeriesKey(this);
@@ -286,10 +260,10 @@ namespace MediaBrowser.Controller.Entities.TV
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
- DtoOptions = options
+ DtoOptions = options,
};
- if (user is null || !user.DisplayMissingEpisodes)
+ if (!shouldIncludeMissingEpisodes)
{
query.IsMissing = false;
}
@@ -299,7 +273,7 @@ namespace MediaBrowser.Controller.Entities.TV
var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
var allEpisodes = allItems.OfType<Season>()
- .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options))
+ .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes))
.Reverse();
// Specials could appear twice based on above - once in season 0, once in the aired season
@@ -311,8 +285,7 @@ namespace MediaBrowser.Controller.Entities.TV
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
- // Refresh bottom up, children first, then the boxset
- // By then hopefully the movies within will have Tmdb collection values
+ // Refresh bottom up, seasons and episodes first, then the series
var items = GetRecursiveChildren();
var totalItems = items.Count;
@@ -375,7 +348,7 @@ namespace MediaBrowser.Controller.Entities.TV
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
}
- public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options)
+ public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
@@ -392,24 +365,22 @@ namespace MediaBrowser.Controller.Entities.TV
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
- if (user is not null)
+
+ if (!shouldIncludeMissingEpisodes)
{
- if (!user.DisplayMissingEpisodes)
- {
- query.IsMissing = false;
- }
+ query.IsMissing = false;
}
var allItems = LibraryManager.GetItemList(query);
- return GetSeasonEpisodes(parentSeason, user, allItems, options);
+ return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
}
- public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options)
+ public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
if (allSeriesEpisodes is null)
{
- return GetSeasonEpisodes(parentSeason, user, options);
+ return GetSeasonEpisodes(parentSeason, user, options, shouldIncludeMissingEpisodes);
}
var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
index 1c558d419..81d50bbc1 100644
--- a/MediaBrowser.Controller/Entities/Trailer.cs
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -23,9 +23,6 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>();
}
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
-
public TrailerType[] TrailerTypes { get; set; }
public override double GetDefaultPrimaryImageAspectRatio()
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index 6c58064ce..7dfda73bf 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -65,6 +65,11 @@ namespace MediaBrowser.Controller.Extensions
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
/// <summary>
+ /// Disable second level cache of sqlite.
+ /// </summary>
+ public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
+
+ /// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to retrieve the value from.</param>
@@ -128,5 +133,15 @@ namespace MediaBrowser.Controller.Extensions
/// <returns>The sqlite cache size.</returns>
public static int? GetSqliteCacheSize(this IConfiguration configuration)
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
+
+ /// <summary>
+ /// Gets whether second level cache disabled from the <see cref="IConfiguration" />.
+ /// </summary>
+ /// <param name="configuration">The configuration to read the setting from.</param>
+ /// <returns>Whether second level cache disabled.</returns>
+ public static bool GetSqliteSecondLevelCacheDisabled(this IConfiguration configuration)
+ {
+ return configuration.GetValue<bool>(SqliteDisableSecondLevelCacheKey);
+ }
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index e4ec7a4e3..03c7faf47 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1189,8 +1189,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
_mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
- arg.Append(" -f concat -safe 0 -i ")
- .Append(tmpConcatPath);
+ arg.Append(" -f concat -safe 0 -i \"")
+ .Append(tmpConcatPath)
+ .Append("\" ");
}
else
{
@@ -2320,7 +2321,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (request.VideoBitRate.HasValue
&& (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value))
{
- return false;
+ // For LiveTV that has no bitrate, let's try copy if other conditions are met
+ if (string.IsNullOrWhiteSpace(request.LiveStreamId) || videoStream.BitRate.HasValue)
+ {
+ return false;
+ }
}
var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec);
diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs
index 3a97127ea..be3b25aee 100644
--- a/MediaBrowser.Controller/Providers/ItemInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemInfo.cs
@@ -11,6 +11,8 @@ namespace MediaBrowser.Controller.Providers
public ItemInfo(BaseItem item)
{
Path = item.Path;
+ ParentId = item.ParentId;
+ IndexNumber = item.IndexNumber;
ContainingFolderPath = item.ContainingFolderPath;
IsInMixedFolder = item.IsInMixedFolder;
@@ -27,6 +29,10 @@ namespace MediaBrowser.Controller.Providers
public string Path { get; set; }
+ public Guid ParentId { get; set; }
+
+ public int? IndexNumber { get; set; }
+
public string ContainingFolderPath { get; set; }
public VideoType VideoType { get; set; }
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 3b7745b6a..914990558 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -89,15 +89,28 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath,
CancellationToken cancellationToken)
{
- using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase));
+ if (shouldExtractOneByOne)
{
- if (!Directory.Exists(outputPath))
+ var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
+ foreach (var i in attachmentIndexes)
{
- await ExtractAllAttachmentsInternal(
- _mediaEncoder.GetInputArgument(inputFile, mediaSource),
- outputPath,
- false,
- cancellationToken).ConfigureAwait(false);
+ var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture));
+ await ExtractAttachment(inputFile, mediaSource, i, newName, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ {
+ if (!Directory.Exists(outputPath))
+ {
+ await ExtractAllAttachmentsInternal(
+ _mediaEncoder.GetInputArgument(inputFile, mediaSource),
+ outputPath,
+ false,
+ cancellationToken).ConfigureAwait(false);
+ }
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 80ef6ecf7..f85510dac 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -456,9 +456,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
extraArgs += " -probesize " + ffmpegProbeSize;
}
- if (request.MediaSource.RequiredHttpHeaders.TryGetValue("user_agent", out var userAgent))
+ if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
{
- extraArgs += " -user_agent " + userAgent;
+ extraArgs += $" -user_agent \"{userAgent}\"";
}
if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 234c5869a..0a98967da 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -121,7 +121,8 @@ namespace MediaBrowser.Providers.Manager
var metadataResult = new MetadataResult<TItemType>
{
- Item = itemOfType
+ Item = itemOfType,
+ People = LibraryManager.GetPeople(item)
};
bool hasRefreshedMetadata = true;
@@ -164,7 +165,7 @@ namespace MediaBrowser.Providers.Manager
}
// Next run remote image providers, but only if local image providers didn't throw an exception
- if (!localImagesFailed && refreshOptions.ImageRefreshMode != MetadataRefreshMode.ValidationOnly)
+ if (!localImagesFailed && refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly)
{
var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
@@ -242,7 +243,7 @@ namespace MediaBrowser.Providers.Manager
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
{
- if (result.Item.SupportsPeople && result.People is not null)
+ if (result.Item.SupportsPeople)
{
var baseItem = result.Item;
@@ -655,26 +656,19 @@ namespace MediaBrowser.Providers.Manager
await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
}
+ if (item.IsLocked)
+ {
+ return refreshResult;
+ }
+
var temp = new MetadataResult<TItemType>
{
Item = CreateNew()
};
temp.Item.Path = item.Path;
+ temp.Item.Id = item.Id;
- // If replacing all metadata, run internet providers first
- if (options.ReplaceAllMetadata)
- {
- var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
- .ConfigureAwait(false);
-
- refreshResult.UpdateType |= remoteResult.UpdateType;
- refreshResult.ErrorMessage = remoteResult.ErrorMessage;
- refreshResult.Failures += remoteResult.Failures;
- }
-
- var hasLocalMetadata = false;
var foundImageTypes = new List<ImageType>();
-
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
{
var providerName = provider.GetType().Name;
@@ -720,15 +714,9 @@ namespace MediaBrowser.Providers.Manager
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
}
- MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true);
+ MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
- // Only one local provider allowed per item
- if (item.IsLocked || localItem.Item.IsLocked || IsFullLocalMetadata(localItem.Item))
- {
- hasLocalMetadata = true;
- }
-
break;
}
@@ -747,10 +735,10 @@ namespace MediaBrowser.Providers.Manager
}
}
- // Local metadata is king - if any is found don't run remote providers
- if (!options.ReplaceAllMetadata && (!hasLocalMetadata || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || !item.StopRefreshIfLocalMetadataFound))
+ var isLocalLocked = temp.Item.IsLocked;
+ if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly))
{
- var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
+ var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
.ConfigureAwait(false);
refreshResult.UpdateType |= remoteResult.UpdateType;
@@ -762,19 +750,20 @@ namespace MediaBrowser.Providers.Manager
{
if (refreshResult.UpdateType > ItemUpdateType.None)
{
- if (hasLocalMetadata)
+ if (!options.RemoveOldMetadata)
+ {
+ // Add existing metadata to provider result if it does not exist there
+ MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
+ }
+
+ if (isLocalLocked)
{
MergeData(temp, metadata, item.LockedFields, true, true);
}
else
{
- if (!options.RemoveOldMetadata)
- {
- MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
- }
-
- // Will always replace all metadata when Scan for new and updated files is used. Else, follow the options.
- MergeData(temp, metadata, item.LockedFields, options.MetadataRefreshMode == MetadataRefreshMode.Default || options.ReplaceAllMetadata, false);
+ var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
+ MergeData(temp, metadata, item.LockedFields, shouldReplace, false);
}
}
}
@@ -787,16 +776,6 @@ namespace MediaBrowser.Providers.Manager
return refreshResult;
}
- protected virtual bool IsFullLocalMetadata(TItemType item)
- {
- if (string.IsNullOrWhiteSpace(item.Name))
- {
- return false;
- }
-
- return true;
- }
-
private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken)
{
Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
@@ -821,7 +800,7 @@ namespace MediaBrowser.Providers.Manager
return new TItemType();
}
- private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken)
+ private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool replaceData, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken)
{
var refreshResult = new RefreshResult();
@@ -846,7 +825,7 @@ namespace MediaBrowser.Providers.Manager
{
result.Provider = provider.Name;
- MergeData(result, temp, Array.Empty<MetadataField>(), false, false);
+ MergeData(result, temp, Array.Empty<MetadataField>(), replaceData, false);
MergeNewData(temp.Item, id);
refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
@@ -949,11 +928,7 @@ namespace MediaBrowser.Providers.Manager
if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
{
- // Safeguard against incoming data having an empty name
- if (!string.IsNullOrWhiteSpace(source.OriginalTitle))
- {
- target.OriginalTitle = source.OriginalTitle;
- }
+ target.OriginalTitle = source.OriginalTitle;
}
if (replaceData || !target.CommunityRating.HasValue)
@@ -1016,7 +991,7 @@ namespace MediaBrowser.Providers.Manager
{
targetResult.People = sourceResult.People;
}
- else if (targetResult.People is not null && sourceResult.People is not null)
+ else if (sourceResult.People is not null && sourceResult.People.Count >= 0)
{
MergePeople(sourceResult.People, targetResult.People);
}
@@ -1049,6 +1024,10 @@ namespace MediaBrowser.Providers.Manager
{
target.Studios = source.Studios;
}
+ else
+ {
+ target.Studios = target.Studios.Concat(source.Studios).Distinct().ToArray();
+ }
}
if (!lockedFields.Contains(MetadataField.Tags))
@@ -1057,6 +1036,10 @@ namespace MediaBrowser.Providers.Manager
{
target.Tags = source.Tags;
}
+ else
+ {
+ target.Tags = target.Tags.Concat(source.Tags).Distinct().ToArray();
+ }
}
if (!lockedFields.Contains(MetadataField.ProductionLocations))
@@ -1065,6 +1048,10 @@ namespace MediaBrowser.Providers.Manager
{
target.ProductionLocations = source.ProductionLocations;
}
+ else
+ {
+ target.Tags = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
+ }
}
foreach (var id in source.ProviderIds)
@@ -1082,17 +1069,28 @@ namespace MediaBrowser.Providers.Manager
}
}
+ if (replaceData || !target.CriticRating.HasValue)
+ {
+ target.CriticRating = source.CriticRating;
+ }
+
+ if (replaceData || target.RemoteTrailers.Count == 0)
+ {
+ target.RemoteTrailers = source.RemoteTrailers;
+ }
+ else
+ {
+ target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).Distinct().ToArray();
+ }
+
MergeAlbumArtist(source, target, replaceData);
- MergeCriticRating(source, target, replaceData);
- MergeTrailers(source, target, replaceData);
MergeVideoInfo(source, target, replaceData);
MergeDisplayOrder(source, target, replaceData);
if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
{
var forcedSortName = source.ForcedSortName;
-
- if (!string.IsNullOrWhiteSpace(forcedSortName))
+ if (!string.IsNullOrEmpty(forcedSortName))
{
target.ForcedSortName = forcedSortName;
}
@@ -1100,22 +1098,44 @@ namespace MediaBrowser.Providers.Manager
if (mergeMetadataSettings)
{
- target.LockedFields = source.LockedFields;
- target.IsLocked = source.IsLocked;
+ if (replaceData || !target.IsLocked)
+ {
+ target.IsLocked = target.IsLocked || source.IsLocked;
+ }
+
+ if (target.LockedFields.Length == 0)
+ {
+ target.LockedFields = source.LockedFields;
+ }
+ else
+ {
+ target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
+ }
- // Grab the value if it's there, but if not then don't overwrite with the default
if (source.DateCreated != default)
{
target.DateCreated = source.DateCreated;
}
- target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
- target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
+ if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
+ {
+ target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
+ }
+
+ if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage))
+ {
+ target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
+ }
}
}
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
{
+ if (target is null)
+ {
+ target = new List<PersonInfo>();
+ }
+
foreach (var person in target)
{
var normalizedName = person.Name.RemoveDiacritics();
@@ -1144,7 +1164,6 @@ namespace MediaBrowser.Providers.Manager
if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
{
var displayOrder = sourceHasDisplayOrder.DisplayOrder;
-
if (!string.IsNullOrWhiteSpace(displayOrder))
{
targetHasDisplayOrder.DisplayOrder = displayOrder;
@@ -1162,22 +1181,10 @@ namespace MediaBrowser.Providers.Manager
{
targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
}
- }
- }
-
- private static void MergeCriticRating(BaseItem source, BaseItem target, bool replaceData)
- {
- if (replaceData || !target.CriticRating.HasValue)
- {
- target.CriticRating = source.CriticRating;
- }
- }
-
- private static void MergeTrailers(BaseItem source, BaseItem target, bool replaceData)
- {
- if (replaceData || target.RemoteTrailers.Count == 0)
- {
- target.RemoteTrailers = source.RemoteTrailers;
+ else if (sourceHasAlbumArtist.AlbumArtists.Count >= 0)
+ {
+ targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct().ToArray();
+ }
}
}
@@ -1185,7 +1192,7 @@ namespace MediaBrowser.Providers.Manager
{
if (source is Video sourceCast && target is Video targetCast)
{
- if (replaceData || targetCast.Video3DFormat is null)
+ if (replaceData || !targetCast.Video3DFormat.HasValue)
{
targetCast.Video3DFormat = sourceCast.Video3DFormat;
}
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 34681fac8..7ffe2f32a 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -151,198 +152,212 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="tryExtractEmbeddedLyrics">Whether to extract embedded lyrics to lrc file. </param>
private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics)
{
- using var file = TagLib.File.Create(audio.Path);
- var tagTypes = file.TagTypesOnDisk;
Tag? tags = null;
-
- if (tagTypes.HasFlag(TagTypes.Id3v2))
- {
- tags = file.GetTag(TagTypes.Id3v2);
- }
- else if (tagTypes.HasFlag(TagTypes.Ape))
- {
- tags = file.GetTag(TagTypes.Ape);
- }
- else if (tagTypes.HasFlag(TagTypes.FlacMetadata))
- {
- tags = file.GetTag(TagTypes.FlacMetadata);
- }
- else if (tagTypes.HasFlag(TagTypes.Apple))
+ try
{
- tags = file.GetTag(TagTypes.Apple);
- }
- else if (tagTypes.HasFlag(TagTypes.Xiph))
- {
- tags = file.GetTag(TagTypes.Xiph);
- }
- else if (tagTypes.HasFlag(TagTypes.AudibleMetadata))
- {
- tags = file.GetTag(TagTypes.AudibleMetadata);
+ using var file = TagLib.File.Create(audio.Path);
+ var tagTypes = file.TagTypesOnDisk;
+
+ if (tagTypes.HasFlag(TagTypes.Id3v2))
+ {
+ tags = file.GetTag(TagTypes.Id3v2);
+ }
+ else if (tagTypes.HasFlag(TagTypes.Ape))
+ {
+ tags = file.GetTag(TagTypes.Ape);
+ }
+ else if (tagTypes.HasFlag(TagTypes.FlacMetadata))
+ {
+ tags = file.GetTag(TagTypes.FlacMetadata);
+ }
+ else if (tagTypes.HasFlag(TagTypes.Apple))
+ {
+ tags = file.GetTag(TagTypes.Apple);
+ }
+ else if (tagTypes.HasFlag(TagTypes.Xiph))
+ {
+ tags = file.GetTag(TagTypes.Xiph);
+ }
+ else if (tagTypes.HasFlag(TagTypes.AudibleMetadata))
+ {
+ tags = file.GetTag(TagTypes.AudibleMetadata);
+ }
+ else if (tagTypes.HasFlag(TagTypes.Id3v1))
+ {
+ tags = file.GetTag(TagTypes.Id3v1);
+ }
}
- else if (tagTypes.HasFlag(TagTypes.Id3v1))
+ catch (Exception e)
{
- tags = file.GetTag(TagTypes.Id3v1);
+ _logger.LogWarning(e, "TagLib-Sharp does not support this audio");
}
- if (tags is not null)
+ tags ??= new TagLib.Id3v2.Tag();
+ tags.AlbumArtists ??= mediaInfo.AlbumArtists;
+ tags.Album ??= mediaInfo.Album;
+ tags.Title ??= mediaInfo.Name;
+ tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
+ tags.Performers ??= mediaInfo.Artists;
+ tags.Genres ??= mediaInfo.Genres;
+ tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track;
+ tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;
+
+ if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
- if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
+ var people = new List<PersonInfo>();
+ var albumArtists = tags.AlbumArtists;
+ foreach (var albumArtist in albumArtists)
{
- var people = new List<PersonInfo>();
- var albumArtists = tags.AlbumArtists;
- foreach (var albumArtist in albumArtists)
+ if (!string.IsNullOrEmpty(albumArtist))
{
- if (!string.IsNullOrEmpty(albumArtist))
+ PeopleHelper.AddPerson(people, new PersonInfo
{
- PeopleHelper.AddPerson(people, new PersonInfo
- {
- Name = albumArtist,
- Type = PersonKind.AlbumArtist
- });
- }
+ Name = albumArtist,
+ Type = PersonKind.AlbumArtist
+ });
}
+ }
- var performers = tags.Performers;
- foreach (var performer in performers)
+ var performers = tags.Performers;
+ foreach (var performer in performers)
+ {
+ if (!string.IsNullOrEmpty(performer))
{
- if (!string.IsNullOrEmpty(performer))
+ PeopleHelper.AddPerson(people, new PersonInfo
{
- PeopleHelper.AddPerson(people, new PersonInfo
- {
- Name = performer,
- Type = PersonKind.Artist
- });
- }
+ Name = performer,
+ Type = PersonKind.Artist
+ });
}
+ }
- foreach (var composer in tags.Composers)
+ foreach (var composer in tags.Composers)
+ {
+ if (!string.IsNullOrEmpty(composer))
{
- if (!string.IsNullOrEmpty(composer))
+ PeopleHelper.AddPerson(people, new PersonInfo
{
- PeopleHelper.AddPerson(people, new PersonInfo
- {
- Name = composer,
- Type = PersonKind.Composer
- });
- }
- }
-
- _libraryManager.UpdatePeople(audio, people);
-
- if (options.ReplaceAllMetadata && performers.Length != 0)
- {
- audio.Artists = performers;
- }
- else if (!options.ReplaceAllMetadata
- && (audio.Artists is null || audio.Artists.Count == 0))
- {
- audio.Artists = performers;
- }
-
- if (albumArtists.Length == 0)
- {
- // Album artists not provided, fall back to performers (artists).
- albumArtists = performers;
- }
-
- if (options.ReplaceAllMetadata && albumArtists.Length != 0)
- {
- audio.AlbumArtists = albumArtists;
- }
- else if (!options.ReplaceAllMetadata
- && (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0))
- {
- audio.AlbumArtists = albumArtists;
+ Name = composer,
+ Type = PersonKind.Composer
+ });
}
}
- if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title))
- {
- audio.Name = tags.Title;
- }
+ _libraryManager.UpdatePeople(audio, people);
- if (options.ReplaceAllMetadata)
+ if (options.ReplaceAllMetadata && performers.Length != 0)
{
- audio.Album = tags.Album;
- audio.IndexNumber = Convert.ToInt32(tags.Track);
- audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
+ audio.Artists = performers;
}
- else
+ else if (!options.ReplaceAllMetadata
+ && (audio.Artists is null || audio.Artists.Count == 0))
{
- audio.Album ??= tags.Album;
- audio.IndexNumber ??= Convert.ToInt32(tags.Track);
- audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc);
+ audio.Artists = performers;
}
- if (tags.Year != 0)
+ if (albumArtists.Length == 0)
{
- var year = Convert.ToInt32(tags.Year);
- audio.ProductionYear = year;
-
- if (!audio.PremiereDate.HasValue)
- {
- try
- {
- audio.PremiereDate = new DateTime(year, 01, 01);
- }
- catch (ArgumentOutOfRangeException ex)
- {
- _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year.", audio.Path, tags.Year);
- }
- }
+ // Album artists not provided, fall back to performers (artists).
+ albumArtists = performers;
}
- if (!audio.LockedFields.Contains(MetadataField.Genres))
+ if (options.ReplaceAllMetadata && albumArtists.Length != 0)
{
- audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
- ? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
- : audio.Genres;
+ audio.AlbumArtists = albumArtists;
}
-
- if (!double.IsNaN(tags.ReplayGainTrackGain))
+ else if (!options.ReplaceAllMetadata
+ && (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0))
{
- audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
- }
-
- if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
- {
- audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId);
+ audio.AlbumArtists = albumArtists;
}
+ }
- if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
- {
- audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId);
- }
+ if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title))
+ {
+ audio.Name = tags.Title;
+ }
- if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
- {
- audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId);
- }
+ if (options.ReplaceAllMetadata)
+ {
+ audio.Album = tags.Album;
+ audio.IndexNumber = Convert.ToInt32(tags.Track);
+ audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
+ }
+ else
+ {
+ audio.Album ??= tags.Album;
+ audio.IndexNumber ??= Convert.ToInt32(tags.Track);
+ audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc);
+ }
- if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
- {
- audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId);
- }
+ if (tags.Year != 0)
+ {
+ var year = Convert.ToInt32(tags.Year);
+ audio.ProductionYear = year;
- if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
+ if (!audio.PremiereDate.HasValue)
{
- // Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`.
- // See https://github.com/mono/taglib-sharp/issues/304
- var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack);
- if (trackMbId is not null)
+ try
{
- audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
+ audio.PremiereDate = new DateTime(year, 01, 01);
+ }
+ catch (ArgumentOutOfRangeException ex)
+ {
+ _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, tags.Year);
}
}
+ }
+
+ if (!audio.LockedFields.Contains(MetadataField.Genres))
+ {
+ audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
+ ? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
+ : audio.Genres;
+ }
+
+ if (!double.IsNaN(tags.ReplayGainTrackGain))
+ {
+ audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
+ }
+
+ if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
+ {
+ audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId);
+ }
+
+ if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
+ {
+ audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId);
+ }
+
+ if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
+ {
+ audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId);
+ }
+
+ if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
+ {
+ audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId);
+ }
- // Save extracted lyrics if they exist,
- // and if the audio doesn't yet have lyrics.
- if (!string.IsNullOrWhiteSpace(tags.Lyrics)
- && tryExtractEmbeddedLyrics)
+ if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
+ {
+ // Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`.
+ // See https://github.com/mono/taglib-sharp/issues/304
+ var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack);
+ if (trackMbId is not null)
{
- await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false);
+ audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
}
}
+
+ // Save extracted lyrics if they exist,
+ // and if the audio doesn't yet have lyrics.
+ if (!string.IsNullOrWhiteSpace(tags.Lyrics)
+ && tryExtractEmbeddedLyrics)
+ {
+ await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false);
+ }
}
private void AddExternalLyrics(
diff --git a/MediaBrowser.Providers/Movies/MovieMetadataService.cs b/MediaBrowser.Providers/Movies/MovieMetadataService.cs
index 984a3c122..8997ddc64 100644
--- a/MediaBrowser.Providers/Movies/MovieMetadataService.cs
+++ b/MediaBrowser.Providers/Movies/MovieMetadataService.cs
@@ -24,22 +24,6 @@ namespace MediaBrowser.Providers.Movies
}
/// <inheritdoc />
- protected override bool IsFullLocalMetadata(Movie item)
- {
- if (string.IsNullOrWhiteSpace(item.Overview))
- {
- return false;
- }
-
- if (!item.ProductionYear.HasValue)
- {
- return false;
- }
-
- return base.IsFullLocalMetadata(item);
- }
-
- /// <inheritdoc />
protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
diff --git a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs
index ad0c5aaa7..e77d2fa8a 100644
--- a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs
+++ b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -24,22 +25,6 @@ namespace MediaBrowser.Providers.Movies
}
/// <inheritdoc />
- protected override bool IsFullLocalMetadata(Trailer item)
- {
- if (string.IsNullOrWhiteSpace(item.Overview))
- {
- return false;
- }
-
- if (!item.ProductionYear.HasValue)
- {
- return false;
- }
-
- return base.IsFullLocalMetadata(item);
- }
-
- /// <inheritdoc />
protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
@@ -48,6 +33,10 @@ namespace MediaBrowser.Providers.Movies
{
target.Item.TrailerTypes = source.Item.TrailerTypes;
}
+ else
+ {
+ target.Item.TrailerTypes = target.Item.TrailerTypes.Concat(source.Item.TrailerTypes).Distinct().ToArray();
+ }
}
}
}
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index e4f34776b..a39bd16ce 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -225,6 +225,10 @@ namespace MediaBrowser.Providers.Music
{
targetItem.Artists = sourceItem.Artists;
}
+ else
+ {
+ targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray();
+ }
if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)))
{
diff --git a/MediaBrowser.Providers/Music/AudioMetadataService.cs b/MediaBrowser.Providers/Music/AudioMetadataService.cs
index a5b7cb895..7b25bc0e4 100644
--- a/MediaBrowser.Providers/Music/AudioMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AudioMetadataService.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -60,6 +61,10 @@ namespace MediaBrowser.Providers.Music
{
targetItem.Artists = sourceItem.Artists;
}
+ else
+ {
+ targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray();
+ }
if (replaceData || string.IsNullOrEmpty(targetItem.Album))
{
diff --git a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
index b97b76630..24c4b5501 100644
--- a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
+++ b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -45,6 +46,10 @@ namespace MediaBrowser.Providers.Music
{
targetItem.Artists = sourceItem.Artists;
}
+ else
+ {
+ targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray();
+ }
}
}
}
diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
index 1bd000a48..43889bfbf 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -49,8 +50,24 @@ namespace MediaBrowser.Providers.Playlists
if (mergeMetadataSettings)
{
targetItem.PlaylistMediaType = sourceItem.PlaylistMediaType;
- targetItem.LinkedChildren = sourceItem.LinkedChildren;
- targetItem.Shares = sourceItem.Shares;
+
+ if (replaceData || targetItem.LinkedChildren.Length == 0)
+ {
+ targetItem.LinkedChildren = sourceItem.LinkedChildren;
+ }
+ else
+ {
+ targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
+ }
+
+ if (replaceData || targetItem.Shares.Count == 0)
+ {
+ targetItem.Shares = sourceItem.Shares;
+ }
+ else
+ {
+ targetItem.Shares = sourceItem.Shares.Concat(targetItem.Shares).DistinctBy(s => s.UserId).ToArray();
+ }
}
}
}
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 2d0bd60b9..41c6bd8f2 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -62,23 +62,7 @@ namespace MediaBrowser.Providers.TV
RemoveObsoleteEpisodes(item);
RemoveObsoleteSeasons(item);
- await UpdateAndCreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
- }
-
- /// <inheritdoc />
- protected override bool IsFullLocalMetadata(Series item)
- {
- if (string.IsNullOrWhiteSpace(item.Overview))
- {
- return false;
- }
-
- if (!item.ProductionYear.HasValue)
- {
- return false;
- }
-
- return base.IsFullLocalMetadata(item);
+ await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -88,24 +72,6 @@ namespace MediaBrowser.Providers.TV
var sourceItem = source.Item;
var targetItem = target.Item;
- var sourceSeasonNames = sourceItem.GetSeasonNames();
- var targetSeasonNames = targetItem.GetSeasonNames();
-
- if (replaceData)
- {
- foreach (var (number, name) in sourceSeasonNames)
- {
- targetItem.SetSeasonName(number, name);
- }
- }
- else
- {
- var newSeasons = sourceSeasonNames.Where(s => !targetSeasonNames.ContainsKey(s.Key));
- foreach (var (number, name) in newSeasons)
- {
- targetItem.SetSeasonName(number, name);
- }
- }
if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
{
@@ -162,7 +128,7 @@ namespace MediaBrowser.Providers.TV
private void RemoveObsoleteEpisodes(Series series)
{
- var episodes = series.GetEpisodes(null, new DtoOptions()).OfType<Episode>().ToList();
+ var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
var numberOfEpisodes = episodes.Count;
// TODO: O(n^2), but can it be done faster without overcomplicating it?
for (var i = 0; i < numberOfEpisodes; i++)
@@ -218,14 +184,12 @@ namespace MediaBrowser.Providers.TV
/// <summary>
/// Creates seasons for all episodes if they don't exist.
/// If no season number can be determined, a dummy season will be created.
- /// Updates seasons names.
/// </summary>
/// <param name="series">The series.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The async task.</returns>
- private async Task UpdateAndCreateSeasonsAsync(Series series, CancellationToken cancellationToken)
+ private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken)
{
- var seasonNames = series.GetSeasonNames();
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
var seasons = seriesChildren.OfType<Season>().ToList();
var uniqueSeasonNumbers = seriesChildren
@@ -237,23 +201,12 @@ namespace MediaBrowser.Providers.TV
foreach (var seasonNumber in uniqueSeasonNumbers)
{
// Null season numbers will have a 'dummy' season created because seasons are always required.
- var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
-
- if (!seasonNumber.HasValue || !seasonNames.TryGetValue(seasonNumber.Value, out var seasonName))
- {
- seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
- }
-
- if (existingSeason is null)
+ if (!seasons.Any(i => i.IndexNumber == seasonNumber))
{
+ var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
series.AddChild(season);
}
- else if (!existingSeason.LockedFields.Contains(MetadataField.Name) && !string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal))
- {
- existingSeason.Name = seasonName;
- await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
- }
}
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
index 3b551acec..d99e11bcd 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
@@ -100,19 +100,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ // Season names are processed by SeriesNfoSeasonParser
case "namedseason":
- {
- var parsed = int.TryParse(reader.GetAttribute("number"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber);
- var name = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(name) && parsed)
- {
- item.SetSeasonName(seasonNumber, name);
- }
-
- break;
- }
-
+ reader.Skip();
+ break;
default:
base.FetchDataFromXmlNode(reader, itemResult);
break;
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoSeasonParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoSeasonParser.cs
new file mode 100644
index 000000000..44ca3f472
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoSeasonParser.cs
@@ -0,0 +1,60 @@
+using System.Globalization;
+using System.Xml;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.XbmcMetadata.Parsers
+{
+ /// <summary>
+ /// NFO parser for seasons based on series NFO.
+ /// </summary>
+ public class SeriesNfoSeasonParser : BaseNfoParser<Season>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeriesNfoSeasonParser"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
+ public SeriesNfoSeasonParser(
+ ILogger logger,
+ IConfigurationManager config,
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService)
+ : base(logger, config, providerManager, userManager, userDataManager, directoryService)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override bool SupportsUrlAfterClosingXmlTag => true;
+
+ /// <inheritdoc />
+ protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Season> itemResult)
+ {
+ var item = itemResult.Item;
+
+ if (reader.Name == "namedseason")
+ {
+ var parsed = int.TryParse(reader.GetAttribute("number"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber);
+ var name = reader.ReadElementContentAsString();
+
+ if (parsed && !string.IsNullOrWhiteSpace(name) && item.IndexNumber.HasValue && seasonNumber == item.IndexNumber.Value)
+ {
+ item.Name = name;
+ }
+ }
+ else
+ {
+ reader.Skip();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
index 9b4e1731d..22c065b5d 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
@@ -42,7 +42,10 @@ namespace MediaBrowser.XbmcMetadata.Providers
try
{
- result.Item = new T();
+ result.Item = new T
+ {
+ IndexNumber = info.IndexNumber
+ };
Fetch(result, path, cancellationToken);
result.HasMetadata = true;
diff --git a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoSeasonProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoSeasonProvider.cs
new file mode 100644
index 000000000..b141b7afb
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoSeasonProvider.cs
@@ -0,0 +1,89 @@
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using MediaBrowser.XbmcMetadata.Parsers;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.XbmcMetadata.Providers
+{
+ /// <summary>
+ /// NFO provider for seasons based on series NFO.
+ /// </summary>
+ public class SeriesNfoSeasonProvider : BaseNfoProvider<Season>
+ {
+ private readonly ILogger<SeriesNfoSeasonProvider> _logger;
+ private readonly IConfigurationManager _config;
+ private readonly IProviderManager _providerManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
+ private readonly IDirectoryService _directoryService;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeriesNfoSeasonProvider"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{SeasonFromSeriesNfoProvider}"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public SeriesNfoSeasonProvider(
+ ILogger<SeriesNfoSeasonProvider> logger,
+ IFileSystem fileSystem,
+ IConfigurationManager config,
+ IProviderManager providerManager,
+ IUserManager userManager,
+ IUserDataManager userDataManager,
+ IDirectoryService directoryService,
+ ILibraryManager libraryManager)
+ : base(fileSystem)
+ {
+ _logger = logger;
+ _config = config;
+ _providerManager = providerManager;
+ _userManager = userManager;
+ _userDataManager = userDataManager;
+ _directoryService = directoryService;
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc />
+ protected override void Fetch(MetadataResult<Season> result, string path, CancellationToken cancellationToken)
+ {
+ new SeriesNfoSeasonParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ {
+ var seasonPath = info.Path;
+ if (seasonPath is not null)
+ {
+ var path = Path.Combine(seasonPath, "tvshow.nfo");
+ if (Path.Exists(path))
+ {
+ return directoryService.GetFile(path);
+ }
+ }
+
+ var seriesPath = _libraryManager.GetItemById(info.ParentId)?.Path;
+ if (seriesPath is not null)
+ {
+ var path = Path.Combine(seriesPath, "tvshow.nfo");
+ if (Path.Exists(path))
+ {
+ return directoryService.GetFile(path);
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs
index 182996852..0cfac384e 100644
--- a/src/Jellyfin.Extensions/StreamExtensions.cs
+++ b/src/Jellyfin.Extensions/StreamExtensions.cs
@@ -1,7 +1,9 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Text;
+using System.Threading;
namespace Jellyfin.Extensions
{
@@ -48,11 +50,12 @@ namespace Jellyfin.Extensions
/// Reads all lines in the <see cref="TextReader" />.
/// </summary>
/// <param name="reader">The <see cref="TextReader" /> to read from.</param>
+ /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>All lines in the stream.</returns>
- public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader)
+ public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
string? line;
- while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) is not null)
+ while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is not null)
{
yield return line;
}
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs
index 2e6ffb5f6..31d2b486b 100644
--- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs
@@ -1,14 +1,19 @@
+using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Auth.FirstTimeSetupPolicy;
using Jellyfin.Api.Constants;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
@@ -18,7 +23,9 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
{
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
+ private readonly DefaultAuthorizationHandler _defaultAuthorizationHandler;
private readonly FirstTimeSetupHandler _firstTimeSetupHandler;
+ private readonly IAuthorizationService _authorizationService;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
@@ -31,6 +38,21 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
_firstTimeSetupHandler = fixture.Create<FirstTimeSetupHandler>();
+ _defaultAuthorizationHandler = fixture.Create<DefaultAuthorizationHandler>();
+
+ var services = new ServiceCollection();
+ services.AddAuthorizationCore();
+ services.AddLogging();
+ services.AddOptions();
+ services.AddSingleton<IAuthorizationHandler>(_defaultAuthorizationHandler);
+ services.AddSingleton<IAuthorizationHandler>(_firstTimeSetupHandler);
+ services.AddAuthorization(options =>
+ {
+ options.AddPolicy("FirstTime", policy => policy.Requirements.Add(new FirstTimeSetupRequirement()));
+ options.AddPolicy("FirstTimeNoAdmin", policy => policy.Requirements.Add(new FirstTimeSetupRequirement(false, false)));
+ options.AddPolicy("FirstTimeSchedule", policy => policy.Requirements.Add(new FirstTimeSetupRequirement(true, false)));
+ });
+ _authorizationService = services.BuildServiceProvider().GetRequiredService<IAuthorizationService>();
}
[Theory]
@@ -45,10 +67,9 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
_httpContextAccessor,
userRole);
- var context = new AuthorizationHandlerContext(_requirements, claims, null);
+ var allowed = await _authorizationService.AuthorizeAsync(claims, "FirstTime");
- await _firstTimeSetupHandler.HandleAsync(context);
- Assert.True(context.HasSucceeded);
+ Assert.True(allowed.Succeeded);
}
[Theory]
@@ -63,17 +84,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
_httpContextAccessor,
userRole);
- var context = new AuthorizationHandlerContext(_requirements, claims, null);
+ var allowed = await _authorizationService.AuthorizeAsync(claims, "FirstTime");
- await _firstTimeSetupHandler.HandleAsync(context);
- Assert.Equal(shouldSucceed, context.HasSucceeded);
+ Assert.Equal(shouldSucceed, allowed.Succeeded);
}
[Theory]
[InlineData(UserRoles.Administrator, true)]
[InlineData(UserRoles.Guest, false)]
[InlineData(UserRoles.User, true)]
- public async Task ShouldRequireUserIfNotRequiresAdmin(string userRole, bool shouldSucceed)
+ public async Task ShouldRequireUserIfNotAdministrator(string userRole, bool shouldSucceed)
{
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
var claims = TestHelpers.SetupUser(
@@ -81,24 +101,26 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
_httpContextAccessor,
userRole);
- var context = new AuthorizationHandlerContext(
- new List<IAuthorizationRequirement> { new FirstTimeSetupRequirement(false, false) },
- claims,
- null);
+ var allowed = await _authorizationService.AuthorizeAsync(claims, "FirstTimeNoAdmin");
- await _firstTimeSetupHandler.HandleAsync(context);
- Assert.Equal(shouldSucceed, context.HasSucceeded);
+ Assert.Equal(shouldSucceed, allowed.Succeeded);
}
[Fact]
- public async Task ShouldAllowAdminApiKeyIfStartupWizardComplete()
+ public async Task ShouldDisallowUserIfOutsideSchedule()
{
+ AccessSchedule[] accessSchedules = { new AccessSchedule(DynamicDayOfWeek.Everyday, 0, 0, Guid.Empty) };
+
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
- var claims = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.Role, UserRoles.Administrator)]));
- var context = new AuthorizationHandlerContext(_requirements, claims, null);
+ var claims = TestHelpers.SetupUser(
+ _userManagerMock,
+ _httpContextAccessor,
+ UserRoles.User,
+ accessSchedules);
+
+ var allowed = await _authorizationService.AuthorizeAsync(claims, "FirstTimeSchedule");
- await _firstTimeSetupHandler.HandleAsync(context);
- Assert.True(context.HasSucceeded);
+ Assert.False(allowed.Succeeded);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs
index 263f74c90..84008cffd 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeExternalSourcesTests.cs
@@ -35,7 +35,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Protocol = MediaProtocol.Http,
RequiredHttpHeaders = new Dictionary<string, string>()
{
- { "user_agent", userAgent },
+ { "User-Agent", userAgent },
}
},
ExtractChapters = false,
@@ -44,7 +44,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
var extraArg = encoder.GetExtraArguments(req);
- Assert.Contains(userAgent, extraArg, StringComparison.InvariantCulture);
+ Assert.Contains($"-user_agent \"{userAgent}\"", extraArg, StringComparison.InvariantCulture);
}
}
}
diff --git a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
index ec4df9981..cedcaf9c0 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
@@ -19,7 +20,7 @@ namespace Jellyfin.Providers.Tests.Manager
[InlineData(true, true)]
public void MergeBaseItemData_MergeMetadataSettings_MergesWhenSet(bool mergeMetadataSettings, bool defaultDate)
{
- var newLocked = new[] { MetadataField.Cast };
+ var newLocked = new[] { MetadataField.Genres, MetadataField.Cast };
var newString = "new";
var newDate = DateTime.Now;
@@ -77,7 +78,7 @@ namespace Jellyfin.Providers.Tests.Manager
[Theory]
[InlineData("Name", MetadataField.Name, false)]
- [InlineData("OriginalTitle", null, false)]
+ [InlineData("OriginalTitle", null)]
[InlineData("OfficialRating", MetadataField.OfficialRating)]
[InlineData("CustomRating")]
[InlineData("Tagline")]