diff options
95 files changed, 1594 insertions, 1383 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml index 72401f60d..cf74a4201 100644 --- a/.ci/azure-pipelines-abi.yml +++ b/.ci/azure-pipelines-abi.yml @@ -35,14 +35,14 @@ jobs: packageType: sdk version: ${{ parameters.DotNetSdkVersion }} - - task: DotNetCoreCLI@2.210.0 + - task: DotNetCoreCLI@2 displayName: 'Install ABI CompatibilityChecker Tool' inputs: command: custom custom: tool arguments: 'update compatibilitychecker -g' - - task: DownloadPipelineArtifact@2.198.0 + - task: DownloadPipelineArtifact@2 displayName: 'Download New Assembly Build Artifact' inputs: source: 'current' @@ -50,7 +50,7 @@ jobs: path: "$(System.ArtifactsDirectory)/new-artifacts" runVersion: "latest" - - task: CopyFiles@2.211.0 + - task: CopyFiles@2 displayName: 'Copy New Assembly Build Artifact' inputs: sourceFolder: $(System.ArtifactsDirectory)/new-artifacts @@ -60,7 +60,7 @@ jobs: overWrite: true flattenFolders: true - - task: DownloadPipelineArtifact@2.198.0 + - task: DownloadPipelineArtifact@2 displayName: 'Download Reference Assembly Build Artifact' enabled: false inputs: @@ -72,7 +72,7 @@ jobs: runVersion: "latestFromBranch" runBranch: "refs/heads/$(System.PullRequest.TargetBranch)" - - task: CopyFiles@2.211.0 + - task: CopyFiles@2 displayName: 'Copy Reference Assembly Build Artifact' enabled: false inputs: @@ -83,7 +83,7 @@ jobs: overWrite: true flattenFolders: true - - task: DotNetCoreCLI@2.210.0 + - task: DotNetCoreCLI@2 displayName: 'Execute ABI Compatibility Check Tool' enabled: false inputs: diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index 6d25aa59f..b7112ba24 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -20,7 +20,7 @@ jobs: submodules: true persistCredentials: true - - task: DownloadPipelineArtifact@2.198.0 + - task: DownloadPipelineArtifact@2 displayName: 'Download Web Branch' condition: in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion') inputs: @@ -31,7 +31,7 @@ jobs: pipeline: 'Jellyfin Web' runBranch: variables['Build.SourceBranch'] - - task: DownloadPipelineArtifact@2.198.0 + - task: DownloadPipelineArtifact@2 displayName: 'Download Web Target' condition: eq(variables['Build.Reason'], 'PullRequest') inputs: @@ -42,7 +42,7 @@ jobs: pipeline: 'Jellyfin Web' runBranch: variables['System.PullRequest.TargetBranch'] - - task: ExtractFiles@1.211.0 + - task: ExtractFiles@1 displayName: 'Extract Web Client' inputs: archiveFilePatterns: '$(Agent.TempDirectory)/*.zip' @@ -55,7 +55,7 @@ jobs: packageType: sdk version: ${{ parameters.DotNetSdkVersion }} - - task: DotNetCoreCLI@2.210.0 + - task: DotNetCoreCLI@2 displayName: 'Publish Server' inputs: command: publish diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 7cf9b7a07..926d1d322 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -69,7 +69,7 @@ jobs: runOptions: 'inline' inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)' - - task: CopyFilesOverSSH@0.212.0 + - task: CopyFilesOverSSH@0 displayName: 'Upload artifacts to repository server' inputs: sshEndpoint: repository @@ -90,7 +90,7 @@ jobs: displayName: Set release version (stable) condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - - task: DownloadPipelineArtifact@2.198.0 + - task: DownloadPipelineArtifact@2 displayName: 'Download OpenAPI Spec' inputs: source: 'current' @@ -105,7 +105,7 @@ jobs: runOptions: 'inline' inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)' - - task: CopyFilesOverSSH@0.212.0 + - task: CopyFilesOverSSH@0 displayName: 'Upload artifacts to repository server' inputs: sshEndpoint: repository @@ -137,7 +137,7 @@ jobs: displayName: Set release version (stable) condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - - task: Docker@2.211.0 + - task: Docker@2 displayName: 'Push Unstable Image' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: @@ -150,7 +150,7 @@ jobs: unstable-$(Build.BuildNumber)-$(BuildConfiguration) unstable-$(BuildConfiguration) - - task: Docker@2.211.0 + - task: Docker@2 displayName: 'Push Stable Image' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: @@ -210,7 +210,7 @@ jobs: packageType: 'sdk' version: '6.0.x' - - task: DotNetCoreCLI@2.210.0 + - task: DotNetCoreCLI@2 displayName: 'Build Stable Nuget packages' condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: @@ -225,7 +225,7 @@ jobs: custom: 'pack' arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion) - - task: DotNetCoreCLI@2.210.0 + - task: DotNetCoreCLI@2 displayName: 'Build Unstable Nuget packages' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: @@ -256,7 +256,7 @@ jobs: publishFeedCredentials: 'NugetOrg' allowPackageConflicts: true # This ignores an error if the version already exists - - task: NuGetAuthenticate@0.203.0 + - task: NuGetAuthenticate@0 displayName: 'Authenticate to unstable Nuget feed' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index 066df8949..cc94dc2c5 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -51,7 +51,7 @@ jobs: organization: 'jellyfin' projectKey: 'jellyfin_jellyfin' - - task: DotNetCoreCLI@2.210.0 + - task: DotNetCoreCLI@2 displayName: 'Run CLI Tests' inputs: command: "test" 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 909972469..8db55a6ae 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1088,15 +1088,7 @@ namespace Emby.Server.Implementations return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort); } - // Published server ends with a / - if (!string.IsNullOrEmpty(PublishedServerUrl)) - { - // Published server ends with a '/', so we need to remove it. - return PublishedServerUrl.Trim('/'); - } - - string smart = NetManager.GetBindInterface(request, out var port); - return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port); + return GetSmartApiUrl(request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); } /// <inheritdoc/> 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 e0f129c3d..a0bbd0c49 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -25,11 +25,11 @@ <ItemGroup> <PackageReference Include="DiscUtils.Udf" Version="0.16.13" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> <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.9" /> + <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/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/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 943fc651f..08db5a30e 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimalizovat databázi", "TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.", "TaskKeyframeExtractor": "Vytahovač klíčových snímků", - "External": "Externí" + "External": "Externí", + "HearingImpaired": "Sluchově postižení" } 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/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 862410c54..243688388 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimise database", "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", "TaskKeyframeExtractor": "Keyframe Extractor", - "External": "External" + "External": "External", + "HearingImpaired": "Hearing Impaired" } diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index 1289172ba..8ad9e8c71 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabase": "Optimización de base de datos", "External": "Externo", "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.", - "TaskKeyframeExtractor": "Extractor de Fotogramas Clave" + "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", + "HearingImpaired": "Personas con discapacidad auditiva" } 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/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index b433c6f68..76a98aa54 100644 --- a/Emby.Server.Implementations/Localization/Core/gl.json +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -47,7 +47,7 @@ "HeaderFavoriteEpisodes": "Episodios Favoritos", "HeaderFavoriteArtists": "Artistas Favoritos", "HeaderFavoriteAlbums": "Álbunes Favoritos", - "HeaderContinueWatching": "Seguir mirando", + "HeaderContinueWatching": "Seguir vendo", "HeaderAlbumArtists": "Artistas do Album", "Genres": "Xéneros", "Forced": "Forzado", @@ -119,5 +119,9 @@ "UserOnlineFromDevice": "{0} está en liña desde {1}", "UserOfflineFromDevice": "{0} desconectouse desde {1}", "TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.", - "TaskOptimizeDatabase": "Optimizar base de datos" + "TaskOptimizeDatabase": "Optimizar base de datos", + "TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.", + "External": "Externo", + "HearingImpaired": "Problemas de audición", + "TaskKeyframeExtractor": "Extractor de fragmentos" } diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index c635dab23..694a3d688 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -123,5 +123,6 @@ "TaskOptimizeDatabaseDescription": "דוחס את מסד הנתונים ומוריד את שטח האחסון שבשימוש. הרצה של פעולה זו לאחר סריקת הספרייה או שינויים אחרים שמשפיעים על מסד הנתונים יכולה לשפר ביצועים.", "TaskKeyframeExtractorDescription": "חלץ תמונות מפתח מקבצי וידאו בכדי ליצור רשימות השמעה מדויקות יותר של HLS. משימה זו עלולה להימשך זמן רב.", "TaskKeyframeExtractor": "מחלץ תמונות מפתח", - "External": "חיצוני" + "External": "חיצוני", + "HearingImpaired": "לקוי שמיעה" } 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/id.json b/Emby.Server.Implementations/Localization/Core/id.json index 3e05525c8..695c0f404 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabase": "Optimalkan basis data", "TaskKeyframeExtractorDescription": "Ekstrak bingkai utama dari file video untuk membuat daftar putar HLS yang lebih tepat. Tugas ini dapat berjalan untuk waktu yang lama.", "TaskKeyframeExtractor": "Ekstraktor Bingkai Utama", - "External": "Luar" + "External": "Luar", + "HearingImpaired": "Gangguan Pendengaran" } 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/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/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/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 3e0fd11c8..92ce616f2 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -122,5 +122,6 @@ "TaskOptimizeDatabaseDescription": "Стискає базу даних та збільшує вільний простір. Виконання цього завдання після сканування медіатеки або внесення інших змін, які передбачають модифікацію бази даних може покращити продуктивність.", "TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.", "TaskKeyframeExtractor": "Екстрактор ключових кадрів", - "External": "Зовнішній" + "External": "Зовнішній", + "HearingImpaired": "З порушеннями слуху" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 281dbb00b..22b283b8a 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"); 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..33b67b389 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -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}."); } 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/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 7e64cf645..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.9" /> + <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/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index aac52805b..5caac4523 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -28,13 +28,13 @@ <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.9" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9"> + <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.9"> + <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/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/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 b2d79050b..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.9" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.9" /> + <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/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/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.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/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.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 3a0e9a225..b00c036e5 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -17,6 +17,7 @@ <ItemGroup> <PackageReference Include="LrcParser" Version="2022.529.1" /> + <PackageReference Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs index 9c27bd7d3..22229e377 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs @@ -1,37 +1,52 @@ -#pragma warning disable CS1591 - using MediaBrowser.Model.Plugins; +using MetaBrainz.MusicBrainz; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; -namespace MediaBrowser.Providers.Plugins.MusicBrainz +/// <summary> +/// MusicBrainz plugin configuration. +/// </summary> +public class PluginConfiguration : BasePluginConfiguration { - public class PluginConfiguration : BasePluginConfiguration - { - private string _server = Plugin.DefaultServer; + private const string DefaultServer = "musicbrainz.org"; - private long _rateLimit = Plugin.DefaultRateLimit; + private const double DefaultRateLimit = 1.0; - public string Server - { - get => _server; - set => _server = value.TrimEnd('/'); - } + private string _server = DefaultServer; + + private double _rateLimit = DefaultRateLimit; + + /// <summary> + /// Gets or sets the server url. + /// </summary> + public string Server + { + get => _server; - public long RateLimit + set => _server = value.TrimEnd('/'); + } + + /// <summary> + /// Gets or sets the rate limit. + /// </summary> + public double RateLimit + { + get => _rateLimit; + set { - get => _rateLimit; - set + if (value < DefaultRateLimit && _server == DefaultServer) { - if (value < Plugin.DefaultRateLimit && _server == Plugin.DefaultServer) - { - _rateLimit = Plugin.DefaultRateLimit; - } - else - { - _rateLimit = value; - } + _rateLimit = DefaultRateLimit; + } + else + { + _rateLimit = value; } } - - public bool ReplaceArtistName { get; set; } } + + /// <summary> + /// Gets or sets a value indicating whether to replace the artist name. + /// </summary> + public bool ReplaceArtistName { get; set; } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs index c54cdda3d..f7850781e 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs @@ -1,28 +1,27 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.MusicBrainz; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// <summary> +/// MusicBrainz album artist external id. +/// </summary> +public class MusicBrainzAlbumArtistExternalId : IExternalId { - public class MusicBrainzAlbumArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString(); + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString(); - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; - /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + /// <inheritdoc /> + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs index 8f7fadd06..a9d4472e7 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs @@ -1,28 +1,27 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.MusicBrainz; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// <summary> +/// MusicBrainz album external id. +/// </summary> +public class MusicBrainzAlbumExternalId : IExternalId { - public class MusicBrainzAlbumExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzAlbum.ToString(); + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzAlbum.ToString(); - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Album; + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Album; - /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}"; + /// <inheritdoc /> + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}"; - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index 915fb97fd..4d9feca6d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -1,805 +1,265 @@ -#nullable disable - -#pragma warning disable CS1591, SA1401 - using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; using System.Linq; -using System.Net; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Xml; -using MediaBrowser.Common.Net; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.MusicBrainz; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Providers.Music -{ - public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable - { - /// <summary> - /// For each single MB lookup/search, this is the maximum number of - /// attempts that shall be made whilst receiving a 503 Server - /// Unavailable (indicating throttled) response. - /// </summary> - private const uint MusicBrainzQueryAttempts = 5u; - - /// <summary> - /// The Jellyfin user-agent is unrestricted but source IP must not exceed - /// one request per second, therefore we rate limit to avoid throttling. - /// Be prudent, use a value slightly above the minimum required. - /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting. - /// </summary> - private readonly long _musicBrainzQueryIntervalMs; - - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger<MusicBrainzAlbumProvider> _logger; - - private readonly string _musicBrainzBaseUrl; - - private SemaphoreSlim _apiRequestLock = new SemaphoreSlim(1, 1); - private Stopwatch _stopWatchMusicBrainz = new Stopwatch(); - - public MusicBrainzAlbumProvider( - IHttpClientFactory httpClientFactory, - ILogger<MusicBrainzAlbumProvider> logger) - { - _httpClientFactory = httpClientFactory; - _logger = logger; - - _musicBrainzBaseUrl = Plugin.Instance.Configuration.Server; - _musicBrainzQueryIntervalMs = Plugin.Instance.Configuration.RateLimit; - - // Use a stopwatch to ensure we don't exceed the MusicBrainz rate limit - _stopWatchMusicBrainz.Start(); - - Current = this; - } - - internal static MusicBrainzAlbumProvider Current { get; private set; } - - /// <inheritdoc /> - public string Name => "MusicBrainz"; +using MediaBrowser.Providers.Music; +using MetaBrainz.MusicBrainz; +using MetaBrainz.MusicBrainz.Interfaces.Entities; +using MetaBrainz.MusicBrainz.Interfaces.Searches; - /// <inheritdoc /> - public int Order => 0; +namespace MediaBrowser.Providers.Plugins.MusicBrainz; - /// <inheritdoc /> - public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) - { - var releaseId = searchInfo.GetReleaseId(); - var releaseGroupId = searchInfo.GetReleaseGroupId(); - - string url; - - if (!string.IsNullOrEmpty(releaseId)) - { - url = "/ws/2/release/?query=reid:" + releaseId.ToString(CultureInfo.InvariantCulture); - } - else if (!string.IsNullOrEmpty(releaseGroupId)) - { - url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture); - } - else - { - var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId(); - - if (!string.IsNullOrWhiteSpace(artistMusicBrainzId)) - { - url = string.Format( - CultureInfo.InvariantCulture, - "/ws/2/release/?query=\"{0}\" AND arid:{1}", - WebUtility.UrlEncode(searchInfo.Name), - artistMusicBrainzId); - } - else - { - // I'm sure there is a better way but for now it resolves search for 12" Mixes - var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal); - - url = string.Format( - CultureInfo.InvariantCulture, - "/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"", - WebUtility.UrlEncode(queryName), - WebUtility.UrlEncode(searchInfo.GetAlbumArtist())); - } - } - - if (!string.IsNullOrWhiteSpace(url)) - { - using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return GetResultsFromResponse(stream); - } - - return Enumerable.Empty<RemoteSearchResult>(); - } +/// <summary> +/// Music album metadata provider for MusicBrainz. +/// </summary> +public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable +{ + private readonly Query _musicBrainzQuery; - private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream) - { - using var oReader = new StreamReader(stream, Encoding.UTF8); - var settings = new XmlReaderSettings() + /// <summary> + /// Initializes a new instance of the <see cref="MusicBrainzAlbumProvider"/> class. + /// </summary> + public MusicBrainzAlbumProvider() + { + MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) => { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true + Query.DefaultServer = MusicBrainz.Plugin.Instance.Configuration.Server; + Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit; }; - using var reader = XmlReader.Create(oReader, settings); - var results = ReleaseResult.Parse(reader); - - return results.Select(i => - { - var result = new RemoteSearchResult - { - Name = i.Title, - ProductionYear = i.Year - }; + _musicBrainzQuery = new Query(); + } - if (i.Artists.Count > 0) - { - result.AlbumArtist = new RemoteSearchResult - { - SearchProviderName = Name, - Name = i.Artists[0].Item1 - }; + /// <inheritdoc /> + public string Name => "MusicBrainz"; - result.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, i.Artists[0].Item2); - } + /// <inheritdoc /> + public int Order => 0; - if (!string.IsNullOrWhiteSpace(i.ReleaseId)) - { - result.SetProviderId(MetadataProvider.MusicBrainzAlbum, i.ReleaseId); - } - - if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId)) - { - result.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, i.ReleaseGroupId); - } + /// <inheritdoc /> + public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) + { + var releaseId = searchInfo.GetReleaseId(); + var releaseGroupId = searchInfo.GetReleaseGroupId(); - return result; - }); + if (!string.IsNullOrEmpty(releaseId)) + { + var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false); + return GetReleaseResult(releaseResult).SingleItemAsEnumerable(); } - /// <inheritdoc /> - public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken) + if (!string.IsNullOrEmpty(releaseGroupId)) { - var releaseId = info.GetReleaseId(); - var releaseGroupId = info.GetReleaseGroupId(); - - var result = new MetadataResult<MusicAlbum> - { - Item = new MusicAlbum() - }; - - // If we have a release group Id but not a release Id... - if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId)) - { - releaseId = await GetReleaseIdFromReleaseGroupId(releaseGroupId, cancellationToken).ConfigureAwait(false); - result.HasMetadata = true; - } - - if (string.IsNullOrWhiteSpace(releaseId)) - { - var artistMusicBrainzId = info.GetMusicBrainzArtistId(); - - var releaseResult = await GetReleaseResult(artistMusicBrainzId, info.GetAlbumArtist(), info.Name, cancellationToken).ConfigureAwait(false); - - if (releaseResult != null) - { - if (!string.IsNullOrWhiteSpace(releaseResult.ReleaseId)) - { - releaseId = releaseResult.ReleaseId; - result.HasMetadata = true; - } - - if (!string.IsNullOrWhiteSpace(releaseResult.ReleaseGroupId)) - { - releaseGroupId = releaseResult.ReleaseGroupId; - result.HasMetadata = true; - } - - result.Item.ProductionYear = releaseResult.Year; - result.Item.Overview = releaseResult.Overview; - } - } + var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false); + return GetReleaseGroupResult(releaseGroupResult.Releases); + } - // If we have a release Id but not a release group Id... - if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId)) - { - releaseGroupId = await GetReleaseGroupFromReleaseId(releaseId, cancellationToken).ConfigureAwait(false); - result.HasMetadata = true; - } + var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId(); - if (!string.IsNullOrWhiteSpace(releaseId) || !string.IsNullOrWhiteSpace(releaseGroupId)) - { - result.HasMetadata = true; - } + if (!string.IsNullOrWhiteSpace(artistMusicBrainzId)) + { + var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{searchInfo.Name}\" AND arid:{artistMusicBrainzId}", null, null, false, cancellationToken) + .ConfigureAwait(false); - if (result.HasMetadata) + if (releaseSearchResults.Results.Count > 0) { - if (!string.IsNullOrEmpty(releaseId)) - { - result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId); - } - - if (!string.IsNullOrEmpty(releaseGroupId)) - { - result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId); - } + return GetReleaseSearchResult(releaseSearchResults.Results); } - - return result; } - - private Task<ReleaseResult> GetReleaseResult(string artistMusicBrainId, string artistName, string albumName, CancellationToken cancellationToken) + else { - if (!string.IsNullOrEmpty(artistMusicBrainId)) - { - return GetReleaseResult(albumName, artistMusicBrainId, cancellationToken); - } + // I'm sure there is a better way but for now it resolves search for 12" Mixes + var queryName = searchInfo.Name.Replace("\"", string.Empty, StringComparison.Ordinal); - if (string.IsNullOrWhiteSpace(artistName)) + var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{queryName}\" AND artist:\"{searchInfo.GetAlbumArtist()}\"c", null, null, false, cancellationToken) + .ConfigureAwait(false); + + if (releaseSearchResults.Results.Count > 0) { - return Task.FromResult(new ReleaseResult()); + return GetReleaseSearchResult(releaseSearchResults.Results); } - - return GetReleaseResultByArtistName(albumName, artistName, cancellationToken); } - private async Task<ReleaseResult> GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken) - { - var url = string.Format( - CultureInfo.InvariantCulture, - "/ws/2/release/?query=\"{0}\" AND arid:{1}", - WebUtility.UrlEncode(albumName), - artistId); - - using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var oReader = new StreamReader(stream, Encoding.UTF8); - var settings = new XmlReaderSettings - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; + return Enumerable.Empty<RemoteSearchResult>(); + } - using var reader = XmlReader.Create(oReader, settings); - return ReleaseResult.Parse(reader).FirstOrDefault(); + private IEnumerable<RemoteSearchResult> GetReleaseSearchResult(IEnumerable<ISearchResult<IRelease>>? releaseSearchResults) + { + if (releaseSearchResults is null) + { + yield break; } - private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken) + foreach (var result in releaseSearchResults) { - var url = string.Format( - CultureInfo.InvariantCulture, - "/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"", - WebUtility.UrlEncode(albumName), - WebUtility.UrlEncode(artistName)); - - using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var oReader = new StreamReader(stream, Encoding.UTF8); - var settings = new XmlReaderSettings() - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; - - using var reader = XmlReader.Create(oReader, settings); - return ReleaseResult.Parse(reader).FirstOrDefault(); + yield return GetReleaseResult(result.Item); } + } - private static (string Name, string ArtistId) ParseArtistCredit(XmlReader reader) + private IEnumerable<RemoteSearchResult> GetReleaseGroupResult(IEnumerable<IRelease>? releaseSearchResults) + { + if (releaseSearchResults is null) { - reader.MoveToContent(); - reader.Read(); - - // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "name-credit": - { - if (reader.IsEmptyElement) - { - reader.Read(); - break; - } - - using var subReader = reader.ReadSubtree(); - return ParseArtistNameCredit(subReader); - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } - - return default; + yield break; } - private static (string Name, string ArtistId) ParseArtistNameCredit(XmlReader reader) + foreach (var result in releaseSearchResults) { - reader.MoveToContent(); - reader.Read(); - - // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "artist": - { - if (reader.IsEmptyElement) - { - reader.Read(); - break; - } - - var id = reader.GetAttribute("id"); - using var subReader = reader.ReadSubtree(); - return ParseArtistArtistCredit(subReader, id); - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } - - return (null, null); + yield return GetReleaseResult(result); } + } - private static (string Name, string ArtistId) ParseArtistArtistCredit(XmlReader reader, string artistId) + private RemoteSearchResult GetReleaseResult(IRelease releaseSearchResult) + { + var searchResult = new RemoteSearchResult { - reader.MoveToContent(); - reader.Read(); - - string name = null; + Name = releaseSearchResult.Title, + ProductionYear = releaseSearchResult.Date?.Year, + PremiereDate = releaseSearchResult.Date?.NearestDate + }; - // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator + if (releaseSearchResult.ArtistCredit?.Count > 0) + { + searchResult.AlbumArtist = new RemoteSearchResult + { + SearchProviderName = Name, + Name = releaseSearchResult.ArtistCredit[0].Name + }; - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + if (releaseSearchResult.ArtistCredit[0].Artist?.Id is not null) { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "name": - { - name = reader.ReadElementContentAsString(); - break; - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } + searchResult.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, releaseSearchResult.ArtistCredit[0].Artist!.Id.ToString()); } - - return (name, artistId); } - private async Task<string> GetReleaseIdFromReleaseGroupId(string releaseGroupId, CancellationToken cancellationToken) - { - var url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture); - - using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var oReader = new StreamReader(stream, Encoding.UTF8); - var settings = new XmlReaderSettings - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; - - using var reader = XmlReader.Create(oReader, settings); - var result = ReleaseResult.Parse(reader).FirstOrDefault(); + searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString()); - return result?.ReleaseId; + if (releaseSearchResult.ReleaseGroup?.Id is not null) + { + searchResult.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseSearchResult.ReleaseGroup.Id.ToString()); } - /// <summary> - /// Gets the release group id internal. - /// </summary> - /// <param name="releaseEntryId">The release entry id.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.String}.</returns> - private async Task<string> GetReleaseGroupFromReleaseId(string releaseEntryId, CancellationToken cancellationToken) - { - var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture); + return searchResult; + } - using var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var oReader = new StreamReader(stream, Encoding.UTF8); - var settings = new XmlReaderSettings - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - Async = true - }; + /// <inheritdoc /> + public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken) + { + // TODO: This sets essentially nothing. As-is, it's mostly useless. Make it actually pull metadata and use it. + var releaseId = info.GetReleaseId(); + var releaseGroupId = info.GetReleaseGroupId(); - using var reader = XmlReader.Create(oReader, settings); - await reader.MoveToContentAsync().ConfigureAwait(false); - await reader.ReadAsync().ConfigureAwait(false); + var result = new MetadataResult<MusicAlbum> + { + Item = new MusicAlbum() + }; - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + // If there is a release group, but no release ID, try to match the release + if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId)) + { + // TODO: Actually try to match the release. Simply taking the first result is stupid. + var releaseGroup = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false); + var release = releaseGroup.Releases?.Count > 0 ? releaseGroup.Releases[0] : null; + if (release != null) { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "release-group-list": - { - if (reader.IsEmptyElement) - { - await reader.ReadAsync().ConfigureAwait(false); - continue; - } - - using var subReader = reader.ReadSubtree(); - return GetFirstReleaseGroupId(subReader); - } - - default: - { - await reader.SkipAsync().ConfigureAwait(false); - break; - } - } - } - else - { - await reader.ReadAsync().ConfigureAwait(false); - } + releaseId = release.Id.ToString(); + result.HasMetadata = true; } - - return null; } - private string GetFirstReleaseGroupId(XmlReader reader) + // If there is no release ID, lookup a release with the info we have + if (string.IsNullOrWhiteSpace(releaseId)) { - reader.MoveToContent(); - reader.Read(); + var artistMusicBrainzId = info.GetMusicBrainzArtistId(); + IRelease? releaseResult = null; - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + if (!string.IsNullOrEmpty(artistMusicBrainzId)) { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "release-group": - { - return reader.GetAttribute("id"); - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } + var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{info.Name}\" AND arid:{artistMusicBrainzId}", null, null, false, cancellationToken) + .ConfigureAwait(false); + releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null; } - - return null; - } - - /// <summary> - /// Makes request to MusicBrainz server and awaits a response. - /// A 503 Service Unavailable response indicates throttling to maintain a rate limit. - /// A number of retries shall be made in order to try and satisfy the request before - /// giving up and returning null. - /// </summary> - /// <param name="url">Address of MusicBrainz server.</param> - /// <param name="cancellationToken">CancellationToken to use for method.</param> - /// <returns>Returns response from MusicBrainz service.</returns> - internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken) - { - await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - try + else if (!string.IsNullOrEmpty(info.GetAlbumArtist())) { - HttpResponseMessage response; - var attempts = 0u; - var requestUrl = _musicBrainzBaseUrl.TrimEnd('/') + url; + var releaseSearchResults = await _musicBrainzQuery.FindReleasesAsync($"\"{info.Name}\" AND artist:{info.GetAlbumArtist()}", null, null, false, cancellationToken) + .ConfigureAwait(false); + releaseResult = releaseSearchResults.Results.Count > 0 ? releaseSearchResults.Results[0].Item : null; + } - do - { - attempts++; - - if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs) - { - // MusicBrainz is extremely adamant about limiting to one request per second. - var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds; - await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false); - } - - // Write time since last request to debug log as evidence we're meeting rate limit - // requirement, before resetting stopwatch back to zero. - _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); - _stopWatchMusicBrainz.Restart(); - - using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); - response = await _httpClientFactory - .CreateClient(NamedClient.MusicBrainz) - .SendAsync(request, cancellationToken) - .ConfigureAwait(false); - - // We retry a finite number of times, and only whilst MB is indicating 503 (throttling). - } - while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable); + if (releaseResult != null) + { + releaseId = releaseResult.Id.ToString(); - // Log error if unable to query MB database due to throttling. - if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable) + if (releaseResult.ReleaseGroup?.Id is not null) { - _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl); + releaseGroupId = releaseResult.ReleaseGroup.Id.ToString(); } - return response; - } - finally - { - _apiRequestLock.Release(); + result.HasMetadata = true; + result.Item.ProductionYear = releaseResult.Date?.Year; + result.Item.Overview = releaseResult.Annotation; } } - /// <inheritdoc /> - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + // If we have a release ID but not a release group ID, lookup the release group + if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId)) { - throw new NotImplementedException(); + var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false); + releaseGroupId = release.ReleaseGroup?.Id.ToString(); + result.HasMetadata = true; } - protected virtual void Dispose(bool disposing) + // If we have a release ID and a release group ID + if (!string.IsNullOrWhiteSpace(releaseId) || !string.IsNullOrWhiteSpace(releaseGroupId)) { - if (disposing) - { - _apiRequestLock?.Dispose(); - } + result.HasMetadata = true; } - /// <inheritdoc /> - public void Dispose() + if (result.HasMetadata) { - Dispose(true); - GC.SuppressFinalize(this); - } - - private class ReleaseResult - { - public string ReleaseId; - public string ReleaseGroupId; - public string Title; - public string Overview; - public int? Year; - - public List<(string, string)> Artists = new(); - - public static IEnumerable<ReleaseResult> Parse(XmlReader reader) + if (!string.IsNullOrEmpty(releaseId)) { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "release-list": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using var subReader = reader.ReadSubtree(); - return ParseReleaseList(subReader).ToList(); - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } - - return Enumerable.Empty<ReleaseResult>(); + result.Item.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseId); } - private static IEnumerable<ReleaseResult> ParseReleaseList(XmlReader reader) + if (!string.IsNullOrEmpty(releaseGroupId)) { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "release": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - var releaseId = reader.GetAttribute("id"); - - using var subReader = reader.ReadSubtree(); - var release = ParseRelease(subReader, releaseId); - if (release != null) - { - yield return release; - } - - break; - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } + result.Item.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, releaseGroupId); } + } - private static ReleaseResult ParseRelease(XmlReader reader, string releaseId) - { - var result = new ReleaseResult - { - ReleaseId = releaseId - }; - - reader.MoveToContent(); - reader.Read(); + return result; + } - // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator + /// <inheritdoc /> + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "title": - { - result.Title = reader.ReadElementContentAsString(); - break; - } - - case "date": - { - var val = reader.ReadElementContentAsString(); - if (DateTime.TryParse(val, out var date)) - { - result.Year = date.Year; - } - - break; - } - - case "annotation": - { - result.Overview = reader.ReadElementContentAsString(); - break; - } - - case "release-group": - { - result.ReleaseGroupId = reader.GetAttribute("id"); - reader.Skip(); - break; - } - - case "artist-credit": - { - if (reader.IsEmptyElement) - { - reader.Read(); - break; - } - - using var subReader = reader.ReadSubtree(); - var artist = ParseArtistCredit(subReader); - - if (!string.IsNullOrEmpty(artist.Name)) - { - result.Artists.Add(artist); - } - - break; - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - return result; - } + /// <summary> + /// Dispose all resources. + /// </summary> + /// <param name="disposing">Whether to dispose.</param> + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _musicBrainzQuery.Dispose(); } } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs index 941ffea72..b89e67270 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs @@ -1,28 +1,27 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.MusicBrainz; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// <summary> +/// MusicBrainz artist external id. +/// </summary> +public class MusicBrainzArtistExternalId : IExternalId { - public class MusicBrainzArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzArtist.ToString(); + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzArtist.ToString(); - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; - /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + /// <inheritdoc /> + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is MusicArtist; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is MusicArtist; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index 906a42f36..2cc3a13be 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -1,15 +1,7 @@ -#nullable disable - -#pragma warning disable CS1591 - using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Linq; -using System.Net; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -18,257 +10,152 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.MusicBrainz; - -namespace MediaBrowser.Providers.Music -{ - public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo> - { - public string Name => "MusicBrainz"; +using MediaBrowser.Providers.Music; +using MetaBrainz.MusicBrainz; +using MetaBrainz.MusicBrainz.Interfaces.Entities; +using MetaBrainz.MusicBrainz.Interfaces.Searches; - /// <inheritdoc /> - public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) - { - var musicBrainzId = searchInfo.GetMusicBrainzArtistId(); +namespace MediaBrowser.Providers.Plugins.MusicBrainz; - if (!string.IsNullOrWhiteSpace(musicBrainzId)) - { - var url = "/ws/2/artist/?query=arid:{0}" + musicBrainzId.ToString(CultureInfo.InvariantCulture); +/// <summary> +/// MusicBrainz artist provider. +/// </summary> +public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable +{ + private readonly Query _musicBrainzQuery; - using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return GetResultsFromResponse(stream); - } - else + /// <summary> + /// Initializes a new instance of the <see cref="MusicBrainzArtistProvider"/> class. + /// </summary> + public MusicBrainzArtistProvider() + { + MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) => { - // They seem to throw bad request failures on any term with a slash - var nameToSearch = searchInfo.Name.Replace('/', ' '); - - var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch)); - - using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) - await using (var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) - { - var results = GetResultsFromResponse(stream).ToList(); + Query.DefaultServer = MusicBrainz.Plugin.Instance.Configuration.Server; + Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit; + }; - if (results.Count > 0) - { - return results; - } - } + _musicBrainzQuery = new Query(); + } - if (searchInfo.Name.HasDiacritics()) - { - // Try again using the search with accent characters url - url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch)); + /// <inheritdoc /> + public string Name => "MusicBrainz"; - using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return GetResultsFromResponse(stream); - } - } + /// <inheritdoc /> + public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) + { + var artistId = searchInfo.GetMusicBrainzArtistId(); - return Enumerable.Empty<RemoteSearchResult>(); + if (!string.IsNullOrWhiteSpace(artistId)) + { + var artistResult = await _musicBrainzQuery.LookupArtistAsync(new Guid(artistId), Include.Aliases, null, null, cancellationToken).ConfigureAwait(false); + return GetResultFromResponse(artistResult).SingleItemAsEnumerable(); } - private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream) + var artistSearchResults = await _musicBrainzQuery.FindArtistsAsync($"\"{searchInfo.Name}\"", null, null, false, cancellationToken) + .ConfigureAwait(false); + if (artistSearchResults.Results.Count > 0) { - using var oReader = new StreamReader(stream, Encoding.UTF8); - var settings = new XmlReaderSettings() - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; - - using var reader = XmlReader.Create(oReader, settings); - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "artist-list": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using var subReader = reader.ReadSubtree(); - return ParseArtistList(subReader).ToList(); - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } - - return Enumerable.Empty<RemoteSearchResult>(); + return GetResultsFromResponse(artistSearchResults.Results); } - private IEnumerable<RemoteSearchResult> ParseArtistList(XmlReader reader) + if (searchInfo.Name.HasDiacritics()) { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + // Try again using the search with an accented characters query + var artistAccentsSearchResults = await _musicBrainzQuery.FindArtistsAsync($"artistaccent:\"{searchInfo.Name}\"", null, null, false, cancellationToken) + .ConfigureAwait(false); + if (artistAccentsSearchResults.Results.Count > 0) { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "artist": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - var mbzId = reader.GetAttribute("id"); - - using var subReader = reader.ReadSubtree(); - var artist = ParseArtist(subReader, mbzId); - if (artist != null) - { - yield return artist; - } - - break; - } - - default: - { - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } + return GetResultsFromResponse(artistAccentsSearchResults.Results); } } - private RemoteSearchResult ParseArtist(XmlReader reader, string artistId) - { - var result = new RemoteSearchResult(); - - reader.MoveToContent(); - reader.Read(); + return Enumerable.Empty<RemoteSearchResult>(); + } - // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator + private IEnumerable<RemoteSearchResult> GetResultsFromResponse(IEnumerable<ISearchResult<IArtist>>? releaseSearchResults) + { + if (releaseSearchResults is null) + { + yield break; + } - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "name": - { - result.Name = reader.ReadElementContentAsString(); - break; - } + foreach (var result in releaseSearchResults) + { + yield return GetResultFromResponse(result.Item); + } + } - case "annotation": - { - result.Overview = reader.ReadElementContentAsString(); - break; - } + private RemoteSearchResult GetResultFromResponse(IArtist artist) + { + var searchResult = new RemoteSearchResult + { + Name = artist.Name, + ProductionYear = artist.LifeSpan?.Begin?.Year, + PremiereDate = artist.LifeSpan?.Begin?.NearestDate + }; - default: - { - // there is sort-name if ever needed - reader.Skip(); - break; - } - } - } - else - { - reader.Read(); - } - } + searchResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Id.ToString()); - result.SetProviderId(MetadataProvider.MusicBrainzArtist, artistId); + return searchResult; + } - if (string.IsNullOrWhiteSpace(artistId) || string.IsNullOrWhiteSpace(result.Name)) - { - return null; - } + /// <inheritdoc /> + public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken) + { + var result = new MetadataResult<MusicArtist> { Item = new MusicArtist() }; - return result; - } + var musicBrainzId = info.GetMusicBrainzArtistId(); - /// <inheritdoc /> - public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken) + if (string.IsNullOrWhiteSpace(musicBrainzId)) { - var result = new MetadataResult<MusicArtist> - { - Item = new MusicArtist() - }; + var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false); - var musicBrainzId = info.GetMusicBrainzArtistId(); + var singleResult = searchResults.FirstOrDefault(); - if (string.IsNullOrWhiteSpace(musicBrainzId)) + if (singleResult != null) { - var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false); + musicBrainzId = singleResult.GetProviderId(MetadataProvider.MusicBrainzArtist); + result.Item.Overview = singleResult.Overview; - var singleResult = searchResults.FirstOrDefault(); - - if (singleResult != null) + if (Plugin.Instance!.Configuration.ReplaceArtistName) { - musicBrainzId = singleResult.GetProviderId(MetadataProvider.MusicBrainzArtist); - result.Item.Overview = singleResult.Overview; - - if (Plugin.Instance.Configuration.ReplaceArtistName) - { - result.Item.Name = singleResult.Name; - } + result.Item.Name = singleResult.Name; } } - - if (!string.IsNullOrWhiteSpace(musicBrainzId)) - { - result.HasMetadata = true; - result.Item.SetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzId); - } - - return result; } - /// <summary> - /// Encodes an URL. - /// </summary> - /// <param name="name">The name.</param> - /// <returns>System.String.</returns> - private static string UrlEncode(string name) + if (!string.IsNullOrWhiteSpace(musicBrainzId)) { - return WebUtility.UrlEncode(name); + result.HasMetadata = true; + result.Item.SetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzId); } - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + return result; + } + + /// <inheritdoc /> + public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Dispose all resources. + /// </summary> + /// <param name="disposing">Whether to dispose.</param> + protected virtual void Dispose(bool disposing) + { + if (disposing) { - throw new NotImplementedException(); + _musicBrainzQuery.Dispose(); } } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs index 05db2d98f..fdaa5574f 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs @@ -1,28 +1,27 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.MusicBrainz; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// <summary> +/// MusicBrainz other artist external id. +/// </summary> +public class MusicBrainzOtherArtistExternalId : IExternalId { - public class MusicBrainzOtherArtistExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzArtist.ToString(); + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzArtist.ToString(); - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; - /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}"; + /// <inheritdoc /> + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs index acb652fe0..0baab9955 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs @@ -1,28 +1,27 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.MusicBrainz; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// <summary> +/// MusicBrainz release group external id. +/// </summary> +public class MusicBrainzReleaseGroupExternalId : IExternalId { - public class MusicBrainzReleaseGroupExternalId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString(); + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString(); - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; - /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}"; + /// <inheritdoc /> + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}"; - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs index 14805b9b7..5c974c411 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs @@ -1,28 +1,27 @@ -#pragma warning disable CS1591 - using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Plugins.MusicBrainz; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// <summary> +/// MusicBrainz track id. +/// </summary> +public class MusicBrainzTrackId : IExternalId { - public class MusicBrainzTrackId : IExternalId - { - /// <inheritdoc /> - public string ProviderName => "MusicBrainz"; + /// <inheritdoc /> + public string ProviderName => "MusicBrainz"; - /// <inheritdoc /> - public string Key => MetadataProvider.MusicBrainzTrack.ToString(); + /// <inheritdoc /> + public string Key => MetadataProvider.MusicBrainzTrack.ToString(); - /// <inheritdoc /> - public ExternalIdMediaType? Type => ExternalIdMediaType.Track; + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Track; - /// <inheritdoc /> - public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}"; + /// <inheritdoc /> + public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}"; - /// <inheritdoc /> - public bool Supports(IHasProviderIds item) => item is Audio; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs index cfa10dd64..39cfd727f 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs @@ -1,45 +1,64 @@ -#nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; +using System.Net.Http.Headers; +using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; +using MetaBrainz.MusicBrainz; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; -namespace MediaBrowser.Providers.Plugins.MusicBrainz +/// <summary> +/// Plugin instance. +/// </summary> +public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages { - public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages + /// <summary> + /// Initializes a new instance of the <see cref="Plugin"/> class. + /// </summary> + /// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> + /// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param> + /// <param name="applicationHost">Instance of the <see cref="IApplicationHost"/> interface.</param> + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, IApplicationHost applicationHost) + : base(applicationPaths, xmlSerializer) { - public const string DefaultServer = "https://musicbrainz.org"; - - public const long DefaultRateLimit = 2000u; + Instance = this; - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - : base(applicationPaths, xmlSerializer) - { - Instance = this; - } + // TODO: Change this to "JellyfinMusicBrainzPlugin" once we take it out of the server repo. + Query.DefaultUserAgent.Add(new ProductInfoHeaderValue(applicationHost.Name.Replace(' ', '-'), applicationHost.ApplicationVersionString)); + Query.DefaultUserAgent.Add(new ProductInfoHeaderValue($"({applicationHost.ApplicationUserAgentAddress})")); + Query.DelayBetweenRequests = Instance.Configuration.RateLimit; + Query.DefaultServer = Instance.Configuration.Server; + } - public static Plugin Instance { get; private set; } + /// <summary> + /// Gets the current plugin instance. + /// </summary> + public static Plugin? Instance { get; private set; } - public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"); + /// <inheritdoc /> + public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"); - public override string Name => "MusicBrainz"; + /// <inheritdoc /> + public override string Name => "MusicBrainz"; - public override string Description => "Get artist and album metadata from any MusicBrainz server."; + /// <inheritdoc /> + public override string Description => "Get artist and album metadata from any MusicBrainz server."; - // TODO remove when plugin removed from server. - public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml"; + /// <inheritdoc /> + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml"; - public IEnumerable<PluginPageInfo> GetPages() + /// <inheritdoc /> + public IEnumerable<PluginPageInfo> GetPages() + { + yield return new PluginPageInfo { - yield return new PluginPageInfo - { - Name = Name, - EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html" - }; - } + Name = Name, + EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html" + }; } } 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/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 0bae42bc8..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/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-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 20aa777b6..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/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-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 ccc0f76cd..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/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-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 893180974..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/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-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 bf1edf777..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/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-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.Extensions/EnumerableExtensions.cs b/src/Jellyfin.Extensions/EnumerableExtensions.cs index a31a57dc6..fd46358a4 100644 --- a/src/Jellyfin.Extensions/EnumerableExtensions.cs +++ b/src/Jellyfin.Extensions/EnumerableExtensions.cs @@ -1,42 +1,31 @@ using System; using System.Collections.Generic; -namespace Jellyfin.Extensions +namespace Jellyfin.Extensions; + +/// <summary> +/// Static extensions for the <see cref="IEnumerable{T}"/> interface. +/// </summary> +public static class EnumerableExtensions { /// <summary> - /// Static extensions for the <see cref="IEnumerable{T}"/> interface. + /// Determines whether the value is contained in the source collection. /// </summary> - public static class EnumerableExtensions + /// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param> + /// <param name="value">The value to look for in the collection.</param> + /// <param name="stringComparison">The string comparison.</param> + /// <returns>A value indicating whether the value is contained in the collection.</returns> + /// <exception cref="ArgumentNullException">The source is null.</exception> + public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison) { - /// <summary> - /// Determines whether the value is contained in the source collection. - /// </summary> - /// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param> - /// <param name="value">The value to look for in the collection.</param> - /// <param name="stringComparison">The string comparison.</param> - /// <returns>A value indicating whether the value is contained in the collection.</returns> - /// <exception cref="ArgumentNullException">The source is null.</exception> - public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison) - { - ArgumentNullException.ThrowIfNull(source); - - if (source is IList<string> list) - { - int len = list.Count; - for (int i = 0; i < len; i++) - { - if (value.Equals(list[i], stringComparison)) - { - return true; - } - } - - return false; - } + ArgumentNullException.ThrowIfNull(source); - foreach (string element in source) + if (source is IList<string> list) + { + int len = list.Count; + for (int i = 0; i < len; i++) { - if (value.Equals(element, stringComparison)) + if (value.Equals(list[i], stringComparison)) { return true; } @@ -44,5 +33,26 @@ namespace Jellyfin.Extensions return false; } + + foreach (string element in source) + { + if (value.Equals(element, stringComparison)) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Gets an IEnumerable from a single item. + /// </summary> + /// <param name="item">The item to return.</param> + /// <typeparam name="T">The type of item.</typeparam> + /// <returns>The IEnumerable{T}.</returns> + public static IEnumerable<T> SingleItemAsEnumerable<T>(this T item) + { + yield return item; } } 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> diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs index eea8cb50a..8f276d03f 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs @@ -7,7 +7,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Music; +using MediaBrowser.Providers.Plugins.MusicBrainz; using MediaBrowser.XbmcMetadata.Parsers; using Microsoft.Extensions.Logging.Abstractions; using Moq; diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs index 8ca3dd96e..78183d9ff 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs @@ -7,7 +7,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.Music; +using MediaBrowser.Providers.Plugins.MusicBrainz; using MediaBrowser.XbmcMetadata.Parsers; using Microsoft.Extensions.Logging.Abstractions; using Moq; |
