diff options
129 files changed, 2031 insertions, 885 deletions
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml index 01cd41a08..0989df64b 100644 --- a/.github/workflows/automation.yml +++ b/.github/workflows/automation.yml @@ -14,7 +14,7 @@ jobs: if: ${{ github.repository == 'jellyfin/jellyfin' }} steps: - name: Apply label - uses: eps1lon/actions-label-merge-conflict@b8bf8341285ec9a4567d4318ba474fee998a6919 # tag=v2.0.1 + uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0 if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} with: dirtyLabel: 'merge conflict' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b551bb5a6..39ba5ea4d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -22,16 +22,16 @@ jobs: - name: Checkout repository uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - name: Setup .NET Core - uses: actions/setup-dotnet@4d4a70f4a5b2a5a5329f13be4ac933f2c9206ac0 # tag=v3 + uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 with: dotnet-version: '6.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@cc7986c02bac29104a72998e67239bb5ee2ee110 # tag=v2 + uses: github/codeql-action/init@c3b6fce4ee2ca25bc1066aa3bf73962fda0e8898 # tag=v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@cc7986c02bac29104a72998e67239bb5ee2ee110 # tag=v2 + uses: github/codeql-action/autobuild@c3b6fce4ee2ca25bc1066aa3bf73962fda0e8898 # tag=v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cc7986c02bac29104a72998e67239bb5ee2ee110 # tag=v2 + uses: github/codeql-action/analyze@c3b6fce4ee2ca25bc1066aa3bf73962fda0e8898 # tag=v2 diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index d438e7801..a29519b29 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # tag=v2 + uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 with: token: ${{ secrets.JF_BOT_TOKEN }} comment-id: ${{ github.event.comment.id }} @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify as seen - uses: peter-evans/create-or-update-comment@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # tag=v2 + uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -54,7 +54,7 @@ jobs: - name: Notify as running id: comment_running - uses: peter-evans/create-or-update-comment@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # tag=v2 + uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 if: ${{ github.event.comment != null }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -89,7 +89,7 @@ jobs: exit ${retcode} - name: Notify with result success - uses: peter-evans/create-or-update-comment@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # tag=v2 + uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 if: ${{ github.event.comment != null && success() }} with: token: ${{ secrets.JF_BOT_TOKEN }} @@ -104,7 +104,7 @@ jobs: reactions: hooray - name: Notify with result failure - uses: peter-evans/create-or-update-comment@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # tag=v2 + uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 if: ${{ github.event.comment != null && failure() }} with: token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index c4300b39a..ca710fe83 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -17,13 +17,13 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET Core - uses: actions/setup-dotnet@4d4a70f4a5b2a5a5329f13be4ac933f2c9206ac0 # tag=v3 + uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 with: dotnet-version: '6.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3 + uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3 with: name: openapi-head retention-days: 14 @@ -41,13 +41,13 @@ jobs: with: ref: ${{ github.base_ref }} - name: Setup .NET Core - uses: actions/setup-dotnet@4d4a70f4a5b2a5a5329f13be4ac933f2c9206ac0 # tag=v3 + uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3 with: dotnet-version: '6.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3 + uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3 with: name: openapi-base retention-days: 14 @@ -63,12 +63,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # tag=v3 + uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # tag=v3 + uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3 with: name: openapi-base path: openapi-base @@ -97,7 +97,7 @@ jobs: direction: last body-includes: openapi-diff-workflow-comment - name: Reply or edit difference comment (changed) - uses: peter-evans/create-or-update-comment@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # tag=v2 + uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 if: ${{ steps.read-diff.outputs.body != '' }} with: issue-number: ${{ github.event.pull_request.number }} @@ -112,7 +112,7 @@ jobs: </details> - name: Edit difference comment (unchanged) - uses: peter-evans/create-or-update-comment@2b2c85d0bf1b8a7b4e7e344bd5c71dc4b9196e9f # tag=v2 + uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2 if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} with: issue-number: ${{ github.event.pull_request.number }} diff --git a/Dockerfile b/Dockerfile index 219b95893..7b69a186f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -89,4 +89,4 @@ ENTRYPOINT ["./jellyfin/jellyfin", \ "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \ - CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1 + CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1 diff --git a/Dockerfile.arm b/Dockerfile.arm index 8e0ba7af5..84ddf499a 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -78,4 +78,4 @@ ENTRYPOINT ["./jellyfin/jellyfin", \ "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \ - CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1 + CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1 diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 790be1c39..d4ae5802c 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -72,4 +72,4 @@ ENTRYPOINT ["./jellyfin/jellyfin", \ "--ffmpeg", "/usr/bin/ffmpeg"] HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \ - CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1 + CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1 diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index d17e23871..68895a7fe 100644 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -127,8 +127,7 @@ namespace Emby.Dlna.Eventing public Task TriggerEvent(string notificationType, IDictionary<string, string> stateVariables) { var subs = _subscriptions.Values - .Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase)) - .ToList(); + .Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase)); var tasks = subs.Select(i => TriggerEvent(i, stateVariables)); diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index 2efe7d526..6e491185d 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -36,8 +36,7 @@ namespace Emby.Naming.AudioBook // File with empty fullname will be sorted out here. var audiobookFileInfos = files .Select(i => _audioBookResolver.Resolve(i.FullName)) - .OfType<AudioBookFileInfo>() - .ToList(); + .OfType<AudioBookFileInfo>(); var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos); diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 513733ab5..0119fa38c 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -175,6 +175,7 @@ namespace Emby.Naming.Common AlbumStackingPrefixes = new[] { "cd", + "digital media", "disc", "disk", "vol", @@ -512,13 +513,13 @@ namespace Emby.Naming.Common MediaType.Video), new ExtraRule( - ExtraType.Clip, + ExtraType.Short, ExtraRuleType.DirectoryName, "shorts", MediaType.Video), new ExtraRule( - ExtraType.Clip, + ExtraType.Featurette, ExtraRuleType.DirectoryName, "featurettes", MediaType.Video), @@ -536,6 +537,12 @@ namespace Emby.Naming.Common MediaType.Video), new ExtraRule( + ExtraType.Clip, + ExtraRuleType.DirectoryName, + "clips", + MediaType.Video), + + new ExtraRule( ExtraType.Trailer, ExtraRuleType.Filename, "trailer", @@ -638,13 +645,13 @@ namespace Emby.Naming.Common MediaType.Video), new ExtraRule( - ExtraType.Clip, + ExtraType.Featurette, ExtraRuleType.Suffix, "-featurette", MediaType.Video), new ExtraRule( - ExtraType.Clip, + ExtraType.Short, ExtraRuleType.Suffix, "-short", MediaType.Video), diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs index 8b281e487..ac90cc8ec 100644 --- a/Emby.Notifications/NotificationManager.cs +++ b/Emby.Notifications/NotificationManager.cs @@ -88,8 +88,7 @@ namespace Emby.Notifications string description, CancellationToken cancellationToken) { - users = users.Where(i => IsEnabledForUser(service, i)) - .ToList(); + users = users.Where(i => IsEnabledForUser(service, i)); var tasks = users.Select(i => SendNotification(request, service, title, description, i, cancellationToken)); diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8db55a6ae..8e4c13def 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -48,6 +48,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; +using Jellyfin.Server.Implementations; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; @@ -101,6 +102,7 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -652,6 +654,17 @@ namespace Emby.Server.Implementations /// <returns>A task representing the service initialization operation.</returns> public async Task InitializeServices() { + var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false); + await using (jellyfinDb.ConfigureAwait(false)) + { + if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any()) + { + Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)"); + await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false); + Logger.LogInformation("EFCore migrations applied successfully"); + } + } + var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); await localizationManager.LoadAll().ConfigureAwait(false); diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 5fc2e39a7..187e0c9b3 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -232,10 +232,10 @@ namespace Emby.Server.Implementations.Collections if (list.Count > 0) { - var newList = collection.LinkedChildren.ToList(); - newList.AddRange(list); - collection.LinkedChildren = newList.ToArray(); - + LinkedChild[] newChildren = new LinkedChild[collection.LinkedChildren.Length + list.Count]; + collection.LinkedChildren.CopyTo(newChildren, 0); + list.CopyTo(newChildren, collection.LinkedChildren.Length); + collection.LinkedChildren = newChildren; collection.UpdateRatingToItems(linkedChildrenList); await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 7622d2fe6..371111dff 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3524,6 +3524,13 @@ namespace Emby.Server.Implementations.Data statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value); } + if (query.MinParentAndIndexNumber.HasValue) + { + whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)"); + statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber); + statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber); + } + if (query.MinDateCreated.HasValue) { whereClauses.Add("DateCreated>=@MinDateCreated"); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index ff1102a05..a0bbd0c49 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -29,7 +29,7 @@ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.10" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.11" /> <PackageReference Include="Mono.Nat" Version="3.0.4" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.3.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 9e35d83aa..d5e4a636e 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints { } - var collectionFolders = _libraryManager.GetCollectionFolders(item).ToList(); + var collectionFolders = _libraryManager.GetCollectionFolders(item); foreach (var collectionFolder in collectionFolders) { diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 657daac3f..c1422c43d 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -79,14 +79,6 @@ namespace Emby.Server.Implementations.IO TemporarilyIgnore(path); } - public bool IsPathLocked(string path) - { - // This method is not used by the core but it used by auto-organize - - var lockedPaths = _tempIgnoredPaths.Keys.ToList(); - return lockedPaths.Any(i => _fileSystem.AreEqual(i, path) || _fileSystem.ContainsSubPath(i, path)); - } - public async void ReportFileSystemChangeComplete(string path, bool refreshPath) { if (string.IsNullOrEmpty(path)) @@ -145,8 +137,7 @@ namespace Emby.Server.Implementations.IO .OfType<Folder>() .SelectMany(f => f.PhysicalLocations) .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(i => i) - .ToList(); + .OrderBy(i => i); foreach (var path in paths) { @@ -372,11 +363,8 @@ namespace Emby.Server.Implementations.IO var monitorPath = !IgnorePatterns.ShouldIgnore(path); - // Ignore certain files - var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList(); - - // If the parent of an ignored path has a change event, ignore that too - if (tempIgnorePaths.Any(i => + // Ignore certain files, If the parent of an ignored path has a change event, ignore that too + if (_tempIgnoredPaths.Keys.Any(i => { if (_fileSystem.AreEqual(i, path)) { @@ -491,7 +479,7 @@ namespace Emby.Server.Implementations.IO { lock (_activeRefreshers) { - foreach (var refresher in _activeRefreshers.ToList()) + foreach (var refresher in _activeRefreshers) { refresher.Completed -= OnNewRefresherCompleted; refresher.Dispose(); diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index cef82ebbc..b688af528 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2590,9 +2590,9 @@ namespace Emby.Server.Implementations.Library { /* Anime series don't generally have a season in their file name, however, - tvdb needs a season to correctly get the metadata. + TVDb needs a season to correctly get the metadata. Hence, a null season needs to be filled with something. */ - // FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified + // FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified episode.ParentIndexNumber = 1; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 8f9e5f01b..84d4688af 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -376,7 +376,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (!justName.IsEmpty) { - // check for tmdb id + // Check for TMDb id var tmdbid = justName.GetAttributeValue("tmdbid"); if (!string.IsNullOrWhiteSpace(tmdbid)) @@ -387,7 +387,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (!string.IsNullOrEmpty(item.Path)) { - // check for imdb id - we use full media path, as we can assume, that this will match in any use case (either id in parent dir or in file name) + // Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name) var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid"); if (!string.IsNullOrWhiteSpace(imdbid)) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 74321a256..cf9be5a54 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -2192,16 +2192,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private void HandleDuplicateShowIds(List<TimerInfo> timers) { - foreach (var timer in timers.Skip(1)) + // sort showings by HD channels first, then by startDate, record earliest showing possible + foreach (var timer in timers.OrderByDescending(t => _liveTvManager.GetLiveTvChannel(t, this).IsHD).ThenBy(t => t.StartDate).Skip(1)) { - // TODO: Get smarter, prefer HD, etc - timer.Status = RecordingStatus.Cancelled; _timerProvider.Update(timer); } } - private void SearchForDuplicateShowIds(List<TimerInfo> timers) + private void SearchForDuplicateShowIds(IEnumerable<TimerInfo> timers) { var groups = timers.ToLookup(i => i.ShowId ?? string.Empty).ToList(); @@ -2282,39 +2281,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (updateTimerSettings) { - // Only update if not currently active - test both new timer and existing in case Id's are different - // Id's could be different if the timer was created manually prior to series timer creation - if (!_activeRecordings.TryGetValue(timer.Id, out _) && !_activeRecordings.TryGetValue(existingTimer.Id, out _)) - { - UpdateExistingTimerWithNewMetadata(existingTimer, timer); - - // Needed by ShouldCancelTimerForSeriesTimer - timer.IsManual = existingTimer.IsManual; - - if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer)) - { - existingTimer.Status = RecordingStatus.Cancelled; - } - else if (!existingTimer.IsManual) - { - existingTimer.Status = RecordingStatus.New; - } - - if (existingTimer.Status != RecordingStatus.Cancelled) - { - enabledTimersForSeries.Add(existingTimer); - } - - existingTimer.KeepUntil = seriesTimer.KeepUntil; - existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired; - existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired; - existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds; - existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds; - existingTimer.Priority = seriesTimer.Priority; - existingTimer.SeriesTimerId = seriesTimer.Id; - - _timerProvider.Update(existingTimer); - } + existingTimer.KeepUntil = seriesTimer.KeepUntil; + existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired; + existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired; + existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds; + existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds; + existingTimer.Priority = seriesTimer.Priority; + existingTimer.SeriesTimerId = seriesTimer.Id; } existingTimer.SeriesTimerId = seriesTimer.Id; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index a861e6ae4..f612565d1 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -122,11 +122,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (_timers.TryAdd(item.Id, timer)) { - Logger.LogInformation( - "Creating recording timer for {Id}, {Name}. Timer will fire in {Minutes} minutes", + if (item.IsSeries) + { + Logger.LogInformation( + "Creating recording timer for {Id}, {Name} {SeasonNumber}x{EpisodeNumber:D2} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}", item.Id, item.Name, - dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture)); + item.SeasonNumber, + item.EpisodeNumber, + item.ChannelId, + dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), + item.StartDate); + } + else + { + Logger.LogInformation( + "Creating recording timer for {Id}, {Name} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}", + item.Id, + item.Name, + item.ChannelId, + dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), + item.StartDate); + } } else { diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 4311db28d..b981ad81a 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -166,12 +166,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings const double DesiredAspect = 2.0 / 3; - programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ?? - GetProgramImage(ApiUrl, allImages, DesiredAspect); + programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect, token) ?? + GetProgramImage(ApiUrl, allImages, DesiredAspect, token); const double WideAspect = 16.0 / 9; - programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect); + programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect, token); // Don't supply the same image twice if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal)) @@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programEntry.ThumbImage = null; } - programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect); + programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect, token); // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? @@ -400,7 +400,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings return info; } - private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect) + private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect, string token) { var match = images .OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i))) @@ -424,7 +424,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings } else { - return apiUrl + "/image/" + uri; + return apiUrl + "/image/" + uri + "?token=" + token; } } @@ -458,6 +458,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings IReadOnlyList<string> programIds, CancellationToken cancellationToken) { + var token = await GetToken(info, cancellationToken).ConfigureAwait(false); + if (programIds.Count == 0) { return Array.Empty<ShowImagesDto>(); @@ -479,6 +481,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json) }; + message.Headers.TryAddWithoutValidation("token", token); try { diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 7570a2bcf..82f0baf32 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -32,18 +32,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings private readonly IServerConfigurationManager _config; private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger<XmlTvListingsProvider> _logger; - private readonly IFileSystem _fileSystem; public XmlTvListingsProvider( IServerConfigurationManager config, IHttpClientFactory httpClientFactory, - ILogger<XmlTvListingsProvider> logger, - IFileSystem fileSystem) + ILogger<XmlTvListingsProvider> logger) { _config = config; _httpClientFactory = httpClientFactory; _logger = logger; - _fileSystem = fileSystem; } public string Name => "XmlTV"; @@ -165,7 +162,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings HasImage = !string.IsNullOrEmpty(program.Icon?.Source), OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value, CommunityRating = program.StarRating, - SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N", CultureInfo.InvariantCulture) + SeriesId = program.Episode == null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) }; if (string.IsNullOrWhiteSpace(program.ProgramId)) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 48d9e316d..e67b5846a 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none"); + return VerifyReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), "none"); } finally { diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 644d2676e..ab04693cc 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimitzar la base de dades", "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.", "TaskKeyframeExtractor": "Extractor de fotogrames clau", - "External": "Extern" + "External": "Extern", + "HearingImpaired": "Discapacitat Auditiva" } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 9c278db4d..e1c3e9de1 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Datenbank optimieren", "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.", "TaskKeyframeExtractor": "Keyframe Extraktor", - "External": "Extern" + "External": "Extern", + "HearingImpaired": "Hörgeschädigt" } diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 9e216a166..8e9287af4 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων", "TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.", "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο", - "External": "Εξωτερικό" + "External": "Εξωτερικό", + "HearingImpaired": "Με προβλήματα ακοής" } diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index a7391cc88..d677cc46c 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos.", "TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.", "TaskKeyframeExtractor": "Extractor de Cuadros Clave", - "External": "Externo" + "External": "Externo", + "HearingImpaired": "Discapacidad Auditiva" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index db65a0c6d..afffdf3bf 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "Optimiza y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.", "TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.", "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", - "External": "Externo" + "External": "Externo", + "HearingImpaired": "Discapacidad Auditiva" } diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index da44e53d0..081462407 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -120,5 +120,8 @@ "UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati", "UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}", "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}", - "External": "Väline" + "External": "Väline", + "HearingImpaired": "Kuulmispuudega", + "TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.", + "TaskKeyframeExtractor": "Võtmekaadri ekstraktor" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 648c878e9..768245a09 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimiser la base de données", "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.", "TaskKeyframeExtractor": "Extracteur d'image clé", - "External": "Externe" + "External": "Externe", + "HearingImpaired": "Malentendants" } diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index c63cd2b94..d01295419 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -123,5 +123,6 @@ "External": "Vanjski", "TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.", "TaskKeyframeExtractor": "Izvoditelj ključnog okvira", - "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." + "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" } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 2aa84c536..3710f03e0 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -123,5 +123,6 @@ "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" + "External": "Esterno", + "HearingImpaired": "con problemi di udito" } diff --git a/Emby.Server.Implementations/Localization/Core/km.json b/Emby.Server.Implementations/Localization/Core/km.json new file mode 100644 index 000000000..02f9d4443 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/km.json @@ -0,0 +1,3 @@ +{ + "Albums": "Albums" +} diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 232b3ec93..e1c937b6c 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -123,5 +123,6 @@ "TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.", "TaskKeyframeExtractor": "Pagrindinių kadrų ištraukėjas", "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazė, gali pagerinti greitaveiką.", - "External": "Išorinis" + "External": "Išorinis", + "HearingImpaired": "Su klausos sutrikimais" } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 77ee46a4f..5c7dec7ef 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "Komprimerer database og frigjør plass. Denne prosessen kan forbedre ytelsen etter skanning av bibliotek eller andre handlinger som fører til databaseendringer.", "TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.", "TaskKeyframeExtractor": "Nøkkelbilde-uttrekker", - "External": "Ekstern" + "External": "Ekstern", + "HearingImpaired": "Hørselshemmet" } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 3f22355d6..c05114f01 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -5,7 +5,7 @@ "Artists": "Artiesten", "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd", "Books": "Boeken", - "CameraImageUploadedFrom": "Nieuwe camera afbeelding toegevoegd vanaf {0}", + "CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}", "Channels": "Kanalen", "ChapterNameValue": "Hoofdstuk {0}", "Collections": "Verzamelingen", @@ -15,7 +15,7 @@ "Favorites": "Favorieten", "Folders": "Mappen", "Genres": "Genres", - "HeaderAlbumArtists": "Album Artiesten", + "HeaderAlbumArtists": "Albumartiesten", "HeaderContinueWatching": "Kijken hervatten", "HeaderFavoriteAlbums": "Favoriete albums", "HeaderFavoriteArtists": "Favoriete artiesten", @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Database optimaliseren", "TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS afspeellijsten te maken. Dit kan lang duren.", "TaskKeyframeExtractor": "Keyframe Extractor", - "External": "Extern" + "External": "Extern", + "HearingImpaired": "Slechthorend" } diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index c2c77ccab..39229f45f 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -120,5 +120,6 @@ "TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.", "TaskOptimizeDatabase": "Otimizar base de dados", "TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.", - "External": "Externo" + "External": "Externo", + "HearingImpaired": "Problemas auditivos" } diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 53456269a..2c10bb477 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -11,7 +11,7 @@ "UserOfflineFromDevice": "{0} s-a deconectat de la {1}", "UserLockedOutWithName": "Utilizatorul {0} a fost blocat", "UserDownloadingItemWithValues": "{0} descarcă {1}", - "UserDeletedWithName": "Utilizatorul {0} a fost eliminat", + "UserDeletedWithName": "Utilizatorul {0} a fost șters", "UserCreatedWithName": "Utilizatorul {0} a fost creat", "User": "Utilizator", "TvShows": "Seriale TV", @@ -20,33 +20,33 @@ "SubtitleDownloadFailureFromForItem": "Subtitrările nu au putut fi descărcate de la {0} pentru {1}", "StartupEmbyServerIsLoading": "Se încarcă serverul Jellyfin. Încercați din nou în scurt timp.", "Songs": "Melodii", - "Shows": "Spectacole", - "ServerNameNeedsToBeRestarted": "{0} trebuie repornit", + "Shows": "Seriale", + "ServerNameNeedsToBeRestarted": "{0} trebuie să fie repornit", "ScheduledTaskStartedWithName": "{0} pornit/ă", "ScheduledTaskFailedWithName": "{0} eșuat/ă", "ProviderValue": "Furnizor: {0}", "PluginUpdatedWithName": "{0} a fost actualizat/ă", "PluginUninstalledWithName": "{0} a fost dezinstalat", "PluginInstalledWithName": "{0} a fost instalat", - "Plugin": "Plugin", - "Playlists": "Liste redare", + "Plugin": "Extensie", + "Playlists": "Liste de redare", "Photos": "Fotografii", "NotificationOptionVideoPlaybackStopped": "Redarea video oprită", "NotificationOptionVideoPlayback": "Redare video începută", "NotificationOptionUserLockedOut": "Utilizatorul a fost blocat", - "NotificationOptionTaskFailed": "Activitate programata eșuată", + "NotificationOptionTaskFailed": "Activitate programată eșuată", "NotificationOptionServerRestartRequired": "Este necesară repornirea serverului", - "NotificationOptionPluginUpdateInstalled": "Actualizare plugin instalată", - "NotificationOptionPluginUninstalled": "Plugin dezinstalat", - "NotificationOptionPluginInstalled": "Plugin instalat", - "NotificationOptionPluginError": "Plugin-ul a eșuat", - "NotificationOptionNewLibraryContent": "Adăugat conținut nou", - "NotificationOptionInstallationFailed": "Eșec la instalare", - "NotificationOptionCameraImageUploaded": "Încarcată imagine cameră", + "NotificationOptionPluginUpdateInstalled": "Actualizarea extensiei este instalată", + "NotificationOptionPluginUninstalled": "Extensie dezinstalată", + "NotificationOptionPluginInstalled": "Extensie instalată", + "NotificationOptionPluginError": "Eroare de extensie", + "NotificationOptionNewLibraryContent": "A fost adăugat conținut nou", + "NotificationOptionInstallationFailed": "Instalare eșuată", + "NotificationOptionCameraImageUploaded": "Imagine încarcată", "NotificationOptionAudioPlaybackStopped": "Redare audio oprită", "NotificationOptionAudioPlayback": "A început redarea audio", "NotificationOptionApplicationUpdateInstalled": "Actualizarea aplicației a fost instalată", - "NotificationOptionApplicationUpdateAvailable": "Disponibilă o actualizare a aplicației", + "NotificationOptionApplicationUpdateAvailable": "Este disponibilă o actualizare a aplicației", "NewVersionIsAvailable": "O nouă versiune a Jellyfin Server este disponibilă pentru descărcare.", "NameSeasonUnknown": "Sezon Necunoscut", "NameSeasonNumber": "Sezonul {0}", @@ -54,8 +54,8 @@ "MusicVideos": "Videoclipuri muzicale", "Music": "Muzică", "Movies": "Filme", - "MixedContent": "Conținut mixt", - "MessageServerConfigurationUpdated": "Configurația serverului a fost actualizată", + "MixedContent": "Conținut amestecat", + "MessageServerConfigurationUpdated": "Configurarea serverului a fost actualizată", "MessageNamedServerConfigurationUpdatedWithValue": "Secțiunea de configurare a serverului {0} a fost acualizata", "MessageApplicationUpdatedTo": "Jellyfin Server a fost actualizat la {0}", "MessageApplicationUpdated": "Jellyfin Server a fost actualizat", @@ -69,7 +69,7 @@ "HeaderRecordingGroups": "Grupuri de înregistrare", "HeaderLiveTV": "TV în Direct", "HeaderFavoriteSongs": "Melodii Favorite", - "HeaderFavoriteShows": "Spectacole Favorite", + "HeaderFavoriteShows": "Seriale TV Favorite", "HeaderFavoriteEpisodes": "Episoade Favorite", "HeaderFavoriteArtists": "Artiști Favoriți", "HeaderFavoriteAlbums": "Albume Favorite", @@ -97,10 +97,10 @@ "TaskRefreshChannels": "Actualizează canale", "TaskCleanTranscodeDescription": "Șterge fișierele de transcodare mai vechi de o zi.", "TaskCleanTranscode": "Curățați directorul de transcodare", - "TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru pluginuri care sunt configurate să se actualizeze automat.", - "TaskUpdatePlugins": "Actualizați plugin-uri", + "TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru extensiile care sunt configurate să se actualizeze automat.", + "TaskUpdatePlugins": "Actualizați Extensile", "TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.", - "TaskRefreshPeople": "Actualizează oamenii", + "TaskRefreshPeople": "Actualizează Persoanele", "TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.", "TaskCleanLogs": "Curățare director jurnal", "TaskRefreshLibraryDescription": "Scanează biblioteca media pentru fișiere noi și reîmprospătează metadatele.", @@ -114,13 +114,14 @@ "TasksLibraryCategory": "Librărie", "TasksMaintenanceCategory": "Mentenanță", "TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.", - "TaskCleanActivityLog": "Curăță Jurnalul de Activitate", + "TaskCleanActivityLog": "Curăță Jurnalul de Activități", "Undefined": "Nedefinit", "Forced": "Forțat", "Default": "Implicit", - "TaskOptimizeDatabaseDescription": "Compactează baza de date și trunchiază spațiul liber. Rularea acestei sarcini după scanarea bibliotecii sau după efectuarea altor modificări care implică modificări ale bazei de date poate îmbunătăți performanța.", + "TaskOptimizeDatabaseDescription": "Comprimă baza de date și trunchiază spațiul liber. Rularea acestei sarcini după scanarea bibliotecii sau după efectuarea altor modificări care implică modificări ale bazei de date poate îmbunătăți performanța.", "TaskOptimizeDatabase": "Optimizează baza de date", "TaskKeyframeExtractorDescription": "Extrage cadrele cheie din fișierele video pentru a crea liste de redare HLS mai precise. Această sarcină poate rula o perioadă lungă de timp.", "External": "Extern", - "TaskKeyframeExtractor": "Extractor de cadre cheie" + "TaskKeyframeExtractor": "Extractor de cadre cheie", + "HearingImpaired": "Ascultare Impară" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index ea9a82d2b..dc45a8264 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -75,7 +75,7 @@ "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}", - "Sync": "Синхро", + "Sync": "Синхронизация", "System": "Система", "TvShows": "ТВ", "User": "Пользователь", @@ -117,11 +117,12 @@ "TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.", "TaskCleanActivityLog": "Очистка журнала активности", "Undefined": "Не определено", - "Forced": "Форсир-ые", + "Forced": "Принудительно", "Default": "По умолчанию", "TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.", "TaskOptimizeDatabase": "Оптимизация базы данных", "TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.", "TaskKeyframeExtractor": "Извлечение ключевых кадров", - "External": "Внешние" + "External": "Внешние", + "HearingImpaired": "Для слабослышащих" } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 7502969a6..858cc40dd 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimalizovať databázu", "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.", "TaskKeyframeExtractor": "Extraktor kľúčových snímkov", - "External": "Externé" + "External": "Externé", + "HearingImpaired": "Sluchovo Postihnutý" } diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index b9e2f1e6c..44ce4ac5b 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu", "TaskKeyframeExtractor": "Trích Xuất Khung Hình", "TaskKeyframeExtractorDescription": "Trích xuất khung hình chính từ các tệp video để tạo danh sách phát HLS chính xác hơn. Tác vụ này có thể chạy trong một thời gian dài.", - "External": "Bên ngoài" + "External": "Bên ngoài", + "HearingImpaired": "Khiếm Thính" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 6c8bf7627..baa9ecc1c 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -123,5 +123,6 @@ "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。", "TaskKeyframeExtractorDescription": "提取關鍵格以創建更準確的HLS播放列表。次指示可能用時很長。", "TaskKeyframeExtractor": "關鍵幀提取器", - "External": "外部" + "External": "外部", + "HearingImpaired": "聽力障礙" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index 102a266f8..4949c5ab6 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -37,7 +37,7 @@ "MixedContent": "混合內容", "Movies": "電影", "Music": "音樂", - "MusicVideos": "音樂錄影帶", + "MusicVideos": "MV", "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知季數", @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "最佳化資料庫", "TaskKeyframeExtractorDescription": "將關鍵幀從影片檔案提取出來並建立更精準的HLS播放清單。這可能需要很長時間。", "TaskKeyframeExtractor": "關鍵幀提取器", - "External": "外部" + "External": "外部", + "HearingImpaired": "聽力障礙" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 281dbb00b..b77168126 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -386,6 +386,7 @@ namespace Emby.Server.Implementations.Localization yield return new LocalizationOption("Español (Dominicana)", "es_DO"); yield return new LocalizationOption("Español (México)", "es-MX"); yield return new LocalizationOption("Eesti", "et"); + yield return new LocalizationOption("Basque", "eu"); yield return new LocalizationOption("فارسی", "fa"); yield return new LocalizationOption("Suomi", "fi"); yield return new LocalizationOption("Filipino", "fil"); @@ -433,8 +434,8 @@ namespace Emby.Server.Implementations.Localization yield return new LocalizationOption("Українська", "uk"); yield return new LocalizationOption("اُردُو", "ur_PK"); yield return new LocalizationOption("Tiếng Việt", "vi"); - yield return new LocalizationOption("汉语 (简化字)", "zh-CN"); - yield return new LocalizationOption("漢語 (繁体字)", "zh-TW"); + yield return new LocalizationOption("汉语 (简体字)", "zh-CN"); + yield return new LocalizationOption("漢語 (繁體字)", "zh-TW"); yield return new LocalizationOption("廣東話 (香港)", "zh-HK"); } } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index ec4e0dbeb..3f7d46822 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -715,6 +715,7 @@ namespace Emby.Server.Implementations.Plugins { // This value is memory only - so that the web will show restart required. plugin.Manifest.Status = PluginStatus.Restart; + plugin.Manifest.AutoUpdate = false; return; } @@ -729,6 +730,7 @@ namespace Emby.Server.Implementations.Plugins // This value is memory only - so that the web will show restart required. plugin.Manifest.Status = PluginStatus.Restart; + plugin.Manifest.AutoUpdate = false; } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs index 98e45fa46..1efacd856 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks { private readonly ILogger<OptimizeDatabaseTask> _logger; private readonly ILocalizationManager _localization; - private readonly JellyfinDbProvider _provider; + private readonly IDbContextFactory<JellyfinDb> _provider; /// <summary> /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class. @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public OptimizeDatabaseTask( ILogger<OptimizeDatabaseTask> logger, ILocalizationManager localization, - JellyfinDbProvider provider) + IDbContextFactory<JellyfinDb> provider) { _logger = logger; _localization = localization; @@ -70,30 +70,31 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } /// <inheritdoc /> - public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { _logger.LogInformation("Optimizing and vacuuming jellyfin.db..."); try { - using var context = _provider.CreateContext(); - if (context.Database.IsSqlite()) + var context = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) { - context.Database.ExecuteSqlRaw("PRAGMA optimize"); - context.Database.ExecuteSqlRaw("VACUUM"); - _logger.LogInformation("jellyfin.db optimized successfully!"); - } - else - { - _logger.LogInformation("This database doesn't support optimization"); + if (context.Database.IsSqlite()) + { + await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false); + _logger.LogInformation("jellyfin.db optimized successfully!"); + } + else + { + _logger.LogInformation("This database doesn't support optimization"); + } } } catch (Exception e) { _logger.LogError(e, "Error while optimizing jellyfin.db"); } - - return Task.CompletedTask; } } } diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 6005896ad..5c9b9df15 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -192,7 +192,6 @@ namespace Emby.Server.Implementations.TV AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, IncludeItemTypes = new[] { BaseItemKind.Episode }, - OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }, IsPlayed = true, Limit = 1, ParentIndexNumberNotEquals = 0, @@ -203,11 +202,10 @@ namespace Emby.Server.Implementations.TV } }; - if (rewatching) - { - // find last watched by date played, not by newest episode watched - lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }; - } + // If rewatching is enabled, sort first by date played and then by season and episode numbers + lastQuery.OrderBy = rewatching + ? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) } + : new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }; var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault(); @@ -226,18 +224,16 @@ namespace Emby.Server.Implementations.TV DtoOptions = dtoOptions }; - Episode nextEpisode; - if (rewatching) - { - nextQuery.Limit = 2; - // get watched episode after most recently watched - nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1); - } - else + // Locate the next up episode based on the last watched episode's season and episode number + var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber; + var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber; + if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue) { - nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault(); + nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1); } + var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault(); + if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons) { var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 80ae5abcb..3ee5b8d73 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -87,9 +87,9 @@ namespace Jellyfin.Api.Controllers /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param> /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param> /// <param name="hasOverview">Optional filter by items that have an overview or not.</param> - /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> - /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> - /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> + /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param> + /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param> + /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param> /// <param name="isMovie">Optional filter for live tv movies.</param> /// <param name="isSeries">Optional filter for live tv series.</param> /// <param name="isNews">Optional filter for live tv news.</param> @@ -100,7 +100,7 @@ namespace Jellyfin.Api.Controllers /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param> /// <param name="searchTerm">Optional. Filter based on a search term.</param> - /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> + /// <param name="sortOrder">Sort Order - Ascending, Descending.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> @@ -282,39 +282,13 @@ namespace Jellyfin.Api.Controllers includeItemTypes = new[] { BaseItemKind.Playlist }; } - var enabledChannels = isApiKey - ? Array.Empty<Guid>() - : user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels); - - // api keys are always enabled for all folders - bool isInEnabledFolder = isApiKey - || Array.IndexOf(user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1 - // Assume all folders inside an EnabledChannel are enabled - || Array.IndexOf(enabledChannels, item.Id) != -1 - // Assume all items inside an EnabledChannel are enabled - || Array.IndexOf(enabledChannels, item.ChannelId) != -1; - - if (!isInEnabledFolder) - { - var collectionFolders = _libraryManager.GetCollectionFolders(item); - foreach (var collectionFolder in collectionFolders) - { - // api keys never enter this block, so user is never null - if (user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id)) - { - isInEnabledFolder = true; - } - } - } - - // api keys are always enabled for all folders, so user is never null if (item is not UserRootFolder - && !isInEnabledFolder - && !user!.HasPermission(PermissionKind.EnableAllFolders) - && !user.HasPermission(PermissionKind.EnableAllChannels) - && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase)) + // api keys can always access all folders + && !isApiKey + // check the item is visible for the user + && !item.IsVisible(user)) { - _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user.Username, item.Name); + _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name); return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); } @@ -562,9 +536,9 @@ namespace Jellyfin.Api.Controllers /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param> /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param> /// <param name="hasOverview">Optional filter by items that have an overview or not.</param> - /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> - /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> - /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> + /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param> + /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param> + /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param> /// <param name="isMovie">Optional filter for live tv movies.</param> /// <param name="isSeries">Optional filter for live tv series.</param> /// <param name="isNews">Optional filter for live tv news.</param> @@ -575,7 +549,7 @@ namespace Jellyfin.Api.Controllers /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param> /// <param name="searchTerm">Optional. Filter based on a search term.</param> - /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> + /// <param name="sortOrder">Sort Order - Ascending, Descending.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index e9492a6a4..7a57bf1a2 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -485,7 +485,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Media folders returned.</response> /// <returns>List of user media folders.</returns> [HttpGet("Library/MediaFolders")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden) { diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 8195fc760..03f864b4a 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers new InternalItemsQuery(user) { Person = name, - // Account for duplicates by imdb id, since the database doesn't support this yet + // Account for duplicates by IMDb id, since the database doesn't support this yet Limit = itemLimit + 2, PersonTypes = new[] { PersonType.Director }, IncludeItemTypes = itemTypes.ToArray(), @@ -232,15 +232,15 @@ namespace Jellyfin.Api.Controllers foreach (var name in names) { var items = _libraryManager.GetItemList(new InternalItemsQuery(user) - { - Person = name, - // Account for duplicates by imdb id, since the database doesn't support this yet - Limit = itemLimit + 2, - IncludeItemTypes = itemTypes.ToArray(), - IsMovie = true, - EnableGroupByMetadataKey = true, - DtoOptions = dtoOptions - }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) + { + Person = name, + // Account for duplicates by IMDb id, since the database doesn't support this yet + Limit = itemLimit + 2, + IncludeItemTypes = itemTypes.ToArray(), + IsMovie = true, + EnableGroupByMetadataKey = true, + DtoOptions = dtoOptions + }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) .Select(x => x.First()) .Take(itemLimit) .ToList(); diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index b296d1c96..53a839e43 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -55,9 +55,9 @@ namespace Jellyfin.Api.Controllers /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param> /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param> /// <param name="hasOverview">Optional filter by items that have an overview or not.</param> - /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> - /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> - /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> + /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param> + /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param> + /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param> /// <param name="isMovie">Optional filter for live tv movies.</param> /// <param name="isSeries">Optional filter for live tv series.</param> /// <param name="isNews">Optional filter for live tv news.</param> @@ -68,7 +68,7 @@ namespace Jellyfin.Api.Controllers /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param> /// <param name="searchTerm">Optional. Filter based on a search term.</param> - /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> + /// <param name="sortOrder">Sort Order - Ascending, Descending.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 595c627f8..a4502b612 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -17,7 +17,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.10" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.11" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" /> diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 592c53fe5..9d6ca6aab 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Activity /// </summary> public class ActivityManager : IActivityManager { - private readonly JellyfinDbProvider _provider; + private readonly IDbContextFactory<JellyfinDb> _provider; /// <summary> /// Initializes a new instance of the <see cref="ActivityManager"/> class. /// </summary> /// <param name="provider">The Jellyfin database provider.</param> - public ActivityManager(JellyfinDbProvider provider) + public ActivityManager(IDbContextFactory<JellyfinDb> provider) { _provider = provider; } @@ -32,10 +32,12 @@ namespace Jellyfin.Server.Implementations.Activity /// <inheritdoc/> public async Task CreateAsync(ActivityLog entry) { - await using var dbContext = _provider.CreateContext(); - - dbContext.ActivityLogs.Add(entry); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + dbContext.ActivityLogs.Add(entry); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry))); } @@ -43,44 +45,47 @@ namespace Jellyfin.Server.Implementations.Activity /// <inheritdoc/> public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query) { - await using var dbContext = _provider.CreateContext(); + var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + IQueryable<ActivityLog> entries = dbContext.ActivityLogs + .OrderByDescending(entry => entry.DateCreated); - IQueryable<ActivityLog> entries = dbContext.ActivityLogs - .AsQueryable() - .OrderByDescending(entry => entry.DateCreated); + if (query.MinDate.HasValue) + { + entries = entries.Where(entry => entry.DateCreated >= query.MinDate); + } - if (query.MinDate.HasValue) - { - entries = entries.Where(entry => entry.DateCreated >= query.MinDate); - } + if (query.HasUserId.HasValue) + { + entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value); + } - if (query.HasUserId.HasValue) - { - entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value); + return new QueryResult<ActivityLogEntry>( + query.Skip, + await entries.CountAsync().ConfigureAwait(false), + await entries + .Skip(query.Skip ?? 0) + .Take(query.Limit ?? 100) + .AsAsyncEnumerable() + .Select(ConvertToOldModel) + .ToListAsync() + .ConfigureAwait(false)); } - - return new QueryResult<ActivityLogEntry>( - query.Skip, - await entries.CountAsync().ConfigureAwait(false), - await entries - .Skip(query.Skip ?? 0) - .Take(query.Limit ?? 100) - .AsAsyncEnumerable() - .Select(ConvertToOldModel) - .ToListAsync() - .ConfigureAwait(false)); } /// <inheritdoc /> public async Task CleanAsync(DateTime startDate) { - await using var dbContext = _provider.CreateContext(); - var entries = dbContext.ActivityLogs - .AsQueryable() - .Where(entry => entry.DateCreated <= startDate); + var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var entries = dbContext.ActivityLogs + .Where(entry => entry.DateCreated <= startDate); - dbContext.RemoveRange(entries); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + dbContext.RemoveRange(entries); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 0728f1179..eeb958c62 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; @@ -22,7 +23,7 @@ namespace Jellyfin.Server.Implementations.Devices /// </summary> public class DeviceManager : IDeviceManager { - private readonly JellyfinDbProvider _dbProvider; + private readonly IDbContextFactory<JellyfinDb> _dbProvider; private readonly IUserManager _userManager; private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new(); @@ -31,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Devices /// </summary> /// <param name="dbProvider">The database provider.</param> /// <param name="userManager">The user manager.</param> - public DeviceManager(JellyfinDbProvider dbProvider, IUserManager userManager) + public DeviceManager(IDbContextFactory<JellyfinDb> dbProvider, IUserManager userManager) { _dbProvider = dbProvider; _userManager = userManager; @@ -49,39 +50,50 @@ namespace Jellyfin.Server.Implementations.Devices /// <inheritdoc /> public async Task UpdateDeviceOptions(string deviceId, string deviceName) { - await using var dbContext = _dbProvider.CreateContext(); - var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false); - if (deviceOptions == null) + DeviceOptions? deviceOptions; + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - deviceOptions = new DeviceOptions(deviceId); - dbContext.DeviceOptions.Add(deviceOptions); + deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false); + if (deviceOptions == null) + { + deviceOptions = new DeviceOptions(deviceId); + dbContext.DeviceOptions.Add(deviceOptions); + } + + deviceOptions.CustomName = deviceName; + await dbContext.SaveChangesAsync().ConfigureAwait(false); } - deviceOptions.CustomName = deviceName; - await dbContext.SaveChangesAsync().ConfigureAwait(false); - DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions))); } /// <inheritdoc /> public async Task<Device> CreateDevice(Device device) { - await using var dbContext = _dbProvider.CreateContext(); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + dbContext.Devices.Add(device); - dbContext.Devices.Add(device); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } - await dbContext.SaveChangesAsync().ConfigureAwait(false); return device; } /// <inheritdoc /> public async Task<DeviceOptions> GetDeviceOptions(string deviceId) { - await using var dbContext = _dbProvider.CreateContext(); - var deviceOptions = await dbContext.DeviceOptions - .AsQueryable() - .FirstOrDefaultAsync(d => d.DeviceId == deviceId) - .ConfigureAwait(false); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + DeviceOptions? deviceOptions; + await using (dbContext.ConfigureAwait(false)) + { + deviceOptions = await dbContext.DeviceOptions + .AsNoTracking() + .FirstOrDefaultAsync(d => d.DeviceId == deviceId) + .ConfigureAwait(false); + } return deviceOptions ?? new DeviceOptions(deviceId); } @@ -97,14 +109,17 @@ namespace Jellyfin.Server.Implementations.Devices /// <inheritdoc /> public async Task<DeviceInfo?> GetDevice(string id) { - await using var dbContext = _dbProvider.CreateContext(); - var device = await dbContext.Devices - .AsQueryable() - .Where(d => d.DeviceId == id) - .OrderByDescending(d => d.DateLastActivity) - .Include(d => d.User) - .FirstOrDefaultAsync() - .ConfigureAwait(false); + Device? device; + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + device = await dbContext.Devices + .Where(d => d.DeviceId == id) + .OrderByDescending(d => d.DateLastActivity) + .Include(d => d.User) + .FirstOrDefaultAsync() + .ConfigureAwait(false); + } var deviceInfo = device == null ? null : ToDeviceInfo(device); @@ -114,41 +129,40 @@ namespace Jellyfin.Server.Implementations.Devices /// <inheritdoc /> public async Task<QueryResult<Device>> GetDevices(DeviceQuery query) { - await using var dbContext = _dbProvider.CreateContext(); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var devices = dbContext.Devices.AsQueryable(); - var devices = dbContext.Devices.AsQueryable(); + if (query.UserId.HasValue) + { + devices = devices.Where(device => device.UserId.Equals(query.UserId.Value)); + } - if (query.UserId.HasValue) - { - devices = devices.Where(device => device.UserId.Equals(query.UserId.Value)); - } + if (query.DeviceId != null) + { + devices = devices.Where(device => device.DeviceId == query.DeviceId); + } - if (query.DeviceId != null) - { - devices = devices.Where(device => device.DeviceId == query.DeviceId); - } + if (query.AccessToken != null) + { + devices = devices.Where(device => device.AccessToken == query.AccessToken); + } - if (query.AccessToken != null) - { - devices = devices.Where(device => device.AccessToken == query.AccessToken); - } + var count = await devices.CountAsync().ConfigureAwait(false); - var count = await devices.CountAsync().ConfigureAwait(false); + if (query.Skip.HasValue) + { + devices = devices.Skip(query.Skip.Value); + } - if (query.Skip.HasValue) - { - devices = devices.Skip(query.Skip.Value); - } + if (query.Limit.HasValue) + { + devices = devices.Take(query.Limit.Value); + } - if (query.Limit.HasValue) - { - devices = devices.Take(query.Limit.Value); + return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false)); } - - return new QueryResult<Device>( - query.Skip, - count, - await devices.ToListAsync().ConfigureAwait(false)); } /// <inheritdoc /> @@ -165,37 +179,43 @@ namespace Jellyfin.Server.Implementations.Devices /// <inheritdoc /> public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync) { - await using var dbContext = _dbProvider.CreateContext(); - var sessions = dbContext.Devices - .Include(d => d.User) - .AsQueryable() - .OrderByDescending(d => d.DateLastActivity) - .ThenBy(d => d.DeviceId) - .AsAsyncEnumerable(); - - if (supportsSync.HasValue) + IAsyncEnumerable<Device> sessions; + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value); - } + sessions = dbContext.Devices + .Include(d => d.User) + .OrderByDescending(d => d.DateLastActivity) + .ThenBy(d => d.DeviceId) + .AsAsyncEnumerable(); - if (userId.HasValue) - { - var user = _userManager.GetUserById(userId.Value); + if (supportsSync.HasValue) + { + sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value); + } - sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); - } + if (userId.HasValue) + { + var user = _userManager.GetUserById(userId.Value); + + sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); + } - var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false); + var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false); - return new QueryResult<DeviceInfo>(array); + return new QueryResult<DeviceInfo>(array); + } } /// <inheritdoc /> public async Task DeleteDevice(Device device) { - await using var dbContext = _dbProvider.CreateContext(); - dbContext.Devices.Remove(device); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + dbContext.Devices.Remove(device); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } /// <inheritdoc /> diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..f98a0aede --- /dev/null +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using EFCoreSecondLevelCacheInterceptor; +using MediaBrowser.Common.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Extensions; + +/// <summary> +/// Extensions for the <see cref="IServiceCollection"/> interface. +/// </summary> +public static class ServiceCollectionExtensions +{ + /// <summary> + /// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled. + /// </summary> + /// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param> + /// <returns>The updated service collection.</returns> + public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection) + { + serviceCollection.AddEFSecondLevelCache(options => + options.UseMemoryCacheProvider() + .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10)) + .DisableLogging(true) + .UseCacheKeyPrefix("EF_") + // Don't cache null values. Remove this optional setting if it's not necessary. + .SkipCachingResults(result => + result.Value == null || (result.Value is EFTableRows rows && rows.RowsCount == 0))); + + serviceCollection.AddPooledDbContextFactory<JellyfinDb>((serviceProvider, opt) => + { + var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>(); + var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>(); + opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}") + .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>()) + .UseLoggerFactory(loggerFactory); + }); + + return serviceCollection; + } +} diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index e1f902efc..5caac4523 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -26,14 +26,15 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.7.3" /> <PackageReference Include="System.Linq.Async" Version="6.0.1" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.10" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.10" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.10"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.11" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.11" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.11"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.10"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.11"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs deleted file mode 100644 index c2c5198d1..000000000 --- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using MediaBrowser.Common.Configuration; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Server.Implementations -{ - /// <summary> - /// Factory class for generating new <see cref="JellyfinDb"/> instances. - /// </summary> - public class JellyfinDbProvider - { - private readonly IServiceProvider _serviceProvider; - private readonly IApplicationPaths _appPaths; - private readonly ILogger<JellyfinDbProvider> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class. - /// </summary> - /// <param name="serviceProvider">The application's service provider.</param> - /// <param name="appPaths">The application paths.</param> - /// <param name="logger">The logger.</param> - public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths, ILogger<JellyfinDbProvider> logger) - { - _serviceProvider = serviceProvider; - _appPaths = appPaths; - _logger = logger; - - using var jellyfinDb = CreateContext(); - if (jellyfinDb.Database.GetPendingMigrations().Any()) - { - _logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)"); - jellyfinDb.Database.Migrate(); - _logger.LogInformation("EFCore migrations applied successfully"); - } - } - - /// <summary> - /// Creates a new <see cref="JellyfinDb"/> context. - /// </summary> - /// <returns>The newly created context.</returns> - public JellyfinDb CreateContext() - { - var contextOptions = new DbContextOptionsBuilder<JellyfinDb>().UseSqlite($"Filename={Path.Combine(_appPaths.DataPath, "jellyfin.db")}"); - return ActivatorUtilities.CreateInstance<JellyfinDb>(_serviceProvider, contextOptions.Options); - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs new file mode 100644 index 000000000..03e3f3c92 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs @@ -0,0 +1,657 @@ +#pragma warning disable CS1591 + +// <auto-generated /> +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20221022080052_AddIndexActivityLogsDateCreated")] + partial class AddIndexActivityLogsDateCreated + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "6.0.9"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property<double>("EndHour") + .HasColumnType("REAL"); + + b.Property<double>("StartHour") + .HasColumnType("REAL"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<string>("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<bool>("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property<string>("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property<int>("Order") + .HasColumnType("INTEGER"); + + b.Property<int>("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("LastModified") + .HasColumnType("TEXT"); + + b.Property<string>("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<bool>("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property<string>("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<int>("SortOrder") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<int>("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<bool>("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<string>("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<string>("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateModified") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<string>("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<bool>("IsActive") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("CustomName") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property<string>("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<string>("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property<bool>("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property<string>("EasyPassword") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property<bool>("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property<bool>("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property<long>("InternalId") + .HasColumnType("INTEGER"); + + b.Property<int>("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property<int?>("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property<int>("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property<int?>("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property<bool>("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property<string>("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property<string>("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property<int?>("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<int>("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property<int>("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property<string>("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "jellyfin"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs new file mode 100644 index 000000000..f09ad2709 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591, SA1601 + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddIndexActivityLogsDateCreated : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_ActivityLogs_DateCreated", + schema: "jellyfin", + table: "ActivityLogs", + column: "DateCreated"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_ActivityLogs_DateCreated", + schema: "jellyfin", + table: "ActivityLogs"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index fcc360e26..2dd7b094a 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +#nullable disable + namespace Jellyfin.Server.Implementations.Migrations { [DbContext(typeof(JellyfinDb))] @@ -15,7 +17,7 @@ namespace Jellyfin.Server.Implementations.Migrations #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "5.0.7"); + .HasAnnotation("ProductVersion", "6.0.9"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -39,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId"); - b.ToTable("AccessSchedules"); + b.ToTable("AccessSchedules", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => @@ -85,7 +87,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); - b.ToTable("ActivityLogs"); + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => @@ -117,7 +121,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "ItemId", "Client", "Key") .IsUnique(); - b.ToTable("CustomItemDisplayPreferences"); + b.ToTable("CustomItemDisplayPreferences", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => @@ -174,7 +178,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "ItemId", "Client") .IsUnique(); - b.ToTable("DisplayPreferences"); + b.ToTable("DisplayPreferences", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => @@ -196,7 +200,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("DisplayPreferencesId"); - b.ToTable("HomeSection"); + b.ToTable("HomeSection", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => @@ -221,7 +225,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId") .IsUnique(); - b.ToTable("ImageInfos"); + b.ToTable("ImageInfos", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => @@ -265,7 +269,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId"); - b.ToTable("ItemDisplayPreferences"); + b.ToTable("ItemDisplayPreferences", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => @@ -296,7 +300,7 @@ namespace Jellyfin.Server.Implementations.Migrations .IsUnique() .HasFilter("[UserId] IS NOT NULL"); - b.ToTable("Permissions"); + b.ToTable("Permissions", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => @@ -329,7 +333,7 @@ namespace Jellyfin.Server.Implementations.Migrations .IsUnique() .HasFilter("[UserId] IS NOT NULL"); - b.ToTable("Preferences"); + b.ToTable("Preferences", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => @@ -358,7 +362,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("AccessToken") .IsUnique(); - b.ToTable("ApiKeys"); + b.ToTable("ApiKeys", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => @@ -416,7 +420,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("UserId", "DeviceId"); - b.ToTable("Devices"); + b.ToTable("Devices", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => @@ -437,7 +441,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("DeviceId") .IsUnique(); - b.ToTable("DeviceOptions"); + b.ToTable("DeviceOptions", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.User", b => @@ -550,7 +554,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasIndex("Username") .IsUnique(); - b.ToTable("Users"); + b.ToTable("Users", "jellyfin"); }); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs new file mode 100644 index 000000000..9a63ed9f2 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs @@ -0,0 +1,17 @@ +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// <summary> +/// FluentAPI configuration for the ActivityLog entity. +/// </summary> +public class ActivityLogConfiguration : IEntityTypeConfiguration<ActivityLog> +{ + /// <inheritdoc/> + public void Configure(EntityTypeBuilder<ActivityLog> builder) + { + builder.HasIndex(entity => entity.DateCreated); + } +} diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs index b79e46469..33c08c8c2 100644 --- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs +++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs @@ -10,13 +10,13 @@ namespace Jellyfin.Server.Implementations.Security /// <inheritdoc /> public class AuthenticationManager : IAuthenticationManager { - private readonly JellyfinDbProvider _dbProvider; + private readonly IDbContextFactory<JellyfinDb> _dbProvider; /// <summary> /// Initializes a new instance of the <see cref="AuthenticationManager"/> class. /// </summary> /// <param name="dbProvider">The database provider.</param> - public AuthenticationManager(JellyfinDbProvider dbProvider) + public AuthenticationManager(IDbContextFactory<JellyfinDb> dbProvider) { _dbProvider = dbProvider; } @@ -24,50 +24,56 @@ namespace Jellyfin.Server.Implementations.Security /// <inheritdoc /> public async Task CreateApiKey(string name) { - await using var dbContext = _dbProvider.CreateContext(); - - dbContext.ApiKeys.Add(new ApiKey(name)); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + dbContext.ApiKeys.Add(new ApiKey(name)); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } /// <inheritdoc /> public async Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys() { - await using var dbContext = _dbProvider.CreateContext(); - - return await dbContext.ApiKeys - .AsAsyncEnumerable() - .Select(key => new AuthenticationInfo - { - AppName = key.Name, - AccessToken = key.AccessToken, - DateCreated = key.DateCreated, - DeviceId = string.Empty, - DeviceName = string.Empty, - AppVersion = string.Empty - }).ToListAsync().ConfigureAwait(false); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + return await dbContext.ApiKeys + .AsAsyncEnumerable() + .Select(key => new AuthenticationInfo + { + AppName = key.Name, + AccessToken = key.AccessToken, + DateCreated = key.DateCreated, + DeviceId = string.Empty, + DeviceName = string.Empty, + AppVersion = string.Empty + }).ToListAsync().ConfigureAwait(false); + } } /// <inheritdoc /> public async Task DeleteApiKey(string accessToken) { - await using var dbContext = _dbProvider.CreateContext(); - - var key = await dbContext.ApiKeys - .AsQueryable() - .Where(apiKey => apiKey.AccessToken == accessToken) - .FirstOrDefaultAsync() - .ConfigureAwait(false); - - if (key == null) + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - return; - } + var key = await dbContext.ApiKeys + .AsQueryable() + .Where(apiKey => apiKey.AccessToken == accessToken) + .FirstOrDefaultAsync() + .ConfigureAwait(false); - dbContext.Remove(key); + if (key == null) + { + return; + } + + dbContext.Remove(key); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } } } diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 9f813f532..4d1a1b3cf 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; +using EFCoreSecondLevelCacheInterceptor; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; @@ -15,12 +16,12 @@ namespace Jellyfin.Server.Implementations.Security { public class AuthorizationContext : IAuthorizationContext { - private readonly JellyfinDbProvider _jellyfinDbProvider; + private readonly IDbContextFactory<JellyfinDb> _jellyfinDbProvider; private readonly IUserManager _userManager; private readonly IServerApplicationHost _serverApplicationHost; public AuthorizationContext( - JellyfinDbProvider jellyfinDb, + IDbContextFactory<JellyfinDb> jellyfinDb, IUserManager userManager, IServerApplicationHost serverApplicationHost) { @@ -121,96 +122,99 @@ namespace Jellyfin.Server.Implementations.Security #pragma warning restore CA1508 authInfo.HasToken = true; - await using var dbContext = _jellyfinDbProvider.CreateContext(); - var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false); - - if (device != null) + var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - authInfo.IsAuthenticated = true; - var updateToken = false; - - // TODO: Remove these checks for IsNullOrWhiteSpace - if (string.IsNullOrWhiteSpace(authInfo.Client)) - { - authInfo.Client = device.AppName; - } + var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) + if (device != null) { - authInfo.DeviceId = device.DeviceId; - } - - // Temporary. TODO - allow clients to specify that the token has been shared with a casting device - var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase); + authInfo.IsAuthenticated = true; + var updateToken = false; - if (string.IsNullOrWhiteSpace(authInfo.Device)) - { - authInfo.Device = device.DeviceName; - } - else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase)) - { - if (allowTokenInfoUpdate) + // TODO: Remove these checks for IsNullOrWhiteSpace + if (string.IsNullOrWhiteSpace(authInfo.Client)) { - updateToken = true; - device.DeviceName = authInfo.Device; + authInfo.Client = device.AppName; } - } - if (string.IsNullOrWhiteSpace(authInfo.Version)) - { - authInfo.Version = device.AppVersion; - } - else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase)) - { - if (allowTokenInfoUpdate) + if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) { - updateToken = true; - device.AppVersion = authInfo.Version; + authInfo.DeviceId = device.DeviceId; } - } - if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3) - { - device.DateLastActivity = DateTime.UtcNow; - updateToken = true; - } + // Temporary. TODO - allow clients to specify that the token has been shared with a casting device + var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase); - authInfo.User = _userManager.GetUserById(device.UserId); + if (string.IsNullOrWhiteSpace(authInfo.Device)) + { + authInfo.Device = device.DeviceName; + } + else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase)) + { + if (allowTokenInfoUpdate) + { + updateToken = true; + device.DeviceName = authInfo.Device; + } + } - if (updateToken) - { - dbContext.Devices.Update(device); - await dbContext.SaveChangesAsync().ConfigureAwait(false); - } - } - else - { - var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false); - if (key != null) - { - authInfo.IsAuthenticated = true; - authInfo.Client = key.Name; - authInfo.Token = key.AccessToken; - if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) + if (string.IsNullOrWhiteSpace(authInfo.Version)) { - authInfo.DeviceId = _serverApplicationHost.SystemId; + authInfo.Version = device.AppVersion; + } + else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase)) + { + if (allowTokenInfoUpdate) + { + updateToken = true; + device.AppVersion = authInfo.Version; + } } - if (string.IsNullOrWhiteSpace(authInfo.Device)) + if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3) { - authInfo.Device = _serverApplicationHost.Name; + device.DateLastActivity = DateTime.UtcNow; + updateToken = true; } - if (string.IsNullOrWhiteSpace(authInfo.Version)) + authInfo.User = _userManager.GetUserById(device.UserId); + + if (updateToken) { - authInfo.Version = _serverApplicationHost.ApplicationVersionString; + dbContext.Devices.Update(device); + await dbContext.SaveChangesAsync().ConfigureAwait(false); } + } + else + { + var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false); + if (key != null) + { + authInfo.IsAuthenticated = true; + authInfo.Client = key.Name; + authInfo.Token = key.AccessToken; + if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) + { + authInfo.DeviceId = _serverApplicationHost.SystemId; + } + + if (string.IsNullOrWhiteSpace(authInfo.Device)) + { + authInfo.Device = _serverApplicationHost.Name; + } + + if (string.IsNullOrWhiteSpace(authInfo.Version)) + { + authInfo.Version = _serverApplicationHost.ApplicationVersionString; + } - authInfo.IsApiKey = true; + authInfo.IsApiKey = true; + } } - } - return authInfo; + return authInfo; + } } /// <summary> diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index 65edb30ad..87babc05c 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -20,10 +20,10 @@ namespace Jellyfin.Server.Implementations.Users /// <summary> /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class. /// </summary> - /// <param name="dbContext">The database context.</param> - public DisplayPreferencesManager(JellyfinDb dbContext) + /// <param name="dbContextFactory">The database context factory.</param> + public DisplayPreferencesManager(IDbContextFactory<JellyfinDb> dbContextFactory) { - _dbContext = dbContext; + _dbContext = dbContextFactory.CreateDbContext(); } /// <inheritdoc /> diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index ed90e81c6..25560707a 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Users /// </summary> public class UserManager : IUserManager { - private readonly JellyfinDbProvider _dbProvider; + private readonly IDbContextFactory<JellyfinDb> _dbProvider; private readonly IEventManager _eventManager; private readonly ICryptoProvider _cryptoProvider; private readonly INetworkManager _networkManager; @@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users /// <param name="imageProcessor">The image processor.</param> /// <param name="logger">The logger.</param> public UserManager( - JellyfinDbProvider dbProvider, + IDbContextFactory<JellyfinDb> dbProvider, IEventManager eventManager, ICryptoProvider cryptoProvider, INetworkManager networkManager, @@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Users _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); _users = new ConcurrentDictionary<Guid, User>(); - using var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateDbContext(); foreach (var user in dbContext.Users .Include(user => user.Permissions) .Include(user => user.Preferences) @@ -139,31 +139,35 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("The new and old names must be different."); } - await using var dbContext = _dbProvider.CreateContext(); - - if (await dbContext.Users - .AsQueryable() - .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id)) - .ConfigureAwait(false)) + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - throw new ArgumentException(string.Format( - CultureInfo.InvariantCulture, - "A user with the name '{0}' already exists.", - newName)); + if (await dbContext.Users + .AsQueryable() + .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id)) + .ConfigureAwait(false)) + { + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "A user with the name '{0}' already exists.", + newName)); + } + + user.Username = newName; + await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false); } - user.Username = newName; - await UpdateUserAsync(user).ConfigureAwait(false); OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user)); } /// <inheritdoc/> public async Task UpdateUserAsync(User user) { - await using var dbContext = _dbProvider.CreateContext(); - dbContext.Users.Update(user); - _users[user.Id] = user; - await dbContext.SaveChangesAsync().ConfigureAwait(false); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false); + } } internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext) @@ -202,12 +206,15 @@ namespace Jellyfin.Server.Implementations.Users name)); } - await using var dbContext = _dbProvider.CreateContext(); - - var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false); + User newUser; + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false); - dbContext.Users.Add(newUser); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + dbContext.Users.Add(newUser); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false); @@ -241,9 +248,13 @@ namespace Jellyfin.Server.Implementations.Users nameof(userId)); } - await using var dbContext = _dbProvider.CreateContext(); - dbContext.Users.Remove(user); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + dbContext.Users.Remove(user); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + _users.Remove(userId); await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false); @@ -288,7 +299,7 @@ namespace Jellyfin.Server.Implementations.Users user.EasyPassword = newPasswordSha1; await UpdateUserAsync(user).ConfigureAwait(false); - _eventManager.Publish(new UserPasswordChangedEventArgs(user)); + await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false); } /// <inheritdoc/> @@ -541,14 +552,17 @@ namespace Jellyfin.Server.Implementations.Users _logger.LogWarning("No users, creating one with username {UserName}", defaultName); - await using var dbContext = _dbProvider.CreateContext(); - var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); - newUser.SetPermission(PermissionKind.IsAdministrator, true); - newUser.SetPermission(PermissionKind.EnableContentDeletion, true); - newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); + newUser.SetPermission(PermissionKind.IsAdministrator, true); + newUser.SetPermission(PermissionKind.EnableContentDeletion, true); + newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true); - dbContext.Users.Add(newUser); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + dbContext.Users.Add(newUser); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } /// <inheritdoc/> @@ -584,105 +598,111 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config) { - await using var dbContext = _dbProvider.CreateContext(); - var user = dbContext.Users - .Include(u => u.Permissions) - .Include(u => u.Preferences) - .Include(u => u.AccessSchedules) - .Include(u => u.ProfileImage) - .FirstOrDefault(u => u.Id.Equals(userId)) - ?? throw new ArgumentException("No user exists with given Id!"); - - user.SubtitleMode = config.SubtitleMode; - user.HidePlayedInLatest = config.HidePlayedInLatest; - user.EnableLocalPassword = config.EnableLocalPassword; - user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack; - user.DisplayCollectionsView = config.DisplayCollectionsView; - user.DisplayMissingEpisodes = config.DisplayMissingEpisodes; - user.AudioLanguagePreference = config.AudioLanguagePreference; - user.RememberAudioSelections = config.RememberAudioSelections; - user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; - user.RememberSubtitleSelections = config.RememberSubtitleSelections; - user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; - - user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); - user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); - user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); - user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); - - dbContext.Update(user); - _users[user.Id] = user; - await dbContext.SaveChangesAsync().ConfigureAwait(false); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var user = dbContext.Users + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstOrDefault(u => u.Id.Equals(userId)) + ?? throw new ArgumentException("No user exists with given Id!"); + + user.SubtitleMode = config.SubtitleMode; + user.HidePlayedInLatest = config.HidePlayedInLatest; + user.EnableLocalPassword = config.EnableLocalPassword; + user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack; + user.DisplayCollectionsView = config.DisplayCollectionsView; + user.DisplayMissingEpisodes = config.DisplayMissingEpisodes; + user.AudioLanguagePreference = config.AudioLanguagePreference; + user.RememberAudioSelections = config.RememberAudioSelections; + user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay; + user.RememberSubtitleSelections = config.RememberSubtitleSelections; + user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; + + user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); + user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); + user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); + user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); + + dbContext.Update(user); + _users[user.Id] = user; + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } /// <inheritdoc/> public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy) { - await using var dbContext = _dbProvider.CreateContext(); - var user = dbContext.Users - .Include(u => u.Permissions) - .Include(u => u.Preferences) - .Include(u => u.AccessSchedules) - .Include(u => u.ProfileImage) - .FirstOrDefault(u => u.Id.Equals(userId)) - ?? throw new ArgumentException("No user exists with given Id!"); - - // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" - int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch - { - -1 => null, - 0 => 3, - _ => policy.LoginAttemptsBeforeLockout - }; + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var user = dbContext.Users + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstOrDefault(u => u.Id.Equals(userId)) + ?? throw new ArgumentException("No user exists with given Id!"); + + // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" + int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch + { + -1 => null, + 0 => 3, + _ => policy.LoginAttemptsBeforeLockout + }; - user.MaxParentalAgeRating = policy.MaxParentalRating; - user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; - user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; - user.AuthenticationProviderId = policy.AuthenticationProviderId; - user.PasswordResetProviderId = policy.PasswordResetProviderId; - user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; - user.LoginAttemptsBeforeLockout = maxLoginAttempts; - user.MaxActiveSessions = policy.MaxActiveSessions; - user.SyncPlayAccess = policy.SyncPlayAccess; - user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); - user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); - user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); - user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); - user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); - user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); - user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); - user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); - user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); - user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); - user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); - user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); - user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); - user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); - user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); - user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); - user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); - user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); - user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); - user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); - user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); - - user.AccessSchedules.Clear(); - foreach (var policyAccessSchedule in policy.AccessSchedules) - { - user.AccessSchedules.Add(policyAccessSchedule); - } - - // TODO: fix this at some point - user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>()); - user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); - user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); - user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); - user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); - user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); - - dbContext.Update(user); - _users[user.Id] = user; - await dbContext.SaveChangesAsync().ConfigureAwait(false); + user.MaxParentalAgeRating = policy.MaxParentalRating; + user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; + user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; + user.AuthenticationProviderId = policy.AuthenticationProviderId; + user.PasswordResetProviderId = policy.PasswordResetProviderId; + user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount; + user.LoginAttemptsBeforeLockout = maxLoginAttempts; + user.MaxActiveSessions = policy.MaxActiveSessions; + user.SyncPlayAccess = policy.SyncPlayAccess; + user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); + user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); + user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); + user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); + user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); + user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); + user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); + user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); + user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); + user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); + user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); + user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); + user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); + user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); + user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); + user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); + user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); + user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); + user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); + + user.AccessSchedules.Clear(); + foreach (var policyAccessSchedule in policy.AccessSchedules) + { + user.AccessSchedules.Add(policyAccessSchedule); + } + + // TODO: fix this at some point + user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>()); + user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); + user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); + user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); + user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); + user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); + + dbContext.Update(user); + _users[user.Id] = user; + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } /// <inheritdoc/> @@ -693,9 +713,13 @@ namespace Jellyfin.Server.Implementations.Users return; } - await using var dbContext = _dbProvider.CreateContext(); - dbContext.Remove(user.ProfileImage); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + dbContext.Remove(user.ProfileImage); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + user.ProfileImage = null; _users[user.Id] = user; } @@ -859,5 +883,12 @@ namespace Jellyfin.Server.Implementations.Users await UpdateUserAsync(user).ConfigureAwait(false); } + + private async Task UpdateUserInternalAsync(JellyfinDb dbContext, User user) + { + dbContext.Users.Update(user); + _users[user.Id] = user; + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 984711dc2..002193baf 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; @@ -71,19 +70,13 @@ namespace Jellyfin.Server Logger.LogWarning("Skia not available. Will fallback to {ImageEncoder}.", nameof(NullImageEncoder)); } - serviceCollection.AddDbContextPool<JellyfinDb>( - options => options - .UseLoggerFactory(LoggerFactory) - .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}")); - serviceCollection.AddEventServices(); serviceCollection.AddSingleton<IBaseItemManager, BaseItemManager>(); serviceCollection.AddSingleton<IEventManager, EventManager>(); - serviceCollection.AddSingleton<JellyfinDbProvider>(); serviceCollection.AddSingleton<IActivityManager, ActivityManager>(); serviceCollection.AddSingleton<IUserManager, UserManager>(); - serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>(); + serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>(); serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); // TODO search the assemblies instead of adding them manually? diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 66fa3bc31..f74152405 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -434,11 +434,15 @@ namespace Jellyfin.Server.Extensions options.MapType<TranscodeReason>(() => new OpenApiSchema { - Type = "string", - Enum = Enum.GetNames<TranscodeReason>() - .Select(e => new OpenApiString(e)) - .Cast<IOpenApiAny>() - .ToArray() + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = nameof(TranscodeReason), + Type = ReferenceType.Schema, + } + } }); // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it. diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index 487948f81..645696e31 100644 --- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Jellyfin.Extensions; using Jellyfin.Server.Migrations; using MediaBrowser.Common.Plugins; @@ -8,6 +9,7 @@ using MediaBrowser.Model.ApiClient; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -56,6 +58,15 @@ namespace Jellyfin.Server.Filters context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository); } + + context.SchemaRepository.AddDefinition(nameof(TranscodeReason), new OpenApiSchema + { + Type = "string", + Enum = Enum.GetNames<TranscodeReason>() + .Select(e => new OpenApiString(e)) + .Cast<IOpenApiAny>() + .ToArray() + }); } } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index a5f20d671..6d77aa1df 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -37,8 +37,8 @@ <PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.10" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.10" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.11" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.11" /> <PackageReference Include="prometheus-net" Version="6.0.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs index 9875df310..6ee5bf38a 100644 --- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs @@ -65,8 +65,9 @@ namespace Jellyfin.Server.Middleware // Always redirect back to the default path if the base prefix is invalid or missing _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); - var uri = new Uri(localPath); - var redirectUri = new Uri(baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]); + var port = httpContext.Request.Host.Port ?? -1; + var uri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, localPath).Uri; + var redirectUri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]).Uri; var target = uri.MakeRelativeUri(redirectUri).ToString(); _logger.LogDebug("Redirecting to {Target}", target); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index 9e22978ae..bf66f75ff 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines private const string DbFilename = "activitylog.db"; private readonly ILogger<MigrateActivityLogDb> _logger; - private readonly JellyfinDbProvider _provider; + private readonly IDbContextFactory<JellyfinDb> _provider; private readonly IServerApplicationPaths _paths; /// <summary> @@ -28,7 +28,7 @@ namespace Jellyfin.Server.Migrations.Routines /// <param name="logger">The logger.</param> /// <param name="paths">The server application paths.</param> /// <param name="provider">The database provider.</param> - public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider) + public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, IDbContextFactory<JellyfinDb> provider) { _logger = logger; _provider = provider; @@ -68,7 +68,7 @@ namespace Jellyfin.Server.Migrations.Routines { using var userDbConnection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null); _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin."); - using var dbContext = _provider.CreateContext(); + using var dbContext = _provider.CreateDbContext(); var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id"); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index ba0e33585..bf1ea8233 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -6,6 +6,7 @@ using Jellyfin.Data.Entities.Security; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -19,7 +20,7 @@ namespace Jellyfin.Server.Migrations.Routines private const string DbFilename = "authentication.db"; private readonly ILogger<MigrateAuthenticationDb> _logger; - private readonly JellyfinDbProvider _dbProvider; + private readonly IDbContextFactory<JellyfinDb> _dbProvider; private readonly IServerApplicationPaths _appPaths; private readonly IUserManager _userManager; @@ -32,7 +33,7 @@ namespace Jellyfin.Server.Migrations.Routines /// <param name="userManager">The user manager.</param> public MigrateAuthenticationDb( ILogger<MigrateAuthenticationDb> logger, - JellyfinDbProvider dbProvider, + IDbContextFactory<JellyfinDb> dbProvider, IServerApplicationPaths appPaths, IUserManager userManager) { @@ -60,7 +61,7 @@ namespace Jellyfin.Server.Migrations.Routines ConnectionFlags.ReadOnly, null)) { - using var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateDbContext(); var authenticatedDevices = connection.Query("SELECT * FROM Tokens"); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 74f2349f5..37716482c 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -10,6 +10,7 @@ using Jellyfin.Server.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -24,7 +25,7 @@ namespace Jellyfin.Server.Migrations.Routines private readonly ILogger<MigrateDisplayPreferencesDb> _logger; private readonly IServerApplicationPaths _paths; - private readonly JellyfinDbProvider _provider; + private readonly IDbContextFactory<JellyfinDb> _provider; private readonly JsonSerializerOptions _jsonOptions; private readonly IUserManager _userManager; @@ -38,7 +39,7 @@ namespace Jellyfin.Server.Migrations.Routines public MigrateDisplayPreferencesDb( ILogger<MigrateDisplayPreferencesDb> logger, IServerApplicationPaths paths, - JellyfinDbProvider provider, + IDbContextFactory<JellyfinDb> provider, IUserManager userManager) { _logger = logger; @@ -84,7 +85,7 @@ namespace Jellyfin.Server.Migrations.Routines var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null)) { - using var dbContext = _provider.CreateContext(); + using var dbContext = _provider.CreateDbContext(); var results = connection.Query("SELECT * FROM userdisplaypreferences"); foreach (var result in results) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 9b2d603c7..0c2cc69a7 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -11,6 +11,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; using JsonSerializer = System.Text.Json.JsonSerializer; @@ -26,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines private readonly ILogger<MigrateUserDb> _logger; private readonly IServerApplicationPaths _paths; - private readonly JellyfinDbProvider _provider; + private readonly IDbContextFactory<JellyfinDb> _provider; private readonly IXmlSerializer _xmlSerializer; /// <summary> @@ -39,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines public MigrateUserDb( ILogger<MigrateUserDb> logger, IServerApplicationPaths paths, - JellyfinDbProvider provider, + IDbContextFactory<JellyfinDb> provider, IXmlSerializer xmlSerializer) { _logger = logger; @@ -65,7 +66,7 @@ namespace Jellyfin.Server.Migrations.Routines using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null)) { - var dbContext = _provider.CreateContext(); + var dbContext = _provider.CreateDbContext(); var queryResult = connection.Query("SELECT * FROM LocalUsersv2"); diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index a6f0b705d..cb763dfa3 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -192,6 +192,7 @@ namespace Jellyfin.Server // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection. appHost.ServiceProvider = webHost.Services; + await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); @@ -236,10 +237,13 @@ namespace Jellyfin.Server { _logger.LogInformation("Running query planner optimizations in the database... This might take a while"); // Run before disposing the application - using var context = appHost.Resolve<JellyfinDbProvider>().CreateContext(); - if (context.Database.IsSqlite()) + var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false); + await using (context.ConfigureAwait(false)) { - context.Database.ExecuteSqlRaw("PRAGMA optimize"); + if (context.Database.IsSqlite()) + { + await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false); + } } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 1954a5c55..49a57aa68 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -9,6 +9,7 @@ using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Infrastructure; using Jellyfin.Server.Middleware; using MediaBrowser.Common.Net; @@ -65,7 +66,7 @@ namespace Jellyfin.Server // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371 services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>(); services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration()); - + services.AddJellyfinDbContext(); services.AddJellyfinApiSwagger(); // configure custom legacy authentication diff --git a/MediaBrowser.Common/Providers/ProviderIdParsers.cs b/MediaBrowser.Common/Providers/ProviderIdParsers.cs index 487b5a6d2..d569167b1 100644 --- a/MediaBrowser.Common/Providers/ProviderIdParsers.cs +++ b/MediaBrowser.Common/Providers/ProviderIdParsers.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Common.Providers /// <returns>True if parsing was successful, false otherwise.</returns> public static bool TryFindImdbId(ReadOnlySpan<char> text, out ReadOnlySpan<char> imdbId) { - // imdb id is at least 9 chars (tt + 7 numbers) + // IMDb id is at least 9 chars (tt + 7 numbers) while (text.Length >= 2 + ImdbMinNumbers) { var ttPos = text.IndexOf(ImdbPrefix); @@ -42,7 +42,7 @@ namespace MediaBrowser.Common.Providers } } - // skip if more than 8 digits + 2 chars for tt + // Skip if more than 8 digits + 2 chars for tt if (i <= ImdbMaxNumbers + 2 && i >= ImdbMinNumbers + 2) { imdbId = text.Slice(0, i); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 24163f1df..7f5f9f74b 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -75,7 +75,9 @@ namespace MediaBrowser.Controller.Entities Model.Entities.ExtraType.DeletedScene, Model.Entities.ExtraType.Interview, Model.Entities.ExtraType.Sample, - Model.Entities.ExtraType.Scene + Model.Entities.ExtraType.Scene, + Model.Entities.ExtraType.Featurette, + Model.Entities.ExtraType.Short }; private string _sortName; diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 13bfd07c3..1bf528538 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -205,6 +205,16 @@ namespace MediaBrowser.Controller.Entities public int? MinIndexNumber { get; set; } + /// <summary> + /// Gets or sets the minimum ParentIndexNumber and IndexNumber. + /// </summary> + /// <remarks> + /// It produces this where clause: + /// <para>(ParentIndexNumber = X and IndexNumber >= Y) or ParentIndexNumber > X. + /// </para> + /// </remarks> + public (int ParentIndexNumber, int IndexNumber)? MinParentAndIndexNumber { get; set; } + public int? AiredDuringSeason { get; set; } public double? MinCriticRating { get; set; } diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 77e70f8fb..3c12acd90 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -33,9 +33,9 @@ namespace MediaBrowser.Controller.Entities.Movies .ToArray(); /// <summary> - /// Gets or sets the name of the TMDB collection. + /// Gets or sets the name of the TMDb collection. /// </summary> - /// <value>The name of the TMDB collection.</value> + /// <value>The name of the TMDb collection.</value> public string TmdbCollectionName { get; set; } [JsonIgnore] diff --git a/MediaBrowser.Controller/Library/ILibraryMonitor.cs b/MediaBrowser.Controller/Library/ILibraryMonitor.cs index 455054bd1..de74aa5a1 100644 --- a/MediaBrowser.Controller/Library/ILibraryMonitor.cs +++ b/MediaBrowser.Controller/Library/ILibraryMonitor.cs @@ -34,12 +34,5 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="path">The path.</param> void ReportFileSystemChanged(string path); - - /// <summary> - /// Determines whether [is path locked] [the specified path]. - /// </summary> - /// <param name="path">The path.</param> - /// <returns><c>true</c> if [is path locked] [the specified path]; otherwise, <c>false</c>.</returns> - bool IsPathLocked(string path); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 235a86138..cee08eeda 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1459,7 +1459,11 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -preset 7"; } - param += " -look_ahead 0"; + // Only h264_qsv has look_ahead option + if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + param += " -look_ahead 0"; + } } else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc) @@ -1497,7 +1501,7 @@ namespace MediaBrowser.Controller.MediaEncoding break; default: - param += " -preset p4"; + param += " -preset p1"; break; } } @@ -3467,6 +3471,12 @@ namespace MediaBrowser.Controller.MediaEncoding // map from d3d11va to qsv. mainFilters.Add("hwmap=derive_device=qsv"); } + else + { + // Insert a qsv scaler to sync the decoder surface, + // msdk will passthrough this internally. + mainFilters.Add("hwmap=derive_device=qsv,scale_qsv"); + } } // hw deint diff --git a/MediaBrowser.Controller/Providers/IHasOrder.cs b/MediaBrowser.Controller/Providers/IHasOrder.cs index 9fde0e695..77b0407a2 100644 --- a/MediaBrowser.Controller/Providers/IHasOrder.cs +++ b/MediaBrowser.Controller/Providers/IHasOrder.cs @@ -1,9 +1,14 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Controller.Providers { + /// <summary> + /// Interface IHasOrder. + /// </summary> public interface IHasOrder { + /// <summary> + /// Gets the order. + /// </summary> + /// <value>The order.</value> int Order { get; } } } diff --git a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs index f146decb6..888ca6c72 100644 --- a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,20 +6,41 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { + /// <summary> + /// Interface IRemoteMetadataProvider. + /// </summary> public interface IRemoteMetadataProvider : IMetadataProvider { } + /// <summary> + /// Interface IRemoteMetadataProvider. + /// </summary> public interface IRemoteMetadataProvider<TItemType, in TLookupInfoType> : IMetadataProvider<TItemType>, IRemoteMetadataProvider, IRemoteSearchProvider<TLookupInfoType> where TItemType : BaseItem, IHasLookupInfo<TLookupInfoType> where TLookupInfoType : ItemLookupInfo, new() { + /// <summary> + /// Gets the metadata for a specific LookupInfoType. + /// </summary> + /// <param name="info">The LookupInfoType to get metadata for.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> + /// <returns>A task returning a MetadataResult for the specific LookupInfoType.</returns> Task<MetadataResult<TItemType>> GetMetadata(TLookupInfoType info, CancellationToken cancellationToken); } + /// <summary> + /// Interface IRemoteMetadataProvider. + /// </summary> public interface IRemoteSearchProvider<in TLookupInfoType> : IRemoteSearchProvider where TLookupInfoType : ItemLookupInfo { + /// <summary> + /// Gets the list of <see cref="RemoteSearchResult"/> for a specific LookupInfoType. + /// </summary> + /// <param name="searchInfo">The LookupInfoType to search for.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> + /// <returns>A task returning RemoteSearchResults for the searchInfo.</returns> Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TLookupInfoType searchInfo, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index a9e1b4a51..92ce14be2 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -68,7 +68,7 @@ namespace MediaBrowser.LocalMetadata.Parsers IgnoreComments = true }; - _validProviderIds = _validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + _validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var idInfos = ProviderManager.GetExternalIdInfos(item.Item); diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index b121a2905..6e9b943f7 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -436,9 +436,9 @@ namespace MediaBrowser.Model.Dlna { containerSupported = true; - videoSupported = videoStream != null && profile.SupportsVideoCodec(videoStream.Codec); + videoSupported = videoStream == null || profile.SupportsVideoCodec(videoStream.Codec); - audioSupported = audioStream != null && profile.SupportsAudioCodec(audioStream.Codec); + audioSupported = audioStream == null || profile.SupportsAudioCodec(audioStream.Codec); if (videoSupported && audioSupported) { @@ -447,18 +447,17 @@ namespace MediaBrowser.Model.Dlna } } - var list = new List<TranscodeReason>(); if (!containerSupported) { reasons |= TranscodeReason.ContainerNotSupported; } - if (videoStream != null && !videoSupported) + if (!videoSupported) { reasons |= TranscodeReason.VideoCodecNotSupported; } - if (audioStream != null && !audioSupported) + if (!audioSupported) { reasons |= TranscodeReason.AudioCodecNotSupported; } @@ -587,21 +586,19 @@ namespace MediaBrowser.Model.Dlna } // Collect candidate audio streams - IEnumerable<MediaStream> candidateAudioStreams = audioStream == null ? Array.Empty<MediaStream>() : new[] { audioStream }; + ICollection<MediaStream> candidateAudioStreams = audioStream == null ? Array.Empty<MediaStream>() : new[] { audioStream }; if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0) { if (audioStream?.IsDefault == true) { - candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault); + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray(); } else { - candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language); + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language).ToArray(); } } - candidateAudioStreams = candidateAudioStreams.ToArray(); - var videoStream = item.VideoStream; var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay); @@ -1057,7 +1054,7 @@ namespace MediaBrowser.Model.Dlna MediaSourceInfo mediaSource, MediaStream videoStream, MediaStream audioStream, - IEnumerable<MediaStream> candidateAudioStreams, + ICollection<MediaStream> candidateAudioStreams, MediaStream subtitleStream, bool isEligibleForDirectPlay, bool isEligibleForDirectStream) @@ -1088,9 +1085,6 @@ namespace MediaBrowser.Model.Dlna bool? isInterlaced = videoStream?.IsInterlaced; string videoCodecTag = videoStream?.CodecTag; bool? isAvc = videoStream?.IsAVC; - // Audio - var defaultLanguage = audioStream?.Language ?? string.Empty; - var defaultMarked = audioStream?.IsDefault ?? false; TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp; int? packetLength = videoStream?.PacketLength; @@ -1122,7 +1116,7 @@ namespace MediaBrowser.Model.Dlna .SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions))); // Check audiocandidates profile conditions - var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream, defaultLanguage, defaultMarked)); + var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream)); TranscodeReason subtitleProfileReasons = 0; if (subtitleStream != null) @@ -1179,14 +1173,18 @@ namespace MediaBrowser.Model.Dlna } // Check audio codec - var selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec)); - if (selectedAudioStream == null) - { - directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported; - } - else + MediaStream selectedAudioStream = null; + if (candidateAudioStreams.Any()) { - audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream); + selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec)); + if (selectedAudioStream == null) + { + directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported; + } + else + { + audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream); + } } var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons; @@ -1239,10 +1237,10 @@ namespace MediaBrowser.Model.Dlna return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); } - private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, string language, bool isDefault) + private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) { var profile = options.Profile; - var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, !audioStream.IsDefault); + var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream)); var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions); if (audioStream?.IsExternal == true) diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index bb9848848..c348e83ae 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -230,19 +230,15 @@ namespace MediaBrowser.Model.Dto public bool? IsSecondaryAudio(MediaStream stream) { - // Look for the first audio track marked as default - foreach (var currentStream in MediaStreams) + if (stream.IsExternal) { - if (currentStream.Type == MediaStreamType.Audio && currentStream.IsDefault) - { - return currentStream.Index != stream.Index; - } + return false; } // Look for the first audio track foreach (var currentStream in MediaStreams) { - if (currentStream.Type == MediaStreamType.Audio) + if (currentStream.Type == MediaStreamType.Audio && !currentStream.IsExternal) { return currentStream.Index != stream.Index; } diff --git a/MediaBrowser.Model/Entities/ExtraType.cs b/MediaBrowser.Model/Entities/ExtraType.cs index aca4bd282..66da80d96 100644 --- a/MediaBrowser.Model/Entities/ExtraType.cs +++ b/MediaBrowser.Model/Entities/ExtraType.cs @@ -13,6 +13,8 @@ namespace MediaBrowser.Model.Entities Scene = 6, Sample = 7, ThemeSong = 8, - ThemeVideo = 9 + ThemeVideo = 9, + Featurette = 10, + Short = 11 } } diff --git a/MediaBrowser.Model/Entities/MetadataProvider.cs b/MediaBrowser.Model/Entities/MetadataProvider.cs index 37e3d8864..bd8db9941 100644 --- a/MediaBrowser.Model/Entities/MetadataProvider.cs +++ b/MediaBrowser.Model/Entities/MetadataProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - namespace MediaBrowser.Model.Entities { /// <summary> @@ -14,38 +12,78 @@ namespace MediaBrowser.Model.Entities Custom = 0, /// <summary> - /// The imdb. + /// The IMDb provider. /// </summary> Imdb = 2, /// <summary> - /// The TMDB. + /// The TMDb provider. /// </summary> Tmdb = 3, /// <summary> - /// The TVDB. + /// The TVDb provider. /// </summary> Tvdb = 4, /// <summary> - /// The tvcom. + /// The tvcom providerd. /// </summary> Tvcom = 5, /// <summary> - /// Tmdb Collection Id. + /// TMDb collection provider. /// </summary> TmdbCollection = 7, + + /// <summary> + /// The MusicBrainz album provider. + /// </summary> MusicBrainzAlbum = 8, + + /// <summary> + /// The MusicBrainz album artist provider. + /// </summary> MusicBrainzAlbumArtist = 9, + + /// <summary> + /// The MusicBrainz artist provider. + /// </summary> MusicBrainzArtist = 10, + + /// <summary> + /// The MusicBrainz release group provider. + /// </summary> MusicBrainzReleaseGroup = 11, + + /// <summary> + /// The Zap2It provider. + /// </summary> Zap2It = 12, + + /// <summary> + /// The TvRage provider. + /// </summary> TvRage = 15, + + /// <summary> + /// The AudioDb artist provider. + /// </summary> AudioDbArtist = 16, + + /// <summary> + /// The AudioDb collection provider. + /// </summary> AudioDbAlbum = 17, + + /// <summary> + /// The MusicBrainz track provider. + /// </summary> MusicBrainzTrack = 18, + + /// <summary> + /// The TvMaze provider. + /// </summary> TvMaze = 19 } } diff --git a/MediaBrowser.Model/Entities/SeriesStatus.cs b/MediaBrowser.Model/Entities/SeriesStatus.cs index c77c4a8ad..1cff24e2a 100644 --- a/MediaBrowser.Model/Entities/SeriesStatus.cs +++ b/MediaBrowser.Model/Entities/SeriesStatus.cs @@ -1,18 +1,23 @@ namespace MediaBrowser.Model.Entities { /// <summary> - /// Enum SeriesStatus. + /// The status of a series. /// </summary> public enum SeriesStatus { /// <summary> - /// The continuing. + /// The continuing status. This indicates that a series is currently releasing. /// </summary> Continuing, /// <summary> - /// The ended. + /// The ended status. This indicates that a series has completed and is no longer being released. /// </summary> - Ended + Ended, + + /// <summary> + /// The unreleased status. This indicates that a series has not been released yet. + /// </summary> + Unreleased } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index ad2ff1ba2..4172e9825 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -34,13 +34,13 @@ <ItemGroup> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.3" /> <PackageReference Include="MimeTypes" Version="2.4.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="System.Globalization" Version="4.3.0" /> - <PackageReference Include="System.Text.Json" Version="6.0.6" /> + <PackageReference Include="System.Text.Json" Version="6.0.7" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index e6c3a6c26..6fa1d778a 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -126,7 +126,7 @@ namespace MediaBrowser.Model.Querying ProductionLocations, /// <summary> - /// Imdb, tmdb, etc. + /// The ids from IMDb, TMDb, etc. /// </summary> ProviderIds, diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index bbb33ddf0..552ded0c4 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -183,7 +183,7 @@ namespace MediaBrowser.Providers.Manager } } - // thetvdb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons... + // 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); diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs index 1bc2edfd8..bb2d584c1 100644 --- a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs @@ -175,12 +175,12 @@ namespace MediaBrowser.Providers.MediaInfo return Array.Empty<ExternalPathParserResult>(); } - var files = directoryService.GetFilePaths(folder, clearCache).ToList(); + var files = directoryService.GetFilePaths(folder, clearCache, true).ToList(); files.Remove(video.Path); var internalMetadataPath = video.GetInternalMetadataPath(); if (_fileSystem.DirectoryExists(internalMetadataPath)) { - files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache)); + files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache, true)); } if (!files.Any()) diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 12ea2d55b..10077e5c8 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -408,10 +408,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb } } - if (isEnglishRequested) - { - item.Overview = result.Plot; - } + item.Overview = result.Plot; if (!Plugin.Instance.Configuration.CastAndCrew) { diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs index cb422ef3d..0bfab9824 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs @@ -1,13 +1,17 @@ -#pragma warning disable CS1591 - -using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Plugins; namespace MediaBrowser.Providers.Plugins.StudioImages.Configuration { + /// <summary> + /// Plugin configuration class for the studio image provider. + /// </summary> public class PluginConfiguration : BasePluginConfiguration { private string _repository = Plugin.DefaultServer; + /// <summary> + /// Gets or sets the studio image repository URL. + /// </summary> public string RepositoryUrl { get diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs index 5e653d039..78150153a 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs @@ -1,5 +1,4 @@ #nullable disable -#pragma warning disable CS1591 using System; using System.Collections.Generic; @@ -11,27 +10,47 @@ using MediaBrowser.Providers.Plugins.StudioImages.Configuration; namespace MediaBrowser.Providers.Plugins.StudioImages { + /// <summary> + /// Artwork Plugin class. + /// </summary> public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages { + /// <summary> + /// Artwork repository URL. + /// </summary> public const string DefaultServer = "https://raw.github.com/jellyfin/emby-artwork/master/studios"; + /// <summary> + /// Initializes a new instance of the <see cref="Plugin"/> class. + /// </summary> + /// <param name="applicationPaths">application paths.</param> + /// <param name="xmlSerializer">xml serializer.</param> public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { Instance = this; } + /// <summary> + /// Gets the instance of Artwork plugin. + /// </summary> public static Plugin Instance { get; private set; } + /// <inheritdoc/> public override Guid Id => new Guid("872a7849-1171-458d-a6fb-3de3d442ad30"); + /// <inheritdoc/> public override string Name => "Studio Images"; + /// <inheritdoc/> public override string Description => "Get artwork for studios from any Jellyfin-compatible repository."; // TODO remove when plugin removed from server. + + /// <inheritdoc/> public override string ConfigurationFileName => "Jellyfin.Plugin.StudioImages.xml"; + /// <inheritdoc/> public IEnumerable<PluginPageInfo> GetPages() { yield return new PluginPageInfo diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs index ef822a22a..ffbb338e8 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -21,12 +19,21 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.StudioImages { + /// <summary> + /// Studio image provider. + /// </summary> public class StudiosImageProvider : IRemoteImageProvider { private readonly IServerConfigurationManager _config; private readonly IHttpClientFactory _httpClientFactory; private readonly IFileSystem _fileSystem; + /// <summary> + /// Initializes a new instance of the <see cref="StudiosImageProvider"/> class. + /// </summary> + /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param> public StudiosImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IFileSystem fileSystem) { _config = config; @@ -34,13 +41,16 @@ namespace MediaBrowser.Providers.Plugins.StudioImages _fileSystem = fileSystem; } + /// <inheritdoc /> public string Name => "Artwork Repository"; + /// <inheritdoc /> public bool Supports(BaseItem item) { return item is Studio; } + /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> @@ -49,6 +59,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages }; } + /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var thumbsPath = Path.Combine(_config.ApplicationPaths.CachePath, "imagesbyname", "remotestudiothumbs.txt"); @@ -103,6 +114,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages return EnsureList(url, file, _fileSystem, cancellationToken); } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); @@ -110,13 +122,13 @@ namespace MediaBrowser.Providers.Plugins.StudioImages } /// <summary> - /// Ensures the list. + /// Ensures the existence of a file listing. /// </summary> /// <param name="url">The URL.</param> /// <param name="file">The file.</param> /// <param name="fileSystem">The file system.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> + /// <returns>A Task to ensure existence of a file listing.</returns> public async Task<string> EnsureList(string url, string file, IFileSystem fileSystem, CancellationToken cancellationToken) { var fileInfo = fileSystem.GetFileInfo(file); @@ -134,6 +146,12 @@ namespace MediaBrowser.Providers.Plugins.StudioImages return file; } + /// <summary> + /// Get matching image for an item. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="images">The enumerable of image strings.</param> + /// <returns>The matching image string.</returns> public string FindMatch(BaseItem item, IEnumerable<string> images) { var name = GetComparableName(item.Name); @@ -151,6 +169,11 @@ namespace MediaBrowser.Providers.Plugins.StudioImages .Replace("/", string.Empty, StringComparison.Ordinal); } + /// <summary> + /// Get available image strings for a file. + /// </summary> + /// <param name="file">The file.</param> + /// <returns>All images strings of a file.</returns> public IEnumerable<string> GetAvailableImages(string file) { using var fileStream = File.OpenRead(file); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs index 0bab7c3ca..ac3df1d5d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs @@ -8,7 +8,7 @@ using TMDbLib.Objects.General; namespace MediaBrowser.Providers.Plugins.Tmdb.Api { /// <summary> - /// The TMDb api controller. + /// The TMDb API controller. /// </summary> [ApiController] [Authorize(Policy = "DefaultAuthorization")] diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs index 3217ac2f1..0e768bb83 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets { /// <summary> - /// External ID for a TMDB box set. + /// External id for a TMDb box set. /// </summary> public class TmdbBoxSetExternalId : IExternalId { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index 29a557c31..ef878e670 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -18,26 +16,38 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets { + /// <summary> + /// BoxSet image provider powered by TMDb. + /// </summary> public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbBoxSetImageProvider"/> class. + /// </summary> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; _tmdbClientManager = tmdbClientManager; } + /// <inheritdoc /> public string Name => TmdbUtils.ProviderName; + /// <inheritdoc /> public int Order => 0; + /// <inheritdoc /> public bool Supports(BaseItem item) { return item is BoxSet; } + /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> @@ -47,6 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets }; } + /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var tmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); @@ -76,6 +87,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets return remoteImages; } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index 62bc9c65f..90f2aa88f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -18,12 +16,21 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets { + /// <summary> + /// BoxSet provider powered by TMDb. + /// </summary> public class TmdbBoxSetProvider : IRemoteMetadataProvider<BoxSet, BoxSetInfo> { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; private readonly ILibraryManager _libraryManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbBoxSetProvider"/> class. + /// </summary> + /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager, ILibraryManager libraryManager) { _httpClientFactory = httpClientFactory; @@ -31,8 +38,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets _libraryManager = libraryManager; } + /// <inheritdoc /> public string Name => TmdbUtils.ProviderName; + /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo searchInfo, CancellationToken cancellationToken) { var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); @@ -81,6 +90,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets return collections; } + /// <inheritdoc /> public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo info, CancellationToken cancellationToken) { var tmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); @@ -124,6 +134,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets return result; } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs index 31310a8d4..38d2c5c69 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs @@ -7,7 +7,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { /// <summary> - /// External ID for a TMBD movie. + /// External id for a TMDb movie. /// </summary> public class TmdbMovieExternalId : IExternalId { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs index 16f0089f8..1646a93d2 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -19,26 +17,38 @@ using TMDbLib.Objects.Find; namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { + /// <summary> + /// Movie image provider powered by TMDb. + /// </summary> public class TmdbMovieImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbMovieImageProvider"/> class. + /// </summary> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbMovieImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; _tmdbClientManager = tmdbClientManager; } + /// <inheritdoc /> public int Order => 0; + /// <inheritdoc /> public string Name => TmdbUtils.ProviderName; + /// <inheritdoc /> public bool Supports(BaseItem item) { return item is Movie || item is Trailer; } + /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> @@ -49,6 +59,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies }; } + /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var language = item.GetPreferredMetadataLanguage(); @@ -96,6 +107,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies return remoteImages; } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index f14f31858..dd2d5d97d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -23,7 +21,7 @@ using TMDbLib.Objects.Search; namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { /// <summary> - /// Class MovieDbProvider. + /// Movie provider powered by TMDb. /// </summary> public class TmdbMovieProvider : IRemoteMetadataProvider<Movie, MovieInfo>, IHasOrder { @@ -31,6 +29,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies private readonly ILibraryManager _libraryManager; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbMovieProvider"/> class. + /// </summary> + /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbMovieProvider( ILibraryManager libraryManager, TmdbClientManager tmdbClientManager, @@ -41,11 +45,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies _httpClientFactory = httpClientFactory; } - public string Name => TmdbUtils.ProviderName; - /// <inheritdoc /> public int Order => 1; + /// <inheritdoc /> + public string Name => TmdbUtils.ProviderName; + + /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken) { if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var id)) @@ -133,6 +139,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies return remoteSearchResults; } + /// <inheritdoc /> public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken) { var tmdbId = info.GetProviderId(MetadataProvider.Tmdb); @@ -144,7 +151,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies // Caller provides the filename with extension stripped and NOT the parsed filename var parsedName = _libraryManager.ParseName(info.Name); var cleanedName = TmdbUtils.CleanName(parsedName.Name); - var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); if (searchResults.Count > 0) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs index 9804d60bd..027399aec 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.People { /// <summary> - /// External ID for a TMDB person. + /// External id for a TMDb person. /// </summary> public class TmdbPersonExternalId : IExternalId { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index 7ce4cfe67..d7f5c99dd 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -14,11 +12,19 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.People { + /// <summary> + /// Person image provider powered by TMDb. + /// </summary> public class TmdbPersonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbPersonImageProvider"/> class. + /// </summary> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbPersonImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; @@ -31,11 +37,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People /// <inheritdoc /> public int Order => 0; + /// <inheritdoc /> public bool Supports(BaseItem item) { return item is Person; } + /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> @@ -44,6 +52,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People }; } + /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var person = (Person)item; @@ -68,6 +77,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return remoteImages; } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 8790e3759..d760ad142 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -16,19 +14,29 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.People { + /// <summary> + /// Person image provider powered by TMDb. + /// </summary> public class TmdbPersonProvider : IRemoteMetadataProvider<Person, PersonLookupInfo> { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbPersonProvider"/> class. + /// </summary> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbPersonProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; _tmdbClientManager = tmdbClientManager; } + /// <inheritdoc /> public string Name => TmdbUtils.ProviderName; + /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) { if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId)) @@ -79,6 +87,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return remoteSearchResults; } + /// <inheritdoc /> public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo info, CancellationToken cancellationToken) { var personTmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); @@ -131,6 +140,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return result; } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index 5eec776b5..943a3a75b 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -17,22 +15,38 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { + /// <summary> + /// TV episode image provider powered by TheMovieDb. + /// </summary> public class TmdbEpisodeImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbEpisodeImageProvider"/> class. + /// </summary> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; _tmdbClientManager = tmdbClientManager; } - // After TheTvDb + /// <inheritdoc /> public int Order => 1; + /// <inheritdoc /> public string Name => TmdbUtils.ProviderName; + /// <inheritdoc /> + public bool Supports(BaseItem item) + { + return item is Controller.Entities.TV.Episode; + } + + /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> @@ -41,6 +55,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV }; } + /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var episode = (Controller.Entities.TV.Episode)item; @@ -81,14 +96,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return remoteImages; } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } - - public bool Supports(BaseItem item) - { - return item is Controller.Entities.TV.Episode; - } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index f50f15877..e20284e6f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -19,22 +17,32 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { + /// <summary> + /// TV episode provider powered by TheMovieDb. + /// </summary> public class TmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbEpisodeProvider"/> class. + /// </summary> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; _tmdbClientManager = tmdbClientManager; } - // After TheTvDb + /// <inheritdoc /> public int Order => 1; + /// <inheritdoc /> public string Name => TmdbUtils.ProviderName; + /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) { // The search query must either provide an episode number or date @@ -68,6 +76,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV }; } + /// <inheritdoc /> public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken) { var metadataResult = new MetadataResult<Episode>(); @@ -209,6 +218,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return metadataResult; } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index 4446fa966..da32ea408 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -16,26 +14,47 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { + /// <summary> + /// TV season image provider powered by TheMovieDb. + /// </summary> public class TmdbSeasonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbSeasonImageProvider"/> class. + /// </summary> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbSeasonImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; _tmdbClientManager = tmdbClientManager; } + /// <inheritdoc/> public int Order => 1; + /// <inheritdoc/> public string Name => TmdbUtils.ProviderName; - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + /// <inheritdoc /> + public bool Supports(BaseItem item) { - return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); + return item is Season; + } + + /// <inheritdoc /> + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) + { + return new List<ImageType> + { + ImageType.Primary + }; } + /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var season = (Season)item; @@ -68,17 +87,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return remoteImages; } - public IEnumerable<ImageType> GetSupportedImages(BaseItem item) - { - return new List<ImageType> - { - ImageType.Primary - }; - } - - public bool Supports(BaseItem item) + /// <inheritdoc /> + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { - return item is Season; + return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 64ed3f408..2cf0f399e 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -17,19 +15,29 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { + /// <summary> + /// TV season provider powered by TheMovieDb. + /// </summary> public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo> { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbSeasonProvider"/> class. + /// </summary> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; _tmdbClientManager = tmdbClientManager; } + /// <inheritdoc /> public string Name => TmdbUtils.ProviderName; + /// <inheritdoc /> public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<Season>(); @@ -114,11 +122,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return result; } + /// <inheritdoc /> public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken) { return Task.FromResult(Enumerable.Empty<RemoteSearchResult>()); } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs index 8a2be80cd..df04cb2e7 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { /// <summary> - /// External ID for a TMDB series. + /// External id for a TMDb series. /// </summary> public class TmdbSeriesExternalId : IExternalId { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index 130d6ce44..e96b680b4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -16,27 +14,38 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { + /// <summary> + /// TV series image provider powered by TheMovieDb. + /// </summary> public class TmdbSeriesImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbSeriesImageProvider"/> class. + /// </summary> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbSeriesImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { _httpClientFactory = httpClientFactory; _tmdbClientManager = tmdbClientManager; } + /// <inheritdoc /> public string Name => TmdbUtils.ProviderName; - // After tvdb and fanart + /// <inheritdoc /> public int Order => 2; + /// <inheritdoc /> public bool Supports(BaseItem item) { return item is Series; } + /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> @@ -47,6 +56,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV }; } + /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var tmdbId = item.GetProviderId(MetadataProvider.Tmdb); @@ -80,6 +90,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return remoteImages; } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 4d26052fa..4e8fdf0ee 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -23,12 +21,21 @@ using TMDbLib.Objects.TvShows; namespace MediaBrowser.Providers.Plugins.Tmdb.TV { + /// <summary> + /// TV series provider powered by TheMovieDb. + /// </summary> public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; private readonly TmdbClientManager _tmdbClientManager; + /// <summary> + /// Initializes a new instance of the <see cref="TmdbSeriesProvider"/> class. + /// </summary> + /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param> + /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> + /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> public TmdbSeriesProvider( ILibraryManager libraryManager, IHttpClientFactory httpClientFactory, @@ -39,11 +46,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV _tmdbClientManager = tmdbClientManager; } + /// <inheritdoc /> public string Name => TmdbUtils.ProviderName; - // After TheTVDB + /// <inheritdoc /> public int Order => 1; + /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) { if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId)) @@ -159,6 +168,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return remoteResult; } + /// <inheritdoc /> public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<Series> @@ -383,6 +393,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV } } + /// <inheritdoc /> public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 685eb222f..44c2c81f4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb private static readonly Regex _nonWords = new(@"[\W_]+", RegexOptions.Compiled); /// <summary> - /// URL of the TMDB instance to use. + /// URL of the TMDb instance to use. /// </summary> public const string BaseTmdbUrl = "https://www.themoviedb.org/"; @@ -50,7 +50,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb } /// <summary> - /// Maps the TMDB provided roles for crew members to Jellyfin roles. + /// Maps the TMDb provided roles for crew members to Jellyfin roles. /// </summary> /// <param name="crew">Crew member to map against the Jellyfin person types.</param> /// <returns>The Jellyfin person type.</returns> @@ -103,9 +103,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb languages.Add(preferredLanguage); - if (preferredLanguage.Length == 5) // like en-US + if (preferredLanguage.Length == 5) // Like en-US { - // Currently, TMDB supports 2-letter language codes only + // Currently, TMDb supports 2-letter language codes only. // They are planning to change this in the future, thus we're // supplying both codes if we're having a 5-letter code. languages.Add(preferredLanguage.Substring(0, 2)); @@ -114,6 +114,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb languages.Add("null"); + // Always add English as fallback language if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase)) { languages.Add("en"); @@ -134,14 +135,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb return language; } - // They require this to be uppercase - // Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api. + // TMDb requires this to be uppercase + // Everything after the hyphen must be written in uppercase due to a way TMDb wrote their API. // See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab var parts = language.Split('-'); if (parts.Length == 2) { - // TMDB doesn't support Switzerland (de-CH, it-CH or fr-CH) so use the language (de, it or fr) without country code + // TMDb doesn't support Switzerland (de-CH, it-CH or fr-CH) so use the language (de, it or fr) without country code if (string.Equals(parts[1], "CH", StringComparison.OrdinalIgnoreCase)) { return parts[0]; @@ -174,14 +175,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb } /// <summary> - /// Combines the metadata country code and the parental rating from the Api into the value we store in our database. + /// Combines the metadata country code and the parental rating from the API into the value we store in our database. /// </summary> - /// <param name="countryCode">The Iso 3166-1 country code of the rating country.</param> - /// <param name="ratingValue">The rating value returned by the Tmdb Api.</param> + /// <param name="countryCode">The ISO 3166-1 country code of the rating country.</param> + /// <param name="ratingValue">The rating value returned by the TMDb API.</param> /// <returns>The combined parental rating of country code+rating value.</returns> public static string BuildParentalRating(string countryCode, string ratingValue) { - // exclude US because we store us values as TV-14 without the country code. + // Exclude US because we store US values as TV-14 without the country code. var ratingPrefix = string.Equals(countryCode, "US", StringComparison.OrdinalIgnoreCase) ? string.Empty : countryCode + "-"; var newRating = ratingPrefix + ratingValue; diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index da348239a..9e197e737 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -23,6 +21,10 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.XbmcMetadata.Parsers { + /// <summary> + /// The BaseNfoParser class. + /// </summary> + /// <typeparam name="T">The type.</typeparam> public class BaseNfoParser<T> where T : BaseItem { @@ -63,16 +65,22 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// </summary> protected ILogger Logger { get; } + /// <summary> + /// Gets the provider manager. + /// </summary> protected IProviderManager ProviderManager { get; } + /// <summary> + /// Gets a value indicating whether URLs after a closing XML tag are supporrted. + /// </summary> protected virtual bool SupportsUrlAfterClosingXmlTag => false; /// <summary> /// Fetches metadata for an item from one xml file. /// </summary> - /// <param name="item">The item.</param> + /// <param name="item">The <see cref="MetadataResult{T}"/>.</param> /// <param name="metadataFile">The metadata file.</param> - /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> /// <exception cref="ArgumentNullException"><c>item</c> is <c>null</c>.</exception> /// <exception cref="ArgumentException"><c>metadataFile</c> is <c>null</c> or empty.</exception> public void Fetch(MetadataResult<T> item, string metadataFile, CancellationToken cancellationToken) @@ -111,10 +119,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// <summary> /// Fetches the specified item. /// </summary> - /// <param name="item">The item.</param> + /// <param name="item">The <see cref="MetadataResult{T}"/>.</param> /// <param name="metadataFile">The metadata file.</param> - /// <param name="settings">The settings.</param> - /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="settings">The <see cref="XmlReaderSettings"/>.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> protected virtual void Fetch(MetadataResult<T> item, string metadataFile, XmlReaderSettings settings, CancellationToken cancellationToken) { if (!SupportsUrlAfterClosingXmlTag) @@ -170,7 +178,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers ParseProviderLinks(item.Item, endingXml); - // If the file is just an imdb url, don't go any further + // If the file is just an IMDb url, don't go any further if (index == 0) { return; @@ -216,6 +224,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } + /// <summary> + /// Parses a XML tag to a provider id. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="xml">The xml tag.</param> protected void ParseProviderLinks(T item, ReadOnlySpan<char> xml) { if (ProviderIdParsers.TryFindImdbId(xml, out var imdbId)) @@ -245,6 +258,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } + /// <summary> + /// Fetches metadata from an XML node. + /// </summary> + /// <param name="reader">The <see cref="XmlReader"/>.</param> + /// <param name="itemResult">The <see cref="MetadataResult{T}"/>.</param> protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult<T> itemResult) { var item = itemResult.Item; @@ -1100,17 +1118,14 @@ namespace MediaBrowser.XbmcMetadata.Parsers switch (reader.Name) { case "language": + _ = reader.ReadElementContentAsString(); + if (item is Video video) { - _ = reader.ReadElementContentAsString(); - - if (item is Video video) - { - video.HasSubtitles = true; - } - - break; + video.HasSubtitles = true; } + break; + default: reader.Skip(); break; @@ -1136,20 +1151,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers switch (reader.Name) { case "rating": - { - if (reader.IsEmptyElement) { - reader.Read(); - continue; - } + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } - var ratingName = reader.GetAttribute("name"); + var ratingName = reader.GetAttribute("name"); - using var subtree = reader.ReadSubtree(); - FetchFromRatingNode(subtree, item, ratingName); + using var subtree = reader.ReadSubtree(); + FetchFromRatingNode(subtree, item, ratingName); - break; - } + break; + } default: reader.Skip(); @@ -1210,9 +1225,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers } /// <summary> - /// Gets the persons from XML node. + /// Gets the persons from a XML node. /// </summary> - /// <param name="reader">The reader.</param> + /// <param name="reader">The <see cref="XmlReader"/>.</param> /// <returns>IEnumerable{PersonInfo}.</returns> private PersonInfo GetPersonFromXmlNode(XmlReader reader) { @@ -1348,10 +1363,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers } /// <summary> - /// Parses the ImageType from the nfo aspect property. + /// Parses the <see cref="ImageType"/> from the NFO aspect property. /// </summary> - /// <param name="aspect">The nfo aspect property.</param> - /// <returns>The image type.</returns> + /// <param name="aspect">The NFO aspect property.</param> + /// <returns>The <see cref="ImageType"/>.</returns> private static ImageType GetImageType(string aspect) { return aspect switch diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 1bdef2d59..fcb880283 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/d3e46476-4494-41b7-a628-c517794c5a6a/6066215f6c0a18b070e8e6e8b715de0b/dotnet-sdk-6.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/1d2007d3-da35-48ad-80cc-a39cbc726908/1f3555baa8b14c3327bb4eaa570d7d07/dotnet-sdk-6.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 945bf8116..c18db7213 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/d3e46476-4494-41b7-a628-c517794c5a6a/6066215f6c0a18b070e8e6e8b715de0b/dotnet-sdk-6.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/1d2007d3-da35-48ad-80cc-a39cbc726908/1f3555baa8b14c3327bb4eaa570d7d07/dotnet-sdk-6.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index a63cd6527..01402184a 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/d3e46476-4494-41b7-a628-c517794c5a6a/6066215f6c0a18b070e8e6e8b715de0b/dotnet-sdk-6.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/1d2007d3-da35-48ad-80cc-a39cbc726908/1f3555baa8b14c3327bb4eaa570d7d07/dotnet-sdk-6.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 2b9ea9bf6..6af22eed9 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/d3e46476-4494-41b7-a628-c517794c5a6a/6066215f6c0a18b070e8e6e8b715de0b/dotnet-sdk-6.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/1d2007d3-da35-48ad-80cc-a39cbc726908/1f3555baa8b14c3327bb4eaa570d7d07/dotnet-sdk-6.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index 3d3e49af8..a7e70a35a 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/d3e46476-4494-41b7-a628-c517794c5a6a/6066215f6c0a18b070e8e6e8b715de0b/dotnet-sdk-6.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/1d2007d3-da35-48ad-80cc-a39cbc726908/1f3555baa8b14c3327bb4eaa570d7d07/dotnet-sdk-6.0.403-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs index 79aa8a354..febe9516a 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs @@ -38,9 +38,28 @@ public static class FfProbeKeyframeExtractor EnableRaisingEvents = true }; - process.Start(); + try + { + process.Start(); - return ParseStream(process.StandardOutput); + return ParseStream(process.StandardOutput); + } + catch (Exception) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + // We do not care if this fails + } + + throw; + } } internal static KeyframeData ParseStream(StreamReader reader) diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index 9585cb60c..8be5cd8dc 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -21,7 +21,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.3" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 9baf6877d..c279b6b4b 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -21,8 +21,8 @@ namespace Jellyfin.Model.Tests [Theory] // Chrome [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] @@ -32,8 +32,8 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // Firefox [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] @@ -59,11 +59,11 @@ namespace Jellyfin.Model.Tests [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // Yatse [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 // RokuSSPlus [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 should be DirectPlay @@ -83,8 +83,8 @@ namespace Jellyfin.Model.Tests [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 // Chrome-NoHLS [InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 - [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] @@ -273,15 +273,15 @@ namespace Jellyfin.Model.Tests [Theory] // Chrome - [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // Firefox - [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // Yatse - [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 - [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 // RokuSSPlus [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index 731580e0c..2c33ab492 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -51,8 +51,9 @@ namespace Jellyfin.Naming.Tests.Video [InlineData(ExtraType.Interview, "interviews")] [InlineData(ExtraType.Scene, "scenes")] [InlineData(ExtraType.Sample, "samples")] - [InlineData(ExtraType.Clip, "shorts")] - [InlineData(ExtraType.Clip, "featurettes")] + [InlineData(ExtraType.Short, "shorts")] + [InlineData(ExtraType.Featurette, "featurettes")] + [InlineData(ExtraType.Clip, "clips")] [InlineData(ExtraType.ThemeVideo, "backdrops")] [InlineData(ExtraType.Unknown, "extras")] public void TestDirectories(ExtraType type, string dirName) diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs new file mode 100644 index 000000000..82ce8fc4e --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs @@ -0,0 +1,70 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; +using Emby.Server.Implementations.LiveTv.Listings; +using MediaBrowser.Model.LiveTv; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.LiveTv.Listings; + +public class XmlTvListingsProviderTests +{ + private readonly Fixture _fixture; + private readonly XmlTvListingsProvider _xmlTvListingsProvider; + + public XmlTvListingsProviderTests() + { + var messageHandler = new Mock<HttpMessageHandler>(); + messageHandler.Protected() + .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) + .Returns<HttpRequestMessage, CancellationToken>( + (m, _) => + { + return Task.FromResult(new HttpResponseMessage() + { + Content = new StreamContent(File.OpenRead(Path.Combine("Test Data/LiveTv/Listings/XmlTv", m.RequestUri!.Segments[^1]))) + }); + }); + + var http = new Mock<IHttpClientFactory>(); + http.Setup(x => x.CreateClient(It.IsAny<string>())) + .Returns(new HttpClient(messageHandler.Object)); + _fixture = new Fixture(); + _fixture.Customize(new AutoMoqCustomization + { + ConfigureMembers = true + }).Inject(http); + _xmlTvListingsProvider = _fixture.Create<XmlTvListingsProvider>(); + } + + [Theory] + [InlineData("Test Data/LiveTv/Listings/XmlTv/notitle.xml")] + [InlineData("https://example.com/notitle.xml")] + public async Task GetProgramsAsync_NoTitle_Success(string path) + { + var info = new ListingsProviderInfo() + { + Path = path + }; + + var startDate = new DateTime(2022, 11, 4); + var programs = await _xmlTvListingsProvider.GetProgramsAsync(info, "3297", startDate, startDate.AddDays(1), CancellationToken.None); + var programsList = programs.ToList(); + Assert.Single(programsList); + var program = programsList[0]; + Assert.Null(program.Name); + Assert.Null(program.SeriesId); + Assert.Null(program.EpisodeTitle); + Assert.True(program.IsSports); + Assert.True(program.HasImage); + Assert.Equal("https://domain.tld/image.png", program.ImageUrl); + Assert.Equal("3297", program.ChannelId); + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml new file mode 100644 index 000000000..5a5be7997 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml @@ -0,0 +1,10 @@ +<tv date="20221104"> + <programme channel="3297" start="20221104130000 -0400" stop="20221105235959 -0400"> + <category lang="en">sports</category> + <episode-num system="original-air-date">2022-11-04 13:00:00</episode-num> + <icon height="" src="https://domain.tld/image.png" width=""/> + <credits/> + <video/> + <date/> + </programme> +</tv> |
