diff options
22 files changed, 381 insertions, 258 deletions
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index ac568a603..00f7e9e6d 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 4aefa0106..1ab7ae029 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -34,94 +34,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} - check-backport: - permissions: - contents: read - - name: Check Backport - if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }} - runs-on: ubuntu-latest - steps: - - name: Notify as seen - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 - if: ${{ github.event.comment != null }} - with: - token: ${{ secrets.JF_BOT_TOKEN }} - comment-id: ${{ github.event.comment.id }} - reactions: eyes - - - name: Checkout the latest code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - token: ${{ secrets.JF_BOT_TOKEN }} - fetch-depth: 0 - - - name: Notify as running - id: comment_running - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 - if: ${{ github.event.comment != null }} - with: - token: ${{ secrets.JF_BOT_TOKEN }} - issue-number: ${{ github.event.issue.number }} - body: | - Running backport tests... - - - name: Perform test backport - id: run_tests - run: | - set +o errexit - git config --global user.name "Jellyfin Bot" - git config --global user.email "team@jellyfin.org" - CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}" - git checkout master - git merge --no-ff ${CURRENT_BRANCH} - MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' ) - git fetch --all - CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' ) - stable_branch="Current stable release branch: ${CURRENT_STABLE}" - echo ${stable_branch} - echo ::set-output name=branch::${stable_branch} - git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE} - git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt - retcode=$? - cat output.txt | grep -v 'hint:' - output="$( grep -v 'hint:' output.txt )" - output="${output//'%'/'%25'}" - output="${output//$'\n'/'%0A'}" - output="${output//$'\r'/'%0D'}" - echo ::set-output name=output::$output - exit ${retcode} - - - name: Notify with result success - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 - if: ${{ github.event.comment != null && success() }} - with: - token: ${{ secrets.JF_BOT_TOKEN }} - comment-id: ${{ steps.comment_running.outputs.comment-id }} - body: | - ${{ steps.run_tests.outputs.branch }} - Output from `git cherry-pick`: - - --- - - ${{ steps.run_tests.outputs.output }} - reactions: hooray - - - name: Notify with result failure - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 - if: ${{ github.event.comment != null && failure() }} - with: - token: ${{ secrets.JF_BOT_TOKEN }} - comment-id: ${{ steps.comment_running.outputs.comment-id }} - body: | - ${{ steps.run_tests.outputs.branch }} - Output from `git cherry-pick`: - - --- - - ${{ steps.run_tests.outputs.output }} - reactions: confused - rename: name: Rename if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER' diff --git a/Directory.Packages.props b/Directory.Packages.props index 7001cbfdb..124597ae5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -79,7 +79,7 @@ <PackageVersion Include="System.Text.Json" Version="9.0.1" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.1" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> - <PackageVersion Include="z440.atl.core" Version="6.15.0" /> + <PackageVersion Include="z440.atl.core" Version="6.16.0" /> <PackageVersion Include="TMDbLib" Version="2.2.0" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index eb045e35e..c483f3c61 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1811,11 +1811,11 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public void CreateItem(BaseItem item, BaseItem? parent) { - CreateOrUpdateItems(new[] { item }, parent, CancellationToken.None); + CreateItems(new[] { item }, parent, CancellationToken.None); } /// <inheritdoc /> - public void CreateOrUpdateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken) + public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken) { _itemRepository.SaveItems(items, cancellationToken); @@ -2972,11 +2972,11 @@ namespace Emby.Server.Implementations.Library { if (createEntity) { - CreateOrUpdateItems([personEntity], null, CancellationToken.None); + CreateItems([personEntity], null, CancellationToken.None); } await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); - CreateOrUpdateItems([personEntity], null, CancellationToken.None); + CreateItems([personEntity], null, CancellationToken.None); } } } diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index 320685b1f..76e564d53 100644 --- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -43,14 +43,26 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask /// <inheritdoc /> public Task Run(IProgress<double> progress, CancellationToken cancellationToken) { - var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); - var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); + var posters = GetItemsWithImageType(ImageType.Primary) + .Select(x => x.GetImages(ImageType.Primary).FirstOrDefault()?.Path) + .Where(path => !string.IsNullOrEmpty(path)) + .Select(path => path!) + .ToList(); + var backdrops = GetItemsWithImageType(ImageType.Thumb) + .Select(x => x.GetImages(ImageType.Thumb).FirstOrDefault()?.Path) + .Where(path => !string.IsNullOrEmpty(path)) + .Select(path => path!) + .ToList(); if (backdrops.Count == 0) { // Thumb images fit better because they include the title in the image but are not provided with TMDb. // Using backdrops as a fallback to generate an image at all _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen"); - backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); + backdrops = GetItemsWithImageType(ImageType.Backdrop) + .Select(x => x.GetImages(ImageType.Backdrop).FirstOrDefault()?.Path) + .Where(path => !string.IsNullOrEmpty(path)) + .Select(path => path!) + .ToList(); } _imageEncoder.CreateSplashscreen(posters, backdrops); diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 5388f6f9a..2d29eb5bf 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -134,5 +134,7 @@ "TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة", "TaskDownloadMissingLyricsDescription": "كلمات", "TaskExtractMediaSegments": "فحص مقاطع الوسائط", - "TaskExtractMediaSegmentsDescription": "وسائط" + "TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.", + "TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة", + "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة." } diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json index 114c76c54..4df4b90d3 100644 --- a/Emby.Server.Implementations/Localization/Core/eu.json +++ b/Emby.Server.Implementations/Localization/Core/eu.json @@ -19,25 +19,25 @@ "Artists": "Artistak", "Albums": "Albumak", "TaskOptimizeDatabase": "Datu basea optimizatu", - "TaskDownloadMissingSubtitlesDescription": "Metadataren konfigurazioan oinarrituta falta diren azpitituluak bilatzen ditu interneten.", + "TaskDownloadMissingSubtitlesDescription": "Falta diren azpitituluak bilatzen ditu interneten metadatuen konfigurazioaren arabera.", "TaskDownloadMissingSubtitles": "Falta diren azpitituluak deskargatu", "TaskRefreshChannelsDescription": "Internet kanalen informazioa eguneratu.", "TaskRefreshChannels": "Kanalak eguneratu", - "TaskCleanTranscodeDescription": "Egun bat baino zaharragoak diren transcode fitxategiak ezabatzen ditu.", - "TaskCleanTranscode": "Transcode direktorioa garbitu", - "TaskUpdatePluginsDescription": "Automatikoki eguneratzeko konfiguratutako pluginen eguneraketak deskargatu eta instalatzen ditu.", + "TaskCleanTranscodeDescription": "Egun bat baino zaharragoak diren transkodifikazio fitxategiak ezabatzen ditu.", + "TaskCleanTranscode": "Transkodifikazio direktorioa garbitu", + "TaskUpdatePluginsDescription": "Automatikoki deskargatu eta instalatu eguneraketak konfiguratutako pluginetarako.", "TaskUpdatePlugins": "Pluginak eguneratu", - "TaskRefreshPeopleDescription": "Zure liburutegiko aktore eta zuzendarien metadata eguneratzen du.", + "TaskRefreshPeopleDescription": "Zure liburutegiko aktore eta zuzendarien metadatuak eguneratzen ditu.", "TaskRefreshPeople": "Jendea eguneratu", "TaskCleanLogsDescription": "{0} egun baino zaharragoak diren log fitxategiak ezabatzen ditu.", "TaskCleanLogs": "Log direktorioa garbitu", - "TaskRefreshLibraryDescription": "Zure multimedia liburutegia eskaneatzen du fitxategi berriak eta metadatak eguneratzeko.", - "TaskRefreshLibrary": "Multimedia Liburutegia eskaneatu", + "TaskRefreshLibraryDescription": "Zure multimedia liburutegia eskaneatzen du fitxategi berriak eta metadatuak eguneratzeko.", + "TaskRefreshLibrary": "Multimedia liburutegia eskaneatu", "TaskRefreshChapterImagesDescription": "Kapituluak dituzten bideoen miniaturak sortzen ditu.", "TaskRefreshChapterImages": "Kapituluen irudiak erauzi", "TaskCleanCacheDescription": "Sistemak behar ez dituen cache fitxategiak ezabatzen ditu.", - "TaskCleanCache": "Cache Directorioa garbitu", - "TaskCleanActivityLogDescription": "Konfiguratuta data baino zaharragoak diren log-ak ezabatu.", + "TaskCleanCache": "Cache direktorioa garbitu", + "TaskCleanActivityLogDescription": "Konfiguratutako baino zaharragoak diren jarduera-log sarrerak ezabatzen ditu.", "TaskCleanActivityLog": "Erabilera Log-a garbitu", "TasksChannelsCategory": "Internet Kanalak", "TasksApplicationCategory": "Aplikazioa", @@ -45,22 +45,22 @@ "TasksMaintenanceCategory": "Mantenua", "VersionNumber": "Bertsioa {0}", "ValueHasBeenAddedToLibrary": "{0} zure multimedia liburutegian gehitu da", - "UserStoppedPlayingItemWithValues": "{0}-ek {1} ikusteaz bukatu du {2}-(a)n", - "UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(a)n", - "UserPolicyUpdatedWithName": "{0} Erabiltzailearen politikak aldatu dira", - "UserPasswordChangedWithName": "{0} Erabiltzailearen pasahitza aldatu da", - "UserOnlineFromDevice": "{0} online dago {1}-tik", - "UserOfflineFromDevice": "{0} {1}-tik deskonektatu da", - "UserLockedOutWithName": "{0} Erabiltzailea blokeatu da", - "UserDownloadingItemWithValues": "{1} {0}-tik deskargatzen", + "UserStoppedPlayingItemWithValues": "{0} {1} ikusten bukatu du {2}-(e)n", + "UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(e)n", + "UserPolicyUpdatedWithName": "{0} erabiltzailearen politikak aldatu dira", + "UserPasswordChangedWithName": "{0} erabiltzailearen pasahitza aldatu da", + "UserOnlineFromDevice": "{0} online dago {1}-(e)tik", + "UserOfflineFromDevice": "{0} {1}-(e)tik deskonektatu da", + "UserLockedOutWithName": "{0} erabiltzailea blokeatu da", + "UserDownloadingItemWithValues": "{0} {1} deskargatzen ari da", "UserDeletedWithName": "{0} Erabiltzailea ezabatu da", "UserCreatedWithName": "{0} Erabiltzailea sortu da", "User": "Erabiltzailea", "Undefined": "Ezezaguna", - "TvShows": "TB showak", + "TvShows": "TB serieak", "System": "Sistema", - "SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0} deskargatzean huts egin du", - "StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduxeago.", + "SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0}-tik deskargatzeak huts egin du", + "StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduago.", "ServerNameNeedsToBeRestarted": "{0} berrabiarazi behar da", "ScheduledTaskStartedWithName": "{0} hasi da", "ScheduledTaskFailedWithName": "{0} huts egin du", @@ -89,26 +89,26 @@ "NameSeasonNumber": "{0} Denboraldia", "NameInstallFailed": "{0} instalazioak huts egin du", "Music": "Musika", - "MixedContent": "Denetariko edukia", + "MixedContent": "Eduki mistoa", "MessageServerConfigurationUpdated": "Zerbitzariaren konfigurazioa eguneratu da", - "MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren konfigurazio {0} atala eguneratu da", + "MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren {0} konfigurazio atala eguneratu da", "MessageApplicationUpdatedTo": "Jellyfin zerbitzaria {0}-ra eguneratu da", "MessageApplicationUpdated": "Jellyfin zerbitzaria eguneratu da", "Latest": "Azkena", - "LabelRunningTimeValue": "Denbora martxan: {0}", + "LabelRunningTimeValue": "Iraupena: {0}", "LabelIpAddressValue": "IP helbidea: {0}", - "ItemRemovedWithName": "{0} liburutegitik ezabatu da", + "ItemRemovedWithName": "{0} liburutegitik kendu da", "ItemAddedWithName": "{0} liburutegira gehitu da", "HomeVideos": "Etxeko bideoak", - "HeaderNextUp": "Nobedadeak", + "HeaderNextUp": "Hurrengoa", "HeaderLiveTV": "Zuzeneko TB", "HeaderFavoriteSongs": "Gogoko abestiak", - "HeaderFavoriteShows": "Gogoko showak", + "HeaderFavoriteShows": "Gogoko serieak", "HeaderFavoriteEpisodes": "Gogoko atalak", "HeaderFavoriteArtists": "Gogoko artistak", "HeaderFavoriteAlbums": "Gogoko albumak", "Forced": "Behartuta", - "FailedLoginAttemptWithUserName": "Login egiten akatsa, saiatu hemen {0}", + "FailedLoginAttemptWithUserName": "{0}-tik saioa hasteak huts egin du", "External": "Kanpokoa", "DeviceOnlineWithName": "{0} konektatu da", "DeviceOfflineWithName": "{0} deskonektatu da", @@ -117,13 +117,23 @@ "AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da", "Application": "Aplikazioa", "AppDeviceValues": "App: {0}, Gailua: {1}", - "HearingImpaired": "Entzunaldia aldatua", + "HearingImpaired": "Entzumen urritasuna", "ProviderValue": "Hornitzailea: {0}", "TaskKeyframeExtractorDescription": "Bideo fitxategietako fotograma gakoak ateratzen ditu HLS erreprodukzio-zerrenda zehatzagoak sortzeko. Zeregin honek denbora asko iraun dezake.", "HeaderRecordingGroups": "Grabaketa taldeak", "Inherit": "Oinordetu", "TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.", "TaskKeyframeExtractor": "Fotograma gakoen erauzgailua", - "TaskRefreshTrickplayImages": "\"Trickplay Irudiak Sortu", - "TaskRefreshTrickplayImagesDescription": "Bideoentzako trickplay aurrebistak sortzen ditu gaitutako liburutegietan." + "TaskRefreshTrickplayImages": "Trickplay irudiak sortu", + "TaskRefreshTrickplayImagesDescription": "Bideoentzako trickplay aurrebistak sortzen ditu gaitutako liburutegietan.", + "TaskAudioNormalization": "Audio normalizazioa", + "TaskDownloadMissingLyrics": "Deskargatu falta diren letrak", + "TaskDownloadMissingLyricsDescription": "Deskargatu abestientzako letrak", + "TaskExtractMediaSegments": "Multimedia segmentuen eskaneoa", + "TaskCleanCollectionsAndPlaylistsDescription": "Jada existitzen ez diren bildumak eta erreprodukzio-zerrendak kentzen ditu.", + "TaskCleanCollectionsAndPlaylists": "Garbitu bildumak eta erreprodukzio-zerrendak", + "TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.", + "TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua", + "TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.", + "TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu." } diff --git a/Emby.Server.Implementations/Localization/Core/lb.json b/Emby.Server.Implementations/Localization/Core/lb.json new file mode 100644 index 000000000..176f2ba2b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/lb.json @@ -0,0 +1,139 @@ +{ + "Albums": "Alben", + "Application": "Applikatioun", + "Artists": "Kënschtler", + "Books": "Bicher", + "Channels": "Kanäl", + "Collections": "Kollektiounen", + "Default": "Standard", + "ChapterNameValue": "Kapitel {0}", + "DeviceOnlineWithName": "{0} ass Online", + "DeviceOfflineWithName": "{0} ass Offline", + "External": "Extern", + "Favorites": "Favoritten", + "Folders": "Dossieren", + "Forced": "Forcéiert", + "HeaderAlbumArtists": "Album Kënschtler", + "HeaderFavoriteAlbums": "Léifsten Alben", + "HeaderFavoriteArtists": "Léifsten Kënschtler", + "HeaderFavoriteEpisodes": "Léifsten Episoden", + "HeaderFavoriteShows": "Léifsten Shows", + "HeaderFavoriteSongs": "Léifsten Lidder", + "Genres": "Generen", + "HeaderContinueWatching": "Weider kucken", + "Inherit": "Iwwerhuelen", + "HeaderNextUp": "Als Nächst", + "HeaderRecordingGroups": "Opname Gruppen", + "HearingImpaired": "Daaf", + "HomeVideos": "Amateur Videoen", + "ItemRemovedWithName": "Element ewech geholl: {0}", + "LabelIpAddressValue": "IP Adress: {0}", + "LabelRunningTimeValue": "Lafzäit: {0}", + "Latest": "Dat Aktuellst", + "MessageApplicationUpdatedTo": "Jellyfin Server aktualiséiert op {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Server Konfiguratiounssektioun {0} aktualiséiert", + "MessageServerConfigurationUpdated": "Server Konfiguratioun aktualiséiert", + "Movies": "Filmer", + "Music": "Musek", + "NameInstallFailed": "{0} Installatioun net gelongen", + "NameSeasonNumber": "Staffel {0}", + "NameSeasonUnknown": "Staffel Onbekannt", + "MusicVideos": "Museksvideoen", + "NotificationOptionApplicationUpdateAvailable": "Applikatiouns Update verfügbar", + "NotificationOptionApplicationUpdateInstalled": "Applikatiouns Update nët Installéiert", + "NotificationOptionAudioPlayback": "Audio ofspillen gestart", + "NotificationOptionAudioPlaybackStopped": "Audio ofspillen gestoppt", + "NotificationOptionCameraImageUploaded": "Kamera Bild eropgelueden", + "NotificationOptionInstallationFailed": "Installatioun net gelongen", + "NotificationOptionNewLibraryContent": "Neien Bibliothéik Inhalt", + "NotificationOptionPluginError": "Plugin Feeler", + "NotificationOptionPluginInstalled": "Plugin installéiert", + "NotificationOptionPluginUninstalled": "Plugin desinstalléiert", + "NotificationOptionPluginUpdateInstalled": "Plugin Update installéiert", + "Photos": "Fotoen", + "NotificationOptionTaskFailed": "Aufgab net gelongen", + "NotificationOptionUserLockedOut": "Benotzer Gesperrt", + "NotificationOptionVideoPlaybackStopped": "Video ofspillen gestoppt", + "NotificationOptionVideoPlayback": "Video ofspillen gestartet", + "Plugin": "Plugin", + "PluginUninstalledWithName": "{0} desinstalléiert", + "PluginUpdatedWithName": "{0} aktualiséiert", + "ProviderValue": "Provider: {0}", + "ScheduledTaskFailedWithName": "Aufgab: {0} net gelongen", + "Playlists": "Playlëschten", + "Shows": "Shows", + "Songs": "Lidder", + "ServerNameNeedsToBeRestarted": "{0} muss nei gestart ginn", + "StartupEmbyServerIsLoading": "Jellyfin Server luedt. Probéier méi spéit nach eng Kéier.", + "Sync": "Synchroniséieren", + "System": "System", + "User": "Benotzer", + "TvShows": "TV Shows", + "Undefined": "Net definéiert", + "UserCreatedWithName": "Benotzer {0} erstellt", + "UserDownloadingItemWithValues": "{0} luet {1} erof", + "UserOfflineFromDevice": "{0} Benotzer Offline um Gerät {1}", + "UserLockedOutWithName": "Benotzer {0} gesperrt", + "UserOnlineFromDevice": "{0} Benotzer Online um Gerät {1}", + "UserPasswordChangedWithName": "Benotzer Passwuert geännert fir {0}", + "UserPolicyUpdatedWithName": "Benotzer Politik aktualiséiert fir: {0}", + "UserStartedPlayingItemWithValues": "{0} spillt {1} op {2} oof", + "ValueHasBeenAddedToLibrary": "{0} der Bibliothéik bäigefüügt", + "VersionNumber": "Versioun {0}", + "TasksMaintenanceCategory": "Ënnerhalt", + "TasksLibraryCategory": "Bibliothéik", + "ValueSpecialEpisodeName": "Spezial-Episodenumm", + "TasksChannelsCategory": "Internet Kanäl", + "TaskCleanActivityLog": "Aktivitéits Log botzen", + "TaskCleanActivityLogDescription": "Läscht Aktivitéitslogs méi al wéi konfiguréiert.", + "TaskCleanCache": "Aufgab Cache Botzen", + "TaskRefreshChapterImages": "Kapitel Biller erstellen", + "TaskRefreshChapterImagesDescription": "Erstellt Miniaturbiller fir Videoen, déi Kapitelen hunn.", + "TaskAudioNormalization": "Audio Normaliséierung", + "TaskRefreshLibrary": "Bibliothéik aktualiséieren", + "TaskRefreshLibraryDescription": "Scannt deng Mediebibliothéik no neien Dateien a frëscht d’Metadata op.", + "TaskCleanLogs": "Log Dateien botzen", + "TaskRefreshPeople": "Persounen aktualiséieren", + "TaskRefreshPeopleDescription": "Aktualiséiert Metadata fir Schauspiller a Regisseuren an denger Mediebibliothéik.", + "TaskRefreshTrickplayImagesDescription": "Erstellt Trickplay-Viraussiichten fir Videoen an aktivéierte Bibliothéiken.", + "TaskCleanTranscode": "Transkodéieren botzen", + "TaskCleanTranscodeDescription": "Läscht Transkodéierungsdateien, déi méi al wéi een Dag sinn.", + "TaskRefreshChannels": "Kanäl aktualiséieren", + "TaskDownloadMissingLyrics": "Fehlend Liddertexter eroflueden", + "TaskDownloadMissingLyricsDescription": "Lued Liddertexter fir Lidder erof", + "TaskDownloadMissingSubtitles": "Fehlend Ënnertitelen eroflueden", + "TaskOptimizeDatabase": "Datebank optiméieren", + "TaskKeyframeExtractor": "Schlësselbild Extrakter", + "TaskCleanCollectionsAndPlaylists": "Sammlungen a Playlisten botzen", + "TaskCleanCollectionsAndPlaylistsDescription": "Ewechhuele vun Elementer aus Sammlungen a Playlisten, déi net méi existéieren.", + "TaskExtractMediaSegments": "Mediesegment-Scan", + "NewVersionIsAvailable": "Nei Versioun fir Jellyfin Server ass verfügbar.", + "CameraImageUploadedFrom": "En neit Kamera Bild gouf vu {0} eropgelueden", + "PluginInstalledWithName": "{0} installéiert", + "TaskMoveTrickplayImagesDescription": "Verschëfft existent Trickplay-Dateien no de Bibliothéik-Astellungen.", + "AppDeviceValues": "App: {0}, Geräter: {1}", + "FailedLoginAttemptWithUserName": "Net Gelongen Umeldung {0}", + "HeaderLiveTV": "LiveTV", + "ItemAddedWithName": "Element derbäi gesat: {0}", + "NotificationOptionServerRestartRequired": "Server Restart Erfuerderlech", + "ScheduledTaskStartedWithName": "Aufgab: {0} gestart", + "AuthenticationSucceededWithUserName": "{0} Authentifikatioun gelongen", + "MixedContent": "Gemëschten Inhalt", + "MessageApplicationUpdated": "Jellyfin Server Aktualiséiert", + "SubtitleDownloadFailureFromForItem": "Ënnertitel Download Feeler vun {0} fir {1}", + "TaskCleanLogsDescription": "Läscht Log-Dateien, déi méi al wéi {0} Deeg sinn.", + "TaskUpdatePlugins": "Plugins aktualiséieren", + "UserDeletedWithName": "Benotzer {0} geläscht", + "TasksApplicationCategory": "Applikatioun", + "TaskCleanCacheDescription": "Läscht Cache-Dateien, déi net méi vum System gebraucht ginn.", + "UserStoppedPlayingItemWithValues": "{0} ass mat {1} op {2} fäerdeg", + "TaskAudioNormalizationDescription": "Scannt Dateien no Donnéeën fir d’Audio-Normaliséierung.", + "TaskRefreshTrickplayImages": "Trickplay-Biller generéieren", + "TaskDownloadMissingSubtitlesDescription": "Sicht am Internet no fehlenden Ënnertitelen op Basis vun der Metadata-Konfiguratioun.", + "TaskMoveTrickplayImages": "Trickplay-Biller-Plaz migréieren", + "TaskUpdatePluginsDescription": "Lued Aktualiséierungen erof a installéiert se fir Plugins, déi fir automatesch Updates konfiguréiert sinn.", + "TaskKeyframeExtractorDescription": "Extrahéiert Schlësselbiller aus Videodateien, fir méi präzis HLS-Playlisten ze erstellen. Dës Aufgab kann eng längere Zäit daueren.", + "TaskRefreshChannelsDescription": "Aktualiséiert Informatiounen iwwer Internetkanäl.", + "TaskExtractMediaSegmentsDescription": "Extrahéiert oder kritt Mediesegmenter aus Plugins, déi MediaSegment ënnerstëtzen.", + "TaskOptimizeDatabaseDescription": "Kompriméiert d’Datebank a schneit de fräie Speicherplatz zou. Dës Aufgab no engem Bibliothéik-Scan oder anere Ännerungen, déi Datebankmodifikatioune mat sech bréngen, auszeféieren, kann d’Performance verbesseren." +} diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index cf8e0fb00..c45a4a60f 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.Session private readonly SessionInfo _session; private readonly List<IWebSocketConnection> _sockets; + private readonly ReaderWriterLockSlim _socketsLock; private bool _disposed = false; public WebSocketController( @@ -31,10 +32,26 @@ namespace Emby.Server.Implementations.Session _logger = logger; _session = session; _sessionManager = sessionManager; - _sockets = new List<IWebSocketConnection>(); + _sockets = new(); + _socketsLock = new(); } - private bool HasOpenSockets => GetActiveSockets().Any(); + private bool HasOpenSockets + { + get + { + ObjectDisposedException.ThrowIf(_disposed, this); + try + { + _socketsLock.EnterReadLock(); + return _sockets.Any(i => i.State == WebSocketState.Open); + } + finally + { + _socketsLock.ExitReadLock(); + } + } + } /// <inheritdoc /> public bool SupportsMediaControl => HasOpenSockets; @@ -42,23 +59,38 @@ namespace Emby.Server.Implementations.Session /// <inheritdoc /> public bool IsSessionActive => HasOpenSockets; - private IEnumerable<IWebSocketConnection> GetActiveSockets() - => _sockets.Where(i => i.State == WebSocketState.Open); - public void AddWebSocket(IWebSocketConnection connection) { _logger.LogDebug("Adding websocket to session {Session}", _session.Id); - _sockets.Add(connection); - - connection.Closed += OnConnectionClosed; + ObjectDisposedException.ThrowIf(_disposed, this); + try + { + _socketsLock.EnterWriteLock(); + _sockets.Add(connection); + connection.Closed += OnConnectionClosed; + } + finally + { + _socketsLock.ExitWriteLock(); + } } private async void OnConnectionClosed(object? sender, EventArgs e) { var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender)); _logger.LogDebug("Removing websocket from session {Session}", _session.Id); - _sockets.Remove(connection); - connection.Closed -= OnConnectionClosed; + ObjectDisposedException.ThrowIf(_disposed, this); + try + { + _socketsLock.EnterWriteLock(); + _sockets.Remove(connection); + connection.Closed -= OnConnectionClosed; + } + finally + { + _socketsLock.ExitWriteLock(); + } + await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false); } @@ -69,7 +101,17 @@ namespace Emby.Server.Implementations.Session T data, CancellationToken cancellationToken) { - var socket = GetActiveSockets().MaxBy(i => i.LastActivityDate); + ObjectDisposedException.ThrowIf(_disposed, this); + IWebSocketConnection? socket; + try + { + _socketsLock.EnterReadLock(); + socket = _sockets.Where(i => i.State == WebSocketState.Open).MaxBy(i => i.LastActivityDate); + } + finally + { + _socketsLock.ExitReadLock(); + } if (socket is null) { @@ -94,12 +136,23 @@ namespace Emby.Server.Implementations.Session return; } - foreach (var socket in _sockets) + try + { + _socketsLock.EnterWriteLock(); + foreach (var socket in _sockets) + { + socket.Closed -= OnConnectionClosed; + socket.Dispose(); + } + + _sockets.Clear(); + } + finally { - socket.Closed -= OnConnectionClosed; - socket.Dispose(); + _socketsLock.ExitWriteLock(); } + _socketsLock.Dispose(); _disposed = true; } @@ -110,12 +163,23 @@ namespace Emby.Server.Implementations.Session return; } - foreach (var socket in _sockets) + try + { + _socketsLock.EnterWriteLock(); + foreach (var socket in _sockets) + { + socket.Closed -= OnConnectionClosed; + await socket.DisposeAsync().ConfigureAwait(false); + } + + _sockets.Clear(); + } + finally { - socket.Closed -= OnConnectionClosed; - await socket.DisposeAsync().ConfigureAwait(false); + _socketsLock.ExitWriteLock(); } + _socketsLock.Dispose(); _disposed = true; } } diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index 3818cc4e2..97f827fde 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -70,8 +70,9 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi /// <param name="message">The message.</param> protected override void Start(WebSocketMessageInfo message) { - if (message.Connection.AuthorizationInfo.User is null - || !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) + if (!message.Connection.AuthorizationInfo.IsApiKey + && (message.Connection.AuthorizationInfo.User is null + || !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))) { throw new AuthenticationException("Only admin users can retrieve the activity log."); } diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index 95e7feab3..6cbab6571 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -79,8 +79,9 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume /// <param name="message">The message.</param> protected override void Start(WebSocketMessageInfo message) { - if (message.Connection.AuthorizationInfo.User is null - || !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) + if (!message.Connection.AuthorizationInfo.IsApiKey + && (message.Connection.AuthorizationInfo.User is null + || !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))) { throw new AuthenticationException("Only admin users can subscribe to session information."); } diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index f186523b9..9e07000bc 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Channels [JsonIgnore] public override SourceType SourceType => SourceType.Channel; - public override bool IsVisible(User user) + public override bool IsVisible(User user, bool skipAllowedTagsCheck = false) { var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels); if (blockedChannelsPreference.Length != 0) @@ -41,7 +41,7 @@ namespace MediaBrowser.Controller.Channels } } - return base.IsVisible(user); + return base.IsVisible(user, skipAllowedTagsCheck); } protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index e7aa0fad5..55553da49 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1303,7 +1303,7 @@ namespace MediaBrowser.Controller.Entities return false; } - if (GetParents().Any(i => !i.IsVisible(user))) + if (GetParents().Any(i => !i.IsVisible(user, true))) { return false; } @@ -1525,13 +1525,14 @@ namespace MediaBrowser.Controller.Entities /// Determines if a given user has access to this item. /// </summary> /// <param name="user">The user.</param> + /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param> /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns> /// <exception cref="ArgumentNullException">If user is null.</exception> - public bool IsParentalAllowed(User user) + public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck) { ArgumentNullException.ThrowIfNull(user); - if (!IsVisibleViaTags(user)) + if (!IsVisibleViaTags(user, skipAllowedTagsCheck)) { return false; } @@ -1603,7 +1604,7 @@ namespace MediaBrowser.Controller.Entities return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } - private bool IsVisibleViaTags(User user) + private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck) { var allTags = GetInheritedTags(); if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase))) @@ -1618,7 +1619,7 @@ namespace MediaBrowser.Controller.Entities } var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags); - if (allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase))) + if (!skipAllowedTagsCheck && allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase))) { return false; } @@ -1658,13 +1659,14 @@ namespace MediaBrowser.Controller.Entities /// Default is just parental allowed. Can be overridden for more functionality. /// </summary> /// <param name="user">The user.</param> + /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param> /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns> /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception> - public virtual bool IsVisible(User user) + public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false) { ArgumentNullException.ThrowIfNull(user); - return IsParentalAllowed(user); + return IsParentalAllowed(user, skipAllowedTagsCheck); } public virtual bool IsVisibleStandalone(User user) diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 4ead477f8..b7b5dac03 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -96,11 +96,11 @@ namespace MediaBrowser.Controller.Entities return GetLibraryOptions(Path); } - public override bool IsVisible(User user) + public override bool IsVisible(User user, bool skipAllowedTagsCheck = false) { if (GetLibraryOptions().Enabled) { - return base.IsVisible(user); + return base.IsVisible(user, skipAllowedTagsCheck); } return false; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index c110f4d9f..957e8b319 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -219,7 +219,7 @@ namespace MediaBrowser.Controller.Entities LibraryManager.CreateItem(item, this); } - public override bool IsVisible(User user) + public override bool IsVisible(User user, bool skipAllowedTagsCheck = false) { if (this is ICollectionFolder && this is not BasePluginFolder) { @@ -241,7 +241,7 @@ namespace MediaBrowser.Controller.Entities } } - return base.IsVisible(user); + return base.IsVisible(user, skipAllowedTagsCheck); } /// <summary> @@ -452,7 +452,7 @@ namespace MediaBrowser.Controller.Entities if (newItems.Count > 0) { - LibraryManager.CreateOrUpdateItems(newItems, this, cancellationToken); + LibraryManager.CreateItems(newItems, this, cancellationToken); } } else diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index d0c9f049a..c9a93d0f5 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -145,14 +145,14 @@ namespace MediaBrowser.Controller.Entities.Movies return GetItemLookupInfo<BoxSetInfo>(); } - public override bool IsVisible(User user) + public override bool IsVisible(User user, bool skipAllowedTagsCheck = false) { if (IsLegacyBoxSet) { - return base.IsVisible(user); + return base.IsVisible(user, skipAllowedTagsCheck); } - if (base.IsVisible(user)) + if (base.IsVisible(user, skipAllowedTagsCheck)) { if (LinkedChildren.Length == 0) { diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 8fcd5f605..47b1cb16e 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -258,7 +258,7 @@ namespace MediaBrowser.Controller.Library /// <param name="items">Items to create.</param> /// <param name="parent">Parent of new items.</param> /// <param name="cancellationToken">CancellationToken to use for operation.</param> - void CreateOrUpdateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken); + void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken); /// <summary> /// Updates the item. diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index bf6871a74..edea54291 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -227,11 +227,11 @@ namespace MediaBrowser.Controller.Playlists return [item]; } - public override bool IsVisible(User user) + public override bool IsVisible(User user, bool skipAllowedTagsCheck = false) { if (!IsSharedItem) { - return base.IsVisible(user); + return base.IsVisible(user, skipAllowedTagsCheck); } if (OpenAccess) diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 64954818a..ee22b4bc6 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Mime; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -551,10 +552,16 @@ namespace MediaBrowser.Providers.Manager var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using (stream.ConfigureAwait(false)) { + var mimetype = response.Content.Headers.ContentType?.MediaType; + if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase)) + { + mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path)); + } + await _providerManager.SaveImage( item, stream, - response.Content.Headers.ContentType?.MediaType, + mimetype, type, null, cancellationToken).ConfigureAwait(false); @@ -677,10 +684,16 @@ namespace MediaBrowser.Providers.Manager var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using (stream.ConfigureAwait(false)) { + var mimetype = response.Content.Headers.ContentType?.MediaType; + if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase)) + { + mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path)); + } + await _providerManager.SaveImage( item, stream, - response.Content.Headers.ContentType?.MediaType, + mimetype, imageType, null, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 6813cfa91..8c45abe25 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -205,27 +205,10 @@ namespace MediaBrowser.Providers.Manager { contentType = MediaTypeNames.Image.Png; } - else - { - // Deduce content type from file extension - contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path)); - } - - // Throw if we still can't determine the content type - if (string.IsNullOrEmpty(contentType)) - { - throw new HttpRequestException("Invalid image received: contentType not set.", null, response.StatusCode); - } - } - - // TVDb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons... - if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase)) - { - throw new HttpRequestException("Invalid image received.", null, HttpStatusCode.NotFound); } - // some iptv/epg providers don't correctly report media type, extract from url if no extension found - if (string.IsNullOrWhiteSpace(MimeTypes.ToExtension(contentType))) + // some providers don't correctly report media type, extract from url if no extension found + if (contentType is null || contentType.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase)) { // Strip query parameters from url to get actual path. contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path)); @@ -233,7 +216,7 @@ namespace MediaBrowser.Providers.Manager if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) { - throw new HttpRequestException($"Request returned {contentType} instead of an image type", null, HttpStatusCode.NotFound); + throw new HttpRequestException($"Request returned '{contentType}' instead of an image type", null, HttpStatusCode.NotFound); } var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 05d43acdc..a0481a642 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -176,9 +176,9 @@ namespace MediaBrowser.Providers.MediaInfo track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title; track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album; - track.Year ??= mediaInfo.ProductionYear; - track.TrackNumber ??= mediaInfo.IndexNumber; - track.DiscNumber ??= mediaInfo.ParentIndexNumber; + track.Year = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year; + track.TrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber; + track.DiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber; if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs index b75cc0fb2..ac59a6d12 100644 --- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs +++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities.Libraries; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using Jellyfin.LiveTv.Configuration; @@ -210,7 +209,7 @@ public class GuideManager : IGuideManager progress.Report(15); numComplete = 0; - var programs = new List<LiveTvProgram>(); + var programIds = new List<Guid>(); var channels = new List<Guid>(); var guideDays = GetGuideDays(); @@ -243,8 +242,8 @@ public class GuideManager : IGuideManager DtoOptions = new DtoOptions(true) }).Cast<LiveTvProgram>().ToDictionary(i => i.Id); - var newPrograms = new List<Guid>(); - var updatedPrograms = new List<Guid>(); + var newPrograms = new List<LiveTvProgram>(); + var updatedPrograms = new List<LiveTvProgram>(); foreach (var program in channelPrograms) { @@ -252,14 +251,14 @@ public class GuideManager : IGuideManager var id = programItem.Id; if (isNew) { - newPrograms.Add(id); + newPrograms.Add(programItem); } else if (isUpdated) { - updatedPrograms.Add(id); + updatedPrograms.Add(programItem); } - programs.Add(programItem); + programIds.Add(programItem.Id); isMovie |= program.IsMovie; isSeries |= program.IsSeries; @@ -276,21 +275,21 @@ public class GuideManager : IGuideManager if (newPrograms.Count > 0) { - var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList(); - _libraryManager.CreateOrUpdateItems(newProgramDtos, null, cancellationToken); + _libraryManager.CreateItems(newPrograms, currentChannel, cancellationToken); + + await PreCacheImages(newPrograms, maxCacheDate).ConfigureAwait(false); } if (updatedPrograms.Count > 0) { - var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList(); await _libraryManager.UpdateItemsAsync( - updatedProgramDtos, + updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); - } - await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false); + await PreCacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false); + } currentChannel.IsMovie = isMovie; currentChannel.IsNews = isNews; @@ -326,7 +325,6 @@ public class GuideManager : IGuideManager } progress.Report(100); - var programIds = programs.Select(p => p.Id).ToList(); return new Tuple<List<Guid>, List<Guid>>(channels, programIds); } @@ -502,35 +500,27 @@ public class GuideManager : IGuideManager forceUpdate = true; } - var seriesId = info.SeriesId; - - if (!item.ParentId.Equals(channel.Id)) + var channelId = channel.Id; + if (!item.ParentId.Equals(channelId)) { + item.ParentId = channel.Id; forceUpdate = true; } - item.ParentId = channel.Id; - item.Audio = info.Audio; - item.ChannelId = channel.Id; - item.CommunityRating ??= info.CommunityRating; - if ((item.CommunityRating ?? 0).Equals(0)) - { - item.CommunityRating = null; - } - + item.ChannelId = channelId; + item.CommunityRating = info.CommunityRating; item.EpisodeTitle = info.EpisodeTitle; item.ExternalId = info.Id; - if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal)) + var seriesId = info.SeriesId; + if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.OrdinalIgnoreCase)) { + item.ExternalSeriesId = seriesId; forceUpdate = true; } - item.ExternalSeriesId = seriesId; - var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle); - if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle)) { item.SeriesName = info.Name; @@ -578,7 +568,6 @@ public class GuideManager : IGuideManager } item.Tags = tags.ToArray(); - item.Genres = info.Genres.ToArray(); if (info.IsHD ?? false) @@ -589,41 +578,35 @@ public class GuideManager : IGuideManager item.IsMovie = info.IsMovie; item.IsRepeat = info.IsRepeat; - if (item.IsSeries != isSeries) { + item.IsSeries = isSeries; forceUpdate = true; } - item.IsSeries = isSeries; - item.Name = info.Name; - item.OfficialRating ??= info.OfficialRating; - item.Overview ??= info.Overview; + item.OfficialRating = info.OfficialRating; + item.Overview = info.Overview; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; - item.ProviderIds = info.ProviderIds; - foreach (var providerId in info.SeriesProviderIds) { info.ProviderIds["Series" + providerId.Key] = providerId.Value; } + item.ProviderIds = info.ProviderIds; if (item.StartDate != info.StartDate) { + item.StartDate = info.StartDate; forceUpdate = true; } - item.StartDate = info.StartDate; - if (item.EndDate != info.EndDate) { + item.EndDate = info.EndDate; forceUpdate = true; } - item.EndDate = info.EndDate; - item.ProductionYear = info.ProductionYear; - if (!isSeries || info.IsRepeat) { item.PremiereDate = info.OriginalAirDate; @@ -632,37 +615,35 @@ public class GuideManager : IGuideManager item.IndexNumber = info.EpisodeNumber; item.ParentIndexNumber = info.SeasonNumber; - forceUpdate = forceUpdate || UpdateImages(item, info); + forceUpdate |= UpdateImages(item, info); if (isNew) { item.OnMetadataChanged(); - return (item, isNew, false); + return (item, true, false); } - var isUpdated = false; - if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag)) + var isUpdated = forceUpdate; + var etag = info.Etag; + if (string.IsNullOrWhiteSpace(etag)) { isUpdated = true; } - else + else if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase)) { - var etag = info.Etag; - - if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase)) - { - item.SetProviderId(EtagKey, etag); - isUpdated = true; - } + item.SetProviderId(EtagKey, etag); + isUpdated = true; } if (isUpdated) { item.OnMetadataChanged(); + + return (item, false, true); } - return (item, isNew, isUpdated); + return (item, false, false); } private static bool UpdateImages(BaseItem item, ProgramInfo info) @@ -679,7 +660,9 @@ public class GuideManager : IGuideManager updated |= UpdateImage(ImageType.Logo, item, info); // Backdrop - return updated || UpdateImage(ImageType.Backdrop, item, info); + updated |= UpdateImage(ImageType.Backdrop, item, info); + + return updated; } private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info) @@ -689,7 +672,7 @@ public class GuideManager : IGuideManager var newImagePath = imageType switch { ImageType.Primary => info.ImagePath, - _ => string.Empty + _ => null }; var newImageUrl = imageType switch { @@ -697,12 +680,12 @@ public class GuideManager : IGuideManager ImageType.Logo => info.LogoImageUrl, ImageType.Primary => info.ImageUrl, ImageType.Thumb => info.ThumbImageUrl, - _ => string.Empty + _ => null }; - var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false - || newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false; - if (!differentImage) + var sameImage = (currentImagePath?.Equals(newImageUrl, StringComparison.OrdinalIgnoreCase) ?? false) + || (currentImagePath?.Equals(newImagePath, StringComparison.OrdinalIgnoreCase) ?? false); + if (sameImage) { return false; } @@ -757,6 +740,7 @@ public class GuideManager : IGuideManager var imageInfo = program.ImageInfos[i]; if (!imageInfo.IsLocalFile) { + _logger.LogDebug("Caching image locally: {Url}", imageInfo.Path); try { program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal( |
