aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-abi.yml12
-rw-r--r--.ci/azure-pipelines-main.yml8
-rw-r--r--.ci/azure-pipelines-package.yml16
-rw-r--r--.ci/azure-pipelines-test.yml2
-rw-r--r--.github/workflows/automation.yml2
-rw-r--r--.github/workflows/codeql-analysis.yml8
-rw-r--r--.github/workflows/commands.yml10
-rw-r--r--.github/workflows/openapi.yml16
-rw-r--r--Dockerfile2
-rw-r--r--Dockerfile.arm2
-rw-r--r--Dockerfile.arm642
-rw-r--r--Emby.Dlna/Eventing/DlnaEventManager.cs3
-rw-r--r--Emby.Naming/AudioBook/AudioBookListResolver.cs3
-rw-r--r--Emby.Naming/Common/NamingOptions.cs15
-rw-r--r--Emby.Notifications/NotificationManager.cs3
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs10
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs8
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs7
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj4
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs2
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs20
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs15
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/et.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/km.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json3
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs1
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs26
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs36
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs2
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj8
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs657
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs28
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs32
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs17
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs14
-rw-r--r--Jellyfin.Server/Filters/AdditionalModelFilter.cs11
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj4
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs5
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs4
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs10
-rw-r--r--MediaBrowser.Controller/Library/ILibraryMonitor.cs7
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs14
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs2
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs46
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs10
-rw-r--r--MediaBrowser.Model/Entities/ExtraType.cs4
-rw-r--r--MediaBrowser.Model/Entities/SeriesStatus.cs13
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj4
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj1
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs65
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs878
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs325
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs71
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs5
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--src/Jellyfin.Extensions/EnumerableExtensions.cs70
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs23
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj2
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs26
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs5
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs70
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml10
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs2
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;