aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci-codeql-analysis.yml8
-rw-r--r--.github/workflows/ci-openapi.yml4
-rw-r--r--.github/workflows/ci-tests.yml2
-rw-r--r--.github/workflows/commands.yml6
-rw-r--r--.github/workflows/issue-template-check.yml2
-rw-r--r--.github/workflows/release-bump-version.yaml4
-rw-r--r--Directory.Packages.props6
-rw-r--r--Emby.Naming/ExternalFiles/ExternalPathParser.cs2
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs6
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ga.json15
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json54
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json16
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/uz.json17
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json4
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/au.csv2
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs13
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs17
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs44
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs45
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs3
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs4
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs1
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs5
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs7
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs12
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs11
-rw-r--r--MediaBrowser.Controller/IO/FileSystemHelper.cs64
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs26
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs8
-rw-r--r--MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs12
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs27
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs16
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs33
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs33
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs51
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs10
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs4
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs300
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs2
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs23
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs4
-rw-r--r--src/Jellyfin.Networking/Manager/NetworkManager.cs10
-rw-r--r--tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs1
51 files changed, 554 insertions, 418 deletions
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index add4a8c4d..7891f7f5a 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup .NET
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
+ uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
+ uses: github/codeql-action/autobuild@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
+ uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index edfd74f87..c142de9a9 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -16,7 +16,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -41,7 +41,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index d5850ecea..1e651068e 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: "${{ matrix.os }}"
steps:
- - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index cbaea507f..ee413bb10 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -128,7 +128,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: pull in script
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml
index 4a0b4f883..f73b2c429 100644
--- a/.github/workflows/issue-template-check.yml
+++ b/.github/workflows/issue-template-check.yml
@@ -10,7 +10,7 @@ jobs:
issues: write
steps:
- name: pull in script
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml
index f471572e5..575f2d756 100644
--- a/.github/workflows/release-bump-version.yaml
+++ b/.github/workflows/release-bump-version.yaml
@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ env.TAG_BRANCH }}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 27818c02b..61e1c8d84 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -20,7 +20,7 @@
<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" />
- <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
+ <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.5" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
@@ -59,11 +59,11 @@
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
- <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
+ <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.1" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
- <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
+ <PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
index 9d54533c2..7a01b02f3 100644
--- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -107,7 +107,7 @@ namespace Emby.Naming.ExternalFiles
pathInfo.Language = culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
- else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
+ else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
{
pathInfo.IsHearingImpaired = true;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index d5afac266..250bec9ea 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -389,7 +389,7 @@ namespace Emby.Server.Implementations.IO
var info = new FileInfo(path);
if (info.Exists &&
- ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
+ (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden != isHidden)
{
if (isHidden)
{
@@ -417,8 +417,8 @@ namespace Emby.Server.Implementations.IO
return;
}
- if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
- && ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
+ if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly == readOnly
+ && (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden == isHidden)
{
return;
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 953fe19e0..ac2248264 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1884,7 +1884,7 @@ namespace Emby.Server.Implementations.Library
try
{
var index = item.GetImageIndex(img);
- image = await ConvertImageToLocal(item, img, index, removeOnFailure: true).ConfigureAwait(false);
+ image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
}
catch (ArgumentException)
{
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 8a95fff15..4245656ff 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -11,7 +11,7 @@
"Collections": "التجميعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
- "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}",
+ "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}",
"Favorites": "المفضلة",
"Folders": "المجلدات",
"Genres": "التصنيفات",
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 68a0180eb..ce98979e6 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -126,7 +126,7 @@
"External": "Extern",
"HearingImpaired": "Hörgeschädigt",
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
- "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken.",
+ "TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
"TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
"TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
"TaskAudioNormalization": "Audio Normalisierung",
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index c6863ff36..e7deefbb0 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -112,7 +112,7 @@
"CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
"Application": "Aplicación",
- "AppDeviceValues": "App: {0}, Dispositivo: {1}",
+ "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
"TaskCleanActivityLog": "Limpiar registro de actividades",
"Undefined": "Sin definir",
@@ -125,5 +125,9 @@
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"HearingImpaired": "Discapacidad auditiva",
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
- "TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción"
+ "TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
+ "TaskAudioNormalization": "Normalización de audio",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
+ "TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
+ "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ga.json b/Emby.Server.Implementations/Localization/Core/ga.json
index 28e54bff5..b511ed6ba 100644
--- a/Emby.Server.Implementations/Localization/Core/ga.json
+++ b/Emby.Server.Implementations/Localization/Core/ga.json
@@ -1,3 +1,16 @@
{
- "Albums": "Albaim"
+ "Albums": "Albaim",
+ "Artists": "Ealaíontóir",
+ "AuthenticationSucceededWithUserName": "{0} fíordheimhnithe",
+ "Books": "leabhair",
+ "CameraImageUploadedFrom": "Tá íomhá ceamara nua uaslódáilte ó {0}",
+ "Channels": "Cainéil",
+ "ChapterNameValue": "Caibidil {0}",
+ "Collections": "Bailiúcháin",
+ "Default": "Mainneachtain",
+ "DeviceOfflineWithName": "scoireadh {0}",
+ "DeviceOnlineWithName": "{0} ceangailte",
+ "External": "Forimeallach",
+ "FailedLoginAttemptWithUserName": "Iarracht ar theip ar fhíordheimhniú ó {0}",
+ "Favorites": "Ceanáin"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 5bb2b7d4d..6a5b8c561 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -126,5 +126,6 @@
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
"HearingImpaired": "Oštećen sluh",
"TaskRefreshTrickplayImages": "Generiraj Trickplay Slike",
- "TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama."
+ "TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama.",
+ "TaskAudioNormalization": "Normalizacija zvuka"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 783aecec7..0e694af02 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -51,10 +51,10 @@
"NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata",
"NotificationOptionInstallationFailed": "Installazione fallita",
"NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto",
- "NotificationOptionPluginError": "Errore del Plug-in",
- "NotificationOptionPluginInstalled": "Plug-in installato",
- "NotificationOptionPluginUninstalled": "Plug-in disinstallato",
- "NotificationOptionPluginUpdateInstalled": "Aggiornamento del plug-in installato",
+ "NotificationOptionPluginError": "Errore del plugin",
+ "NotificationOptionPluginInstalled": "Plugin installato",
+ "NotificationOptionPluginUninstalled": "Plugin disinstallato",
+ "NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato",
"NotificationOptionServerRestartRequired": "Riavvio del server necessario",
"NotificationOptionTaskFailed": "Operazione pianificata fallita",
"NotificationOptionUserLockedOut": "Utente bloccato",
@@ -68,10 +68,10 @@
"PluginUpdatedWithName": "{0} è stato aggiornato",
"ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} fallito",
- "ScheduledTaskStartedWithName": "{0} avviati",
+ "ScheduledTaskStartedWithName": "{0} avviato",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Serie TV",
- "Songs": "Canzoni",
+ "Songs": "Brani",
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
"SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}",
"SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}",
@@ -87,48 +87,48 @@
"UserOnlineFromDevice": "{0} è online su {1}",
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
- "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di \"{1}\" su {2}",
+ "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
"UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1} su {2}",
"ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale",
"ValueSpecialEpisodeName": "Speciale - {0}",
"VersionNumber": "Versione {0}",
- "TaskRefreshChannelsDescription": "Aggiorna le informazioni dei canali Internet.",
+ "TaskRefreshChannelsDescription": "Aggiorna le informazioni dei canali internet.",
"TaskDownloadMissingSubtitlesDescription": "Cerca su internet i sottotitoli mancanti basandosi sulle configurazioni dei metadati.",
"TaskDownloadMissingSubtitles": "Scarica i sottotitoli mancanti",
- "TaskRefreshChannels": "Aggiorna i canali",
- "TaskCleanTranscodeDescription": "Cancella i file di transcode più vecchi di un giorno.",
- "TaskCleanTranscode": "Svuota la cartella del transcoding",
- "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.",
- "TaskUpdatePlugins": "Aggiorna i Plugin",
- "TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.",
- "TaskRefreshPeople": "Aggiornamento Persone",
+ "TaskRefreshChannels": "Aggiorna canali",
+ "TaskCleanTranscodeDescription": "Cancella i file di transcodifica più vecchi di un giorno.",
+ "TaskCleanTranscode": "Svuota la cartella della transcodifica",
+ "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin configurati per l'aggiornamento automatico.",
+ "TaskUpdatePlugins": "Aggiorna i plugin",
+ "TaskRefreshPeopleDescription": "Aggiorna i metadati degli attori e registi nella tua libreria.",
+ "TaskRefreshPeople": "Aggiorna Persone",
"TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.",
"TaskCleanLogs": "Pulisci la cartella dei log",
- "TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.",
- "TaskRefreshLibrary": "Scan Librerie",
- "TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.",
+ "TaskRefreshLibraryDescription": "Scansiona la libreria alla ricerca di nuovi file e aggiorna i metadati.",
+ "TaskRefreshLibrary": "Scansione della libreria",
+ "TaskRefreshChapterImagesDescription": "Crea le miniature per i video che hanno capitoli.",
"TaskRefreshChapterImages": "Estrai immagini capitolo",
"TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",
- "TaskCleanCache": "Pulisci la directory della cache",
+ "TaskCleanCache": "Pulisci la cartella della cache",
"TasksChannelsCategory": "Canali su Internet",
"TasksApplicationCategory": "Applicazione",
"TasksLibraryCategory": "Libreria",
"TasksMaintenanceCategory": "Manutenzione",
"TaskCleanActivityLog": "Attività di Registro Completate",
- "TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata.",
+ "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell’età configurata.",
"Undefined": "Non Definito",
"Forced": "Forzato",
"Default": "Predefinito",
- "TaskOptimizeDatabaseDescription": "Compatta Database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altri cambiamenti inerenti il database potrebbe aumentarne la performance.",
- "TaskOptimizeDatabase": "Ottimizza Database",
+ "TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.",
+ "TaskOptimizeDatabase": "Ottimizza database",
"TaskKeyframeExtractor": "Estrattore di Keyframe",
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
"External": "Esterno",
- "HearingImpaired": "con problemi di udito",
+ "HearingImpaired": "Non Udenti",
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
- "TaskCleanCollectionsAndPlaylists": "Ripulire le raccolte e le playlist",
- "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle raccolte e dalle playlist che non esistono più.",
- "TaskAudioNormalization": "Normalizzazione Audio",
- "TaskAudioNormalizationDescription": "Scansione files per normalizzazione audio."
+ "TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle playlist che non esistono più.",
+ "TaskAudioNormalization": "Normalizzazione dell'audio",
+ "TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 6e58ef834..78c3d0a40 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -17,7 +17,7 @@
"Inherit": "Pārmantot",
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
"VersionNumber": "Versija {0}",
- "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
+ "ValueHasBeenAddedToLibrary": "{0} tika pievienots jūsu multvides bibliotēkai",
"UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
"UserPasswordChangedWithName": "Lietotāja {0} parole tika nomainīta",
@@ -76,7 +76,7 @@
"Genres": "Žanri",
"Folders": "Mapes",
"Favorites": "Izlase",
- "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
+ "FailedLoginAttemptWithUserName": "Neveiksmīgs ielogošanos mēģinājums no {0}",
"DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
"DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
"Collections": "Kolekcijas",
@@ -95,7 +95,7 @@
"TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
"TasksApplicationCategory": "Lietotne",
"TasksLibraryCategory": "Bibliotēka",
- "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
+ "TaskDownloadMissingSubtitlesDescription": "Meklē trūkstošus subtitrus internēta balstoties uz metadatu uzstādījumiem.",
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
"TaskRefreshChannels": "Atjaunot kanālus",
@@ -105,8 +105,8 @@
"TaskUpdatePlugins": "Atjaunot paplašinājumus",
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
"TaskRefreshPeople": "Atjaunot cilvēkus",
- "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
- "TaskCleanLogs": "Iztīrīt logdatņu mapi",
+ "TaskCleanLogsDescription": "Nodzēš žurnāla ierakstus, kas ir senāki par {0} dienām.",
+ "TaskCleanLogs": "Iztīrīt žurnālu mapi",
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
"TaskRefreshLibrary": "Skenēt multivides bibliotēku",
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
@@ -125,5 +125,9 @@
"TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs.",
"TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus",
- "TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās."
+ "TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās.",
+ "TaskAudioNormalization": "Audio normalizācija",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Noņem elemēntus no kolekcijām un atskaņošanas sarakstiem, kuri vairs neeksistē.",
+ "TaskAudioNormalizationDescription": "Skanē failus priekš audio normālizācijas informācijas.",
+ "TaskCleanCollectionsAndPlaylists": "Notīrīt kolekcijas un atskaņošanas sarakstus"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index b6c15d871..b66818ddc 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -126,5 +126,9 @@
"External": "Ekstern",
"HearingImpaired": "Hørselshemmet",
"TaskRefreshTrickplayImages": "Generer Trickplay bilder",
- "TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker."
+ "TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker.",
+ "TaskCleanCollectionsAndPlaylists": "Rydd kolleksjoner og spillelister",
+ "TaskAudioNormalization": "Lyd Normalisering",
+ "TaskAudioNormalizationDescription": "Skan filer for lyd normaliserende data",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uz.json b/Emby.Server.Implementations/Localization/Core/uz.json
index 43935f224..a1b3035f3 100644
--- a/Emby.Server.Implementations/Localization/Core/uz.json
+++ b/Emby.Server.Implementations/Localization/Core/uz.json
@@ -8,5 +8,20 @@
"Channels": "Kanallar",
"Books": "Kitoblar",
"Artists": "Ijrochilar",
- "Albums": "Albomlar"
+ "Albums": "Albomlar",
+ "AuthenticationSucceededWithUserName": "{0} muvaffaqiyatli tasdiqlandi",
+ "AppDeviceValues": "Ilova: {0}, Qurilma: {1}",
+ "Application": "Ilova",
+ "CameraImageUploadedFrom": "{0}dan yangi kamera rasmi yuklandi",
+ "DeviceOnlineWithName": "{0} ulangan",
+ "ItemRemovedWithName": "{0} kutbxonadan o'chirildi",
+ "External": "Tashqi",
+ "FailedLoginAttemptWithUserName": "Muvafaqiyatsiz kirishlar soni {0}",
+ "Forced": "Majburiy",
+ "ChapterNameValue": "{0}chi bo'lim",
+ "DeviceOfflineWithName": "{0} aloqa uzildi",
+ "HeaderLiveTV": "Jonli TV",
+ "HeaderNextUp": "Keyingisi",
+ "ItemAddedWithName": "{0} kutbxonaga qo'shildi",
+ "LabelIpAddressValue": "IP manzil: {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 4f77eea3b..f06bbc591 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -127,5 +127,7 @@
"TaskRefreshTrickplayImages": "生成快轉縮圖",
"TaskRefreshTrickplayImagesDescription": "為啟用快轉縮圖的媒體庫生成快轉縮圖。",
"TaskCleanCollectionsAndPlaylists": "清理系列和播放清單",
- "TaskCleanCollectionsAndPlaylistsDescription": "清理系列和播放清單中已不存在的項目。"
+ "TaskCleanCollectionsAndPlaylistsDescription": "清理系列和播放清單中已不存在的項目。",
+ "TaskAudioNormalization": "音量標準化",
+ "TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。"
}
diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv
index 688125917..6e12759a4 100644
--- a/Emby.Server.Implementations/Localization/Ratings/au.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/au.csv
@@ -1,11 +1,11 @@
Exempt,0
G,0
7+,7
+PG,15
M,15
MA,15
MA15+,15
MA 15+,15
-PG,16
16+,16
R,18
R18+,18
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 8a35b96b3..47ff22c0b 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -170,8 +170,13 @@ namespace Emby.Server.Implementations.Playlists
private List<Playlist> GetUserPlaylists(Guid userId)
{
var user = _userManager.GetUserById(userId);
+ var playlistsFolder = GetPlaylistsFolder(userId);
+ if (playlistsFolder is null)
+ {
+ return [];
+ }
- return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>().ToList();
+ return playlistsFolder.GetChildren(user, true).OfType<Playlist>().ToList();
}
private static string GetTargetPath(string path)
@@ -184,11 +189,11 @@ namespace Emby.Server.Implementations.Playlists
return path;
}
- private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
+ private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, User user, DtoOptions options)
{
var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
- return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
+ return Playlist.GetPlaylistItems(items, user, options);
}
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
@@ -208,7 +213,7 @@ namespace Emby.Server.Implementations.Playlists
?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
// Retrieve all the items to be added to the playlist
- var newItems = GetPlaylistItems(newItemIds, playlist.MediaType, user, options)
+ var newItems = GetPlaylistItems(newItemIds, user, options)
.Where(i => i.SupportsAddingToPlaylist);
// Filter out duplicate items, if necessary
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
index 7f3a8e291..df0fdcab8 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
@@ -106,13 +106,20 @@ public partial class AudioNormalizationTask : IScheduledTask
continue;
}
- var tempFile = Path.Join(_configurationManager.GetTranscodePath(), Guid.NewGuid() + ".concat");
+ _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
+ var tempFile = Path.Join(_configurationManager.GetTranscodePath(), a.Id + ".concat");
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
- a.LUFS = await CalculateLUFSAsync(
- string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
- cancellationToken).ConfigureAwait(false);
- File.Delete(tempFile);
+ try
+ {
+ a.LUFS = await CalculateLUFSAsync(
+ string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
+ cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
}
_itemRepository.SaveItems(albums, cancellationToken);
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
index 19b245464..804097219 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -127,15 +127,8 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
{
_logger.LogDebug("Updating {FolderName}", folder.Name);
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
+ _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit);
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
-
- _providerManager.QueueRefresh(
- folder.Id,
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- ForceSave = true
- },
- RefreshPriority.High);
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index 03935b384..fc3ad90f6 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
@@ -133,53 +134,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
cancellationToken.ThrowIfCancellationRequested();
- DeleteFile(file.FullName);
+ FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
index++;
}
- DeleteEmptyFolders(directory);
+ FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
progress.Report(100);
}
-
- private void DeleteEmptyFolders(string parent)
- {
- foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
- {
- DeleteEmptyFolders(directory);
- if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
- {
- try
- {
- Directory.Delete(directory, false);
- }
- catch (UnauthorizedAccessException ex)
- {
- _logger.LogError(ex, "Error deleting directory {Path}", directory);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting directory {Path}", directory);
- }
- }
- }
- }
-
- private void DeleteFile(string path)
- {
- try
- {
- _fileSystem.DeleteFile(path);
- }
- catch (UnauthorizedAccessException ex)
- {
- _logger.LogError(ex, "Error deleting file {Path}", path);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting file {Path}", path);
- }
- }
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index e4e565c64..6cb06d31c 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
@@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
@@ -113,53 +113,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
cancellationToken.ThrowIfCancellationRequested();
- DeleteFile(file.FullName);
+ FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
index++;
}
- DeleteEmptyFolders(directory);
+ FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
progress.Report(100);
}
-
- private void DeleteEmptyFolders(string parent)
- {
- foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
- {
- DeleteEmptyFolders(directory);
- if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
- {
- try
- {
- Directory.Delete(directory, false);
- }
- catch (UnauthorizedAccessException ex)
- {
- _logger.LogError(ex, "Error deleting directory {Path}", directory);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting directory {Path}", directory);
- }
- }
- }
- }
-
- private void DeleteFile(string path)
- {
- try
- {
- _fileSystem.DeleteFile(path);
- }
- catch (UnauthorizedAccessException ex)
- {
- _logger.LogError(ex, "Error deleting file {Path}", path);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting file {Path}", path);
- }
- }
}
}
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index c1343b130..d7a8c37c4 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -80,7 +80,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|| replaceAllImages
|| replaceAllMetadata,
- IsAutomated = false
+ IsAutomated = false,
+ RemoveOldMetadata = replaceAllMetadata
};
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index cd4a0a23b..d33634412 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -76,6 +76,7 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
/// <param name="hasTrailer">Optional filter by items with trailers.</param>
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="indexNumber">Optional filter by index number.</param>
/// <param name="parentIndexNumber">Optional filter by parent index number.</param>
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
/// <param name="isHd">Optional filter by items that are HD or not.</param>
@@ -165,6 +166,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? hasSpecialFeature,
[FromQuery] bool? hasTrailer,
[FromQuery] Guid? adjacentTo,
+ [FromQuery] int? indexNumber,
[FromQuery] int? parentIndexNumber,
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
@@ -366,6 +368,7 @@ public class ItemsController : BaseJellyfinApiController
MinCommunityRating = minCommunityRating,
MinCriticRating = minCriticRating,
ParentId = parentId ?? Guid.Empty,
+ IndexNumber = indexNumber,
ParentIndexNumber = parentIndexNumber,
EnableTotalRecordCount = enableTotalRecordCount,
ExcludeItemIds = excludeItemIds,
@@ -717,6 +720,7 @@ public class ItemsController : BaseJellyfinApiController
hasSpecialFeature,
hasTrailer,
adjacentTo,
+ null,
parentIndexNumber,
hasParentalRating,
isHd,
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 4fbaafa2a..d7d0cc454 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -215,6 +215,7 @@ public class TrailersController : BaseJellyfinApiController
hasSpecialFeature,
hasTrailer,
adjacentTo,
+ null,
parentIndexNumber,
hasParentalRating,
isHd,
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index af4a9e689..535ef27c3 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -154,6 +154,11 @@ public static class StreamingHelpers
// Some channels from HDHomerun will experience A/V sync issues
streamingRequest.SegmentContainer = "ts";
streamingRequest.VideoCodec = "h264";
+ streamingRequest.AudioCodec = "aac";
+ state.SupportedVideoCodecs = ["h264"];
+ state.Request.VideoCodec = "h264";
+ state.SupportedAudioCodecs = ["aac"];
+ state.Request.AudioCodec = "aac";
}
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 184bb4d68..8bd4fb4f3 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1949,14 +1949,15 @@ namespace MediaBrowser.Controller.Entities
return;
}
- // Remove it from the item
- RemoveImage(info);
-
+ // Remove from file system
if (info.IsLocalFile)
{
FileSystem.DeleteFile(info.Path);
}
+ // Remove from item
+ RemoveImage(info);
+
await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index fcb45e7e5..b2e5d7263 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Security;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
@@ -370,9 +371,18 @@ namespace MediaBrowser.Controller.Entities
{
nonCachedChildren = GetNonCachedChildren(directoryService);
}
+ catch (IOException ex)
+ {
+ Logger.LogError(ex, "Error retrieving children from file system");
+ }
+ catch (SecurityException ex)
+ {
+ Logger.LogError(ex, "Error retrieving children from file system");
+ }
catch (Exception ex)
{
- Logger.LogError(ex, "Error retrieving children folder");
+ Logger.LogError(ex, "Error retrieving children");
+ return;
}
progress.Report(ProgressHelpers.RetrievedChildren);
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index d704208cd..6297b67e4 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -350,17 +350,10 @@ namespace MediaBrowser.Controller.Entities.TV
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
- var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
-
- // add optimization when this setting is not enabled
- var seriesKey = queryFromSeries ?
- GetUniqueSeriesKey(this) :
- GetUniqueSeriesKey(parentSeason);
-
var query = new InternalItemsQuery(user)
{
- AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
- SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
+ AncestorWithPresentationUniqueKey = null,
+ SeriesPresentationUniqueKey = GetUniqueSeriesKey(this),
IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs
new file mode 100644
index 000000000..1a33c3aa8
--- /dev/null
+++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs
@@ -0,0 +1,64 @@
+using System;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.IO;
+
+/// <summary>
+/// Helper methods for file system management.
+/// </summary>
+public static class FileSystemHelper
+{
+ /// <summary>
+ /// Deletes the file.
+ /// </summary>
+ /// <param name="fileSystem">The fileSystem.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="logger">The logger.</param>
+ public static void DeleteFile(IFileSystem fileSystem, string path, ILogger logger)
+ {
+ try
+ {
+ fileSystem.DeleteFile(path);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ logger.LogError(ex, "Error deleting file {Path}", path);
+ }
+ catch (IOException ex)
+ {
+ logger.LogError(ex, "Error deleting file {Path}", path);
+ }
+ }
+
+ /// <summary>
+ /// Recursively delete empty folders.
+ /// </summary>
+ /// <param name="fileSystem">The fileSystem.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="logger">The logger.</param>
+ public static void DeleteEmptyFolders(IFileSystem fileSystem, string path, ILogger logger)
+ {
+ foreach (var directory in fileSystem.GetDirectoryPaths(path))
+ {
+ DeleteEmptyFolders(fileSystem, directory, logger);
+ if (!fileSystem.GetFileSystemEntryPaths(directory).Any())
+ {
+ try
+ {
+ Directory.Delete(directory, false);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ logger.LogError(ex, "Error deleting directory {Path}", directory);
+ }
+ catch (IOException ex)
+ {
+ logger.LogError(ex, "Error deleting directory {Path}", directory);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 03c7faf47..9d7d2fd12 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
@@ -1208,8 +1209,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var subtitlePath = state.SubtitleStream.Path;
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
- if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
- || subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
+ // dvdsub/vobsub graphical subtitles use .sub+.idx pairs
+ if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
{
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
if (File.Exists(idxFile))
@@ -5709,16 +5710,29 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var bitDepth = GetVideoColorBitDepth(state);
- // Only HEVC, VP9 and AV1 formats have 10-bit hardware decoder support now.
+ // Only HEVC, VP9 and AV1 formats have 10-bit hardware decoder support for most platforms
if (bitDepth == 10
&& !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase)))
{
- // One exception is that RKMPP decoder can handle H.264 High 10.
- if (!(string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)
- && string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)))
+ // RKMPP has H.264 Hi10P decoder
+ bool hasHardwareHi10P = string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase);
+
+ // VideoToolbox on Apple Silicon has H.264 Hi10P mode enabled after macOS 14.6
+ if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ var ver = Environment.OSVersion.Version;
+ var arch = RuntimeInformation.OSArchitecture;
+ if (arch.Equals(Architecture.Arm64) && ver >= new Version(14, 6))
+ {
+ hasHardwareHi10P = true;
+ }
+ }
+
+ if (!hasHardwareHi10P
+ && string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
{
return null;
}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 6fc9a7e1b..45aefacf6 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -166,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists
return base.GetChildren(user, true, query);
}
- public static IReadOnlyList<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
+ public static IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
{
if (user is not null)
{
@@ -177,14 +177,14 @@ namespace MediaBrowser.Controller.Playlists
foreach (var item in inputItems)
{
- var playlistItems = GetPlaylistItems(item, user, playlistMediaType, options);
+ var playlistItems = GetPlaylistItems(item, user, options);
list.AddRange(playlistItems);
}
return list;
}
- private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options)
+ private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, DtoOptions options)
{
if (item is MusicGenre musicGenre)
{
@@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Playlists
{
Recursive = true,
IsFolder = false,
- MediaTypes = [mediaType],
+ MediaTypes = [MediaType.Audio, MediaType.Video],
EnableTotalRecordCount = false,
DtoOptions = options
};
diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
index 6a5e3bf04..f00d508bb 100644
--- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
@@ -40,13 +40,12 @@ namespace MediaBrowser.LocalMetadata.Images
var parentPathFiles = directoryService.GetFiles(parentPath);
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()).ToString();
- var thumbName = string.Concat(nameWithoutExtension, "-thumb");
- var images = GetImageFilesFromFolder(thumbName, parentPathFiles);
+ var images = GetImageFilesFromFolder(nameWithoutExtension, parentPathFiles);
- var metadataSubPath = directoryService.GetDirectories(parentPath).Where(d => d.Name.EndsWith("metadata", StringComparison.OrdinalIgnoreCase)).ToList();
- foreach (var path in metadataSubPath)
+ var metadataSubDir = directoryService.GetDirectories(parentPath).FirstOrDefault(d => d.Name.Equals("metadata", StringComparison.Ordinal));
+ if (metadataSubDir is not null)
{
- var files = directoryService.GetFiles(path.FullName);
+ var files = directoryService.GetFiles(metadataSubDir.FullName);
images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files));
}
@@ -55,9 +54,8 @@ namespace MediaBrowser.LocalMetadata.Images
private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths)
{
- var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
-
var list = new List<LocalImageInfo>(1);
+ var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
foreach (var i in filePaths)
{
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index f85510dac..d0d41c2d3 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -1155,10 +1155,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Get all files from the BDMV/STREAMING directory
// Only return playable local .m2ts files
+ var files = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")).ToList();
return validPlaybackFiles
- .Select(f => _fileSystem.GetFileInfo(Path.Join(path, "BDMV", "STREAM", f)))
- .Where(f => f.Exists)
- .Select(f => f.FullName)
+ .Select(validFile => files.FirstOrDefault(f => Path.GetFileName(f.FullName.AsSpan()).Equals(validFile, StringComparison.OrdinalIgnoreCase))?.FullName)
+ .Where(f => f is not null)
.ToList();
}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index a587fa9db..8b2685fe1 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -280,8 +280,8 @@ namespace MediaBrowser.MediaEncoding.Probing
splitFormat[i] = "mpeg";
}
- // Handle MPEG-2 container
- else if (string.Equals(splitFormat[i], "mpeg", StringComparison.OrdinalIgnoreCase))
+ // Handle MPEG-TS container
+ else if (string.Equals(splitFormat[i], "mpegts", StringComparison.OrdinalIgnoreCase))
{
splitFormat[i] = "ts";
}
@@ -624,15 +624,19 @@ namespace MediaBrowser.MediaEncoding.Probing
{
if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase))
{
- codec = "dvbsub";
+ codec = "DVBSUB";
}
- else if ((codec ?? string.Empty).Contains("PGS", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(codec, "dvb_teletext", StringComparison.OrdinalIgnoreCase))
{
- codec = "PGSSUB";
+ codec = "DVBTXT";
}
- else if ((codec ?? string.Empty).Contains("DVD", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(codec, "dvd_subtitle", StringComparison.OrdinalIgnoreCase))
{
- codec = "DVDSUB";
+ codec = "DVDSUB"; // .sub+.idx
+ }
+ else if (string.Equals(codec, "hdmv_pgs_subtitle", StringComparison.OrdinalIgnoreCase))
+ {
+ codec = "PGSSUB"; // .sup
}
return codec;
@@ -779,11 +783,10 @@ namespace MediaBrowser.MediaEncoding.Probing
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
if (isAudio
- && (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
- || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
- || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
- || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
- || string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase)))
+ || string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase))
{
stream.Type = MediaStreamType.EmbeddedImage;
}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 3e01c6efc..e1082adea 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -656,14 +656,14 @@ namespace MediaBrowser.Model.Entities
{
string codec = format ?? string.Empty;
- // sub = external .sub file
-
- return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
- && !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase)
- && !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase);
+ // microdvd and dvdsub/vobsub share the ".sub" file extension, but it's text-based.
+
+ return codec.Contains("microdvd", StringComparison.OrdinalIgnoreCase)
+ || (!codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
+ && !codec.Contains("dvdsub", StringComparison.OrdinalIgnoreCase)
+ && !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase));
}
public bool SupportsSubtitleConversionTo(string toCodec)
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index e0677aa9f..9a676cb2e 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -188,11 +189,27 @@ namespace MediaBrowser.Providers.Manager
{
_fileSystem.DeleteFile(currentPath);
- // Remove containing directory if empty
- var folder = Path.GetDirectoryName(currentPath);
- if (!_fileSystem.GetFiles(folder).Any())
+ // Remove local episode metadata directory if it exists and is empty
+ var directory = Path.GetDirectoryName(currentPath);
+ if (item is Episode && directory.Equals("metadata", StringComparison.Ordinal))
{
- Directory.Delete(folder);
+ var parentDirectoryPath = Directory.GetParent(currentPath).FullName;
+ if (_fileSystem.DirectoryExists(parentDirectoryPath) && !_fileSystem.GetFiles(parentDirectoryPath).Any())
+ {
+ try
+ {
+ _logger.LogInformation("Deleting empty local metadata folder {Folder}", parentDirectoryPath);
+ Directory.Delete(parentDirectoryPath);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ _logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath);
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath);
+ }
+ }
}
}
catch (FileNotFoundException)
@@ -410,13 +427,15 @@ namespace MediaBrowser.Providers.Manager
if (type == ImageType.Backdrop && saveLocally)
{
- if (season is not null && season.IndexNumber.HasValue)
+ if (season is not null
+ && season.IndexNumber.HasValue
+ && (imageIndex is null || imageIndex == 0))
{
var seriesFolder = season.SeriesPath;
var seasonMarker = season.IndexNumber.Value == 0
- ? "-specials"
- : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
+ ? "-specials"
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-fanart" + extension;
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index bee420d95..1bb7ffcce 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers;
@@ -96,7 +97,7 @@ namespace MediaBrowser.Providers.Manager
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
{
var hasChanges = false;
- IDirectoryService directoryService = refreshOptions?.DirectoryService;
+ var directoryService = refreshOptions?.DirectoryService;
if (item is not Photo)
{
@@ -158,7 +159,7 @@ namespace MediaBrowser.Providers.Manager
}
}
- // only delete existing multi-images if new ones were added
+ // Only delete existing multi-images if new ones were added
if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
{
PruneImages(item, oldBackdropImages);
@@ -359,10 +360,8 @@ namespace MediaBrowser.Providers.Manager
private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
{
- for (var i = 0; i < images.Count; i++)
+ foreach (var image in images)
{
- var image = images[i];
-
if (image.IsLocalFile)
{
try
@@ -377,19 +376,20 @@ namespace MediaBrowser.Providers.Manager
{
_logger.LogWarning(ex, "Unable to delete {Image}", image.Path);
}
- finally
- {
- // Always remove empty parent folder
- var folder = Path.GetDirectoryName(image.Path);
- if (Directory.Exists(folder) && !_fileSystem.GetFiles(folder).Any())
- {
- Directory.Delete(folder);
- }
- }
}
}
item.RemoveImages(images);
+
+ // Cleanup old metadata directory for episodes if empty
+ if (item is Episode)
+ {
+ var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
+ if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())
+ {
+ Directory.Delete(oldLocalMetadataDirectory);
+ }
+ }
}
/// <summary>
@@ -422,14 +422,11 @@ namespace MediaBrowser.Providers.Manager
{
var changed = item.ValidateImages();
var foundImageTypes = new List<ImageType>();
-
for (var i = 0; i < _singularImages.Length; i++)
{
var type = _singularImages[i];
var image = GetFirstLocalImageInfoByType(images, type);
-
- // Only use local images if we are not replacing and saving
- if (image is not null && !(item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages))
+ if (image is not null)
{
var currentImage = item.GetImageInfo(type, 0);
// if image file is stored with media, don't replace that later
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 61a4d7586..8af4ed2a8 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -92,10 +92,6 @@ namespace MediaBrowser.Providers.Manager
}
}
- var localImagesFailed = false;
-
- var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
-
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
{
if (ImageProvider.RemoveImages(item))
@@ -104,19 +100,29 @@ namespace MediaBrowser.Providers.Manager
}
}
- // Start by validating images
- try
+ var localImagesFailed = false;
+ var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
+
+ // Only validate already registered images if we are replacing and saving locally
+ if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
{
- // Always validate images and check for new locally stored ones.
- if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
- {
- updateType |= ItemUpdateType.ImageUpdate;
- }
+ item.ValidateImages();
}
- catch (Exception ex)
+ else
{
- localImagesFailed = true;
- Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
+ // Run full image validation and register new local images
+ try
+ {
+ if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
+ {
+ updateType |= ItemUpdateType.ImageUpdate;
+ }
+ }
+ catch (Exception ex)
+ {
+ localImagesFailed = true;
+ Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
+ }
}
var metadataResult = new MetadataResult<TItemType>
@@ -669,6 +675,8 @@ namespace MediaBrowser.Providers.Manager
};
temp.Item.Path = item.Path;
temp.Item.Id = item.Id;
+ temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
+ temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
var foundImageTypes = new List<ImageType>();
@@ -811,19 +819,16 @@ namespace MediaBrowser.Providers.Manager
{
var refreshResult = new RefreshResult();
- var tmpDataMerged = false;
+ if (id is not null)
+ {
+ MergeNewData(temp.Item, id);
+ }
foreach (var provider in providers)
{
var providerName = provider.GetType().Name;
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
- if (id is not null && !tmpDataMerged)
- {
- MergeNewData(temp.Item, id);
- tmpDataMerged = true;
- }
-
try
{
var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
@@ -998,7 +1003,7 @@ namespace MediaBrowser.Providers.Manager
{
targetResult.People = sourceResult.People;
}
- else if (sourceResult.People is not null && sourceResult.People.Count >= 0)
+ else if (sourceResult.People is not null && sourceResult.People.Count > 0)
{
MergePeople(sourceResult.People, targetResult.People);
}
@@ -1188,7 +1193,7 @@ namespace MediaBrowser.Providers.Manager
{
targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
}
- else if (sourceHasAlbumArtist.AlbumArtists.Count >= 0)
+ else if (sourceHasAlbumArtist.AlbumArtists.Count > 0)
{
targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct().ToArray();
}
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 7ffe2f32a..9eacfc2b6 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -136,6 +137,10 @@ namespace MediaBrowser.Providers.MediaInfo
if (!audio.IsLocked)
{
await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
+ if (tryExtractEmbeddedLyrics)
+ {
+ AddExternalLyrics(audio, mediaStreams, options);
+ }
}
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
@@ -369,7 +374,10 @@ namespace MediaBrowser.Providers.MediaInfo
var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false);
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
- currentStreams.AddRange(externalLyricFiles);
+ if (externalLyricFiles.Count > 0)
+ {
+ currentStreams.Add(externalLyricFiles[0]);
+ }
}
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 5d0fccbe1..1d4e66570 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -358,6 +358,10 @@ namespace MediaBrowser.Providers.MediaInfo
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height;
+ blurayVideoStream.ColorRange = ffmpegVideoStream.ColorRange;
+ blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace;
+ blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer;
+ blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries;
}
}
diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
index 4eb75b82f..51a3ba0c7 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
@@ -1,7 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -18,182 +16,212 @@ using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using PlaylistsNET.Content;
-namespace MediaBrowser.Providers.Playlists
+namespace MediaBrowser.Providers.Playlists;
+
+/// <summary>
+/// Local playlist provider.
+/// </summary>
+public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>,
+ IHasOrder,
+ IForcedProvider,
+ IHasItemChangeMonitor
{
- public class PlaylistItemsProvider : ICustomMetadataProvider<Playlist>,
- IHasOrder,
- IForcedProvider,
- IPreRefreshProvider,
- IHasItemChangeMonitor
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<PlaylistItemsProvider> _logger;
+ private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlaylistItemsProvider"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{PlaylistItemsProvider}"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem)
{
- private readonly IFileSystem _fileSystem;
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<PlaylistItemsProvider> _logger;
- private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
+ _logger = logger;
+ _libraryManager = libraryManager;
+ _fileSystem = fileSystem;
+ }
+
+ /// <inheritdoc />
+ public string Name => "Playlist Item Provider";
+
+ /// <inheritdoc />
+ public int Order => 100;
- public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem)
+ /// <inheritdoc />
+ public Task<MetadataResult<Playlist>> GetMetadata(
+ ItemInfo info,
+ IDirectoryService directoryService,
+ CancellationToken cancellationToken)
+ {
+ var result = new MetadataResult<Playlist>()
{
- _logger = logger;
- _libraryManager = libraryManager;
- _fileSystem = fileSystem;
+ Item = new Playlist
+ {
+ Path = info.Path
+ }
+ };
+ Fetch(result);
+
+ return Task.FromResult(result);
+ }
+
+ private void Fetch(MetadataResult<Playlist> result)
+ {
+ var item = result.Item;
+ var path = item.Path;
+ if (!Playlist.IsPlaylistFile(path))
+ {
+ return;
}
- public string Name => "Playlist Reader";
+ var extension = Path.GetExtension(path);
+ if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ var items = GetItems(path, extension).ToArray();
+ if (items.Length > 0)
+ {
+ result.HasMetadata = true;
+ item.LinkedChildren = items;
+ }
- // Run last
- public int Order => 100;
+ return;
+ }
- public Task<ItemUpdateType> FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ private IEnumerable<LinkedChild> GetItems(string path, string extension)
+ {
+ var libraryRoots = _libraryManager.GetUserRootFolder().Children
+ .OfType<CollectionFolder>()
+ .Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value))
+ .SelectMany(f => f.PhysicalLocations)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ using (var stream = File.OpenRead(path))
{
- var path = item.Path;
- if (!Playlist.IsPlaylistFile(path))
+ if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
{
- return Task.FromResult(ItemUpdateType.None);
+ return GetWplItems(stream, path, libraryRoots);
}
- var extension = Path.GetExtension(path);
- if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
{
- return Task.FromResult(ItemUpdateType.None);
+ return GetZplItems(stream, path, libraryRoots);
}
- var items = GetItems(path, extension).ToArray();
-
- item.LinkedChildren = items;
-
- return Task.FromResult(ItemUpdateType.MetadataImport);
- }
+ if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
+ {
+ return GetM3uItems(stream, path, libraryRoots);
+ }
- private IEnumerable<LinkedChild> GetItems(string path, string extension)
- {
- var libraryRoots = _libraryManager.GetUserRootFolder().Children
- .OfType<CollectionFolder>()
- .Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value))
- .SelectMany(f => f.PhysicalLocations)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToList();
-
- using (var stream = File.OpenRead(path))
+ if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
- {
- return GetWplItems(stream, path, libraryRoots);
- }
-
- if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
- {
- return GetZplItems(stream, path, libraryRoots);
- }
-
- if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
- {
- return GetM3uItems(stream, path, libraryRoots);
- }
-
- if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
- {
- return GetM3uItems(stream, path, libraryRoots);
- }
-
- if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
- {
- return GetPlsItems(stream, path, libraryRoots);
- }
+ return GetM3uItems(stream, path, libraryRoots);
}
- return Enumerable.Empty<LinkedChild>();
+ if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
+ {
+ return GetPlsItems(stream, path, libraryRoots);
+ }
}
- private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
- {
- var content = new PlsContent();
- var playlist = content.GetFromStream(stream);
+ return Enumerable.Empty<LinkedChild>();
+ }
- return playlist.PlaylistEntries
- .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
- .Where(i => i is not null);
- }
+ private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
+ {
+ var content = new PlsContent();
+ var playlist = content.GetFromStream(stream);
- private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
- {
- var content = new M3uContent();
- var playlist = content.GetFromStream(stream);
+ return playlist.PlaylistEntries
+ .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
+ .Where(i => i is not null);
+ }
- return playlist.PlaylistEntries
- .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
- .Where(i => i is not null);
- }
+ private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
+ {
+ var content = new M3uContent();
+ var playlist = content.GetFromStream(stream);
- private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
- {
- var content = new ZplContent();
- var playlist = content.GetFromStream(stream);
+ return playlist.PlaylistEntries
+ .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
+ .Where(i => i is not null);
+ }
- return playlist.PlaylistEntries
- .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
- .Where(i => i is not null);
- }
+ private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
+ {
+ var content = new ZplContent();
+ var playlist = content.GetFromStream(stream);
- private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
- {
- var content = new WplContent();
- var playlist = content.GetFromStream(stream);
+ return playlist.PlaylistEntries
+ .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
+ .Where(i => i is not null);
+ }
- return playlist.PlaylistEntries
- .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
- .Where(i => i is not null);
- }
+ private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
+ {
+ var content = new WplContent();
+ var playlist = content.GetFromStream(stream);
+
+ return playlist.PlaylistEntries
+ .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
+ .Where(i => i is not null);
+ }
- private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
+ private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
+ {
+ if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath))
{
- if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath))
+ return new LinkedChild
{
- return new LinkedChild
- {
- Path = parsedPath,
- Type = LinkedChildType.Manual
- };
- }
-
- return null;
+ Path = parsedPath,
+ Type = LinkedChildType.Manual
+ };
}
- private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path)
+ return null;
+ }
+
+ private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path)
+ {
+ path = null;
+ string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
+ if (!File.Exists(pathToCheck))
{
- path = null;
- string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
- if (!File.Exists(pathToCheck))
- {
- return false;
- }
+ return false;
+ }
- foreach (var libraryPath in libraryPaths)
+ foreach (var libraryPath in libraryPaths)
+ {
+ if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
{
- if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
- {
- path = pathToCheck;
- return true;
- }
+ path = pathToCheck;
+ return true;
}
-
- return false;
}
- public bool HasChanged(BaseItem item, IDirectoryService directoryService)
- {
- var path = item.Path;
+ return false;
+ }
- if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
+ /// <inheritdoc />
+ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ {
+ var path = item.Path;
+ if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
+ {
+ var file = directoryService.GetFile(path);
+ if (file is not null && file.LastWriteTimeUtc != item.DateModified)
{
- var file = directoryService.GetFile(path);
- if (file is not null && file.LastWriteTimeUtc != item.DateModified)
- {
- _logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
- return true;
- }
+ _logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
+ return true;
}
-
- return false;
}
+
+ return false;
}
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index d0bd7d609..c35324746 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -250,7 +250,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
// If we have a release ID but not a release group ID, lookup the release group
if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
{
- var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false);
+ var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
releaseGroupId = release.ReleaseGroup?.Id.ToString();
result.HasMetadata = true;
}
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 2389bce57..b03d6ffb5 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -61,8 +61,8 @@ namespace MediaBrowser.Providers.TV
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
RemoveObsoleteEpisodes(item);
- RemoveObsoleteSeasons(item);
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
+ RemoveObsoleteSeasons(item);
}
/// <inheritdoc />
@@ -91,7 +91,7 @@ namespace MediaBrowser.Providers.TV
private void RemoveObsoleteSeasons(Series series)
{
- // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync.
+ // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync.
var physicalSeasonNumbers = new HashSet<int>();
var virtualSeasons = new List<Season>();
foreach (var existingSeason in series.Children.OfType<Season>())
@@ -203,11 +203,16 @@ namespace MediaBrowser.Providers.TV
foreach (var seasonNumber in uniqueSeasonNumbers)
{
// Null season numbers will have a 'dummy' season created because seasons are always required.
- if (!seasons.Any(i => i.IndexNumber == seasonNumber))
+ var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
+ if (existingSeason is null)
{
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
- var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
- series.AddChild(season);
+ await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
+ }
+ else if (existingSeason.IsVirtualItem)
+ {
+ existingSeason.IsVirtualItem = false;
+ await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -220,7 +225,7 @@ namespace MediaBrowser.Providers.TV
/// <param name="seasonNumber">The season number.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The newly created season.</returns>
- private async Task<Season> CreateSeasonAsync(
+ private async Task CreateSeasonAsync(
Series series,
string? seasonName,
int? seasonNumber,
@@ -237,14 +242,12 @@ namespace MediaBrowser.Providers.TV
typeof(Season)),
IsVirtualItem = false,
SeriesId = series.Id,
- SeriesName = series.Name
+ SeriesName = series.Name,
+ SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
};
series.AddChild(season);
-
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
-
- return season;
}
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 97cdc6854..d049c5a8e 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -519,7 +519,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
{
item.PremiereDate = releaseDate;
- item.ProductionYear = releaseDate.Year;
+
+ // Production year can already be set by the year tag
+ item.ProductionYear ??= releaseDate.Year;
}
break;
diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs
index 148b33fcb..cf6a2cc55 100644
--- a/src/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -919,10 +919,14 @@ public class NetworkManager : INetworkManager, IDisposable
{
ArgumentNullException.ThrowIfNull(address);
- // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
+ // Map IPv6 mapped IPv4 back to IPv4 (happens if Kestrel runs in dual-socket mode)
+ if (address.IsIPv4MappedToIPv6)
+ {
+ address = address.MapToIPv4();
+ }
+
if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
- || address.Equals(IPAddress.Loopback)
- || address.Equals(IPAddress.IPv6Loopback))
+ || IPAddress.IsLoopback(address))
{
return true;
}
diff --git a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
index ba602b5d2..0b8b1f644 100644
--- a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
+++ b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs
@@ -104,6 +104,7 @@ public class ExternalPathParserTests
[InlineData(".en.cc.title", "title", "eng", false, false, true)]
[InlineData(".hi.en.title", "title", "eng", false, false, true)]
[InlineData(".en.hi.title", "title", "eng", false, false, true)]
+ [InlineData(".Subs for Chinese Audio.eng", "Subs for Chinese Audio", "eng", false, false, false)]
public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, string? language, bool isDefault = false, bool isForced = false, bool isHearingImpaired = false)
{
var path = "My.Video" + tokens + ".srt";