diff options
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"; |
