diff options
739 files changed, 10287 insertions, 5321 deletions
diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml deleted file mode 100644 index 0e944e6f4..000000000 --- a/.ci/azure-pipelines-api-client.yml +++ /dev/null @@ -1,59 +0,0 @@ -parameters: - - name: LinuxImage - type: string - default: "ubuntu-latest" - - name: GeneratorVersion - type: string - default: "5.0.1" - -jobs: -- job: GenerateApiClients - displayName: 'Generate Api Clients' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - dependsOn: Test - - pool: - vmImage: "${{ parameters.LinuxImage }}" - - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download OpenAPI Spec Artifact' - inputs: - source: 'current' - artifact: "OpenAPI Spec" - path: "$(System.ArtifactsDirectory)/openapispec" - runVersion: "latest" - - - task: CmdLine@2 - displayName: 'Download OpenApi Generator' - inputs: - script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" - -## Authenticate with npm registry - - task: npmAuthenticate@0 - inputs: - workingFile: ./.npmrc - customEndpoint: 'jellyfin-bot for NPM' - -## Generate npm api client - - task: CmdLine@2 - displayName: 'Build stable typescript axios client' - inputs: - script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)" - -## Run npm install - - task: Npm@1 - displayName: 'Install npm dependencies' - inputs: - command: install - workingDir: ./apiclient/generated/typescript/axios - -## Publish npm packages - - task: Npm@1 - displayName: 'Publish stable typescript axios client' - inputs: - command: custom - customCommand: publish --access public - publishRegistry: useExternalRegistry - publishEndpoint: 'jellyfin-bot for NPM' - workingDir: ./apiclient/generated/typescript/axios diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 20f4dfe33..543fd7fc6 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -160,7 +160,6 @@ jobs: dependsOn: - BuildPackage - BuildDocker - condition: and(succeeded('BuildPackage'), succeeded('BuildDocker')) pool: vmImage: 'ubuntu-latest' @@ -186,9 +185,6 @@ jobs: - job: PublishNuget displayName: 'Publish NuGet packages' - dependsOn: - - BuildPackage - condition: succeeded('BuildPackage') pool: vmImage: 'ubuntu-latest' diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index 95e0d8c58..7838b3b02 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -94,5 +94,5 @@ jobs: displayName: 'Publish OpenAPI Artifact' condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) inputs: - targetPath: "tests/Jellyfin.Api.Tests/bin/Release/net5.0/openapi.json" + targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json" artifactName: 'OpenAPI Spec' diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 6430503f9..c028b6e3e 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -61,6 +61,3 @@ jobs: - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - template: azure-pipelines-package.yml - -- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - - template: azure-pipelines-api-client.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0874cae2e..70bcd4973 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,4 +6,10 @@ updates: interval: weekly time: '12:00' open-pull-requests-limit: 10 - + +- package-ecosystem: github-actions + directory: '/' + schedule: + interval: weekly + time: '12:00' + open-pull-requests-limit: 10 diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml new file mode 100644 index 000000000..2529d8099 --- /dev/null +++ b/.github/workflows/automation.yml @@ -0,0 +1,64 @@ +name: Automation + +on: + pull_request: + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Does PR has the stable backport label? + uses: Dreamcodeio/does-pr-has-label@v1.2 + id: checkLabel + with: + label: stable backport + + - name: Remove from 'Current Release' project + uses: alex-page/github-project-automation-plus@v0.7.1 + if: (github.event.pull_request || github.event.issue.pull_request) && !steps.checkLabel.outputs.hasLabel + continue-on-error: true + with: + project: Current Release + action: delete + repo-token: ${{ secrets.GH_TOKEN }} + + - name: Add to 'Release Next' project + uses: alex-page/github-project-automation-plus@v0.7.1 + if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened' + continue-on-error: true + with: + project: Release Next + column: In progress + repo-token: ${{ secrets.GH_TOKEN }} + + - name: Add to 'Current Release' project + uses: alex-page/github-project-automation-plus@v0.7.1 + if: (github.event.pull_request || github.event.issue.pull_request) && steps.checkLabel.outputs.hasLabel + continue-on-error: true + with: + project: Current Release + column: In progress + repo-token: ${{ secrets.GH_TOKEN }} + + - name: Check number of comments from the team member + if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' + id: member_comments + run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)" + + - name: Move issue to needs triage + uses: alex-page/github-project-automation-plus@v0.7.1 + if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1 + continue-on-error: true + with: + project: Issue Triage for Main Repo + column: Needs triage + repo-token: ${{ secrets.GH_TOKEN }} + + - name: Add issue to triage project + uses: alex-page/github-project-automation-plus@v0.7.1 + if: github.event.issue.pull_request == '' && github.event.action == 'opened' + continue-on-error: true + with: + project: Issue Triage for Main Repo + column: Pending response + repo-token: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/label-commenter-config.yml b/.github/workflows/label-commenter-config.yml new file mode 100644 index 000000000..78b75be43 --- /dev/null +++ b/.github/workflows/label-commenter-config.yml @@ -0,0 +1,43 @@ +comment: + header: Hello @{{ issue.user.login }} + footer: "\ + ---\n\n + > This is an automated comment created by the [peaceiris/actions-label-commenter]. \ + Responding to the bot or mentioning it won't have any effect.\n\n + [peaceiris/actions-label-commenter]: https://github.com/peaceiris/actions-label-commenter + " + +labels: + - name: stable backport + labeled: + pr: + body: | + This pull request has been tagged as a stable backport. It will be cherry-picked into the next stable point release. + + Please observe the following: + + * Any dependent PRs that this PR requires **must** be tagged for stable backporting as well. + + * Any issue(s) this PR fixes or closes **should** target the current stable release or a previous stable release to which a fix has not yet entered the current stable release. + + * This PR **must** be test cherry-picked against the current release branch (`release-X.Y.z` where X and Y are numbers). It must apply cleanly, or a diff of the expected change must be provided. + + To do this, run the following commands from your local copy of the Jellyfin repository: + + 1. `git checkout master` + + 1. `git merge --no-ff <myPullRequestBranch>` + + 1. `git log` -> `commit xxxxxxxxx`, grab hash + + 1. `git checkout release-X.Y.z` replacing X and Y with the *current* stable version (e.g. `release-10.7.z`) + + 1. `git cherry-pick -sx -m1 <hash>` + + Ensure the `cherry-pick` applies cleanly. If it does not, fix any merge conflicts *preserving as much of the original code as possible*, and make note of the resulting diff. + + Test your changes with a build to ensure they are successful. If not, adjust the diff accordingly. + + **Do not** push your merges to either branch. Use `git reset --hard HEAD~1` to revert both branches to their original state. + + Reply to this PR with a comment beginning "Cherry-pick test completed." and including the merge-conflict-fixing diff(s) if applicable. diff --git a/.github/workflows/label-commenter.yml b/.github/workflows/label-commenter.yml new file mode 100644 index 000000000..be9216cc1 --- /dev/null +++ b/.github/workflows/label-commenter.yml @@ -0,0 +1,22 @@ +name: Label Commenter + +on: + issues: + types: + - labeled + - unlabeled + pull_request_target: + types: + - labeled + - unlabeled + +jobs: + comment: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + with: + ref: master + + - name: Label Commenter + uses: peaceiris/actions-label-commenter@v1 diff --git a/.github/workflows/merge-conflicts.yml b/.github/workflows/merge-conflicts.yml new file mode 100644 index 000000000..ce808617a --- /dev/null +++ b/.github/workflows/merge-conflicts.yml @@ -0,0 +1,17 @@ +name: 'Merge Conflicts' + +on: + push: + branches: + - master + pull_request_target: + types: + - synchronize +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: eps1lon/actions-label-merge-conflict@v2.0.1 + with: + dirtyLabel: 'merge conflict' + repoToken: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml new file mode 100644 index 000000000..3172ec0d9 --- /dev/null +++ b/.github/workflows/rebase.yml @@ -0,0 +1,27 @@ +name: Automatic Rebase +on: + issue_comment: + +jobs: + rebase: + name: Rebase + if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER' + runs-on: ubuntu-latest + steps: + - name: Notify as seen + uses: peter-evans/create-or-update-comment@v1.4.5 + with: + token: ${{ secrets.GH_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: '+1' + + - name: Checkout the latest code + uses: actions/checkout@v2 + with: + token: ${{ secrets.GH_TOKEN }} + fetch-depth: 0 + + - name: Automatic Rebase + uses: cirrus-actions/rebase@1.4 + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1200275d5..7a763a46c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -17,6 +17,7 @@ - [bugfixin](https://github.com/bugfixin) - [chaosinnovator](https://github.com/chaosinnovator) - [ckcr4lyf](https://github.com/ckcr4lyf) + - [cocool97](https://github.com/cocool97) - [ConfusedPolarBear](https://github.com/ConfusedPolarBear) - [crankdoofus](https://github.com/crankdoofus) - [crobibero](https://github.com/crobibero) @@ -49,6 +50,7 @@ - [h1nk](https://github.com/h1nk) - [hawken93](https://github.com/hawken93) - [HelloWorld017](https://github.com/HelloWorld017) + - [ikomhoog](https://github.com/ikomhoog) - [jftuga](https://github.com/jftuga) - [joern-h](https://github.com/joern-h) - [joshuaboniface](https://github.com/joshuaboniface) @@ -104,6 +106,7 @@ - [shemanaev](https://github.com/shemanaev) - [skaro13](https://github.com/skaro13) - [sl1288](https://github.com/sl1288) + - [Smith00101010](https://github.com/Smith00101010) - [sorinyo2004](https://github.com/sorinyo2004) - [sparky8251](https://github.com/sparky8251) - [spookbits](https://github.com/spookbits) diff --git a/Dockerfile b/Dockerfile index 41dd3d081..ebe5eb00c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,10 @@ ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ - && yarn install \ + && npm ci --no-audit \ && mv dist /dist -FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim as builder +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 diff --git a/Dockerfile.arm b/Dockerfile.arm index e0eaca0ed..d63dbee75 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -10,7 +10,7 @@ ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ - && yarn install \ + && npm ci --no-audit \ && mv dist /dist diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index db7de935c..e95999f2a 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -10,7 +10,7 @@ ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ - && yarn install \ + && npm ci --no-audit \ && mv dist /dist diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs index e63a85860..5ceeb5530 100644 --- a/Emby.Dlna/Configuration/DlnaOptions.cs +++ b/Emby.Dlna/Configuration/DlnaOptions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 namespace Emby.Dlna.Configuration diff --git a/Emby.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs index fc02e1751..3ca43052a 100644 --- a/Emby.Dlna/ConfigurationExtension.cs +++ b/Emby.Dlna/ConfigurationExtension.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 using Emby.Dlna.Configuration; diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs index 2f8d197a7..1a1790ee6 100644 --- a/Emby.Dlna/ConnectionManager/ControlHandler.cs +++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs @@ -31,7 +31,7 @@ namespace Emby.Dlna.ConnectionManager } /// <inheritdoc /> - protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) + protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter) { if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase)) { diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs index 2f3107450..7b8c50440 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 27f1fdaba..27c5b2268 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -1,5 +1,6 @@ +#nullable disable + using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -7,7 +8,6 @@ using System.Linq; using System.Text; using System.Threading; using System.Xml; -using Emby.Dlna.Configuration; using Emby.Dlna.Didl; using Emby.Dlna.Service; using Jellyfin.Data.Entities; @@ -121,7 +121,7 @@ namespace Emby.Dlna.ContentDirectory } /// <inheritdoc /> - protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) + protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter) { if (xmlWriter == null) { @@ -201,8 +201,8 @@ namespace Emby.Dlna.ContentDirectory /// <summary> /// Adds a "XSetBookmark" element to the xml document. /// </summary> - /// <param name="sparams">The <see cref="IDictionary"/>.</param> - private void HandleXSetBookmark(IDictionary<string, string> sparams) + /// <param name="sparams">The method parameters.</param> + private void HandleXSetBookmark(IReadOnlyDictionary<string, string> sparams) { var id = sparams["ObjectID"]; @@ -306,34 +306,17 @@ namespace Emby.Dlna.ContentDirectory } /// <summary> - /// Returns the value in the key of the dictionary, or defaultValue if it doesn't exist. - /// </summary> - /// <param name="sparams">The <see cref="IDictionary"/>.</param> - /// <param name="key">The key.</param> - /// <param name="defaultValue">The defaultValue.</param> - /// <returns>The <see cref="string"/>.</returns> - public static string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue) - { - if (sparams != null && sparams.TryGetValue(key, out string val)) - { - return val; - } - - return defaultValue; - } - - /// <summary> /// Builds the "Browse" xml response. /// </summary> /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - /// <param name="sparams">The <see cref="IDictionary"/>.</param> + /// <param name="sparams">The method parameters.</param> /// <param name="deviceId">The device Id to use.</param> - private void HandleBrowse(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) + private void HandleBrowse(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId) { var id = sparams["ObjectID"]; var flag = sparams["BrowseFlag"]; - var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); - var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty)); + var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); + var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty)); var provided = 0; @@ -435,9 +418,9 @@ namespace Emby.Dlna.ContentDirectory /// Builds the response to the "X_BrowseByLetter request. /// </summary> /// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param> - /// <param name="sparams">The <see cref="IDictionary"/>.</param> + /// <param name="sparams">The method parameters.</param> /// <param name="deviceId">The device id.</param> - private void HandleXBrowseByLetter(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) + private void HandleXBrowseByLetter(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId) { // TODO: Implement this method HandleSearch(xmlWriter, sparams, deviceId); @@ -447,13 +430,13 @@ namespace Emby.Dlna.ContentDirectory /// Builds a response to the "Search" request. /// </summary> /// <param name="xmlWriter">The xmlWriter<see cref="XmlWriter"/>.</param> - /// <param name="sparams">The sparams<see cref="IDictionary"/>.</param> + /// <param name="sparams">The method parameters.</param> /// <param name="deviceId">The deviceId<see cref="string"/>.</param> - private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId) + private void HandleSearch(XmlWriter xmlWriter, IReadOnlyDictionary<string, string> sparams, string deviceId) { - var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty)); - var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty)); - var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); + var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", string.Empty)); + var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", string.Empty)); + var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); // sort example: dc:title, dc:date diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs index 4ea4e4e48..8ee6325e9 100644 --- a/Emby.Dlna/ControlRequest.cs +++ b/Emby.Dlna/ControlRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.IO; diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs index d827eef26..a7f2d4a73 100644 --- a/Emby.Dlna/ControlResponse.cs +++ b/Emby.Dlna/ControlResponse.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 8b50d47fb..2982ce97e 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -208,7 +210,8 @@ namespace Emby.Dlna.Didl var targetWidth = streamInfo.TargetWidth; var targetHeight = streamInfo.TargetHeight; - var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader( + var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader( + _profile, streamInfo.Container, streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(), @@ -599,7 +602,8 @@ namespace Emby.Dlna.Didl ? MimeTypes.GetMimeType(filename) : mediaProfile.MimeType; - var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader( + var contentFeatures = ContentFeatureBuilder.BuildAudioHeader( + _profile, streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), targetAudioBitrate, @@ -974,15 +978,28 @@ namespace Emby.Dlna.Didl return; } - var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg"); + // TODO: Remove these default values + var albumArtUrlInfo = GetImageUrl( + imageInfo, + _profile.MaxAlbumArtWidth ?? 10000, + _profile.MaxAlbumArtHeight ?? 10000, + "jpg"); writer.WriteStartElement("upnp", "albumArtURI", NsUpnp); - writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn); - writer.WriteString(albumartUrlInfo.url); + if (!string.IsNullOrEmpty(_profile.AlbumArtPn)) + { + writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn); + } + + writer.WriteString(albumArtUrlInfo.url); writer.WriteFullEndElement(); - // TOOD: Remove these default values - var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg"); + // TODO: Remove these default values + var iconUrlInfo = GetImageUrl( + imageInfo, + _profile.MaxIconWidth ?? 48, + _profile.MaxIconHeight ?? 48, + "jpg"); writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url); if (!_profile.EnableAlbumArtInDidl) @@ -1033,8 +1050,7 @@ namespace Emby.Dlna.Didl var width = albumartUrlInfo.width ?? maxWidth; var height = albumartUrlInfo.height ?? maxHeight; - var contentFeatures = new ContentFeatureBuilder(_profile) - .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn); + var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn); writer.WriteAttributeString( "protocolInfo", @@ -1206,8 +1222,7 @@ namespace Emby.Dlna.Didl if (width.HasValue && height.HasValue) { - var newSize = DrawingUtils.Resize( - new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight); + var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight); width = newSize.Width; height = newSize.Height; diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs index 2b86ea333..b66f53ece 100644 --- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs +++ b/Emby.Dlna/Didl/StringWriterWithEncoding.cs @@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl { public class StringWriterWithEncoding : StringWriter { - private readonly Encoding _encoding; + private readonly Encoding? _encoding; public StringWriterWithEncoding() { diff --git a/Emby.Dlna/DlnaConfigurationFactory.cs b/Emby.Dlna/DlnaConfigurationFactory.cs index 4c6ca869a..6cc6b73a0 100644 --- a/Emby.Dlna/DlnaConfigurationFactory.cs +++ b/Emby.Dlna/DlnaConfigurationFactory.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 9ab324038..a1b106704 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -36,7 +38,7 @@ namespace Emby.Dlna private readonly ILogger<DlnaManager> _logger; private readonly IServerApplicationHost _appHost; private static readonly Assembly _assembly = typeof(DlnaManager).Assembly; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal); @@ -111,7 +113,7 @@ namespace Emby.Dlna if (profile != null) { - _logger.LogDebug("Found matching device profile: {0}", profile.Name); + _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name); } else { @@ -126,92 +128,57 @@ namespace Emby.Dlna var builder = new StringBuilder(); builder.AppendLine("No matching device profile found. The default will need to be used."); - builder.Append("FriendlyName:").AppendLine(profile.FriendlyName); - builder.Append("Manufacturer:").AppendLine(profile.Manufacturer); - builder.Append("ManufacturerUrl:").AppendLine(profile.ManufacturerUrl); - builder.Append("ModelDescription:").AppendLine(profile.ModelDescription); - builder.Append("ModelName:").AppendLine(profile.ModelName); - builder.Append("ModelNumber:").AppendLine(profile.ModelNumber); - builder.Append("ModelUrl:").AppendLine(profile.ModelUrl); - builder.Append("SerialNumber:").AppendLine(profile.SerialNumber); + builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName); + builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer); + builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl); + builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription); + builder.Append("ModelName: ").AppendLine(profile.ModelName); + builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber); + builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl); + builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber); _logger.LogInformation(builder.ToString()); } - private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo) + /// <summary> + /// Attempts to match a device with a profile. + /// Rules: + /// - If the profile field has no value, the field matches irregardless of its contents. + /// - the profile field can be an exact match, or a reg exp. + /// </summary> + /// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param> + /// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param> + /// <returns><b>True</b> if they match.</returns> + public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo) { - if (!string.IsNullOrEmpty(profileInfo.FriendlyName)) - { - if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)) - { - return false; - } - } - - if (!string.IsNullOrEmpty(profileInfo.Manufacturer)) - { - if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)) - { - return false; - } - } - - if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl)) - { - if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)) - { - return false; - } - } - - if (!string.IsNullOrEmpty(profileInfo.ModelDescription)) - { - if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)) - { - return false; - } - } - - if (!string.IsNullOrEmpty(profileInfo.ModelName)) - { - if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)) - { - return false; - } - } - - if (!string.IsNullOrEmpty(profileInfo.ModelNumber)) - { - if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)) - { - return false; - } - } + return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName) + && IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer) + && IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl) + && IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription) + && IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName) + && IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber) + && IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl) + && IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber); + } - if (!string.IsNullOrEmpty(profileInfo.ModelUrl)) + private bool IsRegexOrSubstringMatch(string input, string pattern) + { + if (string.IsNullOrEmpty(pattern)) { - if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)) - { - return false; - } + // In profile identification: An empty pattern matches anything. + return true; } - if (!string.IsNullOrEmpty(profileInfo.SerialNumber)) + if (string.IsNullOrEmpty(input)) { - if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber)) - { - return false; - } + // The profile contains a value, and the device doesn't. + return false; } - return true; - } - - private bool IsRegexOrSubstringMatch(string input, string pattern) - { try { - return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + return input.Equals(pattern, StringComparison.OrdinalIgnoreCase) + || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } catch (ArgumentException ex) { @@ -333,7 +300,12 @@ namespace Emby.Dlna throw new ArgumentNullException(nameof(id)); } - var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); + var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); + + if (info == null) + { + return null; + } return ParseProfileFile(info.Path, info.Info.Type); } @@ -395,7 +367,8 @@ namespace Emby.Dlna { Directory.CreateDirectory(systemProfilesPath); - using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 8b057a095..a40578e40 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -21,11 +21,11 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> </PropertyGroup> <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs index 1b1bd426c..8c82dcbf6 100644 --- a/Emby.Dlna/EventSubscriptionResponse.cs +++ b/Emby.Dlna/EventSubscriptionResponse.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index ff81e83b5..2e672b886 100644 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/Eventing/EventSubscription.cs b/Emby.Dlna/Eventing/EventSubscription.cs index 40d73ee0e..4fd7f8169 100644 --- a/Emby.Dlna/Eventing/EventSubscription.cs +++ b/Emby.Dlna/Eventing/EventSubscription.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index ec8716029..0309926ab 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -5,7 +7,6 @@ using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Sockets; -using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; @@ -128,7 +129,8 @@ namespace Emby.Dlna.Main _netConfig = config.GetConfiguration<NetworkConfiguration>("network"); _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps; - if (_disabled) + + if (_disabled && _config.GetDlnaConfiguration().EnableServer) { _logger.LogError("The DLNA specification does not support HTTPS."); } @@ -316,7 +318,7 @@ namespace Emby.Dlna.Main _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri); - if (_appHost.PublishedServerUrl == null) + if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl)) { // DLNA will only work over http, so we must reset to http:// : {port}. uri.Scheme = "http"; diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs index 464f71a6f..d8fb12742 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs @@ -24,7 +24,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar } /// <inheritdoc /> - protected override void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter) + protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter) { if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) { diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs index 37840cd09..f3789a791 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using Emby.Dlna.Common; using Emby.Dlna.Service; -using MediaBrowser.Model.Dlna; namespace Emby.Dlna.MediaReceiverRegistrar { diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 7bf7047fb..5fa1fd589 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -219,7 +221,7 @@ namespace Emby.Dlna.PlayTo { var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); if (command == null) { return false; @@ -259,7 +261,7 @@ namespace Emby.Dlna.PlayTo { var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); if (command == null) { return; @@ -290,7 +292,7 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); if (command == null) { return; @@ -323,7 +325,7 @@ namespace Emby.Dlna.PlayTo _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); if (command == null) { return; @@ -403,6 +405,10 @@ namespace Emby.Dlna.PlayTo public async Task SetPlay(CancellationToken cancellationToken) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); + if (avCommands == null) + { + return; + } await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); @@ -413,7 +419,7 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); if (command == null) { return; @@ -437,7 +443,7 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); + var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); if (command == null) { return; @@ -565,7 +571,7 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); if (command == null) { return; @@ -615,7 +621,7 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); + var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); if (command == null) { return; @@ -702,6 +708,10 @@ namespace Emby.Dlna.PlayTo } var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); + if (rendererCommands == null) + { + return null; + } var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, @@ -770,6 +780,11 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); + if (rendererCommands == null) + { + return (false, null); + } + var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, @@ -951,6 +966,10 @@ namespace Emby.Dlna.PlayTo var httpClient = new SsdpHttpClient(_httpClientFactory); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); + if (document == null) + { + return null; + } AvCommands = TransportCommands.Create(document); return AvCommands; @@ -979,6 +998,10 @@ namespace Emby.Dlna.PlayTo var httpClient = new SsdpHttpClient(_httpClientFactory); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); + if (document == null) + { + return null; + } RendererCommands = TransportCommands.Create(document); return RendererCommands; @@ -1010,6 +1033,10 @@ namespace Emby.Dlna.PlayTo var ssdpHttpClient = new SsdpHttpClient(httpClientFactory); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); + if (document == null) + { + return null; + } var friendlyNames = new List<string>(); diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs index d3daab9e0..2acfff4eb 100644 --- a/Emby.Dlna/PlayTo/DeviceInfo.cs +++ b/Emby.Dlna/PlayTo/DeviceInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs index dabd079af..2bc4d8cc2 100644 --- a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs +++ b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index e4923b9eb..1e6a5fadb 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -132,7 +134,7 @@ namespace Emby.Dlna.PlayTo private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e) { - if (_disposed) + if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url)) { return; } @@ -499,8 +501,8 @@ namespace Emby.Dlna.PlayTo if (streamInfo.MediaType == DlnaProfileType.Audio) { - return new ContentFeatureBuilder(profile) - .BuildAudioHeader( + return ContentFeatureBuilder.BuildAudioHeader( + profile, streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetAudioBitrate, @@ -514,8 +516,8 @@ namespace Emby.Dlna.PlayTo if (streamInfo.MediaType == DlnaProfileType.Video) { - var list = new ContentFeatureBuilder(profile) - .BuildVideoHeader( + var list = ContentFeatureBuilder.BuildVideoHeader( + profile, streamInfo.Container, streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(), @@ -943,11 +945,7 @@ namespace Emby.Dlna.PlayTo request.DeviceId = values.GetValueOrDefault("DeviceId"); request.MediaSourceId = values.GetValueOrDefault("MediaSourceId"); request.LiveStreamId = values.GetValueOrDefault("LiveStreamId"); - - // Be careful, IsDirectStream==true by default (Static != false or not in query). - // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true. - request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase); - + request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase); request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); request.StartPositionTicks = GetLongValue(values, "StartPositionTicks"); diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index a6793a708..35bf5927c 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -178,12 +180,17 @@ namespace Emby.Dlna.PlayTo if (controller == null) { var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false); + if (device == null) + { + _logger.LogError("Ignoring device as xml response is invalid."); + return; + } string deviceName = device.Properties.Name; _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); - string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress); + string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress); controller = new PlayToController( sessionInfo, diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs index d14617c8a..c7d2b28df 100644 --- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs index 3f8d55263..f8a14f411 100644 --- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs index deeb47918..6661f92ac 100644 --- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs index 85846166c..5056e69ae 100644 --- a/Emby.Dlna/PlayTo/PlaylistItem.cs +++ b/Emby.Dlna/PlayTo/PlaylistItem.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Model.Dlna; diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs index e28840a89..657491303 100644 --- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.IO; diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index 557bc69a7..f14f73bb6 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -1,8 +1,9 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Globalization; -using System.IO; using System.Net.Http; using System.Net.Mime; using System.Text; @@ -45,10 +46,10 @@ namespace Emby.Dlna.PlayTo cancellationToken) .ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var reader = new StreamReader(stream, Encoding.UTF8); - return XDocument.Parse( - await reader.ReadToEndAsync().ConfigureAwait(false), - LoadOptions.PreserveWhitespace); + return await XDocument.LoadAsync( + stream, + LoadOptions.PreserveWhitespace, + cancellationToken).ConfigureAwait(false); } private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) @@ -94,10 +95,17 @@ namespace Emby.Dlna.PlayTo options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var reader = new StreamReader(stream, Encoding.UTF8); - return XDocument.Parse( - await reader.ReadToEndAsync().ConfigureAwait(false), - LoadOptions.PreserveWhitespace); + try + { + return await XDocument.LoadAsync( + stream, + LoadOptions.PreserveWhitespace, + cancellationToken).ConfigureAwait(false); + } + catch + { + return null; + } } private async Task<HttpResponseMessage> PostSoapDataAsync( diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs index 0865968ad..b58669355 100644 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ b/Emby.Dlna/PlayTo/TransportCommands.cs @@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo public class TransportCommands { private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>"; - private List<StateVariable> _stateVariables = new List<StateVariable>(); - private List<ServiceAction> _serviceActions = new List<ServiceAction>(); - public List<StateVariable> StateVariables => _stateVariables; + public List<StateVariable> StateVariables { get; } = new List<StateVariable>(); - public List<ServiceAction> ServiceActions => _serviceActions; + public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>(); public static TransportCommands Create(XDocument document) { @@ -48,7 +46,7 @@ namespace Emby.Dlna.PlayTo { var serviceAction = new ServiceAction { - Name = container.GetValue(UPnpNamespaces.Svc + "name"), + Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, }; var argumentList = serviceAction.ArgumentList; @@ -70,9 +68,9 @@ namespace Emby.Dlna.PlayTo return new Argument { - Name = container.GetValue(UPnpNamespaces.Svc + "name"), - Direction = container.GetValue(UPnpNamespaces.Svc + "direction"), - RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") + Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, + Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty, + RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty }; } @@ -91,8 +89,8 @@ namespace Emby.Dlna.PlayTo return new StateVariable { - Name = container.GetValue(UPnpNamespaces.Svc + "name"), - DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"), + Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, + DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty, AllowedValues = allowedValues }; } @@ -168,7 +166,7 @@ namespace Emby.Dlna.PlayTo return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString); } - private string BuildArgumentXml(Argument argument, string value, string commandParameter = "") + private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "") { var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs index 0d9478e42..02d2da58d 100644 --- a/Emby.Dlna/PlayTo/uBaseObject.cs +++ b/Emby.Dlna/PlayTo/uBaseObject.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs index d4af72b62..8eaf12ba9 100644 --- a/Emby.Dlna/Profiles/DefaultProfile.cs +++ b/Emby.Dlna/Profiles/DefaultProfile.cs @@ -1,5 +1,7 @@ #pragma warning disable CS1591 +using System; +using System.Globalization; using System.Linq; using MediaBrowser.Model.Dlna; @@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles { public DefaultProfile() { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); Name = "Generic Device"; ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*"; diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs index 09525aae4..3f3dfccd3 100644 --- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -250,7 +250,8 @@ namespace Emby.Dlna.Server url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/'); - return SecurityElement.Escape(url); + // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released + return SecurityElement.Escape(url) ?? string.Empty; } private IEnumerable<DeviceIcon> GetIcons() diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index 8d2486fee..904c23d99 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -47,7 +47,7 @@ namespace Emby.Dlna.Service private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request) { - ControlRequestInfo requestInfo = null; + ControlRequestInfo? requestInfo = null; using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8)) { @@ -151,7 +151,7 @@ namespace Emby.Dlna.Service private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader) { - string namespaceURI = null, localName = null; + string? namespaceURI = null, localName = null; await reader.MoveToContentAsync().ConfigureAwait(false); await reader.ReadAsync().ConfigureAwait(false); @@ -210,7 +210,7 @@ namespace Emby.Dlna.Service } } - protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter); + protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter); private void LogRequest(ControlRequest request) { diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs index 8c7d961f3..391dda147 100644 --- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs +++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -69,7 +71,7 @@ namespace Emby.Dlna.Ssdp { lock (_syncLock) { - if (_listenerCount > 0 && _deviceLocator == null) + if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null) { _deviceLocator = new SsdpDeviceLocator(_commsServer); @@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp { Location = e.DiscoveredDevice.DescriptionLocation, Headers = headers, - LocalIpAddress = e.LocalIpAddress + RemoteIpAddress = e.RemoteIpAddress }); DeviceDiscoveredInternal?.Invoke(this, args); diff --git a/Emby.Dlna/Ssdp/SsdpExtensions.cs b/Emby.Dlna/Ssdp/SsdpExtensions.cs index e7a52f168..d00eb02b4 100644 --- a/Emby.Dlna/Ssdp/SsdpExtensions.cs +++ b/Emby.Dlna/Ssdp/SsdpExtensions.cs @@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp { public static class SsdpExtensions { - public static string GetValue(this XElement container, XName name) + public static string? GetValue(this XElement container, XName name) { var node = container.Element(name); return node?.Value; } - public static string GetAttributeValue(this XElement container, XName name) + public static string? GetAttributeValue(this XElement container, XName name) { var node = container.Attribute(name); return node?.Value; } - public static string GetDescendantValue(this XElement container, XName name) + public static string? GetDescendantValue(this XElement container, XName name) => container.Descendants(name).FirstOrDefault()?.Value; } } diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index 7d479a5c6..5c5afe1c6 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -25,7 +25,6 @@ <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 8a2301d2d..7d952aa23 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Common.Extensions; @@ -171,21 +172,31 @@ namespace Emby.Drawing return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } - ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null); int quality = options.Quality; ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency); - string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer); + string cacheFilePath = GetCacheFilePath( + originalImagePath, + options.Width, + options.Height, + options.MaxWidth, + options.MaxHeight, + options.FillWidth, + options.FillHeight, + quality, + dateModified, + outputFormat, + options.AddPlayedIndicator, + options.PercentPlayed, + options.UnplayedCount, + options.Blur, + options.BackgroundColor, + options.ForegroundLayer); try { if (!File.Exists(cacheFilePath)) { - if (options.CropWhiteSpace && !SupportsTransparency(originalImagePath)) - { - options.CropWhiteSpace = false; - } - string resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat); if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase)) @@ -246,48 +257,111 @@ namespace Emby.Drawing /// <summary> /// Gets the cache file path based on a set of parameters. /// </summary> - private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer) + private string GetCacheFilePath( + string originalPath, + int? width, + int? height, + int? maxWidth, + int? maxHeight, + int? fillWidth, + int? fillHeight, + int quality, + DateTime dateModified, + ImageFormat format, + bool addPlayedIndicator, + double percentPlayed, + int? unwatchedCount, + int? blur, + string backgroundColor, + string foregroundLayer) { - var filename = originalPath - + "width=" + outputSize.Width - + "height=" + outputSize.Height - + "quality=" + quality - + "datemodified=" + dateModified.Ticks - + "f=" + format; + var filename = new StringBuilder(256); + filename.Append(originalPath); + + filename.Append(",quality="); + filename.Append(quality); + + filename.Append(",datemodified="); + filename.Append(dateModified.Ticks); + + filename.Append(",f="); + filename.Append(format); + + if (width.HasValue) + { + filename.Append(",width="); + filename.Append(width.Value); + } + + if (height.HasValue) + { + filename.Append(",height="); + filename.Append(height.Value); + } + + if (maxWidth.HasValue) + { + filename.Append(",maxwidth="); + filename.Append(maxWidth.Value); + } + + if (maxHeight.HasValue) + { + filename.Append(",maxheight="); + filename.Append(maxHeight.Value); + } + + if (fillWidth.HasValue) + { + filename.Append(",fillwidth="); + filename.Append(fillWidth.Value); + } + + if (fillHeight.HasValue) + { + filename.Append(",fillheight="); + filename.Append(fillHeight.Value); + } if (addPlayedIndicator) { - filename += "pl=true"; + filename.Append(",pl=true"); } if (percentPlayed > 0) { - filename += "p=" + percentPlayed; + filename.Append(",p="); + filename.Append(percentPlayed); } if (unwatchedCount.HasValue) { - filename += "p=" + unwatchedCount.Value; + filename.Append(",p="); + filename.Append(unwatchedCount.Value); } if (blur.HasValue) { - filename += "blur=" + blur.Value; + filename.Append(",blur="); + filename.Append(blur.Value); } if (!string.IsNullOrEmpty(backgroundColor)) { - filename += "b=" + backgroundColor; + filename.Append(",b="); + filename.Append(backgroundColor); } if (!string.IsNullOrEmpty(foregroundLayer)) { - filename += "fl=" + foregroundLayer; + filename.Append(",fl="); + filename.Append(foregroundLayer); } - filename += "v=" + Version; + filename.Append(",v="); + filename.Append(Version); - return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant()); + return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant()); } /// <inheritdoc /> @@ -352,8 +426,13 @@ namespace Emby.Drawing } /// <inheritdoc /> - public string GetImageCacheTag(User user) + public string? GetImageCacheTag(User user) { + if (user.ProfileImage == null) + { + return null; + } + return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5() .ToString("N", CultureInfo.InvariantCulture); } diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index 2a1cfd3da..1c05aa916 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -32,7 +32,7 @@ namespace Emby.Drawing => throw new NotImplementedException(); /// <inheritdoc /> - public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) + public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat) { throw new NotImplementedException(); } diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index b43203e9d..63116f368 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -44,7 +44,6 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> --> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs index f7df58786..c63aec64e 100644 --- a/Emby.Naming/TV/EpisodeResolver.cs +++ b/Emby.Naming/TV/EpisodeResolver.cs @@ -68,6 +68,11 @@ namespace Emby.Naming.TV var parsingResult = new EpisodePathParser(_options) .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo); + if (!parsingResult.Success && !isStub) + { + return null; + } + return new EpisodeInfo(path) { Container = container, diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs index 09a0cd189..4eef3ebc5 100644 --- a/Emby.Naming/Video/CleanStringParser.cs +++ b/Emby.Naming/Video/CleanStringParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; namespace Emby.Naming.Video @@ -16,8 +17,14 @@ namespace Emby.Naming.Video /// <param name="expressions">List of regex to parse name and year from.</param> /// <param name="newName">Parsing result string.</param> /// <returns>True if parsing was successful.</returns> - public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName) + public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName) { + if (string.IsNullOrEmpty(name)) + { + newName = ReadOnlySpan<char>.Empty; + return false; + } + var len = expressions.Count; for (int i = 0; i < len; i++) { @@ -41,7 +48,7 @@ namespace Emby.Naming.Video return true; } - newName = string.Empty; + newName = ReadOnlySpan<char>.Empty; return false; } } diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 09a030d2d..7b6a1705b 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -221,20 +221,21 @@ namespace Emby.Naming.Video string testFilename = Path.GetFileNameWithoutExtension(testFilePath); if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) { - if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName)) + // Remove the folder name before cleaning as we don't care about cleaning that part + if (folderName.Length <= testFilename.Length) { - testFilename = cleanName.ToString(); + testFilename = testFilename.Substring(folderName.Length).Trim(); } - if (folderName.Length <= testFilename.Length) + if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName)) { - testFilename = testFilename.Substring(folderName.Length).Trim(); + testFilename = cleanName.Trim().ToString(); } + // The CleanStringParser should have removed common keywords etc. return string.IsNullOrEmpty(testFilename) - || testFilename[0] == '-' - || testFilename[0] == '_' - || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)); + || testFilename[0] == '-' + || Regex.IsMatch(testFilename, @"^\[([^]]*)\]"); } return false; diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index 619d1520e..79a6da8f7 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Emby.Naming.Common; @@ -146,7 +147,7 @@ namespace Emby.Naming.Video /// <param name="name">Raw name.</param> /// <param name="newName">Clean name.</param> /// <returns>True if cleaning of name was successful.</returns> - public bool TryCleanString(string name, out ReadOnlySpan<char> newName) + public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName) { return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName); } diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj index 16ee918c4..5a2aea642 100644 --- a/Emby.Notifications/Emby.Notifications.csproj +++ b/Emby.Notifications/Emby.Notifications.csproj @@ -11,6 +11,8 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> @@ -25,14 +27,9 @@ <!-- Code analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 62e33e6c4..2b6618159 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -24,18 +24,15 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs index 77819c764..29bac6634 100644 --- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs +++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs @@ -3,7 +3,6 @@ using System; using System.IO; using System.Linq; -using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Serialization; namespace Emby.Server.Implementations.AppBase @@ -53,7 +52,8 @@ namespace Emby.Server.Implementations.AppBase Directory.CreateDirectory(directory); // Save it after load in case we got new items - using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) { fs.Write(newBytes, 0, newBytesLen); } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index e8aa019ab..75d8fc113 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -10,8 +10,6 @@ using System.Net; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Emby.Dlna; @@ -43,6 +41,7 @@ using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; +using Emby.Server.Implementations.Udp; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; using Jellyfin.Networking.Configuration; @@ -50,7 +49,6 @@ using Jellyfin.Networking.Manager; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; -using MediaBrowser.Common.Json; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; @@ -99,6 +97,7 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; @@ -118,6 +117,7 @@ namespace Emby.Server.Implementations private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; private readonly IFileSystem _fileSystemManager; + private readonly IConfiguration _startupConfig; private readonly IXmlSerializer _xmlSerializer; private readonly IStartupOptions _startupOptions; private readonly IPluginManager _pluginManager; @@ -126,7 +126,6 @@ namespace Emby.Server.Implementations private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; private string[] _urlPrefixes; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); /// <summary> /// Gets a value indicating whether this instance can self restart. @@ -135,9 +134,6 @@ namespace Emby.Server.Implementations public bool CoreStartupHasCompleted { get; private set; } - /// <inheritdoc /> - public Uri PublishedServerUrl => _startupOptions.PublishedServerUrl; - public virtual bool CanLaunchWebBrowser { get @@ -214,7 +210,7 @@ namespace Emby.Server.Implementations /// Gets or sets the configuration manager. /// </summary> /// <value>The configuration manager.</value> - protected IConfigurationManager ConfigurationManager { get; set; } + public ServerConfigurationManager ConfigurationManager { get; set; } /// <summary> /// Gets or sets the service provider. @@ -232,10 +228,9 @@ namespace Emby.Server.Implementations public int HttpsPort { get; private set; } /// <summary> - /// Gets the server configuration manager. + /// Gets the value of the PublishedServerUrl setting. /// </summary> - /// <value>The server configuration manager.</value> - public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager; + public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey]; /// <summary> /// Initializes a new instance of the <see cref="ApplicationHost"/> class. @@ -243,51 +238,37 @@ namespace Emby.Server.Implementations /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param> + /// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param> public ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, + IConfiguration startupConfig, IFileSystem fileSystem, IServiceCollection serviceCollection) { - _xmlSerializer = new MyXmlSerializer(); - - ServiceCollection = serviceCollection; - ApplicationPaths = applicationPaths; LoggerFactory = loggerFactory; + _startupOptions = options; + _startupConfig = startupConfig; _fileSystemManager = fileSystem; - - ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); - // Have to migrate settings here as migration subsystem not yet initialised. - MigrateNetworkConfiguration(); - - // Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised. - ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>(); - NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>()); + ServiceCollection = serviceCollection; Logger = LoggerFactory.CreateLogger<ApplicationHost>(); - - _startupOptions = options; - - // Initialize runtime stat collection - if (ServerConfigurationManager.Configuration.EnableMetrics) - { - DotNetRuntimeStatsBuilder.Default().StartCollecting(); - } - fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersionString = ApplicationVersion.ToString(3); ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; + _xmlSerializer = new MyXmlSerializer(); + ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); _pluginManager = new PluginManager( LoggerFactory.CreateLogger<PluginManager>(), this, - ServerConfigurationManager.Configuration, + ConfigurationManager.Configuration, ApplicationPaths.PluginsPath, ApplicationVersion); } @@ -302,9 +283,9 @@ namespace Emby.Server.Implementations if (!File.Exists(path)) { var networkSettings = new NetworkConfiguration(); - ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings); + ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings); _xmlSerializer.SerializeToFile(networkSettings, path); - Logger?.LogDebug("Successfully migrated network settings."); + Logger.LogDebug("Successfully migrated network settings."); } } @@ -354,10 +335,7 @@ namespace Emby.Server.Implementations { get { - if (_deviceId == null) - { - _deviceId = new DeviceId(ApplicationPaths, LoggerFactory); - } + _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory); return _deviceId.Value; } @@ -389,10 +367,7 @@ namespace Emby.Server.Implementations /// <returns>System.Object.</returns> protected object CreateInstanceSafe(Type type) { - if (_creatingInstances == null) - { - _creatingInstances = new List<Type>(); - } + _creatingInstances ??= new List<Type>(); if (_creatingInstances.IndexOf(type) != -1) { @@ -463,7 +438,7 @@ namespace Emby.Server.Implementations } /// <inheritdoc /> - public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true) + public IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true) { // Convert to list so this isn't executed for each iteration var parts = GetExportTypes<T>() @@ -487,8 +462,9 @@ namespace Emby.Server.Implementations /// Runs the startup tasks. /// </summary> /// <returns><see cref="Task" />.</returns> - public async Task RunStartupTasksAsync() + public async Task RunStartupTasksAsync(CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); Logger.LogInformation("Running startup tasks"); Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); @@ -502,14 +478,21 @@ namespace Emby.Server.Implementations var entryPoints = GetExports<IServerEntryPoint>(); + cancellationToken.ThrowIfCancellationRequested(); + var stopWatch = new Stopwatch(); stopWatch.Start(); + await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false); Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); CoreStartupHasCompleted = true; + + cancellationToken.ThrowIfCancellationRequested(); + stopWatch.Restart(); + await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); stopWatch.Stop(); @@ -533,7 +516,21 @@ namespace Emby.Server.Implementations /// <inheritdoc/> public void Init() { - var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); + DiscoverTypes(); + + ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); + + // Have to migrate settings here as migration subsystem not yet initialised. + MigrateNetworkConfiguration(); + NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>()); + + // Initialize runtime stat collection + if (ConfigurationManager.Configuration.EnableMetrics) + { + DotNetRuntimeStatsBuilder.Default().StartCollecting(); + } + + var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); HttpPort = networkConfiguration.HttpServerPortNumber; HttpsPort = networkConfiguration.HttpsPortNumber; @@ -551,8 +548,6 @@ namespace Emby.Server.Implementations }; Certificate = GetCertificate(CertificateInfo); - DiscoverTypes(); - RegisterServices(); _pluginManager.RegisterServices(ServiceCollection); @@ -567,7 +562,8 @@ namespace Emby.Server.Implementations ServiceCollection.AddMemoryCache(); - ServiceCollection.AddSingleton(ConfigurationManager); + ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager); + ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager); ServiceCollection.AddSingleton<IApplicationHost>(this); ServiceCollection.AddSingleton<IPluginManager>(_pluginManager); ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); @@ -594,8 +590,6 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton<IServerApplicationHost>(this); ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); - ServiceCollection.AddSingleton(ServerConfigurationManager); - ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); @@ -607,12 +601,8 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>(); - // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required - ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>)); - - // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required - ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>)); ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); + ServiceCollection.AddSingleton<EncodingHelper>(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); @@ -677,14 +667,14 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); - ServiceCollection.AddSingleton<EncodingHelper>(); - ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); ServiceCollection.AddSingleton<TranscodingJobHelper>(); ServiceCollection.AddScoped<MediaInfoHelper>(); ServiceCollection.AddScoped<AudioHelper>(); ServiceCollection.AddScoped<DynamicHlsHelper>(); + + ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>(); } /// <summary> @@ -782,7 +772,7 @@ namespace Emby.Server.Implementations { // For now there's no real way to inject these properly BaseItem.Logger = Resolve<ILogger<BaseItem>>(); - BaseItem.ConfigurationManager = ServerConfigurationManager; + BaseItem.ConfigurationManager = ConfigurationManager; BaseItem.LibraryManager = Resolve<ILibraryManager>(); BaseItem.ProviderManager = Resolve<IProviderManager>(); BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); @@ -804,13 +794,12 @@ namespace Emby.Server.Implementations /// </summary> private void FindParts() { - if (!ServerConfigurationManager.Configuration.IsPortAuthorized) + if (!ConfigurationManager.Configuration.IsPortAuthorized) { - ServerConfigurationManager.Configuration.IsPortAuthorized = true; + ConfigurationManager.Configuration.IsPortAuthorized = true; ConfigurationManager.SaveConfiguration(); } - ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); _pluginManager.CreatePlugins(); _urlPrefixes = GetUrlPrefixes().ToArray(); @@ -914,7 +903,7 @@ namespace Emby.Server.Implementations protected void OnConfigurationUpdated(object sender, EventArgs e) { var requiresRestart = false; - var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); + var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); // Don't do anything if these haven't been set yet if (HttpPort != 0 && HttpsPort != 0) @@ -923,10 +912,10 @@ namespace Emby.Server.Implementations if (networkConfiguration.HttpServerPortNumber != HttpPort || networkConfiguration.HttpsPortNumber != HttpsPort) { - if (ServerConfigurationManager.Configuration.IsPortAuthorized) + if (ConfigurationManager.Configuration.IsPortAuthorized) { - ServerConfigurationManager.Configuration.IsPortAuthorized = false; - ServerConfigurationManager.SaveConfiguration(); + ConfigurationManager.Configuration.IsPortAuthorized = false; + ConfigurationManager.SaveConfiguration(); requiresRestart = true; } @@ -1142,16 +1131,16 @@ namespace Emby.Server.Implementations } /// <inheritdoc/> - public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps; + public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps; /// <inheritdoc/> public string GetSmartApiUrl(IPAddress ipAddress, int? port = null) { // Published server ends with a / - if (_startupOptions.PublishedServerUrl != null) + if (!string.IsNullOrEmpty(PublishedServerUrl)) { // Published server ends with a '/', so we need to remove it. - return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + return PublishedServerUrl.Trim('/'); } string smart = NetManager.GetBindInterface(ipAddress, out port); @@ -1168,10 +1157,10 @@ namespace Emby.Server.Implementations public string GetSmartApiUrl(HttpRequest request, int? port = null) { // Published server ends with a / - if (_startupOptions.PublishedServerUrl != null) + if (!string.IsNullOrEmpty(PublishedServerUrl)) { // Published server ends with a '/', so we need to remove it. - return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + return PublishedServerUrl.Trim('/'); } string smart = NetManager.GetBindInterface(request, out port); @@ -1188,10 +1177,10 @@ namespace Emby.Server.Implementations public string GetSmartApiUrl(string hostname, int? port = null) { // Published server ends with a / - if (_startupOptions.PublishedServerUrl != null) + if (!string.IsNullOrEmpty(PublishedServerUrl)) { // Published server ends with a '/', so we need to remove it. - return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + return PublishedServerUrl.Trim('/'); } string smart = NetManager.GetBindInterface(hostname, out port); @@ -1226,14 +1215,14 @@ namespace Emby.Server.Implementations Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), Host = host, Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), - Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl + Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl }.ToString().TrimEnd('/'); } public string FriendlyName => - string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName) + string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName) ? Environment.MachineName - : ServerConfigurationManager.Configuration.ServerName; + : ConfigurationManager.Configuration.ServerName; /// <summary> /// Shuts down. diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 8c5fa09f6..7324b0ee9 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -49,7 +48,7 @@ namespace Emby.Server.Implementations.Channels private readonly IProviderManager _providerManager; private readonly IMemoryCache _memoryCache; private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; /// <summary> /// Initializes a new instance of the <see cref="ChannelManager"/> class. diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 1ab2bdfbe..1b85a9d4b 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -8,11 +7,9 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Collections; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; @@ -124,7 +121,7 @@ namespace Emby.Server.Implementations.Collections private IEnumerable<BoxSet> GetCollections(User user) { - var folder = GetCollectionsFolder(false).Result; + var folder = GetCollectionsFolder(false).GetAwaiter().GetResult(); return folder == null ? Enumerable.Empty<BoxSet>() @@ -319,11 +316,11 @@ namespace Emby.Server.Implementations.Collections { var results = new Dictionary<Guid, BaseItem>(); - var allBoxsets = GetCollections(user).ToList(); + var allBoxSets = GetCollections(user).ToList(); foreach (var item in items) { - if (!(item is ISupportsBoxSetGrouping)) + if (item is not ISupportsBoxSetGrouping) { results[item.Id] = item; } @@ -331,20 +328,44 @@ namespace Emby.Server.Implementations.Collections { var itemId = item.Id; - var currentBoxSets = allBoxsets - .Where(i => i.ContainsLinkedChildByItemId(itemId)) - .ToList(); + var itemIsInBoxSet = false; + foreach (var boxSet in allBoxSets) + { + if (!boxSet.ContainsLinkedChildByItemId(itemId)) + { + continue; + } + + itemIsInBoxSet = true; + + results.TryAdd(boxSet.Id, boxSet); + } - if (currentBoxSets.Count > 0) + // skip any item that is in a box set + if (itemIsInBoxSet) { - foreach (var boxset in currentBoxSets) + continue; + } + + var alreadyInResults = false; + // this is kind of a performance hack because only Video has alternate versions that should be in a box set? + if (item is Video video) + { + foreach (var childId in video.GetLocalAlternateVersionIds()) { - results[boxset.Id] = boxset; + if (!results.ContainsKey(childId)) + { + continue; + } + + alreadyInResults = true; + break; } } - else + + if (!alreadyInResults) { - results[item.Id] = item; + results[itemId] = item; } } } diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index cd9dbb1bd..01dc728c1 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Emby.Server.Implementations.HttpServer; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Emby.Server.Implementations diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index d78b93bd7..72c9d82ad 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Buffers.Text; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -88,7 +89,7 @@ namespace Emby.Server.Implementations.Data _imageProcessor = imageProcessor; _typeMapper = new TypeMapper(); - _jsonOptions = JsonDefaults.GetOptions(); + _jsonOptions = JsonDefaults.Options; DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); } @@ -502,7 +503,7 @@ namespace Emby.Server.Implementations.Data using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) { saveImagesStatement.TryBind("@Id", item.Id.ToByteArray()); - saveImagesStatement.TryBind("@Images", SerializeImages(item)); + saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); saveImagesStatement.MoveNext(); } @@ -897,8 +898,8 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); saveItemStatement.TryBind("@Tagline", item.Tagline); - saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item)); - saveItemStatement.TryBind("@Images", SerializeImages(item)); + saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds)); + saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); if (item.ProductionLocations.Length > 0) { @@ -968,10 +969,10 @@ namespace Emby.Server.Implementations.Data saveItemStatement.MoveNext(); } - private static string SerializeProviderIds(BaseItem item) + internal static string SerializeProviderIds(Dictionary<string, string> providerIds) { StringBuilder str = new StringBuilder(); - foreach (var i in item.ProviderIds) + foreach (var i in providerIds) { // Ideally we shouldn't need this IsNullOrWhiteSpace check, // but we're seeing some cases of bad data slip through @@ -995,35 +996,25 @@ namespace Emby.Server.Implementations.Data return str.ToString(); } - private static void DeserializeProviderIds(string value, BaseItem item) + internal static void DeserializeProviderIds(string value, IHasProviderIds item) { if (string.IsNullOrWhiteSpace(value)) { return; } - if (item.ProviderIds.Count > 0) + foreach (var part in value.SpanSplit('|')) { - return; - } - - var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries); - - foreach (var part in parts) - { - var idParts = part.Split('='); - - if (idParts.Length == 2) + var providerDelimiterIndex = part.IndexOf('='); + if (providerDelimiterIndex != -1 && providerDelimiterIndex == part.LastIndexOf('=')) { - item.SetProviderId(idParts[0], idParts[1]); + item.SetProviderId(part.Slice(0, providerDelimiterIndex).ToString(), part.Slice(providerDelimiterIndex + 1).ToString()); } } } - private string SerializeImages(BaseItem item) + internal string SerializeImages(ItemImageInfo[] images) { - var images = item.ImageInfos; - if (images.Length == 0) { return null; @@ -1045,21 +1036,15 @@ namespace Emby.Server.Implementations.Data return str.ToString(); } - private void DeserializeImages(string value, BaseItem item) + internal ItemImageInfo[] DeserializeImages(string value) { if (string.IsNullOrWhiteSpace(value)) { - return; - } - - if (item.ImageInfos.Length > 0) - { - return; + return Array.Empty<ItemImageInfo>(); } - var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries); var list = new List<ItemImageInfo>(); - foreach (var part in parts) + foreach (var part in value.SpanSplit('|')) { var image = ItemImageInfoFromValueString(part); @@ -1069,15 +1054,14 @@ namespace Emby.Server.Implementations.Data } } - item.ImageInfos = list.ToArray(); + return list.ToArray(); } - public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) + private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) { const char Delimiter = '*'; var path = image.Path ?? string.Empty; - var hash = image.BlurHash ?? string.Empty; bldr.Append(GetPathToSave(path)) .Append(Delimiter) @@ -1087,48 +1071,105 @@ namespace Emby.Server.Implementations.Data .Append(Delimiter) .Append(image.Width) .Append(Delimiter) - .Append(image.Height) - .Append(Delimiter) - // Replace delimiters with other characters. - // This can be removed when we migrate to a proper DB. - .Append(hash.Replace('*', '/').Replace('|', '\\')); + .Append(image.Height); + + var hash = image.BlurHash; + if (!string.IsNullOrEmpty(hash)) + { + bldr.Append(Delimiter) + // Replace delimiters with other characters. + // This can be removed when we migrate to a proper DB. + .Append(hash.Replace('*', '/').Replace('|', '\\')); + } } - public ItemImageInfo ItemImageInfoFromValueString(string value) + internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan<char> value) { - var parts = value.Split('*', StringSplitOptions.None); + var nextSegment = value.IndexOf('*'); + if (nextSegment == -1) + { + return null; + } - if (parts.Length < 3) + ReadOnlySpan<char> path = value[..nextSegment]; + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf('*'); + if (nextSegment == -1) { return null; } - var image = new ItemImageInfo(); + ReadOnlySpan<char> dateModified = value[..nextSegment]; + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf('*'); + if (nextSegment == -1) + { + nextSegment = value.Length; + } + + ReadOnlySpan<char> imageType = value[..nextSegment]; - image.Path = RestorePath(parts[0]); + var image = new ItemImageInfo + { + Path = RestorePath(path.ToString()) + }; - if (long.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)) + if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)) { image.DateModified = new DateTime(ticks, DateTimeKind.Utc); } - if (Enum.TryParse(parts[2], true, out ImageType type)) + if (Enum.TryParse(imageType.ToString(), true, out ImageType type)) { image.Type = type; } - if (parts.Length >= 5) + // Optional parameters: width*height*blurhash + if (nextSegment + 1 < value.Length - 1) { - if (int.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) - && int.TryParse(parts[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf('*'); + if (nextSegment == -1 || nextSegment == value.Length) + { + return image; + } + + ReadOnlySpan<char> widthSpan = value[..nextSegment]; + + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf('*'); + if (nextSegment == -1) + { + nextSegment = value.Length; + } + + ReadOnlySpan<char> heightSpan = value[..nextSegment]; + + if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) + && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) { image.Width = width; image.Height = height; } - if (parts.Length >= 6) + if (nextSegment < value.Length - 1) { - image.BlurHash = parts[5].Replace('/', '*').Replace('\\', '|'); + value = value[(nextSegment + 1)..]; + var length = value.Length; + + Span<char> blurHashSpan = stackalloc char[length]; + for (int i = 0; i < length; i++) + { + var c = value[i]; + blurHashSpan[i] = c switch + { + '/' => '*', + '\\' => '|', + _ => c + }; + } + + image.BlurHash = new string(blurHashSpan); } } @@ -1311,7 +1352,14 @@ namespace Emby.Server.Implementations.Data if (!reader.IsDBNull(index)) { - item.ChannelId = new Guid(reader.GetString(index)); + if (!Utf8Parser.TryParse(reader[index].ToBlob(), out Guid value, out _, standardFormat: 'N')) + { + var str = reader.GetString(index); + Logger.LogWarning("{ChannelId} isn't in the expected format", str); + value = new Guid(str); + } + + item.ChannelId = value; } index++; @@ -1790,7 +1838,7 @@ namespace Emby.Server.Implementations.Data index++; } - if (!reader.IsDBNull(index)) + if (item.ProviderIds.Count == 0 && !reader.IsDBNull(index)) { DeserializeProviderIds(reader.GetString(index), item); } @@ -1799,9 +1847,9 @@ namespace Emby.Server.Implementations.Data if (query.DtoOptions.EnableImages) { - if (!reader.IsDBNull(index)) + if (item.ImageInfos.Length == 0 && !reader.IsDBNull(index)) { - DeserializeImages(reader.GetString(index), item); + item.ImageInfos = DeserializeImages(reader.GetString(index)); } index++; @@ -2116,30 +2164,7 @@ namespace Emby.Server.Implementations.Data || query.IsLiked.HasValue; } - private readonly ItemFields[] _allFields = Enum.GetNames(typeof(ItemFields)) - .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) - .ToArray(); - - private string[] GetColumnNamesFromField(ItemFields field) - { - switch (field) - { - case ItemFields.Settings: - return new[] { "IsLocked", "PreferredMetadataCountryCode", "PreferredMetadataLanguage", "LockedFields" }; - case ItemFields.ServiceName: - return new[] { "ExternalServiceId" }; - case ItemFields.SortName: - return new[] { "ForcedSortName" }; - case ItemFields.Taglines: - return new[] { "Tagline" }; - case ItemFields.Tags: - return new[] { "Tags" }; - case ItemFields.IsHD: - return Array.Empty<string>(); - default: - return new[] { field.ToString() }; - } - } + private readonly ItemFields[] _allFields = Enum.GetValues<ItemFields>(); private bool HasField(InternalItemsQuery query, ItemFields name) { @@ -2329,9 +2354,32 @@ namespace Emby.Server.Implementations.Data { if (!HasField(query, field)) { - foreach (var fieldToRemove in GetColumnNamesFromField(field)) + switch (field) { - list.Remove(fieldToRemove); + case ItemFields.Settings: + list.Remove("IsLocked"); + list.Remove("PreferredMetadataCountryCode"); + list.Remove("PreferredMetadataLanguage"); + list.Remove("LockedFields"); + break; + case ItemFields.ServiceName: + list.Remove("ExternalServiceId"); + break; + case ItemFields.SortName: + list.Remove("ForcedSortName"); + break; + case ItemFields.Taglines: + list.Remove("Tagline"); + break; + case ItemFields.Tags: + list.Remove("Tags"); + break; + case ItemFields.IsHD: + // do nothing + break; + default: + list.Remove(field.ToString()); + break; } } } @@ -2578,9 +2626,9 @@ namespace Emby.Server.Implementations.Data } var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" })) - + GetFromText() - + GetJoinUserDataText(query); + + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" })) + + GetFromText() + + GetJoinUserDataText(query); var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) @@ -2721,87 +2769,22 @@ namespace Emby.Server.Implementations.Data private string FixUnicodeChars(string buffer) { - if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2013', '-'); // en dash - } - - if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2014', '-'); // em dash - } - - if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2015', '-'); // horizontal bar - } - - if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2017', '_'); // double low line - } - - if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2018', '\''); // left single quotation mark - } - - if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2019', '\''); // right single quotation mark - } - - if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark - } - - if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark - } - - if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark - } - - if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark - } - - if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark - } - - if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis - } - - if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2032', '\''); // prime - } - - if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u2033', '\"'); // double prime - } - - if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u0060', '\''); // grave accent - } - - if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1) - { - buffer = buffer.Replace('\u00B4', '\''); // acute accent - } - - return buffer; + buffer = buffer.Replace('\u2013', '-'); // en dash + buffer = buffer.Replace('\u2014', '-'); // em dash + buffer = buffer.Replace('\u2015', '-'); // horizontal bar + buffer = buffer.Replace('\u2017', '_'); // double low line + buffer = buffer.Replace('\u2018', '\''); // left single quotation mark + buffer = buffer.Replace('\u2019', '\''); // right single quotation mark + buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark + buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark + buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark + buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark + buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark + buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis + buffer = buffer.Replace('\u2032', '\''); // prime + buffer = buffer.Replace('\u2033', '\"'); // double prime + buffer = buffer.Replace('\u0060', '\''); // grave accent + return buffer.Replace('\u00B4', '\''); // acute accent } private void AddItem(List<BaseItem> items, BaseItem newItem) @@ -3584,11 +3567,11 @@ namespace Emby.Server.Implementations.Data statement?.TryBind("@IsFolder", query.IsFolder); } - var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); + var includeTypes = query.IncludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray(); // Only specify excluded types if no included types are specified if (includeTypes.Length == 0) { - var excludeTypes = query.ExcludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); + var excludeTypes = query.ExcludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray(); if (excludeTypes.Length == 1) { whereClauses.Add("type<>@type"); @@ -4532,7 +4515,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb")); } - var includedItemByNameTypes = GetItemByNameTypesInQuery(query).SelectMany(MapIncludeItemTypes).ToList(); + var includedItemByNameTypes = GetItemByNameTypesInQuery(query); var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; var queryTopParentIds = query.TopParentIds; @@ -4790,27 +4773,27 @@ namespace Emby.Server.Implementations.Data if (IsTypeInQuery(nameof(Person), query)) { - list.Add(nameof(Person)); + list.Add(typeof(Person).FullName); } if (IsTypeInQuery(nameof(Genre), query)) { - list.Add(nameof(Genre)); + list.Add(typeof(Genre).FullName); } if (IsTypeInQuery(nameof(MusicGenre), query)) { - list.Add(nameof(MusicGenre)); + list.Add(typeof(MusicGenre).FullName); } if (IsTypeInQuery(nameof(MusicArtist), query)) { - list.Add(nameof(MusicArtist)); + list.Add(typeof(MusicArtist).FullName); } if (IsTypeInQuery(nameof(Studio), query)) { - list.Add(nameof(Studio)); + list.Add(typeof(Studio).FullName); } return list; @@ -4915,15 +4898,10 @@ namespace Emby.Server.Implementations.Data typeof(AggregateFolder) }; - public void UpdateInheritedValues(CancellationToken cancellationToken) - { - UpdateInheritedTags(cancellationToken); - } - - private void UpdateInheritedTags(CancellationToken cancellationToken) + public void UpdateInheritedValues() { string sql = string.Join( - ";", + ';', new string[] { "delete from itemvalues where type = 6", @@ -4946,37 +4924,38 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } } - private static Dictionary<string, string[]> GetTypeMapDictionary() + private static Dictionary<string, string> GetTypeMapDictionary() { - var dict = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase); + var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); foreach (var t in _knownTypes) { - dict[t.Name] = new[] { t.FullName }; + dict[t.Name] = t.FullName ; } - dict["Program"] = new[] { typeof(LiveTvProgram).FullName }; - dict["TvChannel"] = new[] { typeof(LiveTvChannel).FullName }; + dict["Program"] = typeof(LiveTvProgram).FullName; + dict["TvChannel"] = typeof(LiveTvChannel).FullName; return dict; } // Not crazy about having this all the way down here, but at least it's in one place - private readonly Dictionary<string, string[]> _types = GetTypeMapDictionary(); + private readonly Dictionary<string, string> _types = GetTypeMapDictionary(); - private string[] MapIncludeItemTypes(string value) + private string MapIncludeItemTypes(string value) { - if (_types.TryGetValue(value, out string[] result)) + if (_types.TryGetValue(value, out string result)) { return result; } if (IsValidType(value)) { - return new[] { value }; + return value; } - return Array.Empty<string>(); + Logger.LogWarning("Unknown item type: {ItemType}", value); + return null; } public void DeleteItem(Guid id) @@ -5279,31 +5258,46 @@ AND Type = @InternalPersonType)"); public List<string> GetStudioNames() { - return GetItemValueNames(new[] { 3 }, new List<string>(), new List<string>()); + return GetItemValueNames(new[] { 3 }, Array.Empty<string>(), Array.Empty<string>()); } public List<string> GetAllArtistNames() { - return GetItemValueNames(new[] { 0, 1 }, new List<string>(), new List<string>()); + return GetItemValueNames(new[] { 0, 1 }, Array.Empty<string>(), Array.Empty<string>()); } public List<string> GetMusicGenreNames() { - return GetItemValueNames(new[] { 2 }, new List<string> { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" }, new List<string>()); + return GetItemValueNames( + new[] { 2 }, + new string[] + { + typeof(Audio).FullName, + typeof(MusicVideo).FullName, + typeof(MusicAlbum).FullName, + typeof(MusicArtist).FullName + }, + Array.Empty<string>()); } public List<string> GetGenreNames() { - return GetItemValueNames(new[] { 2 }, new List<string>(), new List<string> { "Audio", "MusicVideo", "MusicAlbum", "MusicArtist" }); + return GetItemValueNames( + new[] { 2 }, + Array.Empty<string>(), + new string[] + { + typeof(Audio).FullName, + typeof(MusicVideo).FullName, + typeof(MusicAlbum).FullName, + typeof(MusicArtist).FullName + }); } - private List<string> GetItemValueNames(int[] itemValueTypes, List<string> withItemTypes, List<string> excludeItemTypes) + private List<string> GetItemValueNames(int[] itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes) { CheckDisposed(); - withItemTypes = withItemTypes.SelectMany(MapIncludeItemTypes).ToList(); - excludeItemTypes = excludeItemTypes.SelectMany(MapIncludeItemTypes).ToList(); - var now = DateTime.UtcNow; var typeClause = itemValueTypes.Length == 1 ? @@ -5415,7 +5409,6 @@ AND Type = @InternalPersonType)"); ItemIds = query.ItemIds, TopParentIds = query.TopParentIds, ParentId = query.ParentId, - IsPlayed = query.IsPlayed, IsAiring = query.IsAiring, IsMovie = query.IsMovie, IsSports = query.IsSports, @@ -5441,6 +5434,7 @@ AND Type = @InternalPersonType)"); var outerQuery = new InternalItemsQuery(query.User) { + IsPlayed = query.IsPlayed, IsFavorite = query.IsFavorite, IsFavoriteOrLiked = query.IsFavoriteOrLiked, IsLiked = query.IsLiked, @@ -5809,7 +5803,10 @@ AND Type = @InternalPersonType)"); var endIndex = Math.Min(people.Count, startIndex + Limit); for (var i = startIndex; i < endIndex; i++) { - insertText.AppendFormat("(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),", i.ToString(CultureInfo.InvariantCulture)); + insertText.AppendFormat( + CultureInfo.InvariantCulture, + "(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),", + i.ToString(CultureInfo.InvariantCulture)); } // Remove last comma @@ -6261,7 +6258,7 @@ AND Type = @InternalPersonType)"); CheckDisposed(); if (id == Guid.Empty) { - throw new ArgumentException(nameof(id)); + throw new ArgumentException("Guid can't be empty.", nameof(id)); } if (attachments == null) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 54b18a8c8..4ae35039a 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -665,10 +665,7 @@ namespace Emby.Server.Implementations.Dto var tag = GetImageCacheTag(item, image); if (!string.IsNullOrEmpty(image.BlurHash)) { - if (dto.ImageBlurHashes == null) - { - dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>(); - } + dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>(); if (!dto.ImageBlurHashes.ContainsKey(image.Type)) { @@ -702,10 +699,7 @@ namespace Emby.Server.Implementations.Dto if (hashes.Count > 0) { - if (dto.ImageBlurHashes == null) - { - dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>(); - } + dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>(); dto.ImageBlurHashes[imageType] = hashes; } @@ -898,10 +892,7 @@ namespace Emby.Server.Implementations.Dto dto.Taglines = new string[] { item.Tagline }; } - if (dto.Taglines == null) - { - dto.Taglines = Array.Empty<string>(); - } + dto.Taglines ??= Array.Empty<string>(); } dto.Type = item.GetBaseItemKind(); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index f03f04e02..b8a544b8c 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -27,10 +27,11 @@ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" /> <PackageReference Include="Mono.Nat" Version="3.0.1" /> - <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" /> - <PackageReference Include="sharpcompress" Version="0.28.1" /> - <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> + <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.0.0" /> + <PackageReference Include="sharpcompress" Version="0.28.2" /> + <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.2.0" /> <PackageReference Include="DotNet.Glob" Version="3.1.2" /> </ItemGroup> @@ -45,20 +46,17 @@ <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors> <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> <NoWarn>AD0001</NoWarn> + <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> <EmbeddedResource Include="Localization\iso6392.txt" /> <EmbeddedResource Include="Localization\countries.json" /> diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index a12a6b26c..3624e079f 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -1,5 +1,6 @@ #nullable enable +using System; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -51,6 +52,8 @@ namespace Emby.Server.Implementations.EntryPoints /// <inheritdoc /> public Task RunAsync() { + CheckDisposed(); + try { _udpServer = new UdpServer(_logger, _appHost, _config); @@ -64,6 +67,14 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + } + /// <inheritdoc /> public void Dispose() { diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 4a0fc8239..9afabf527 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 -using System; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 040b6b9e4..dd77b45d8 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.HttpServer.Security var authorization = _authContext.GetAuthorizationInfo(requestContext); var user = authorization.User; - return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user); + return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user); } public SessionInfo GetSession(object requestContext) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 7e0c2c1da..06acb5606 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.HttpServer RemoteEndPoint = remoteEndPoint; QueryString = query; - _jsonOptions = JsonDefaults.GetOptions(); + _jsonOptions = JsonDefaults.Options; LastActivityDate = DateTime.Now; } diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index d6cf6233e..1bee1ac31 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -14,15 +14,18 @@ namespace Emby.Server.Implementations.HttpServer public class WebSocketManager : IWebSocketManager { private readonly IWebSocketListener[] _webSocketListeners; + private readonly IAuthService _authService; private readonly ILogger<WebSocketManager> _logger; private readonly ILoggerFactory _loggerFactory; public WebSocketManager( + IAuthService authService, IEnumerable<IWebSocketListener> webSocketListeners, ILogger<WebSocketManager> logger, ILoggerFactory loggerFactory) { _webSocketListeners = webSocketListeners.ToArray(); + _authService = authService; _logger = logger; _loggerFactory = loggerFactory; } @@ -30,6 +33,7 @@ namespace Emby.Server.Implementations.HttpServer /// <inheritdoc /> public async Task WebSocketRequestHandler(HttpContext context) { + _ = _authService.Authenticate(context.Request); try { _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index c0e757543..27096ed33 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -2,11 +2,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Text; +using System.Runtime.InteropServices; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; @@ -24,7 +23,7 @@ namespace Emby.Server.Implementations.IO private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); private readonly string _tempPath; - private readonly bool _isEnvironmentCaseInsensitive; + private static readonly bool _isEnvironmentCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); public ManagedFileSystem( ILogger<ManagedFileSystem> logger, @@ -32,8 +31,6 @@ namespace Emby.Server.Implementations.IO { Logger = logger; _tempPath = applicationPaths.TempDirectory; - - _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows; } public virtual void AddShortcutHandler(IShortcutHandler handler) @@ -72,7 +69,7 @@ namespace Emby.Server.Implementations.IO } var extension = Path.GetExtension(filename); - var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); + var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); return handler?.Resolve(filename); } @@ -249,13 +246,20 @@ namespace Emby.Server.Implementations.IO // Issue #2354 get the size of files behind symbolic links if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) { - using (Stream thisFileStream = File.OpenRead(fileInfo.FullName)) + try { - result.Length = thisFileStream.Length; + using (Stream thisFileStream = File.OpenRead(fileInfo.FullName)) + { + result.Length = thisFileStream.Length; + } + } + catch (FileNotFoundException ex) + { + // Dangling symlinks cannot be detected before opening the file unfortunately... + Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName); + result.Exists = false; } } - - result.DirectoryName = fileInfo.DirectoryName; } result.CreationTimeUtc = GetCreationTimeUtc(info); @@ -294,16 +298,37 @@ namespace Emby.Server.Implementations.IO /// <param name="filename">The filename.</param> /// <returns>System.String.</returns> /// <exception cref="ArgumentNullException">The filename is null.</exception> - public virtual string GetValidFilename(string filename) + public string GetValidFilename(string filename) { - var builder = new StringBuilder(filename); - - foreach (var c in Path.GetInvalidFileNameChars()) + var invalid = Path.GetInvalidFileNameChars(); + var first = filename.IndexOfAny(invalid); + if (first == -1) { - builder = builder.Replace(c, ' '); + // Fast path for clean strings + return filename; } - return builder.ToString(); + return string.Create( + filename.Length, + (filename, invalid, first), + (chars, state) => + { + state.filename.AsSpan().CopyTo(chars); + + chars[state.first++] = ' '; + + var len = chars.Length; + foreach (var c in state.invalid) + { + for (int i = state.first; i < len; i++) + { + if (chars[i] == c) + { + chars[i] = ' '; + } + } + } + }); } /// <summary> @@ -487,26 +512,9 @@ namespace Emby.Server.Implementations.IO throw new ArgumentNullException(nameof(path)); } - var separatorChar = Path.DirectorySeparatorChar; - - return path.IndexOf(parentPath.TrimEnd(separatorChar) + separatorChar, StringComparison.OrdinalIgnoreCase) != -1; - } - - public virtual bool IsRootPath(string path) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - var parent = Path.GetDirectoryName(path); - - if (!string.IsNullOrEmpty(parent)) - { - return false; - } - - return true; + return path.Contains( + Path.TrimEndingDirectorySeparator(parentPath) + Path.DirectorySeparatorChar, + _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } public virtual string NormalizePath(string path) @@ -521,7 +529,7 @@ namespace Emby.Server.Implementations.IO return path; } - return path.TrimEnd(Path.DirectorySeparatorChar); + return Path.TrimEndingDirectorySeparator(path); } public virtual bool AreEqual(string path1, string path2) @@ -536,7 +544,10 @@ namespace Emby.Server.Implementations.IO return false; } - return string.Equals(NormalizePath(path1), NormalizePath(path2), StringComparison.OrdinalIgnoreCase); + return string.Equals( + NormalizePath(path1), + NormalizePath(path2), + _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } public virtual string GetFileNameWithoutExtension(FileSystemMetadata info) @@ -689,20 +700,5 @@ namespace Emby.Server.Implementations.IO AttributesToSkip = 0 }; } - - private static void RunProcess(string path, string args, string workingDirectory) - { - using (var process = Process.Start(new ProcessStartInfo - { - Arguments = args, - FileName = path, - CreateNoWindow = true, - WorkingDirectory = workingDirectory, - WindowStyle = ProcessWindowStyle.Normal - })) - { - process.WaitForExit(); - } - } } } diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index 4bef59543..f719dc5f8 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 - -using System; +#nullable enable namespace Emby.Server.Implementations { @@ -9,7 +8,7 @@ namespace Emby.Server.Implementations /// <summary> /// Gets the value of the --ffmpeg command line option. /// </summary> - string FFmpegPath { get; } + string? FFmpegPath { get; } /// <summary> /// Gets the value of the --service command line option. @@ -19,21 +18,21 @@ namespace Emby.Server.Implementations /// <summary> /// Gets the value of the --package-name command line option. /// </summary> - string PackageName { get; } + string? PackageName { get; } /// <summary> /// Gets the value of the --restartpath command line option. /// </summary> - string RestartPath { get; } + string? RestartPath { get; } /// <summary> /// Gets the value of the --restartargs command line option. /// </summary> - string RestartArgs { get; } + string? RestartArgs { get; } /// <summary> /// Gets the value of the --published-server-url command line option. /// </summary> - Uri PublishedServerUrl { get; } + string? PublishedServerUrl { get; } } } diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs index afa4ec7b1..e96b64595 100644 --- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs +++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs @@ -2,20 +2,12 @@ using System; using System.Collections.Generic; -using System.Linq; -using Emby.Server.Implementations.Images; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Images { diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 462eb03a8..50c531482 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Emby.Server.Implementations.Images; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs index 0ce1b91e8..a4c106e87 100644 --- a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs @@ -29,9 +29,7 @@ namespace Emby.Server.Implementations.Images { var subItem = i.Item2; - var episode = subItem as Episode; - - if (episode != null) + if (subItem is Episode episode) { var series = episode.Series; if (series != null && series.HasImage(ImageType.Primary)) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index d9ffe64b3..4d207471a 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -48,6 +48,7 @@ using MediaBrowser.Providers.MediaInfo; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; +using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; using Genre = MediaBrowser.Controller.Entities.Genre; using Person = MediaBrowser.Controller.Entities.Person; using VideoResolver = Emby.Naming.Video.VideoResolver; @@ -175,10 +176,7 @@ namespace Emby.Server.Implementations.Library { lock (_rootFolderSyncLock) { - if (_rootFolder == null) - { - _rootFolder = CreateRootFolder(); - } + _rootFolder ??= CreateRootFolder(); } } @@ -196,33 +194,33 @@ namespace Emby.Server.Implementations.Library /// Gets or sets the postscan tasks. /// </summary> /// <value>The postscan tasks.</value> - private ILibraryPostScanTask[] PostscanTasks { get; set; } + private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>(); /// <summary> /// Gets or sets the intro providers. /// </summary> /// <value>The intro providers.</value> - private IIntroProvider[] IntroProviders { get; set; } + private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>(); /// <summary> /// Gets or sets the list of entity resolution ignore rules. /// </summary> /// <value>The entity resolution ignore rules.</value> - private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } + private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>(); /// <summary> /// Gets or sets the list of currently registered entity resolvers. /// </summary> /// <value>The entity resolvers enumerable.</value> - private IItemResolver[] EntityResolvers { get; set; } + private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>(); - private IMultiItemResolver[] MultiItemResolvers { get; set; } + private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>(); /// <summary> /// Gets or sets the comparers. /// </summary> /// <value>The comparers.</value> - private IBaseItemComparer[] Comparers { get; set; } + private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>(); public bool IsScanRunning { get; private set; } @@ -558,7 +556,6 @@ namespace Emby.Server.Implementations.Library var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService) { Parent = parent, - Path = fullPath, FileInfo = fileInfo, CollectionType = collectionType, LibraryOptions = libraryOptions @@ -684,7 +681,7 @@ namespace Emby.Server.Implementations.Library foreach (var item in items) { - ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService); + ResolverHelper.SetInitialItemValues(item, parent, this, directoryService); } items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions)); @@ -1163,7 +1160,7 @@ namespace Emby.Server.Implementations.Library progress.Report(percent * 100); } - _itemRepository.UpdateInheritedValues(cancellationToken); + _itemRepository.UpdateInheritedValues(); progress.Report(100); } @@ -1247,7 +1244,7 @@ namespace Emby.Server.Implementations.Library { // TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it // https://github.com/dotnet/runtime/issues/20008 - if (Enum.TryParse<CollectionTypeOptions>(Path.GetExtension(file), true, out var res)) + if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res)) { return res; } @@ -1914,12 +1911,17 @@ namespace Emby.Server.Implementations.Library } catch (ArgumentException) { - _logger.LogWarning("Cannot get image index for {0}", img.Path); + _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path); + continue; + } + catch (Exception ex) when (ex is InvalidOperationException || ex is IOException) + { + _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path); continue; } - catch (InvalidOperationException) + catch (HttpRequestException ex) { - _logger.LogWarning("Cannot fetch image from {0}", img.Path); + _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", img.Path, ex.StatusCode); continue; } } @@ -1932,7 +1934,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path); + _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path); image.Width = 0; image.Height = 0; continue; @@ -1944,7 +1946,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path); + _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path); image.BlurHash = string.Empty; } @@ -1954,7 +1956,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Cannot update DateModified for {0}", image.Path); + _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path); } } @@ -2512,7 +2514,7 @@ namespace Emby.Server.Implementations.Library public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) { var series = episode.Series; - bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); + bool? isAbsoluteNaming = series != null && string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase); if (!isAbsoluteNaming.Value) { // In other words, no filter applied @@ -2524,9 +2526,23 @@ namespace Emby.Server.Implementations.Library var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; // TODO nullable - what are we trying to do there with empty episodeInfo? - var episodeInfo = episode.IsFileProtocol - ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path) - : new Naming.TV.EpisodeInfo(episode.Path); + EpisodeInfo episodeInfo = null; + if (episode.IsFileProtocol) + { + episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming); + // Resolve from parent folder if it's not the Season folder + if (episodeInfo == null && episode.Parent.GetType() == typeof(Folder)) + { + episodeInfo = resolver.Resolve(episode.Parent.Path, true, null, null, isAbsoluteNaming); + if (episodeInfo != null) + { + // add the container + episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.'); + } + } + } + + episodeInfo ??= new EpisodeInfo(episode.Path); try { @@ -2776,6 +2792,7 @@ namespace Emby.Server.Implementations.Library public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem) { + string newPath; if (ownerItem != null) { var libraryOptions = GetLibraryOptions(ownerItem); @@ -2783,15 +2800,9 @@ namespace Emby.Server.Implementations.Library { foreach (var pathInfo in libraryOptions.PathInfos) { - if (string.IsNullOrWhiteSpace(pathInfo.Path) || string.IsNullOrWhiteSpace(pathInfo.NetworkPath)) - { - continue; - } - - var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath); - if (substitutionResult.Item2) + if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath)) { - return substitutionResult.Item1; + return newPath; } } } @@ -2800,24 +2811,16 @@ namespace Emby.Server.Implementations.Library var metadataPath = _configurationManager.Configuration.MetadataPath; var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath; - if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath)) + if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath)) { - var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath); - if (metadataSubstitutionResult.Item2) - { - return metadataSubstitutionResult.Item1; - } + return newPath; } foreach (var map in _configurationManager.Configuration.PathSubstitutions) { - if (!string.IsNullOrWhiteSpace(map.From)) + if (path.TryReplaceSubPath(map.From, map.To, out newPath)) { - var substitutionResult = SubstitutePathInternal(path, map.From, map.To); - if (substitutionResult.Item2) - { - return substitutionResult.Item1; - } + return newPath; } } @@ -2826,47 +2829,12 @@ namespace Emby.Server.Implementations.Library public string SubstitutePath(string path, string from, string to) { - return SubstitutePathInternal(path, from, to).Item1; - } - - private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - if (string.IsNullOrWhiteSpace(from)) - { - throw new ArgumentNullException(nameof(from)); - } - - if (string.IsNullOrWhiteSpace(to)) + if (path.TryReplaceSubPath(from, to, out var newPath)) { - throw new ArgumentNullException(nameof(to)); + return newPath; } - from = from.Trim(); - to = to.Trim(); - - var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase); - var changed = false; - - if (!string.Equals(newPath, path, StringComparison.Ordinal)) - { - if (to.IndexOf('/', StringComparison.Ordinal) != -1) - { - newPath = newPath.Replace('\\', '/'); - } - else - { - newPath = newPath.Replace('/', '\\'); - } - - changed = true; - } - - return new Tuple<string, bool>(newPath, changed); + return path; } private void SetExtraTypeFromFilename(Video item) @@ -2924,12 +2892,20 @@ namespace Emby.Server.Implementations.Library public void UpdatePeople(BaseItem item, List<PersonInfo> people) { + UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult(); + } + + /// <inheritdoc /> + public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken) + { if (!item.SupportsPeople) { return; } _itemRepository.UpdatePeople(item.Id, people); + + await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false); } public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex) @@ -3001,7 +2977,7 @@ namespace Emby.Server.Implementations.Library if (collectionType != null) { - var path = Path.Combine(virtualFolderPath, collectionType.ToString() + ".collection"); + var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection"); File.WriteAllBytes(path, Array.Empty<byte>()); } @@ -3033,6 +3009,58 @@ namespace Emby.Server.Implementations.Library } } + private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken) + { + var personsToSave = new List<BaseItem>(); + + foreach (var person in people) + { + cancellationToken.ThrowIfCancellationRequested(); + + var itemUpdateType = ItemUpdateType.MetadataDownload; + var saveEntity = false; + var personEntity = GetPerson(person.Name); + + // if PresentationUniqueKey is empty it's likely a new item. + if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey)) + { + personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); + saveEntity = true; + } + + foreach (var id in person.ProviderIds) + { + if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)) + { + personEntity.SetProviderId(id.Key, id.Value); + saveEntity = true; + } + } + + if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary)) + { + personEntity.SetImage( + new ItemImageInfo + { + Path = person.ImageUrl, + Type = ImageType.Primary + }, + 0); + + saveEntity = true; + itemUpdateType = ItemUpdateType.ImageUpdate; + } + + if (saveEntity) + { + personsToSave.Add(personEntity); + await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); + } + } + + CreateItems(personsToSave, null, CancellationToken.None); + } + private void StartScanInBackground() { Task.Run(() => diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index 2070df31e..c2951dd15 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Library private readonly IMediaEncoder _mediaEncoder; private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths) { diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index c63eb7017..85d6d3043 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.Library private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private IMediaSourceProvider[] _providers; @@ -199,10 +199,15 @@ namespace Emby.Server.Implementations.Library { source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding); } + else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding); + source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing); + } } } - return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList(); + return SortMediaSources(list); } public MediaProtocol GetPathProtocol(string path) @@ -436,7 +441,7 @@ namespace Emby.Server.Implementations.Library } } - private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources) + private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources) { return sources.OrderBy(i => { @@ -451,8 +456,9 @@ namespace Emby.Server.Implementations.Library { var stream = i.VideoStream; - return stream == null || stream.Width == null ? 0 : stream.Width.Value; + return stream?.Width ?? 0; }) + .Where(i => i.Type != MediaSourceType.Placeholder) .ToList(); } @@ -584,18 +590,9 @@ namespace Emby.Server.Implementations.Library public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) { - var info = _openStreams.Values.FirstOrDefault(i => - { - var liveStream = i as ILiveStream; - if (liveStream != null) - { - return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase); - } - - return false; - }); + var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase)); - return Task.FromResult(info as IDirectStreamProvider); + return Task.FromResult(info.Value as IDirectStreamProvider); } public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 658c53f28..f8bae4fd1 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -100,8 +100,7 @@ namespace Emby.Server.Implementations.Library public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions) { - var genre = item as MusicGenre; - if (genre != null) + if (item is MusicGenre genre) { return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions); } diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 06ff3e611..0de4edb7e 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -1,7 +1,8 @@ #nullable enable using System; -using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; +using MediaBrowser.Common.Providers; namespace Emby.Server.Implementations.Library { @@ -41,11 +42,78 @@ namespace Emby.Server.Implementations.Library // for imdbid we also accept pattern matching if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase)) { - var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); - return m.Success ? m.Value : null; + var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId); + return match ? imdbId.ToString() : null; } return null; } + + /// <summary> + /// Replaces a sub path with another sub path and normalizes the final path. + /// </summary> + /// <param name="path">The original path.</param> + /// <param name="subPath">The original sub path.</param> + /// <param name="newSubPath">The new sub path.</param> + /// <param name="newPath">The result of the sub path replacement</param> + /// <returns>The path after replacing the sub path.</returns> + /// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception> + public static bool TryReplaceSubPath( + [NotNullWhen(true)] this string? path, + [NotNullWhen(true)] string? subPath, + [NotNullWhen(true)] string? newSubPath, + [NotNullWhen(true)] out string? newPath) + { + newPath = null; + + if (string.IsNullOrEmpty(path) + || string.IsNullOrEmpty(subPath) + || string.IsNullOrEmpty(newSubPath) + || subPath.Length > path.Length) + { + return false; + } + + char oldDirectorySeparatorChar; + char newDirectorySeparatorChar; + // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162 + // The reasoning behind this is that a forward slash likely means it's a Linux path and + // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much). + if (newSubPath.Contains('/', StringComparison.Ordinal)) + { + oldDirectorySeparatorChar = '\\'; + newDirectorySeparatorChar = '/'; + } + else + { + oldDirectorySeparatorChar = '/'; + newDirectorySeparatorChar = '\\'; + } + + path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar); + subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar); + + // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results + // when the sub path matches a similar but in-complete subpath + var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar; + if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (path.Length > subPath.Length + && !oldSubPathEndsWithSeparator + && path[subPath.Length] != newDirectorySeparatorChar) + { + return false; + } + + var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar); + // Ensure that the path with the old subpath removed starts with a leading dir separator + int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length; + newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx)); + + return true; + } } } diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 4e4cac75b..1d9b44874 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.IO; using System.Linq; @@ -18,11 +20,10 @@ namespace Emby.Server.Implementations.Library /// </summary> /// <param name="item">The item.</param> /// <param name="parent">The parent.</param> - /// <param name="fileSystem">The file system.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="directoryService">The directory service.</param> - /// <exception cref="ArgumentException">Item must have a path</exception> - public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService) + /// <exception cref="ArgumentException">Item must have a path.</exception> + public static void SetInitialItemValues(BaseItem item, Folder? parent, ILibraryManager libraryManager, IDirectoryService directoryService) { // This version of the below method has no ItemResolveArgs, so we have to require the path already being set if (string.IsNullOrEmpty(item.Path)) @@ -43,9 +44,14 @@ namespace Emby.Server.Implementations.Library // Make sure DateCreated and DateModified have values var fileInfo = directoryService.GetFile(item.Path); - SetDateCreated(item, fileSystem, fileInfo); + if (fileInfo == null) + { + throw new FileNotFoundException("Can't find item path.", item.Path); + } + + SetDateCreated(item, fileInfo); - EnsureName(item, item.Path, fileInfo); + EnsureName(item, fileInfo); } /// <summary> @@ -72,9 +78,9 @@ namespace Emby.Server.Implementations.Library item.Id = libraryManager.GetNewItemId(item.Path, item.GetType()); // Make sure the item has a name - EnsureName(item, item.Path, args.FileInfo); + EnsureName(item, args.FileInfo); - item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 || + item.IsLocked = item.Path.Contains("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) || item.GetParents().Any(i => i.IsLocked); // Make sure DateCreated and DateModified have values @@ -84,29 +90,16 @@ namespace Emby.Server.Implementations.Library /// <summary> /// Ensures the name. /// </summary> - private static void EnsureName(BaseItem item, string fullPath, FileSystemMetadata fileInfo) + private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo) { // If the subclass didn't supply a name, add it here - if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(fullPath)) + if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path)) { - var fileName = fileInfo == null ? Path.GetFileName(fullPath) : fileInfo.Name; - - item.Name = GetDisplayName(fileName, fileInfo != null && fileInfo.IsDirectory); + item.Name = fileInfo.IsDirectory ? fileInfo.Name : Path.GetFileNameWithoutExtension(fileInfo.Name); } } /// <summary> - /// Gets the display name. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="isDirectory">if set to <c>true</c> [is directory].</param> - /// <returns>System.String.</returns> - private static string GetDisplayName(string path, bool isDirectory) - { - return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path); - } - - /// <summary> /// Ensures DateCreated and DateModified have values. /// </summary> /// <param name="fileSystem">The file system.</param> @@ -114,21 +107,6 @@ namespace Emby.Server.Implementations.Library /// <param name="args">The args.</param> private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args) { - if (fileSystem == null) - { - throw new ArgumentNullException(nameof(fileSystem)); - } - - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } - - if (args == null) - { - throw new ArgumentNullException(nameof(args)); - } - // See if a different path came out of the resolver than what went in if (!fileSystem.AreEqual(args.Path, item.Path)) { @@ -136,7 +114,7 @@ namespace Emby.Server.Implementations.Library if (childData != null) { - SetDateCreated(item, fileSystem, childData); + SetDateCreated(item, childData); } else { @@ -144,17 +122,17 @@ namespace Emby.Server.Implementations.Library if (fileData.Exists) { - SetDateCreated(item, fileSystem, fileData); + SetDateCreated(item, fileData); } } } else { - SetDateCreated(item, fileSystem, args.FileInfo); + SetDateCreated(item, args.FileInfo); } } - private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info) + private static void SetDateCreated(BaseItem item, FileSystemMetadata? info) { var config = BaseItem.ConfigurationManager.GetMetadataConfiguration(); @@ -163,7 +141,7 @@ namespace Emby.Server.Implementations.Library // directoryService.getFile may return null if (info != null) { - var dateCreated = fileSystem.GetCreationTimeUtc(info); + var dateCreated = info.CreationTimeUtc; if (dateCreated.Equals(DateTime.MinValue)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 90b6a8a7d..4ad84579d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -201,6 +201,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio continue; } + if (resolvedItem.Files.Count == 0) + { + continue; + } + var firstMedia = resolvedItem.Files[0]; var libraryItem = new T diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 2f5e46038..6e688693b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>`0.</returns> - protected override T Resolve(ItemResolveArgs args) + public override T Resolve(ItemResolveArgs args) { return ResolveVideo<T>(args, false); } @@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <param name="args">The args.</param> /// <param name="parseName">if set to <c>true</c> [parse name].</param> /// <returns>``0.</returns> - protected TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) + protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) where TVideoType : Video, new() { var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 86242d137..0525c7e30 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" }; - protected override Book Resolve(ItemResolveArgs args) + public override Book Resolve(ItemResolveArgs args) { var collectionType = args.GetCollectionType(); diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs index 9ca76095b..92fb2a753 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs @@ -12,6 +12,12 @@ namespace Emby.Server.Implementations.Library.Resolvers where T : BaseItem, new() { /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public virtual ResolverPriority Priority => ResolverPriority.First; + + /// <summary> /// Resolves the specified args. /// </summary> /// <param name="args">The args.</param> @@ -22,12 +28,6 @@ namespace Emby.Server.Implementations.Library.Resolvers } /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public virtual ResolverPriority Priority => ResolverPriority.First; - - /// <summary> /// Sets initial values on the newly resolved item. /// </summary> /// <param name="item">The item.</param> diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 8ef7172de..16bf4dc4a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -69,6 +69,110 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return result; } + /// <summary> + /// Resolves the specified args. + /// </summary> + /// <param name="args">The args.</param> + /// <returns>Video.</returns> + public override Video Resolve(ItemResolveArgs args) + { + var collectionType = args.GetCollectionType(); + + // Find movies with their own folders + if (args.IsDirectory) + { + if (IsInvalid(args.Parent, collectionType)) + { + return null; + } + + var files = args.FileSystemChildren + .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) + .ToList(); + + if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) + { + return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + } + + if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) + { + return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); + } + + if (string.IsNullOrEmpty(collectionType)) + { + // Owned items will be caught by the plain video resolver + if (args.Parent == null) + { + // return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType); + return null; + } + + if (args.HasParent<Series>()) + { + return null; + } + + { + return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); + } + } + + if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) + { + return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); + } + + return null; + } + + // Handle owned items + if (args.Parent == null) + { + return base.Resolve(args); + } + + if (IsInvalid(args.Parent, collectionType)) + { + return null; + } + + Video item = null; + + if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) + { + item = ResolveVideo<MusicVideo>(args, false); + } + + // To find a movie file, the collection type must be movies or boxsets + else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) + { + item = ResolveVideo<Movie>(args, true); + } + else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) || + string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) + { + item = ResolveVideo<Video>(args, false); + } + else if (string.IsNullOrEmpty(collectionType)) + { + if (args.HasParent<Series>()) + { + return null; + } + + item = ResolveVideo<Video>(args, false); + } + + if (item != null) + { + item.IsInMixedFolder = true; + } + + return item; + } + private MultiItemResolverResult ResolveMultipleInternal( Folder parent, List<FileSystemMetadata> files, @@ -217,110 +321,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } /// <summary> - /// Resolves the specified args. - /// </summary> - /// <param name="args">The args.</param> - /// <returns>Video.</returns> - protected override Video Resolve(ItemResolveArgs args) - { - var collectionType = args.GetCollectionType(); - - // Find movies with their own folders - if (args.IsDirectory) - { - if (IsInvalid(args.Parent, collectionType)) - { - return null; - } - - var files = args.FileSystemChildren - .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) - .ToList(); - - if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) - { - return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); - } - - if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) - { - return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false); - } - - if (string.IsNullOrEmpty(collectionType)) - { - // Owned items will be caught by the plain video resolver - if (args.Parent == null) - { - // return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType); - return null; - } - - if (args.HasParent<Series>()) - { - return null; - } - - { - return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); - } - } - - if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) - { - return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true); - } - - return null; - } - - // Handle owned items - if (args.Parent == null) - { - return base.Resolve(args); - } - - if (IsInvalid(args.Parent, collectionType)) - { - return null; - } - - Video item = null; - - if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) - { - item = ResolveVideo<MusicVideo>(args, false); - } - - // To find a movie file, the collection type must be movies or boxsets - else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) - { - item = ResolveVideo<Movie>(args, true); - } - else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) || - string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) - { - item = ResolveVideo<Video>(args, false); - } - else if (string.IsNullOrEmpty(collectionType)) - { - if (args.HasParent<Series>()) - { - return null; - } - - item = ResolveVideo<Video>(args, false); - } - - if (item != null) - { - item.IsInMixedFolder = true; - } - - return item; - } - - /// <summary> /// Sets the initial item values. /// </summary> /// <param name="item">The item.</param> @@ -376,7 +376,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { var multiDiscFolders = new List<FileSystemMetadata>(); - var libraryOptions = args.GetLibraryOptions(); + var libraryOptions = args.LibraryOptions; var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos; var photos = new List<FileSystemMetadata>(); diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs index 3ac837057..204c8a62e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs @@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Library.Resolvers public class PhotoAlbumResolver : FolderResolver<PhotoAlbum> { private readonly IImageProcessor _imageProcessor; - private ILibraryManager _libraryManager; + private readonly ILibraryManager _libraryManager; /// <summary> /// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class. @@ -26,6 +26,9 @@ namespace Emby.Server.Implementations.Library.Resolvers _libraryManager = libraryManager; } + /// <inheritdoc /> + public override ResolverPriority Priority => ResolverPriority.Second; + /// <summary> /// Resolves the specified args. /// </summary> @@ -39,8 +42,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // Must be an image file within a photo collection var collectionType = args.GetCollectionType(); - if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) || - (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos)) + if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) + || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos)) { if (HasPhotos(args)) { @@ -84,8 +87,5 @@ namespace Emby.Server.Implementations.Library.Resolvers return false; } - - /// <inheritdoc /> - public override ResolverPriority Priority => ResolverPriority.Second; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index bcfcee9c6..3cb6542cf 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library.Resolvers var collectionType = args.CollectionType; if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) - || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos)) + || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos)) { if (IsImageFile(args.Path, _imageProcessor)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index c76d41e5c..5f051321f 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -63,7 +63,8 @@ namespace Emby.Server.Implementations.Library.Resolvers { Path = args.Path, Name = Path.GetFileNameWithoutExtension(args.Path), - IsInMixedFolder = true + IsInMixedFolder = true, + PlaylistMediaType = MediaType.Audio }; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 2f7af60c0..6f29bc649 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; @@ -12,11 +13,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV public class EpisodeResolver : BaseVideoResolver<Episode> { /// <summary> + /// Initializes a new instance of the <see cref="EpisodeResolver"/> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + public EpisodeResolver(ILibraryManager libraryManager) + : base(libraryManager) + { + } + + /// <summary> /// Resolves the specified args. /// </summary> /// <param name="args">The args.</param> /// <returns>Episode.</returns> - protected override Episode Resolve(ItemResolveArgs args) + public override Episode Resolve(ItemResolveArgs args) { var parent = args.Parent; @@ -25,30 +35,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } - var season = parent as Season; - // Just in case the user decided to nest episodes. // Not officially supported but in some cases we can handle it. - if (season == null) - { - season = parent.GetParents().OfType<Season>().FirstOrDefault(); - } - // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something + var season = parent as Season ?? parent.GetParents().OfType<Season>().FirstOrDefault(); + + // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something // Also handle flat tv folders - if (season != null || - string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || - args.HasParent<Series>()) + if ((season != null || + string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || + args.HasParent<Series>()) + && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase))) { var episode = ResolveVideo<Episode>(args, false); if (episode != null) { - var series = parent as Series; - if (series == null) - { - series = parent.GetParents().OfType<Series>().FirstOrDefault(); - } + var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault(); if (series != null) { @@ -74,14 +77,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } - - /// <summary> - /// Initializes a new instance of the <see cref="EpisodeResolver"/> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - public EpisodeResolver(ILibraryManager libraryManager) - : base(libraryManager) - { - } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 3332e1806..768e2e4f5 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV CultureInfo.InvariantCulture, _localization.GetLocalizedString("NameSeasonNumber"), seasonNumber, - args.GetLibraryOptions().PreferredMetadataLanguage); + args.LibraryOptions.PreferredMetadataLanguage); } return season; diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 732bfd94d..8fc3e3e75 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -19,19 +19,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// </summary> public class SeriesResolver : FolderResolver<Series> { - private readonly IFileSystem _fileSystem; private readonly ILogger<SeriesResolver> _logger; private readonly ILibraryManager _libraryManager; /// <summary> /// Initializes a new instance of the <see cref="SeriesResolver"/> class. /// </summary> - /// <param name="fileSystem">The file system.</param> /// <param name="logger">The logger.</param> /// <param name="libraryManager">The library manager.</param> - public SeriesResolver(IFileSystem fileSystem, ILogger<SeriesResolver> logger, ILibraryManager libraryManager) + public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager) { - _fileSystem = fileSystem; _logger = logger; _libraryManager = libraryManager; } @@ -59,15 +56,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var collectionType = args.GetCollectionType(); if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { - // if (args.ContainsFileSystemEntryByName("tvshow.nfo")) - //{ - // return new Series - // { - // Path = args.Path, - // Name = Path.GetFileName(args.Path) - // }; - //} - var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path); if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { @@ -100,7 +88,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } - if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, false)) + if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false)) { return new Series { @@ -117,8 +105,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV public static bool IsSeriesFolder( string path, IEnumerable<FileSystemMetadata> fileSystemChildren, - IDirectoryService directoryService, - IFileSystem fileSystem, ILogger<SeriesResolver> logger, ILibraryManager libraryManager, bool isTvContentType) @@ -127,7 +113,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (child.IsDirectory) { - if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager)) + if (IsSeasonFolder(child.FullName, isTvContentType)) { logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); return true; @@ -161,31 +147,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV } /// <summary> - /// Determines whether [is place holder] [the specified path]. - /// </summary> - /// <param name="path">The path.</param> - /// <returns><c>true</c> if [is place holder] [the specified path]; otherwise, <c>false</c>.</returns> - /// <exception cref="ArgumentNullException">path</exception> - private static bool IsVideoPlaceHolder(string path) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - var extension = Path.GetExtension(path); - - return string.Equals(extension, ".disc", StringComparison.OrdinalIgnoreCase); - } - - /// <summary> /// Determines whether [is season folder] [the specified path]. /// </summary> /// <param name="path">The path.</param> /// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param> - /// <param name="libraryManager">The library manager.</param> /// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns> - private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager) + private static bool IsSeasonFolder(string path, bool isTvContentType) { var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber; diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 94602582b..bcdf854ca 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -12,7 +12,6 @@ using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Search; -using Microsoft.Extensions.Logging; using Genre = MediaBrowser.Controller.Entities.Genre; using Person = MediaBrowser.Controller.Entities.Person; diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index d16275b19..827e3c64b 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -13,8 +13,8 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using Book = MediaBrowser.Controller.Entities.Book; using AudioBook = MediaBrowser.Controller.Entities.AudioBook; +using Book = MediaBrowser.Controller.Entities.Book; namespace Emby.Server.Implementations.Library { @@ -248,15 +248,15 @@ namespace Emby.Server.Implementations.Library } else if (positionTicks > 0 && hasRuntime && item is AudioBook) { - var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes; - var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes; + var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes; + var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes; - if (minIn > _config.Configuration.MinAudiobookResume) + if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume) { // ignore progress during the beginning positionTicks = 0; } - else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks) + else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks) { // mark as completed close to the end positionTicks = 0; diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index b6b7ea949..ac041bcf6 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using Jellyfin.Data.Entities; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 341194f23..7a6b1d8b6 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -45,7 +45,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); - using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None)) { onStarted(); @@ -70,7 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); - await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read); + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None); onStarted(); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 13b5a1c55..28a2095e1 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -17,7 +17,6 @@ using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; @@ -802,22 +801,22 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public ActiveRecordingInfo GetActiveRecordingInfo(string path) { - if (string.IsNullOrWhiteSpace(path)) + if (string.IsNullOrWhiteSpace(path) || _activeRecordings.IsEmpty) { return null; } - foreach (var recording in _activeRecordings.Values) + foreach (var (_, recordingInfo) in _activeRecordings) { - if (string.Equals(recording.Path, path, StringComparison.Ordinal) && !recording.CancellationTokenSource.IsCancellationRequested) + if (string.Equals(recordingInfo.Path, path, StringComparison.Ordinal) && !recordingInfo.CancellationTokenSource.IsCancellationRequested) { - var timer = recording.Timer; + var timer = recordingInfo.Timer; if (timer.Status != RecordingStatus.InProgress) { return null; } - return recording; + return recordingInfo; } } @@ -1622,9 +1621,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } return _activeRecordings - .Values - .ToList() - .Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase)); + .Any(i => string.Equals(i.Value.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Value.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase)); } private IRecorder GetRecorder(MediaSourceInfo mediaSource) @@ -1856,7 +1853,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None)) { var settings = new XmlWriterSettings { @@ -1920,7 +1918,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None)) { var settings = new XmlWriterSettings { @@ -2238,14 +2237,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var enabledTimersForSeries = new List<TimerInfo>(); foreach (var timer in allTimers) { - var existingTimer = _timerProvider.GetTimer(timer.Id); - - if (existingTimer == null) - { - existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) + var existingTimer = _timerProvider.GetTimer(timer.Id) + ?? (string.IsNullOrWhiteSpace(timer.ProgramId) ? null - : _timerProvider.GetTimerByProgramId(timer.ProgramId); - } + : _timerProvider.GetTimerByProgramId(timer.ProgramId)); if (existingTimer == null) { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 78a82118e..9372b0f6c 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -10,6 +10,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Json; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; @@ -28,7 +29,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IServerApplicationPaths _appPaths; private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private bool _hasExited; private Stream _logFileStream; private string _targetPath; @@ -307,13 +308,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { using (var reader = new StreamReader(source)) { - while (!reader.EndOfStream) + await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) { - var line = await reader.ReadLineAsync().ConfigureAwait(false); - var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); - await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + await target.WriteAsync(bytes.AsMemory()).ConfigureAwait(false); await target.FlushAsync().ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs index 463d0ed0a..8c27ca76e 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs @@ -9,55 +9,44 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV internal class EpgChannelData { + + private readonly Dictionary<string, ChannelInfo> _channelsById; + + private readonly Dictionary<string, ChannelInfo> _channelsByNumber; + + private readonly Dictionary<string, ChannelInfo> _channelsByName; + public EpgChannelData(IEnumerable<ChannelInfo> channels) { - ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); - ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); - ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); + _channelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); + _channelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); + _channelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); foreach (var channel in channels) { - ChannelsById[channel.Id] = channel; + _channelsById[channel.Id] = channel; if (!string.IsNullOrEmpty(channel.Number)) { - ChannelsByNumber[channel.Number] = channel; + _channelsByNumber[channel.Number] = channel; } var normalizedName = NormalizeName(channel.Name ?? string.Empty); if (!string.IsNullOrWhiteSpace(normalizedName)) { - ChannelsByName[normalizedName] = channel; + _channelsByName[normalizedName] = channel; } } } - private Dictionary<string, ChannelInfo> ChannelsById { get; set; } - - private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; } - - private Dictionary<string, ChannelInfo> ChannelsByName { get; set; } - public ChannelInfo GetChannelById(string id) - { - ChannelsById.TryGetValue(id, out var result); - - return result; - } + => _channelsById.GetValueOrDefault(id); public ChannelInfo GetChannelByNumber(string number) - { - ChannelsByNumber.TryGetValue(number, out var result); - - return result; - } + => _channelsByNumber.GetValueOrDefault(number); public ChannelInfo GetChannelByName(string name) - { - ChannelsByName.TryGetValue(name, out var result); - - return result; - } + => _channelsByName.GetValueOrDefault(name); public static string NormalizeName(string value) { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index 57424f043..1cac9cb96 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -4,9 +4,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Text.Json; -using System.Threading.Tasks; using MediaBrowser.Common.Json; using Microsoft.Extensions.Logging; @@ -17,7 +15,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { private readonly string _dataPath; private readonly object _fileDataLock = new object(); - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private T[] _items; public ItemDataProvider( diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs index da707fec6..b1259de23 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs @@ -2,7 +2,6 @@ using System; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.EmbyTV diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 6d7c5ac6e..9af65cabb 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings private readonly ICryptoProvider _cryptoProvider; private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>(); - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private DateTime _lastErrorResponse; public SchedulesDirect( @@ -787,14 +787,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings { var channelNumber = GetChannelNumber(channel); - var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase)); - if (station == null) - { - station = new ScheduleDirect.Station + var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase)) + ?? new ScheduleDirect.Station { stationID = channel.stationID }; - } var channelInfo = new ChannelInfo { diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 76c875737..6824aa442 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Threading; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 63a3146aa..1145d8aa1 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -987,10 +987,7 @@ namespace Emby.Server.Implementations.LiveTv var externalProgramId = programTuple.Item2; string externalSeriesId = programTuple.Item3; - if (timerList == null) - { - timerList = (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items; - } + timerList ??= (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items; var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase)); var foundSeriesTimer = false; @@ -1018,10 +1015,7 @@ namespace Emby.Server.Implementations.LiveTv continue; } - if (seriesTimerList == null) - { - seriesTimerList = (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items; - } + seriesTimerList ??= (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items; var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase)); @@ -1974,10 +1968,7 @@ namespace Emby.Server.Implementations.LiveTv }; } - if (service == null) - { - service = _services[0]; - } + service ??= _services[0]; var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 0760e8127..324109bcf 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -8,10 +8,8 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Json; using MediaBrowser.Common.Net; @@ -19,7 +17,6 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -61,7 +58,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun _networkManager = networkManager; _streamHelper = streamHelper; - _jsonOptions = JsonDefaults.GetOptions(); + _jsonOptions = JsonDefaults.Options; } public string Name => "HD Homerun"; @@ -185,16 +182,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); var tuners = new List<LiveTvTunerInfo>(); - while (!sr.EndOfStream) + await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) { - string line = StripXML(sr.ReadLine()); - if (line.Contains("Channel", StringComparison.Ordinal)) + string stripedLine = StripXML(line); + if (stripedLine.Contains("Channel", StringComparison.Ordinal)) { LiveTvTunerStatus status; - var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = line.Substring(0, index - 1); - var currentChannel = line.Substring(index + 7); - if (currentChannel != "none") + var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); + var name = stripedLine.Substring(0, index - 1); + var currentChannel = stripedLine.Substring(index + 7); + if (string.Equals(currentChannel, "none", StringComparison.Ordinal)) { status = LiveTvTunerStatus.LiveTv; } @@ -424,10 +421,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun string audioCodec = channelInfo.AudioCodec; - if (!videoBitrate.HasValue) - { - videoBitrate = isHd ? 15000000 : 2000000; - } + videoBitrate ??= isHd ? 15000000 : 2000000; int? audioBitrate = isHd ? 448000 : 192000; @@ -664,7 +658,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun _modelCache.Clear(); } - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token; + using var timedCancellationToken = new CancellationTokenSource(discoveryDurationMs); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timedCancellationToken.Token, cancellationToken); + cancellationToken = linkedCancellationTokenSource.Token; var list = new List<TunerHostInfo>(); // Create udp broadcast discovery message diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index f09338330..a7fda1d72 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -2,6 +2,7 @@ using System; using System.Buffers; +using System.Buffers.Binary; using System.Collections.Generic; using System.Globalization; using System.Net; @@ -10,6 +11,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common; using MediaBrowser.Controller.LiveTv; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun @@ -120,17 +122,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken) { - var lockkeyMsg = CreateGetMessage(tuner, "lockkey"); - await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); - byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); try { - int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + var msgLen = WriteGetMessage(buffer, tuner, "lockkey"); + await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false); - ParseReturnMessage(buffer, receivedBytes, out string returnVal); + int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase); + return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none"); } finally { @@ -166,24 +166,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun _activeTuner = i; var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue); - var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); - await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); - int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null); + await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false); + int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out _)) + if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _)) { continue; } foreach (var command in commands.GetCommands()) { - var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); - await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); - receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + var channelMsgLen = WriteSetMessage(buffer, i, command.Item1, command.Item2, lockKeyValue); + await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false); + receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out _)) + if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _)) { await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); continue; @@ -191,13 +191,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort); - var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); + var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue); - await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false); - receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false); + receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out _)) + if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _)) { await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); continue; @@ -232,12 +232,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { foreach (var command in commandList) { - var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey); - await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); - int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.Item1, command.Item2, _lockkey); + await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false); + int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out _)) + if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _)) { return; } @@ -265,17 +265,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var stream = client.GetStream(); - var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue); - await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false); - var buffer = ArrayPool<byte>.Shared.Rent(8192); try { - await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue); + var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue); + await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false); + + await stream.ReadAsync(buffer).ConfigureAwait(false); + var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue); _lockkey = null; - await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false); - await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false); + await stream.ReadAsync(buffer).ConfigureAwait(false); } finally { @@ -283,249 +283,136 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - private static byte[] CreateGetMessage(int tuner, string name) + internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name) { - var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name)); - int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length - - var message = new byte[messageLength]; + var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name); + int offset = WriteHeaderAndPayload(buffer, byteName); + return FinishPacket(buffer, offset); + } - int offset = InsertHeaderAndName(byteName, messageLength, message); + internal static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey) + { + var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name); + int offset = WriteHeaderAndPayload(buffer, byteName); - bool flipEndian = BitConverter.IsLittleEndian; + buffer[offset++] = GetSetValue; + offset += WriteNullTerminatedString(buffer.Slice(offset), value); - // calculate crc and insert at the end of the message - var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4)); - if (flipEndian) + if (lockkey.HasValue) { - Array.Reverse(crcBytes); + buffer[offset++] = GetSetLockkey; + buffer[offset++] = 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value); + offset += 4; } - Buffer.BlockCopy(crcBytes, 0, message, offset, 4); - - return message; + return FinishPacket(buffer, offset); } - private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey) + internal static int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> payload) { - var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name)); - var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value)); + int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1; - int messageLength = byteName.Length + byteValue.Length + 12; - if (lockkey.HasValue) - { - messageLength += 6; - } - - var message = new byte[messageLength]; + // TODO: variable length: this can be 2 bytes if len > 127 + // Write length in front of value + buffer[0] = Convert.ToByte(len); - int offset = InsertHeaderAndName(byteName, messageLength, message); + // null-terminate + buffer[len++] = 0; - bool flipEndian = BitConverter.IsLittleEndian; + return len; + } - message[offset++] = GetSetValue; - message[offset++] = Convert.ToByte(byteValue.Length); - Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length); - offset += byteValue.Length; - if (lockkey.HasValue) - { - message[offset++] = GetSetLockkey; - message[offset++] = 4; - var lockKeyBytes = BitConverter.GetBytes(lockkey.Value); - if (flipEndian) - { - Array.Reverse(lockKeyBytes); - } + private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload) + { + // Packet type + BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest); - Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4); - offset += 4; - } + // We write the payload length at the end + int offset = 4; - // calculate crc and insert at the end of the message - var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4)); - if (flipEndian) - { - Array.Reverse(crcBytes); - } + // Tag + buffer[offset++] = GetSetName; - Buffer.BlockCopy(crcBytes, 0, message, offset, 4); + // Payload length + data + int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload); + offset += strLen; - return message; + return offset; } - private static int InsertHeaderAndName(byte[] byteName, int messageLength, byte[] message) + private static int FinishPacket(Span<byte> buffer, int offset) { - // check to see if we need to flip endiannes - bool flipEndian = BitConverter.IsLittleEndian; - int offset = 0; - - // create header bytes - var getSetBytes = BitConverter.GetBytes(GetSetRequest); - var msgLenBytes = BitConverter.GetBytes((ushort)(messageLength - 8)); // Subtrace 4 bytes for header and 4 bytes for crc - - if (flipEndian) - { - Array.Reverse(getSetBytes); - Array.Reverse(msgLenBytes); - } - - // insert header bytes into message - Buffer.BlockCopy(getSetBytes, 0, message, offset, 2); - offset += 2; - Buffer.BlockCopy(msgLenBytes, 0, message, offset, 2); - offset += 2; + // Payload length + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4)); - // insert tag name and length - message[offset++] = GetSetName; - message[offset++] = Convert.ToByte(byteName.Length); + // calculate crc and insert at the end of the message + var crc = Crc32.Compute(buffer.Slice(0, offset)); + BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc); - // insert name string - Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length); - offset += byteName.Length; + return offset + 4; + } - return offset; + internal static bool VerifyReturnValueOfGetSet(ReadOnlySpan<byte> buffer, string expected) + { + return TryGetReturnValueOfGetSet(buffer, out var value) + && string.Equals(Encoding.UTF8.GetString(value), expected, StringComparison.OrdinalIgnoreCase); } - private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal) + internal static bool TryGetReturnValueOfGetSet(ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> value) { - returnVal = string.Empty; + value = ReadOnlySpan<byte>.Empty; - if (numBytes < 4) + if (buffer.Length < 8) { return false; } - var flipEndian = BitConverter.IsLittleEndian; - int offset = 0; - byte[] msgTypeBytes = new byte[2]; - Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length); - - if (flipEndian) + uint crc = BinaryPrimitives.ReadUInt32LittleEndian(buffer[^4..]); + if (crc != Crc32.Compute(buffer[..^4])) { - Array.Reverse(msgTypeBytes); + return false; } - var msgType = BitConverter.ToUInt16(msgTypeBytes, 0); - offset += 2; - - if (msgType != GetSetReply) + if (BinaryPrimitives.ReadUInt16BigEndian(buffer) != GetSetReply) { return false; } - byte[] msgLengthBytes = new byte[2]; - Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length); - if (flipEndian) + var msgLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2)); + if (buffer.Length != 2 + 2 + 4 + msgLength) { - Array.Reverse(msgLengthBytes); + return false; } - var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0); - offset += 2; - - if (numBytes < msgLength + 8) + var offset = 4; + if (buffer[offset++] != GetSetName) { return false; } - offset++; // Name Tag - - var nameLength = buf[offset++]; + var nameLength = buffer[offset++]; + if (buffer.Length < 4 + 1 + offset + nameLength) + { + return false; + } - // skip the name field to get to value for return offset += nameLength; - offset++; // Value Tag - - var valueLength = buf[offset++]; - - returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator - return true; - } - - private static class HdHomerunCrc - { - private static uint[] crc_table = { - 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, - 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, - 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, - 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, - 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, - 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, - 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, - 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, - 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, - 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, - 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, - 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, - 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, - 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, - 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, - 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, - 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, - 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, - 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, - 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, - 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, - 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, - 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, - 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, - 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, - 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, - 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, - 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, - 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, - 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, - 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, - 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, - 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, - 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, - 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, - 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, - 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, - 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, - 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, - 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, - 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, - 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, - 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, - 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, - 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, - 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, - 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, - 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, - 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, - 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, - 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, - 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, - 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, - 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, - 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, - 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, - 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, - 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, - 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, - 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, - 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, - 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, - 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, - 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d }; - - public static uint GetCrc32(byte[] bytes, int numBytes) + if (buffer[offset++] != GetSetValue) { - var hash = 0xffffffff; - for (var i = 0; i < numBytes; i++) - { - hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff]; - } + return false; + } - var tmp = ~hash & 0xffffffff; - var b0 = tmp & 0xff; - var b1 = (tmp >> 8) & 0xff; - var b2 = (tmp >> 16) & 0xff; - var b3 = (tmp >> 24) & 0xff; - return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; + var valueLength = buffer[offset++]; + if (buffer.Length < 4 + offset + valueLength) + { + return false; } + + // remove null terminator + value = buffer.Slice(offset, valueLength - 1); + return true; } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 78e62ff0a..f8baf55da 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -150,7 +150,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken) { - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token).Token; + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token); + cancellationToken = linkedCancellationTokenSource.Token; // use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039 var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index c4f173c7a..84d416149 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; @@ -36,16 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts // Read the file and display it line by line. using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false))) { - return GetChannels(reader, channelIdPrefix, info.Id); - } - } - - public List<ChannelInfo> ParseString(string text, string channelIdPrefix, string tunerHostId) - { - // Read the file and display it line by line. - using (var reader = new StringReader(text)) - { - return GetChannels(reader, channelIdPrefix, tunerHostId); + return await GetChannelsAsync(reader, channelIdPrefix, info.Id).ConfigureAwait(false); } } @@ -71,43 +61,42 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private const string ExtInfPrefix = "#EXTINF:"; - private List<ChannelInfo> GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId) + private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId) { var channels = new List<ChannelInfo>(); - string line; string extInf = string.Empty; - while ((line = reader.ReadLine()) != null) + await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) { - line = line.Trim(); - if (string.IsNullOrWhiteSpace(line)) + var trimmedLine = line.Trim(); + if (string.IsNullOrWhiteSpace(trimmedLine)) { continue; } - if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase)) + if (trimmedLine.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase)) { continue; } - if (line.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase)) + if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase)) { - extInf = line.Substring(ExtInfPrefix.Length).Trim(); + extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim(); _logger.LogInformation("Found m3u channel: {0}", extInf); } - else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith('#')) + else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) { - var channel = GetChannelnfo(extInf, tunerHostId, line); + var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine); if (string.IsNullOrWhiteSpace(channel.Id)) { - channel.Id = channelIdPrefix + line.GetMD5().ToString("N", CultureInfo.InvariantCulture); + channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); } else { channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture); } - channel.Path = line; + channel.Path = trimmedLine; channels.Add(channel); extInf = string.Empty; } @@ -133,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts channel.ImageUrl = value; } + if (attributes.TryGetValue("group-title", out string groupTitle)) + { + channel.ChannelGroup = groupTitle; + } + channel.Name = GetChannelName(extInf, attributes); channel.Number = GetChannelNumber(extInf, attributes, mediaUrl); diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 4ee4eb989..051d6d009 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -1,5 +1,5 @@ { - "Albums": "Albums", + "Albums": "Albummer", "AppDeviceValues": "App: {0}, Enhed: {1}", "Application": "Applikation", "Artists": "Kunstnere", diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json index 3ff7eddae..ca615cc8c 100644 --- a/Emby.Server.Implementations/Localization/Core/eo.json +++ b/Emby.Server.Implementations/Localization/Core/eo.json @@ -22,5 +22,26 @@ "Artists": "Artistoj", "Application": "Aplikaĵo", "AppDeviceValues": "Aplikaĵo: {0}, Aparato: {1}", - "Albums": "Albumoj" + "Albums": "Albumoj", + "TasksLibraryCategory": "Libraro", + "VersionNumber": "Versio {0}", + "UserDownloadingItemWithValues": "{0} elŝutas {1}", + "UserCreatedWithName": "Uzanto {0} kreiĝis", + "User": "Uzanto", + "System": "Sistemo", + "Songs": "Kantoj", + "ScheduledTaskStartedWithName": "{0} komencis", + "ScheduledTaskFailedWithName": "{0} malsukcesis", + "PluginUninstalledWithName": "{0} malinstaliĝis", + "PluginInstalledWithName": "{0} instaliĝis", + "Plugin": "Kromprogramo", + "Playlists": "Ludlistoj", + "Photos": "Fotoj", + "NotificationOptionPluginUninstalled": "Kromprogramo malinstaliĝis", + "NotificationOptionNewLibraryContent": "Nova enhavo aldoniĝis", + "NotificationOptionPluginInstalled": "Kromprogramo instaliĝis", + "MusicVideos": "Muzikvideoj", + "LabelIpAddressValue": "IP-adreso: {0}", + "Genres": "Ĝenroj", + "DeviceOfflineWithName": "{0} malkonektis" } diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index 12bcd793e..11139d32a 100644 --- a/Emby.Server.Implementations/Localization/Core/gl.json +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -57,5 +57,27 @@ "DeviceOnlineWithName": "{0} conectouse", "DeviceOfflineWithName": "{0} desconectouse", "Default": "Por defecto", - "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}" + "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}", + "TaskCleanLogs": "Limpar Carpeta de Rexistros", + "TaskCleanActivityLog": "Limpar Rexistro de Actividade", + "TasksChannelsCategory": "Canáis de Internet", + "TaskUpdatePlugins": "Actualizar Plugins", + "User": "Usuario", + "Undefined": "Sen definir", + "TvShows": "Programas de TV", + "System": "Sistema", + "Sync": "Sincronizar", + "SubtitleDownloadFailureFromForItem": "Fallou a descarga de subtítulos para {1} dende {0}", + "StartupEmbyServerIsLoading": "O Servidor Jellyfin está cargando. Por favor, reinténteo en breve.", + "Songs": "Cancións", + "Shows": "Programas", + "ServerNameNeedsToBeRestarted": "{0} precisa ser reiniciado", + "ScheduledTaskStartedWithName": "{0} comezou", + "ScheduledTaskFailedWithName": "{0} fallou", + "ProviderValue": "Provedor: {0}", + "PluginUpdatedWithName": "{0} foi actualizado", + "PluginUninstalledWithName": "{0} foi desinstalado", + "PluginInstalledWithName": "{0} foi instalado", + "Playlists": "Listas de reproducción", + "Photos": "Fotos" } diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index a321e35d0..829a29ad4 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -109,14 +109,14 @@ "TasksMaintenanceCategory": "Qyzmet körsetu", "Undefined": "Anyqtalmağan", "Forced": "Mäjbürlı", - "TaskDownloadMissingSubtitlesDescription": "Metaderekter teŋşelımderı negızınde joq subtitrlerdı Internetten ızdeidı.", + "TaskDownloadMissingSubtitlesDescription": "Metaderekter teŋşelımderı negızınde joq subtitrlerdı İnternetten ızdeidı.", "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jaŋğyrtady.", "TaskCleanTranscodeDescription": "Bіr künnen asqan qaita kodtau faildaryn joiady.", "TaskUpdatePluginsDescription": "Avtomatty türde jaŋartuğa teŋşelgen plaginder üşın jaŋartulardy jüktep alady jäne ornatady.", "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jaŋartady.", "TaskCleanLogsDescription": "{0} künnen asqan jūrnal faildaryn joiady.", "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaŋa faildardy skanerleidі jäne metaderekterdı jaŋğyrtady.", - "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşіn nobailar jasaidy.", + "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.", "TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.", "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teŋşelgen jasynan asqan jazbalary joiady." } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index d4cb592ef..9920ef4d5 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -113,5 +113,9 @@ "TasksChannelsCategory": "Internetiniai Kanalai", "TasksApplicationCategory": "Programa", "TasksLibraryCategory": "Mediateka", - "TasksMaintenanceCategory": "Priežiūra" + "TasksMaintenanceCategory": "Priežiūra", + "TaskCleanActivityLog": "Švarus veiklos žurnalas", + "Undefined": "Neapibrėžtas", + "Forced": "Priverstas", + "Default": "Numatytas" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 5ec8f1e88..323dcced0 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -16,7 +16,7 @@ "Folders": "Pastas", "Genres": "Gêneros", "HeaderAlbumArtists": "Artistas do Álbum", - "HeaderContinueWatching": "Continuar Assistindo", + "HeaderContinueWatching": "Continuar assistindo", "HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteEpisodes": "Episódios favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 71dd2c7a3..5bf58baf8 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -50,7 +50,7 @@ "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ", "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ", "HeaderContinueWatching": "ดูต่อ", - "HeaderAlbumArtists": "อัลบั้มศิลปิน", + "HeaderAlbumArtists": "ศิลปินอัลบั้ม", "Genres": "ประเภท", "Folders": "โฟลเดอร์", "Favorites": "รายการโปรด", @@ -112,5 +112,6 @@ "System": "ระบบ", "Sync": "ซิงค์", "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้", - "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่" + "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่", + "Default": "ค่าเริ่มต้น" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 3f9e22106..220e423bf 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -7,11 +7,11 @@ using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Localization @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Localization private List<CultureDto> _cultures; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; /// <summary> /// Initializes a new instance of the <see cref="LocalizationManager" /> class. @@ -73,8 +73,7 @@ namespace Emby.Server.Implementations.Localization using (var str = _assembly.GetManifestResourceStream(resource)) using (var reader = new StreamReader(str)) { - string line; - while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) + await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) { if (string.IsNullOrWhiteSpace(line)) { @@ -119,10 +118,8 @@ namespace Emby.Server.Implementations.Localization using (var stream = _assembly.GetManifestResourceStream(ResourcePath)) using (var reader = new StreamReader(stream)) { - while (!reader.EndOfStream) + await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) { - var line = await reader.ReadLineAsync().ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(line)) { continue; @@ -180,7 +177,7 @@ namespace Emby.Server.Implementations.Localization /// <inheritdoc /> public IEnumerable<CountryInfo> GetCountries() { - StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json")); + using StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json")); return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions); } @@ -316,10 +313,9 @@ namespace Emby.Server.Implementations.Localization } const string Prefix = "Core"; - var key = Prefix + culture; return _dictionaries.GetOrAdd( - key, + culture, f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); } diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index c6e931448..031b5d2e7 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -15,7 +15,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.MediaEncoder @@ -82,11 +81,6 @@ namespace Emby.Server.Implementations.MediaEncoder return false; } - if (video.VideoType == VideoType.Dvd) - { - return false; - } - if (video.IsShortcut) { return false; diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 932f721ab..2d1a559f1 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.Playlists // Create a list of the new linked children to add to the playlist var childrenToAdd = newItems - .Select(i => LinkedChild.Create(i)) + .Select(LinkedChild.Create) .ToList(); // Log duplicates that have been ignored, if any diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 7bc9f0a7e..14df20936 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; @@ -34,7 +35,7 @@ namespace Emby.Server.Implementations.Plugins private readonly ILogger<PluginManager> _logger; private readonly IApplicationHost _appHost; private readonly ServerConfiguration _config; - private readonly IList<LocalPlugin> _plugins; + private readonly List<LocalPlugin> _plugins; private readonly Version _minimumVersion; private IHttpClientFactory? _httpClientFactory; @@ -43,12 +44,7 @@ namespace Emby.Server.Implementations.Plugins { get { - if (_httpClientFactory == null) - { - _httpClientFactory = _appHost.Resolve<IHttpClientFactory>(); - } - - return _httpClientFactory; + return _httpClientFactory ?? (_httpClientFactory = _appHost.Resolve<IHttpClientFactory>()); } } @@ -70,7 +66,7 @@ namespace Emby.Server.Implementations.Plugins _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _pluginsPath = pluginsPath; _appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion)); - _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions()) + _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options) { WriteIndented = true }; @@ -94,7 +90,7 @@ namespace Emby.Server.Implementations.Plugins /// <summary> /// Gets the Plugins. /// </summary> - public IList<LocalPlugin> Plugins => _plugins; + public IReadOnlyList<LocalPlugin> Plugins => _plugins; /// <summary> /// Returns all the assemblies. @@ -165,9 +161,7 @@ namespace Emby.Server.Implementations.Plugins /// </summary> public void CreatePlugins() { - _ = _appHost.GetExports<IPlugin>(CreatePluginInstance) - .Where(i => i != null) - .ToArray(); + _ = _appHost.GetExports<IPlugin>(CreatePluginInstance); } /// <summary> @@ -277,11 +271,7 @@ namespace Emby.Server.Implementations.Plugins // If no version is given, return the current instance. var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList(); - plugin = plugins.FirstOrDefault(p => p.Instance != null); - if (plugin == null) - { - plugin = plugins.OrderByDescending(p => p.Version).FirstOrDefault(); - } + plugin = plugins.FirstOrDefault(p => p.Instance != null) ?? plugins.OrderByDescending(p => p.Version).FirstOrDefault(); } else { @@ -368,7 +358,7 @@ namespace Emby.Server.Implementations.Plugins } /// <inheritdoc/> - public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path) + public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status) { if (packageInfo == null) { @@ -411,9 +401,9 @@ namespace Emby.Server.Implementations.Plugins Overview = packageInfo.Overview, Owner = packageInfo.Owner, TargetAbi = versionInfo.TargetAbi ?? string.Empty, - Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp), + Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture), Version = versionInfo.Version, - Status = PluginStatus.Active, + Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state. AutoUpdate = true, ImagePath = imagePath }; @@ -678,7 +668,7 @@ namespace Emby.Server.Implementations.Plugins var entry = versions[x]; if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase)) { - entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories)); + entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories); if (entry.IsEnabledAndSupported) { lastName = entry.Name; diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 7bed06de3..0259dc436 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Globalization; using System.Linq; using System.Security.Cryptography; -using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; @@ -258,20 +257,17 @@ namespace Emby.Server.Implementations.QuickConnect } // Expire stale connection requests - var code = string.Empty; - var values = _currentRequests.Values.ToList(); - - for (int i = 0; i < values.Count; i++) + foreach (var (_, currentRequest) in _currentRequests) { - var added = values[i].DateAdded ?? DateTime.UnixEpoch; - if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll) + var added = currentRequest.DateAdded ?? DateTime.UnixEpoch; + if (expireAll || DateTime.UtcNow > added.AddMinutes(Timeout)) { - code = values[i].Code; - _logger.LogDebug("Removing expired request {code}", code); + var code = currentRequest.Code; + _logger.LogDebug("Removing expired request {Code}", code); if (!_currentRequests.TryRemove(code, out _)) { - _logger.LogWarning("Request {code} already expired", code); + _logger.LogWarning("Request {Code} already expired", code); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index b302303f8..101d9b537 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -4,7 +4,6 @@ using System; using System.Globalization; using System.IO; using System.Linq; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -69,7 +68,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// The options for the json Serializer. /// </summary> - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; /// <summary> /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class. @@ -178,7 +177,8 @@ namespace Emby.Server.Implementations.ScheduledTasks lock (_lastExecutionResultSyncLock) { using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - JsonSerializer.SerializeAsync(createStream, value, _jsonOptions); + using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream); + JsonSerializer.Serialize(jsonStream, value, _jsonOptions); } } } @@ -301,12 +301,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { get { - if (_id == null) - { - _id = ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - - return _id; + return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture); } } @@ -348,9 +343,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var trigger = (ITaskTrigger)sender; - var configurableTask = ScheduledTask as IConfigurableScheduledTask; - - if (configurableTask != null && !configurableTask.IsEnabled) + if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled) { return; } @@ -578,7 +571,8 @@ namespace Emby.Server.Implementations.ScheduledTasks Directory.CreateDirectory(Path.GetDirectoryName(path)); using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - JsonSerializer.SerializeAsync(createStream, triggers, _jsonOptions); + using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream); + JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions); } /// <summary> diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 649305fd5..2312c85d9 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -12,9 +12,9 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index c384cf4bb..57d294a40 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -5,8 +5,8 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; namespace Emby.Server.Implementations.ScheduledTasks { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index a69380cbb..11a5fb79f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index e470adcf4..51b620404 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -6,8 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Library; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; namespace Emby.Server.Implementations.ScheduledTasks { diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 10e28c33a..6844152ea 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1475,17 +1475,14 @@ namespace Emby.Server.Implementations.Session user = _userManager.GetUserById(request.UserId); } - if (user == null) - { - user = _userManager.GetUserByName(request.Username); - } + user ??= _userManager.GetUserByName(request.Username); if (enforcePassword) { user = await _userManager.AuthenticateUser( request.Username, request.Password, - request.PasswordSha1, + null, request.RemoteEndPoint, true).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs index 1f68a9c81..60698e803 100644 --- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs +++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs @@ -131,11 +131,11 @@ namespace Emby.Server.Implementations.Sorting return GetSpecialCompareValue(x).CompareTo(GetSpecialCompareValue(y)); } - private static int GetSpecialCompareValue(Episode item) + private static long GetSpecialCompareValue(Episode item) { // First sort by season number // Since there are three sort orders, pad with 9 digits (3 for each, figure 1000 episode buffer should be enough) - var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000; + var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000L; // Second sort order is if it airs after the season if (item.AirsAfterSeasonNumber.HasValue) diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index aee959c53..72c0a838e 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay _sessionManager = sessionManager; _libraryManager = libraryManager; _logger = loggerFactory.CreateLogger<SyncPlayManager>(); - _sessionManager.SessionControllerConnected += OnSessionControllerConnected; + _sessionManager.SessionEnded += OnSessionEnded; } /// <inheritdoc /> @@ -269,14 +269,17 @@ namespace Emby.Server.Implementations.SyncPlay var user = _userManager.GetUserById(session.UserId); List<GroupInfoDto> list = new List<GroupInfoDto>(); - foreach (var group in _groups.Values) + lock (_groupsLock) { - // Locking required as group is not thread-safe. - lock (group) + foreach (var (_, group) in _groups) { - if (group.HasAccessToPlayQueue(user)) + // Locking required as group is not thread-safe. + lock (group) { - list.Add(group.GetInfo()); + if (group.HasAccessToPlayQueue(user)) + { + list.Add(group.GetInfo()); + } } } } @@ -352,18 +355,18 @@ namespace Emby.Server.Implementations.SyncPlay return; } - _sessionManager.SessionControllerConnected -= OnSessionControllerConnected; + _sessionManager.SessionEnded -= OnSessionEnded; _disposed = true; } - private void OnSessionControllerConnected(object sender, SessionEventArgs e) + private void OnSessionEnded(object sender, SessionEventArgs e) { var session = e.SessionInfo; if (_sessionToGroupMap.TryGetValue(session.Id, out var group)) { - var request = new JoinGroupRequest(group.GroupId); - JoinGroup(session, request, CancellationToken.None); + var leaveGroupRequest = new LeaveGroupRequest(); + LeaveGroup(session, leaveGroupRequest, CancellationToken.None); } } diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 839b62448..829df64bf 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -11,7 +10,6 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.TV; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Series = MediaBrowser.Controller.Entities.TV.Series; @@ -23,12 +21,14 @@ namespace Emby.Server.Implementations.TV private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _configurationManager; - public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager) + public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager) { _userManager = userManager; _userDataManager = userDataManager; _libraryManager = libraryManager; + _configurationManager = configurationManager; } public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions) @@ -43,9 +43,7 @@ namespace Emby.Server.Implementations.TV string presentationUniqueKey = null; if (!string.IsNullOrEmpty(request.SeriesId)) { - var series = _libraryManager.GetItemById(request.SeriesId) as Series; - - if (series != null) + if (_libraryManager.GetItemById(request.SeriesId) is Series series) { presentationUniqueKey = GetUniqueSeriesKey(series); } @@ -95,9 +93,7 @@ namespace Emby.Server.Implementations.TV int? limit = null; if (!string.IsNullOrEmpty(request.SeriesId)) { - var series = _libraryManager.GetItemById(request.SeriesId) as Series; - - if (series != null) + if (_libraryManager.GetItemById(request.SeriesId) is Series series) { presentationUniqueKey = GetUniqueSeriesKey(series); limit = 1; @@ -200,13 +196,10 @@ namespace Emby.Server.Implementations.TV ParentIndexNumberNotEquals = 0, DtoOptions = new DtoOptions { - Fields = new ItemFields[] - { - ItemFields.SortName - }, + Fields = new[] { ItemFields.SortName }, EnableImages = false } - }).FirstOrDefault(); + }).Cast<Episode>().FirstOrDefault(); Func<Episode> getEpisode = () => { @@ -224,6 +217,43 @@ namespace Emby.Server.Implementations.TV DtoOptions = dtoOptions }).Cast<Episode>().FirstOrDefault(); + if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons) + { + var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + AncestorWithPresentationUniqueKey = null, + SeriesPresentationUniqueKey = seriesKey, + ParentIndexNumber = 0, + IncludeItemTypes = new[] { nameof(Episode) }, + IsPlayed = false, + IsVirtualItem = false, + DtoOptions = dtoOptions + }) + .Cast<Episode>() + .Where(episode => episode.AirsBeforeSeasonNumber != null || episode.AirsAfterSeasonNumber != null) + .ToList(); + + if (lastWatchedEpisode != null) + { + // Last watched episode is added, because there could be specials that aired before the last watched episode + consideredEpisodes.Add(lastWatchedEpisode); + } + + if (nextEpisode != null) + { + consideredEpisodes.Add(nextEpisode); + } + + var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) }) + .Cast<Episode>(); + if (lastWatchedEpisode != null) + { + sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => episode.Id != lastWatchedEpisode.Id).Skip(1); + } + + nextEpisode = sortedConsideredEpisodes.FirstOrDefault(); + } + if (nextEpisode != null) { var userData = _userDataManager.GetUserData(user, nextEpisode); diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index 4fd7ac0c1..db5265e79 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -53,12 +53,7 @@ namespace Emby.Server.Implementations.Udp if (!string.IsNullOrEmpty(localUrl)) { - var response = new ServerDiscoveryInfo - { - Address = localUrl, - Id = _appHost.SystemId, - Name = _appHost.FriendlyName - }; + var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName); try { @@ -79,7 +74,7 @@ namespace Emby.Server.Implementations.Udp /// Starts the specified port. /// </summary> /// <param name="port">The port.</param> - /// <param name="cancellationToken"></param> + /// <param name="cancellationToken">The cancellation token to cancel operation.</param> public void Start(int port, CancellationToken cancellationToken) { _endpoint = new IPEndPoint(IPAddress.Any, port); diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 7af52ea65..653b1381b 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Model.IO; +using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging; @@ -92,7 +93,7 @@ namespace Emby.Server.Implementations.Updates _httpClientFactory = httpClientFactory; _config = config; _zipClient = zipClient; - _jsonSerializerOptions = JsonDefaults.GetOptions(); + _jsonSerializerOptions = JsonDefaults.Options; _pluginManager = pluginManager; } @@ -194,7 +195,7 @@ namespace Emby.Server.Implementations.Updates var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber); if (plugin != null) { - await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path); + await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false); } // Remove versions with a target ABI greater then the current application version. @@ -500,7 +501,8 @@ namespace Emby.Server.Implementations.Updates var plugins = _pluginManager.Plugins; foreach (var plugin in plugins) { - if (plugin.Manifest?.AutoUpdate == false) + // Don't auto update when plugin marked not to, or when it's disabled. + if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled) { continue; } @@ -515,7 +517,7 @@ namespace Emby.Server.Implementations.Updates } } - private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken) + private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken) { var extension = Path.GetExtension(package.SourceUrl); if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase)) @@ -567,7 +569,7 @@ namespace Emby.Server.Implementations.Updates stream.Position = 0; _zipClient.ExtractAllFromZip(stream, targetDir, true); - await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir); + await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); _pluginManager.ImportPluginFrom(targetDir); } @@ -576,7 +578,7 @@ namespace Emby.Server.Implementations.Updates LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version)) ?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version)); - await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false); + await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false); _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version); return plugin != null; diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index 7d68aecf9..392498c53 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -77,8 +77,9 @@ namespace Jellyfin.Api.Auth return false; } - var ip = _httpContextAccessor.HttpContext.GetNormalizedRemoteIp(); - var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip); + var isInLocalNetwork = _httpContextAccessor.HttpContext != null + && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp()); + // User cannot access remotely and user is remote if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork) { diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 616fe5b91..a6e70e72d 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -168,22 +168,22 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -309,7 +309,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -333,22 +333,22 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index e1c9f69f6..b6309baab 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Text.Json; @@ -25,7 +26,7 @@ namespace Jellyfin.Api.Controllers private readonly IServerConfigurationManager _configurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options; /// <summary> /// Initializes a new instance of the <see cref="ConfigurationController"/> class. @@ -94,6 +95,11 @@ namespace Jellyfin.Api.Controllers { var configurationType = _configurationManager.GetConfigurationType(key); var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false); + if (configuration == null) + { + throw new ArgumentException("Body doesn't contain a valid configuration"); + } + _configurationManager.SaveConfiguration(key, configuration); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index a2c2ecd66..445733c24 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -95,9 +95,9 @@ namespace Jellyfin.Api.Controllers return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1)); } - private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin? plugin) + private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin) { - if (plugin?.Instance is not IHasWebPages hasWebPages) + if (plugin.Instance is not IHasWebPages hasWebPages) { return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>(); } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index e375645cf..b4154b361 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -52,8 +52,6 @@ namespace Jellyfin.Api.Controllers private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ILogger<DynamicHlsController> _logger; @@ -72,12 +70,11 @@ namespace Jellyfin.Api.Controllers /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param> /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public DynamicHlsController( ILibraryManager libraryManager, IUserManager userManager, @@ -87,15 +84,12 @@ namespace Jellyfin.Api.Controllers IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, ILogger<DynamicHlsController> logger, - DynamicHlsHelper dynamicHlsHelper) + DynamicHlsHelper dynamicHlsHelper, + EncodingHelper encodingHelper) { - _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); - _libraryManager = libraryManager; _userManager = userManager; _dlnaManager = dlnaManager; @@ -104,12 +98,12 @@ namespace Jellyfin.Api.Controllers _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _logger = logger; _dynamicHlsHelper = dynamicHlsHelper; + _encodingHelper = encodingHelper; + _encodingOptions = serverConfigurationManager.GetEncodingOptions(); } @@ -203,7 +197,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -218,14 +212,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true) { var streamingRequest = new HlsVideoRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -249,28 +243,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; @@ -370,7 +364,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -385,14 +379,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true) { var streamingRequest = new HlsAudioRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -416,28 +410,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; @@ -533,7 +527,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -548,14 +542,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new VideoRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -579,28 +573,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions }; @@ -698,7 +692,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -713,14 +707,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new StreamingRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -744,28 +738,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions }; @@ -868,7 +862,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -883,14 +877,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { var streamingRequest = new VideoRequestDto { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -914,28 +908,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions }; @@ -1040,7 +1034,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -1055,14 +1049,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { var streamingRequest = new StreamingRequestDto { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -1086,28 +1080,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions }; @@ -1126,9 +1120,7 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, @@ -1211,9 +1203,7 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 25abe73ed..473bdc523 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -61,7 +61,13 @@ namespace Jellyfin.Api.Controllers { // TODO: Deprecate with new iOS app var file = segmentId + Path.GetExtension(Request.Path); - file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); + var transcodePath = _serverConfigurationManager.GetTranscodePath(); + file = Path.GetFullPath(Path.Combine(transcodePath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath)) + { + return BadRequest("Invalid segment."); + } return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext); } @@ -81,7 +87,13 @@ namespace Jellyfin.Api.Controllers public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) { var file = playlistId + Path.GetExtension(Request.Path); - file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); + var transcodePath = _serverConfigurationManager.GetTranscodePath(); + file = Path.GetFullPath(Path.Combine(transcodePath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8") + { + return BadRequest("Invalid segment."); + } return GetFileResult(file, file); } @@ -96,7 +108,9 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Videos/ActiveEncodings")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId) + public ActionResult StopEncodingProcess( + [FromQuery, Required] string deviceId, + [FromQuery, Required] string playSessionId) { _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true); return NoContent(); @@ -128,7 +142,12 @@ namespace Jellyfin.Api.Controllers var file = segmentId + Path.GetExtension(Request.Path); var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); - file = Path.Combine(transcodeFolderPath, file); + file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath)) + { + return BadRequest("Invalid segment."); + } var normalizedPlaylistId = playlistId; diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 198dbc51f..e1b808098 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers : type; var path = BaseItem.SupportedImageExtensions - .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)) + .Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i))) .FirstOrDefault(System.IO.File.Exists); if (path == null) @@ -82,6 +82,11 @@ namespace Jellyfin.Api.Controllers return NotFound(); } + if (!path.StartsWith(_applicationPaths.GeneralPath)) + { + return BadRequest("Invalid image path."); + } + var contentType = MimeTypes.GetMimeType(path); return File(System.IO.File.OpenRead(path), contentType); } @@ -163,7 +168,8 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> private ActionResult GetImageFile(string basePath, string theme, string? name) { - var themeFolder = Path.Combine(basePath, theme); + var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme)); + if (Directory.Exists(themeFolder)) { var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i)) @@ -171,12 +177,18 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { + if (!path.StartsWith(basePath)) + { + return BadRequest("Invalid image path."); + } + var contentType = MimeTypes.GetMimeType(path); + return PhysicalFile(path, contentType); } } - var allFolder = Path.Combine(basePath, "all"); + var allFolder = Path.GetFullPath(Path.Combine(basePath, "all")); if (Directory.Exists(allFolder)) { var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i)) @@ -184,6 +196,11 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { + if (!path.StartsWith(basePath)) + { + return BadRequest("Invalid image path."); + } + var contentType = MimeTypes.GetMimeType(path); return PhysicalFile(path, contentType); } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index a50d6e46b..8f7500ac6 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -196,6 +196,11 @@ namespace Jellyfin.Api.Controllers } var user = _userManager.GetUserById(userId); + if (user?.ProfileImage == null) + { + return NoContent(); + } + try { System.IO.File.Delete(user.ProfileImage.Path); @@ -235,6 +240,11 @@ namespace Jellyfin.Api.Controllers } var user = _userManager.GetUserById(userId); + if (user?.ProfileImage == null) + { + return NoContent(); + } + try { System.IO.File.Delete(user.ProfileImage.Path); @@ -392,7 +402,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, - [FromQuery] int newIndex) + [FromQuery, Required] int newIndex) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -480,6 +490,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param> @@ -509,8 +521,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, [FromQuery] string? tag, - [FromQuery] bool? cropWhitespace, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] ImageFormat? format, [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, @@ -539,7 +553,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -560,6 +575,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param> @@ -589,8 +606,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, [FromQuery] string? tag, - [FromQuery] bool? cropWhitespace, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] ImageFormat? format, [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, @@ -618,7 +637,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -638,6 +658,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param> @@ -667,8 +689,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, [FromRoute, Required] string tag, - [FromQuery] bool? cropWhitespace, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromRoute, Required] ImageFormat format, [FromQuery] bool? addPlayedIndicator, [FromRoute, Required] double percentPlayed, @@ -697,7 +721,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -721,6 +746,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -741,7 +768,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> GetArtistImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -750,7 +777,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -776,7 +805,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -800,6 +830,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -820,7 +852,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> GetGenreImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -829,7 +861,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -855,7 +889,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -880,6 +915,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -900,7 +937,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -909,7 +946,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -934,7 +973,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -958,6 +998,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -978,7 +1020,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> GetMusicGenreImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -987,7 +1029,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1013,7 +1057,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1038,6 +1083,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1058,7 +1105,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -1067,7 +1114,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1092,7 +1141,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1116,6 +1166,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1136,7 +1188,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> GetPersonImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -1145,7 +1197,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1171,7 +1225,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1196,6 +1251,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1216,7 +1273,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, [FromRoute, Required] int imageIndex, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -1225,7 +1282,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1250,7 +1309,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1274,6 +1334,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1303,7 +1365,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1329,7 +1393,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1354,6 +1419,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1383,7 +1450,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1408,7 +1477,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1432,6 +1502,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1461,7 +1533,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1469,7 +1543,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? imageIndex) { var user = _userManager.GetUserById(userId); - if (user == null) + if (user?.ProfileImage == null) { return NotFound(); } @@ -1504,7 +1578,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1530,6 +1605,8 @@ namespace Jellyfin.Api.Controllers /// <param name="width">The fixed image width to return.</param> /// <param name="height">The fixed image height to return.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param> /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param> /// <param name="blur">Optional. Blur image.</param> @@ -1559,7 +1636,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1601,7 +1680,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1685,7 +1765,8 @@ namespace Jellyfin.Api.Controllers int? width, int? height, int? quality, - bool? cropWhitespace, + int? fillWidth, + int? fillHeight, bool? addPlayedIndicator, int? blur, string? backgroundColor, @@ -1727,8 +1808,6 @@ namespace Jellyfin.Api.Controllers } } - cropWhitespace ??= imageType == ImageType.Logo || imageType == ImageType.Art; - var outputFormats = GetOutputFormats(format); TimeSpan? cacheDuration = null; @@ -1748,11 +1827,13 @@ namespace Jellyfin.Api.Controllers item, itemId, imageIndex, + width, height, - maxHeight, maxWidth, + maxHeight, + fillWidth, + fillHeight, quality, - width, addPlayedIndicator, percentPlayed, unplayedCount, @@ -1760,7 +1841,6 @@ namespace Jellyfin.Api.Controllers backgroundColor, foregroundLayer, imageInfo, - cropWhitespace.Value, outputFormats, cacheDuration, responseHeaders, @@ -1779,17 +1859,15 @@ namespace Jellyfin.Api.Controllers private ImageFormat[] GetClientSupportedFormats() { - var acceptTypes = Request.Headers[HeaderNames.Accept]; - var supportedFormats = new List<string>(); - if (acceptTypes.Count > 0) + var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); + for (var i = 0; i < supportedFormats.Length; i++) { - foreach (var type in acceptTypes) + // Remove charsets etc. (anything after semi-colon) + var type = supportedFormats[i]; + int index = type.IndexOf(';', StringComparison.Ordinal); + if (index != -1) { - int index = type.IndexOf(';', StringComparison.Ordinal); - if (index != -1) - { - supportedFormats.Add(type.Substring(0, index)); - } + supportedFormats[i] = type.Substring(0, index); } } @@ -1847,11 +1925,13 @@ namespace Jellyfin.Api.Controllers BaseItem? item, Guid itemId, int? index, + int? width, int? height, - int? maxHeight, int? maxWidth, + int? maxHeight, + int? fillWidth, + int? fillHeight, int? quality, - int? width, bool? addPlayedIndicator, double? percentPlayed, int? unplayedCount, @@ -1859,7 +1939,6 @@ namespace Jellyfin.Api.Controllers string? backgroundColor, string? foregroundLayer, ItemImageInfo imageInfo, - bool cropWhitespace, IReadOnlyCollection<ImageFormat> supportedFormats, TimeSpan? cacheDuration, IDictionary<string, string> headers, @@ -1872,7 +1951,6 @@ namespace Jellyfin.Api.Controllers var options = new ImageProcessingOptions { - CropWhiteSpace = cropWhitespace, Height = height, ImageIndex = index ?? 0, Image = imageInfo, @@ -1880,6 +1958,8 @@ namespace Jellyfin.Api.Controllers ItemId = itemId, MaxHeight = maxHeight, MaxWidth = maxWidth, + FillHeight = fillHeight, + FillWidth = fillWidth, Quality = quality ?? 100, Width = width, AddPlayedIndicator = addPlayedIndicator ?? false, diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index f061755c3..f232dffaa 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given album. /// </summary> /// <param name="id">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given playlist. /// </summary> /// <param name="id">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given genre. /// </summary> /// <param name="name">The genre name.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -172,7 +172,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> [HttpGet("MusicGenres/{name}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre( + public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName( [FromRoute, Required] string name, [FromQuery] Guid? userId, [FromQuery] int? limit, @@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given artist. /// </summary> /// <param name="id">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given genre. /// </summary> /// <param name="id">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -243,7 +243,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> [HttpGet("MusicGenres/{id}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres( + public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById( [FromRoute, Required] Guid id, [FromQuery] Guid? userId, [FromQuery] int? limit, @@ -265,7 +265,7 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given song. + /// Creates an instant playlist based on a given item. /// </summary> /// <param name="id">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> @@ -300,6 +300,80 @@ namespace Jellyfin.Api.Controllers return GetResult(items, user, limit, dtoOptions); } + /// <summary> + /// Creates an instant playlist based on a given artist. + /// </summary> + /// <param name="id">The item id.</param> + /// <param name="userId">Optional. Filter by user id, and attach user data.</param> + /// <param name="limit">Optional. The maximum number of records to return.</param> + /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> + /// <param name="enableImages">Optional. Include image information in output.</param> + /// <param name="enableUserData">Optional. Include user data.</param> + /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> + /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> + /// <response code="200">Instant playlist returned.</response> + /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> + [HttpGet("Artists/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Use GetInstantMixFromArtists")] + public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2( + [FromQuery, Required] Guid id, + [FromQuery] Guid? userId, + [FromQuery] int? limit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] bool? enableImages, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + { + return GetInstantMixFromArtists( + id, + userId, + limit, + fields, + enableImages, + enableUserData, + imageTypeLimit, + enableImageTypes); + } + + /// <summary> + /// Creates an instant playlist based on a given genre. + /// </summary> + /// <param name="id">The item id.</param> + /// <param name="userId">Optional. Filter by user id, and attach user data.</param> + /// <param name="limit">Optional. The maximum number of records to return.</param> + /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> + /// <param name="enableImages">Optional. Include image information in output.</param> + /// <param name="enableUserData">Optional. Include user data.</param> + /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> + /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> + /// <response code="200">Instant playlist returned.</response> + /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> + [HttpGet("MusicGenres/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Use GetInstantMixFromMusicGenres instead")] + public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2( + [FromQuery, Required] Guid id, + [FromQuery] Guid? userId, + [FromQuery] int? limit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] bool? enableImages, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + { + return GetInstantMixFromMusicGenreById( + id, + userId, + limit, + fields, + enableImages, + enableUserData, + imageTypeLimit, + enableImageTypes); + } + private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions) { var list = items; diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index dfc68ffce..9fa307858 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -238,48 +238,6 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Gets a remote image. - /// </summary> - /// <param name="imageUrl">The image url.</param> - /// <param name="providerName">The provider name.</param> - /// <response code="200">Remote image retrieved.</response> - /// <returns> - /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. - /// The task result contains an <see cref="FileStreamResult"/> containing the images file stream. - /// </returns> - [HttpGet("Items/RemoteSearch/Image")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesImageFile] - public async Task<ActionResult> GetRemoteSearchImage( - [FromQuery, Required] string imageUrl, - [FromQuery, Required] string providerName) - { - var urlHash = imageUrl.GetMD5(); - var pointerCachePath = GetFullCachePath(urlHash.ToString()); - - try - { - var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - if (System.IO.File.Exists(contentPath)) - { - return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath)); - } - } - catch (FileNotFoundException) - { - // Means the file isn't cached yet - } - catch (IOException) - { - // Means the file isn't cached yet - } - - await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); - var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath)); - } - - /// <summary> /// Applies search criteria to an item and refreshes metadata. /// </summary> /// <param name="itemId">Item id.</param> @@ -320,53 +278,5 @@ namespace Jellyfin.Api.Controllers return NoContent(); } - - /// <summary> - /// Downloads the image. - /// </summary> - /// <param name="providerName">Name of the provider.</param> - /// <param name="url">The URL.</param> - /// <param name="urlHash">The URL hash.</param> - /// <param name="pointerCachePath">The pointer cache path.</param> - /// <returns>Task.</returns> - private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath) - { - using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); - if (result.Content.Headers.ContentType?.MediaType == null) - { - throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType)); - } - - var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1]; - var fullCachePath = GetFullCachePath(urlHash + "." + ext); - - var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid."); - Directory.CreateDirectory(directory); - using (var stream = result.Content) - { - await using var fileStream = new FileStream( - fullCachePath, - FileMode.Create, - FileAccess.Write, - FileShare.Read, - IODefaults.FileStreamBufferSize, - true); - - await stream.CopyToAsync(fileStream).ConfigureAwait(false); - } - - var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath)); - - Directory.CreateDirectory(pointerCacheDirectory); - await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false); - } - - /// <summary> - /// Gets the full cache path. - /// </summary> - /// <param name="filename">The filename.</param> - /// <returns>System.String.</returns> - private string GetFullCachePath(string filename) - => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); } } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 9e1a39853..a9f4a5a58 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Items/{itemId}/ContentType")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType) + public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType) { var item = _libraryManager.GetItemById(itemId); if (item == null) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 2c9760f6d..74cf3b162 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -246,8 +246,13 @@ namespace Jellyfin.Api.Controllers folder = _libraryManager.GetUserRootFolder(); } - if (folder is IHasCollectionType hasCollectionType - && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + string? collectionType = null; + if (folder is IHasCollectionType hasCollectionType) + { + collectionType = hasCollectionType.CollectionType; + } + + if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) { recursive = true; includeItemTypes = new[] { BaseItemKind.Playlist }; @@ -270,10 +275,11 @@ namespace Jellyfin.Api.Controllers } } - if (!(item is UserRootFolder) + if (item is not UserRootFolder && !isInEnabledFolder && !user.HasPermission(PermissionKind.EnableAllFolders) - && !user.HasPermission(PermissionKind.EnableAllChannels)) + && !user.HasPermission(PermissionKind.EnableAllChannels) + && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase)) { _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 db4aa9668..4ed15e1d5 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -114,7 +114,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path)); + return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true); } /// <summary> @@ -303,7 +303,7 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <response code="204">Library scan started.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> - [HttpGet("Library/Refresh")] + [HttpPost("Library/Refresh")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> RefreshLibrary() @@ -590,17 +590,17 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Reports that new movies have been added by an external source. /// </summary> - /// <param name="updates">A list of updated media paths.</param> + /// <param name="dto">The update paths.</param> /// <response code="204">Report success.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Library/Media/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates) + public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto) { - foreach (var item in updates) + foreach (var item in dto.Updates) { - _libraryMonitor.ReportFileSystemChanged(item.Path); + _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null.")); } return NoContent(); @@ -666,7 +666,7 @@ namespace Jellyfin.Api.Controllers } // TODO determine non-ASCII validity. - return PhysicalFile(path, MimeTypes.GetMimeType(path), filename); + return PhysicalFile(path, MimeTypes.GetMimeType(path), filename, true); } /// <summary> @@ -777,7 +777,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo( [FromQuery] string? libraryContentType, - [FromQuery] bool isNewLibrary) + [FromQuery] bool isNewLibrary = false) { var result = new LibraryOptionsResultDto(); diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 328efea26..be9127dd3 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -241,23 +241,20 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Updates a media path. /// </summary> - /// <param name="name">The name of the library.</param> - /// <param name="pathInfo">The path info.</param> + /// <param name="mediaPathRequestDto">The name of the library and path infos.</param> /// <returns>A <see cref="NoContentResult"/>.</returns> /// <response code="204">Media path updated.</response> /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> [HttpPost("Paths/Update")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateMediaPath( - [FromQuery] string? name, - [FromBody] MediaPathInfo? pathInfo) + public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto) { - if (string.IsNullOrWhiteSpace(name)) + if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name)) { - throw new ArgumentNullException(nameof(name)); + throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty"); } - _libraryManager.UpdateMediaPath(name, pathInfo); + _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 4d788ad7d..d0a2358ae 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers // nameof(LiveTvProgram) }, // IsMovie = true - OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) }, Limit = 7, ParentId = parentIdGuid, Recursive = true, @@ -110,7 +110,7 @@ namespace Jellyfin.Api.Controllers { IncludeItemTypes = itemTypes.ToArray(), IsMovie = true, - OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, Limit = 10, IsFavoriteOrLiked = true, ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(), @@ -120,7 +120,7 @@ namespace Jellyfin.Api.Controllers DtoOptions = dtoOptions }); - var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList(); + var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6)); // Get recently played directors var recentDirectors = GetDirectors(mostRecentMovies) .ToList(); @@ -191,7 +191,8 @@ namespace Jellyfin.Api.Controllers foreach (var name in names) { - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + var items = _libraryManager.GetItemList( + new InternalItemsQuery(user) { Person = name, // Account for duplicates by imdb id, since the database doesn't support this yet diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 0ceda6815..420630cdf 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using Jellyfin.Api.Constants; @@ -86,26 +87,19 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Sends a notification to all admins. /// </summary> - /// <param name="url">The URL of the notification.</param> - /// <param name="level">The level of the notification.</param> - /// <param name="name">The name of the notification.</param> - /// <param name="description">The description of the notification.</param> + /// <param name="notificationDto">The notification request.</param> /// <response code="204">Notification sent.</response> /// <returns>A <cref see="NoContentResult"/>.</returns> [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateAdminNotification( - [FromQuery] string? url, - [FromQuery] NotificationLevel? level, - [FromQuery] string name = "", - [FromQuery] string description = "") + public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto) { var notification = new NotificationRequest { - Name = name, - Description = description, - Url = url, - Level = level ?? NotificationLevel.Normal, + Name = notificationDto.Name, + Description = notificationDto.Description, + Url = notificationDto.Url, + Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal, UserIds = _userManager.Users .Where(user => user.HasPermission(PermissionKind.IsAdministrator)) .Select(user => user.Id) @@ -114,7 +108,6 @@ namespace Jellyfin.Api.Controllers }; _notificationManager.SendNotification(notification, CancellationToken.None); - return NoContent(); } diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index ec7b84ff6..f256c8c25 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Sessions/Playing/Ping")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PingPlaybackSession([FromQuery] string playSessionId) + public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId) { _transcodingJobHelper.PingTranscodingJob(playSessionId, null); return NoContent(); @@ -202,9 +202,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? mediaSourceId, [FromQuery] int? audioStreamIndex, [FromQuery] int? subtitleStreamIndex, - [FromQuery] PlayMethod playMethod, + [FromQuery] PlayMethod? playMethod, [FromQuery] string? liveStreamId, - [FromQuery] string playSessionId, + [FromQuery] string? playSessionId, [FromQuery] bool canSeek = false) { var playbackStartInfo = new PlaybackStartInfo @@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers MediaSourceId = mediaSourceId, AudioStreamIndex = audioStreamIndex, SubtitleStreamIndex = subtitleStreamIndex, - PlayMethod = playMethod, + PlayMethod = playMethod ?? PlayMethod.Transcode, PlaySessionId = playSessionId, LiveStreamId = liveStreamId }; @@ -254,10 +254,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? audioStreamIndex, [FromQuery] int? subtitleStreamIndex, [FromQuery] int? volumeLevel, - [FromQuery] PlayMethod playMethod, + [FromQuery] PlayMethod? playMethod, [FromQuery] string? liveStreamId, - [FromQuery] string playSessionId, - [FromQuery] RepeatMode repeatMode, + [FromQuery] string? playSessionId, + [FromQuery] RepeatMode? repeatMode, [FromQuery] bool isPaused = false, [FromQuery] bool isMuted = false) { @@ -271,10 +271,10 @@ namespace Jellyfin.Api.Controllers AudioStreamIndex = audioStreamIndex, SubtitleStreamIndex = subtitleStreamIndex, VolumeLevel = volumeLevel, - PlayMethod = playMethod, + PlayMethod = playMethod ?? PlayMethod.Transcode, PlaySessionId = playSessionId, LiveStreamId = liveStreamId, - RepeatMode = repeatMode + RepeatMode = repeatMode ?? RepeatMode.RepeatNone }; playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); @@ -352,7 +352,7 @@ namespace Jellyfin.Api.Controllers return _userDataRepository.GetUserDataDto(item, user); } - private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId) + private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId) { if (method == PlayMethod.Transcode) { diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index a5aa9bfca..7a6130719 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -12,7 +12,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Json; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; using Microsoft.AspNetCore.Authorization; @@ -45,7 +44,7 @@ namespace Jellyfin.Api.Controllers { _installationManager = installationManager; _pluginManager = pluginManager; - _serializerOptions = JsonDefaults.GetOptions(); + _serializerOptions = JsonDefaults.Options; _config = config; } @@ -208,12 +207,7 @@ namespace Jellyfin.Api.Controllers var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)); // Select the un-instanced one first. - var plugin = plugins.FirstOrDefault(p => p.Instance == null); - if (plugin == null) - { - // Then by the status. - plugin = plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault(); - } + var plugin = plugins.FirstOrDefault(p => p.Instance == null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault(); if (plugin != null) { diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 5284888d8..ec836f43e 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -146,58 +146,6 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Gets a remote image. - /// </summary> - /// <param name="imageUrl">The image url.</param> - /// <response code="200">Remote image returned.</response> - /// <response code="404">Remote image not found.</response> - /// <returns>Image Stream.</returns> - [HttpGet("Images/Remote")] - [Produces(MediaTypeNames.Application.Octet)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl) - { - var urlHash = imageUrl.ToString().GetMD5(); - var pointerCachePath = GetFullCachePath(urlHash.ToString()); - - string? contentPath = null; - var hasFile = false; - - try - { - contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - if (System.IO.File.Exists(contentPath)) - { - hasFile = true; - } - } - catch (FileNotFoundException) - { - // The file isn't cached yet - } - catch (IOException) - { - // The file isn't cached yet - } - - if (!hasFile) - { - await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); - contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - } - - if (string.IsNullOrEmpty(contentPath)) - { - return NotFound(); - } - - var contentType = MimeTypes.GetMimeType(contentPath); - return PhysicalFile(contentPath, contentType); - } - - /// <summary> /// Downloads a remote image for an item. /// </summary> /// <param name="itemId">Item Id.</param> @@ -259,7 +207,8 @@ namespace Jellyfin.Api.Controllers var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid."); Directory.CreateDirectory(fullCacheDirectory); - await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true); await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath)); diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 6c22050a7..73bdf9018 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -228,10 +228,7 @@ namespace Jellyfin.Api.Controllers itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb); } - if (itemWithImage == null) - { - itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb); - } + itemWithImage ??= GetParentWithImage<BaseItem>(item, ImageType.Thumb); if (itemWithImage != null) { diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e2269a2ce..7bd0b6918 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -153,6 +153,10 @@ namespace Jellyfin.Api.Controllers /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param> /// <param name="itemIds">The ids of the items to play, comma delimited.</param> /// <param name="startPositionTicks">The starting position of the first item.</param> + /// <param name="mediaSourceId">Optional. The media source id.</param> + /// <param name="audioStreamIndex">Optional. The index of the audio stream to play.</param> + /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to play.</param> + /// <param name="startIndex">Optional. The start index.</param> /// <response code="204">Instruction sent to session.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Sessions/{sessionId}/Playing")] @@ -162,13 +166,21 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string sessionId, [FromQuery, Required] PlayCommand playCommand, [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds, - [FromQuery] long? startPositionTicks) + [FromQuery] long? startPositionTicks, + [FromQuery] string? mediaSourceId, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] int? startIndex) { var playRequest = new PlayRequest { ItemIds = itemIds, StartPositionTicks = startPositionTicks, - PlayCommand = playCommand + PlayCommand = playCommand, + MediaSourceId = mediaSourceId, + AudioStreamIndex = audioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex, + StartIndex = startIndex }; _sessionManager.SendPlayCommand( @@ -301,9 +313,7 @@ namespace Jellyfin.Api.Controllers /// Issues a command to a client to display a message to the user. /// </summary> /// <param name="sessionId">The session id.</param> - /// <param name="text">The message test.</param> - /// <param name="header">The message header.</param> - /// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param> + /// <param name="command">The <see cref="MessageCommand" /> object containing Header, Message Text, and TimeoutMs.</param> /// <response code="204">Message sent.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Sessions/{sessionId}/Message")] @@ -311,16 +321,12 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendMessageCommand( [FromRoute, Required] string sessionId, - [FromQuery, Required] string text, - [FromQuery] string? header, - [FromQuery] long? timeoutMs) + [FromBody, Required] MessageCommand command) { - var command = new MessageCommand + if (string.IsNullOrWhiteSpace(command.Header)) { - Header = string.IsNullOrEmpty(header) ? "Message from Server" : header, - TimeoutMs = timeoutMs, - Text = text - }; + command.Header = "Message from Server"; + } _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None); diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index d9cb34557..a01a617fc 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -132,7 +132,10 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.Users.First(); - user.Username = startupUserDto.Name; + if (startupUserDto.Name != null) + { + user.Username = startupUserDto.Name; + } await _userManager.UpdateUserAsync(user).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 16a47f2d8..1669a659d 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -182,6 +182,10 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Gets subtitles in a specified format. /// </summary> + /// <param name="routeItemId">The (route) item id.</param> + /// <param name="routeMediaSourceId">The (route) media source id.</param> + /// <param name="routeIndex">The (route) subtitle stream index.</param> + /// <param name="routeFormat">The (route) format of the returned subtitle.</param> /// <param name="itemId">The item id.</param> /// <param name="mediaSourceId">The media source id.</param> /// <param name="index">The subtitle stream index.</param> @@ -189,22 +193,32 @@ namespace Jellyfin.Api.Controllers /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param> /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param> /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param> - /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param> + /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param> /// <response code="200">File returned.</response> /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns> - [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] + [HttpGet("Videos/{routeItemId}/routeMediaSourceId/Subtitles/{routeIndex}/Stream.{routeFormat}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile("text/*")] public async Task<ActionResult> GetSubtitle( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] string mediaSourceId, - [FromRoute, Required] int index, - [FromRoute, Required] string format, + [FromRoute, Required] Guid routeItemId, + [FromRoute, Required] string routeMediaSourceId, + [FromRoute, Required] int routeIndex, + [FromRoute, Required] string routeFormat, + [FromQuery, ParameterObsolete] Guid? itemId, + [FromQuery, ParameterObsolete] string? mediaSourceId, + [FromQuery, ParameterObsolete] int? index, + [FromQuery, ParameterObsolete] string? format, [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps = false, [FromQuery] bool addVttTimeMap = false, [FromQuery] long startPositionTicks = 0) { + // Set parameters to route value if not provided via query. + itemId ??= routeItemId; + mediaSourceId ??= routeMediaSourceId; + index ??= routeIndex; + format ??= routeFormat; + if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase)) { format = "json"; @@ -212,9 +226,9 @@ namespace Jellyfin.Api.Controllers if (string.IsNullOrEmpty(format)) { - var item = (Video)_libraryManager.GetItemById(itemId); + var item = (Video)_libraryManager.GetItemById(itemId.Value); - var idString = itemId.ToString("N", CultureInfo.InvariantCulture); + var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture); var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false) .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal)); @@ -226,7 +240,7 @@ namespace Jellyfin.Api.Controllers if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap) { - await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); + await using Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); using var reader = new StreamReader(stream); var text = await reader.ReadToEndAsync().ConfigureAwait(false); @@ -238,9 +252,9 @@ namespace Jellyfin.Api.Controllers return File( await EncodeSubtitles( - itemId, + itemId.Value, mediaSourceId, - index, + index.Value, format, startPositionTicks, endPositionTicks, @@ -251,30 +265,44 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Gets subtitles in a specified format. /// </summary> + /// <param name="routeItemId">The (route) item id.</param> + /// <param name="routeMediaSourceId">The (route) media source id.</param> + /// <param name="routeIndex">The (route) subtitle stream index.</param> + /// <param name="routeStartPositionTicks">The (route) start position of the subtitle in ticks.</param> + /// <param name="routeFormat">The (route) format of the returned subtitle.</param> /// <param name="itemId">The item id.</param> /// <param name="mediaSourceId">The media source id.</param> /// <param name="index">The subtitle stream index.</param> - /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param> + /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param> /// <param name="format">The format of the returned subtitle.</param> /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param> /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param> /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param> /// <response code="200">File returned.</response> /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns> - [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")] + [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile("text/*")] public Task<ActionResult> GetSubtitleWithTicks( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] string mediaSourceId, - [FromRoute, Required] int index, - [FromRoute, Required] long startPositionTicks, - [FromRoute, Required] string format, + [FromRoute, Required] Guid routeItemId, + [FromRoute, Required] string routeMediaSourceId, + [FromRoute, Required] int routeIndex, + [FromRoute, Required] long routeStartPositionTicks, + [FromRoute, Required] string routeFormat, + [FromQuery, ParameterObsolete] Guid? itemId, + [FromQuery, ParameterObsolete] string? mediaSourceId, + [FromQuery, ParameterObsolete] int? index, + [FromQuery, ParameterObsolete] long? startPositionTicks, + [FromQuery, ParameterObsolete] string? format, [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps = false, [FromQuery] bool addVttTimeMap = false) { return GetSubtitle( + routeItemId, + routeMediaSourceId, + routeIndex, + routeFormat, itemId, mediaSourceId, index, @@ -282,7 +310,7 @@ namespace Jellyfin.Api.Controllers endPositionTicks, copyTimestamps, addVttTimeMap, - startPositionTicks); + startPositionTicks ?? routeStartPositionTicks); } /// <summary> diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index a55f13e66..a811a29c3 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions().AddClientFields(Request); var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { - OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, MediaTypes = mediaType, IncludeItemTypes = type, IsVirtualItem = false, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index e1c67f830..59400db2a 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) { IncludeItemTypes = new[] { nameof(Episode) }, - OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, MinPremiereDate = minPremiereDate, StartIndex = startIndex, Limit = limit, diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 5aa033ccf..679f055bc 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] bool? enableRemoteMedia, - [FromQuery] bool breakOnNonKeyFrames, + [FromQuery] bool breakOnNonKeyFrames = false, [FromQuery] bool enableRedirection = true) { var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); @@ -219,10 +219,10 @@ namespace Jellyfin.Api.Controllers AudioBitRate = audioBitRate ?? maxStreamingBitrate, StartTimeTicks = startTimeTicks, SubtitleMethod = SubtitleDeliveryMethod.Hls, - RequireAvc = true, - DeInterlace = true, - RequireNonAnamorphic = true, - EnableMpegtsM2TsMode = true, + RequireAvc = false, + DeInterlace = false, + RequireNonAnamorphic = false, + EnableMpegtsM2TsMode = false, TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), Context = EncodingContext.Static, StreamOptions = new Dictionary<string, string>(), @@ -298,9 +298,9 @@ namespace Jellyfin.Api.Controllers { Type = DlnaProfileType.Audio, Context = EncodingContext.Streaming, - Container = transcodingContainer, - AudioCodec = audioCodec, - Protocol = transcodingProtocol, + Container = transcodingContainer ?? "mp3", + AudioCodec = audioCodec ?? "mp3", + Protocol = transcodingProtocol ?? "http", BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture) } diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 43ee309b7..b13db4baa 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -21,6 +21,7 @@ using MediaBrowser.Model.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { @@ -36,6 +37,7 @@ namespace Jellyfin.Api.Controllers private readonly IDeviceManager _deviceManager; private readonly IAuthorizationContext _authContext; private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="UserController"/> class. @@ -46,13 +48,15 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> public UserController( IUserManager userManager, ISessionManager sessionManager, INetworkManager networkManager, IDeviceManager deviceManager, IAuthorizationContext authContext, - IServerConfigurationManager config) + IServerConfigurationManager config, + ILogger<UserController> logger) { _userManager = userManager; _sessionManager = sessionManager; @@ -60,6 +64,7 @@ namespace Jellyfin.Api.Controllers _deviceManager = deviceManager; _authContext = authContext; _config = config; + _logger = logger; } /// <summary> @@ -118,7 +123,7 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp()); + var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString()); return result; } @@ -172,11 +177,9 @@ namespace Jellyfin.Api.Controllers return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed."); } - // Password should always be null AuthenticateUserByName request = new AuthenticateUserByName { Username = user.Username, - Password = null, Pw = pw }; return await AuthenticateUserByName(request).ConfigureAwait(false); @@ -203,8 +206,7 @@ namespace Jellyfin.Api.Controllers DeviceId = auth.DeviceId, DeviceName = auth.Device, Password = request.Pw, - PasswordSha1 = request.Password, - RemoteEndPoint = HttpContext.GetNormalizedRemoteIp(), + RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(), Username = request.Username }).ConfigureAwait(false); @@ -291,7 +293,7 @@ namespace Jellyfin.Api.Controllers user.Username, request.CurrentPw, request.CurrentPw, - HttpContext.GetNormalizedRemoteIp(), + HttpContext.GetNormalizedRemoteIp().ToString(), false).ConfigureAwait(false); if (success == null) @@ -483,7 +485,7 @@ namespace Jellyfin.Api.Controllers await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); } - var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp()); + var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString()); return result; } @@ -498,8 +500,14 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest) { + var ip = HttpContext.GetNormalizedRemoteIp(); var isLocal = HttpContext.IsLocal() - || _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()); + || _networkManager.IsInLocalNetwork(ip); + + if (isLocal) + { + _logger.LogWarning("Password reset proccess initiated from outside the local network with IP: {IP}", ip); + } var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false); @@ -581,7 +589,7 @@ namespace Jellyfin.Api.Controllers var result = users .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp())); + .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString())); return result; } diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index ba51aa43e..308334b23 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -48,9 +48,6 @@ namespace Jellyfin.Api.Controllers private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ILogger<VideoHlsController> _logger; @@ -60,9 +57,6 @@ namespace Jellyfin.Api.Controllers /// Initializes a new instance of the <see cref="VideoHlsController"/> class. /// </summary> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> @@ -72,11 +66,9 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param> /// <param name="logger">Instance of the <see cref="ILogger{VideoHlsController}"/>.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public VideoHlsController( IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDlnaManager dlnaManager, IUserManager userManger, IAuthorizationContext authorizationContext, @@ -85,10 +77,9 @@ namespace Jellyfin.Api.Controllers IServerConfigurationManager serverConfigurationManager, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - ILogger<VideoHlsController> logger) + ILogger<VideoHlsController> logger, + EncodingHelper encodingHelper) { - _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); - _dlnaManager = dlnaManager; _authContext = authorizationContext; _userManager = userManger; @@ -96,12 +87,11 @@ namespace Jellyfin.Api.Controllers _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _logger = logger; + _encodingHelper = encodingHelper; + _encodingOptions = serverConfigurationManager.GetEncodingOptions(); } @@ -198,7 +188,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -213,7 +203,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, @@ -223,7 +213,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -247,28 +237,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, MaxHeight = maxHeight, MaxWidth = maxWidth, @@ -285,9 +275,7 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 44dc63952..e544d001e 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -49,12 +49,10 @@ namespace Jellyfin.Api.Controllers private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly IHttpClientFactory _httpClientFactory; + private readonly EncodingHelper _encodingHelper; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; @@ -69,12 +67,10 @@ namespace Jellyfin.Api.Controllers /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public VideosController( ILibraryManager libraryManager, IUserManager userManager, @@ -84,12 +80,10 @@ namespace Jellyfin.Api.Controllers IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - IHttpClientFactory httpClientFactory) + IHttpClientFactory httpClientFactory, + EncodingHelper encodingHelper) { _libraryManager = libraryManager; _userManager = userManager; @@ -99,12 +93,10 @@ namespace Jellyfin.Api.Controllers _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _httpClientFactory = httpClientFactory; + _encodingHelper = encodingHelper; } /// <summary> @@ -217,9 +209,7 @@ namespace Jellyfin.Api.Controllers return BadRequest("Please supply at least two videos to merge."); } - var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList(); - - var primaryVersion = videosWithVersions.FirstOrDefault(); + var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId)); if (primaryVersion == null) { primaryVersion = items @@ -364,7 +354,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -379,7 +369,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; @@ -388,7 +378,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -412,28 +402,28 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, - Context = context, + Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions }; @@ -446,9 +436,7 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, @@ -517,8 +505,7 @@ namespace Jellyfin.Api.Controllers // Need to start ffmpeg (because media can't be returned directly) var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); - var ffmpegCommandLineArguments = encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast"); + var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast"); return await FileStreamResponseHelpers.GetTranscodedFile( state, isHeadRequest, @@ -540,7 +527,7 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="playSessionId">The play session id.</param> /// <param name="segmentContainer">The segment container.</param> - /// <param name="segmentLength">The segment lenght.</param> + /// <param name="segmentLength">The segment length.</param> /// <param name="minSegments">The minimum number of segments.</param> /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> @@ -569,7 +556,7 @@ namespace Jellyfin.Api.Controllers /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> - /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> + /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param> /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param> /// <param name="liveStreamId">The live stream id.</param> @@ -583,8 +570,8 @@ namespace Jellyfin.Api.Controllers /// <param name="streamOptions">Optional. The streaming options.</param> /// <response code="200">Video stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> - [HttpGet("{itemId}/{stream=stream}.{container}")] - [HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")] + [HttpGet("{itemId}/stream.{container}")] + [HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesVideoFile] public Task<ActionResult> GetVideoStreamByContainer( @@ -620,7 +607,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, [FromQuery] int? maxRefFrames, [FromQuery] int? maxVideoBitDepth, [FromQuery] bool? requireAvc, @@ -635,7 +622,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext context, + [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { return GetVideoStream( diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs index e0c744325..06173315a 100644 --- a/Jellyfin.Api/Extensions/DtoExtensions.cs +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs index 21ec2d32f..cf35ee23a 100644 --- a/Jellyfin.Api/Helpers/AudioHelper.cs +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -1,5 +1,4 @@ -using System; -using System.Net.Http; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; @@ -33,13 +32,11 @@ namespace Jellyfin.Api.Helpers private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly EncodingHelper _encodingHelper; /// <summary> /// Initializes a new instance of the <see cref="AudioHelper"/> class. @@ -51,13 +48,11 @@ namespace Jellyfin.Api.Helpers /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param> /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public AudioHelper( IDlnaManager dlnaManager, IAuthorizationContext authContext, @@ -66,13 +61,11 @@ namespace Jellyfin.Api.Helpers IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, IHttpClientFactory httpClientFactory, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + EncodingHelper encodingHelper) { _dlnaManager = dlnaManager; _authContext = authContext; @@ -81,13 +74,11 @@ namespace Jellyfin.Api.Helpers _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _httpClientFactory = httpClientFactory; _httpContextAccessor = httpContextAccessor; + _encodingHelper = encodingHelper; } /// <summary> @@ -117,9 +108,7 @@ namespace Jellyfin.Api.Helpers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, @@ -188,8 +177,7 @@ namespace Jellyfin.Api.Helpers // Need to start ffmpeg (because media can't be returned directly) var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); - var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); + var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); return await FileStreamResponseHelpers.GetTranscodedFile( state, isHeadRequest, diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 16380f0bb..fcada0e77 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; -using System.Net.Mime; using System.Security.Claims; using System.Text; using System.Threading; @@ -41,14 +40,12 @@ namespace Jellyfin.Api.Helpers private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly INetworkManager _networkManager; private readonly ILogger<DynamicHlsHelper> _logger; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly EncodingHelper _encodingHelper; /// <summary> /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class. @@ -60,14 +57,12 @@ namespace Jellyfin.Api.Helpers /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param> /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public DynamicHlsHelper( ILibraryManager libraryManager, IUserManager userManager, @@ -76,14 +71,12 @@ namespace Jellyfin.Api.Helpers IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, INetworkManager networkManager, ILogger<DynamicHlsHelper> logger, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + EncodingHelper encodingHelper) { _libraryManager = libraryManager; _userManager = userManager; @@ -92,14 +85,12 @@ namespace Jellyfin.Api.Helpers _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _networkManager = networkManager; _logger = logger; _httpContextAccessor = httpContextAccessor; + _encodingHelper = encodingHelper; } /// <summary> @@ -145,9 +136,7 @@ namespace Jellyfin.Api.Helpers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, @@ -228,9 +217,8 @@ namespace Jellyfin.Api.Helpers var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main"); sdrVideoUrl += "&AllowVideoStreamCopy=false"; - EncodingHelper encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); - var sdrOutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0; - var sdrOutputAudioBitrate = encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0; + var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0; + var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0; var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); @@ -434,7 +422,7 @@ namespace Jellyfin.Api.Helpers } } - private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, string ipAddress) + private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress) { // Within the local network this will likely do more harm than good. if (_networkManager.IsInLocalNetwork(ipAddress)) diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 89d36ab09..b0fd59e5e 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -46,7 +46,8 @@ namespace Jellyfin.Api.Helpers if (isHeadRequest) { - return new FileContentResult(Array.Empty<byte>(), contentType); + httpContext.Response.Headers[HeaderNames.ContentType] = contentType; + return new OkResult(); } return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType); @@ -68,10 +69,10 @@ namespace Jellyfin.Api.Helpers { httpContext.Response.ContentType = contentType; - // if the request is a head request, return a NoContent result with the same headers as it would with a GET request + // if the request is a head request, return an OkResult (200) with the same headers as it would with a GET request if (isHeadRequest) { - return new NoContentResult(); + return new OkResult(); } return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true }; @@ -107,7 +108,8 @@ namespace Jellyfin.Api.Helpers // Headers only if (isHeadRequest) { - return new FileContentResult(Array.Empty<byte>(), contentType); + httpContext.Response.Headers[HeaderNames.ContentType] = contentType; + return new OkResult(); } var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath); diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs index 18e23fb5c..d0666034e 100644 --- a/Jellyfin.Api/Helpers/HlsHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsHelpers.cs @@ -118,10 +118,7 @@ namespace Jellyfin.Api.Helpers /// <returns>The playlist text as a string.</returns> public static string GetLivePlaylistText(string path, StreamState state) { - using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - using var reader = new StreamReader(stream); - - var text = reader.ReadToEnd(); + var text = File.ReadAllText(path); var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index ce6740fc9..295cfaf08 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using System.Net; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -179,7 +180,7 @@ namespace Jellyfin.Api.Helpers bool enableTranscoding, bool allowVideoStreamCopy, bool allowAudioStreamCopy, - string ipAddress) + IPAddress ipAddress) { var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); @@ -282,6 +283,7 @@ namespace Jellyfin.Api.Helpers if (streamInfo != null) { SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } } @@ -307,7 +309,7 @@ namespace Jellyfin.Api.Helpers { if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) + && user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) { options.ForceDirectStream = true; } @@ -326,6 +328,7 @@ namespace Jellyfin.Api.Helpers if (streamInfo != null) { SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } } @@ -353,6 +356,7 @@ namespace Jellyfin.Api.Helpers // Do this after the above so that StartPositionTicks is set SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } else @@ -390,6 +394,7 @@ namespace Jellyfin.Api.Helpers // Do this after the above so that StartPositionTicks is set SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } } @@ -551,7 +556,7 @@ namespace Jellyfin.Api.Helpers } } - private int? GetMaxBitrate(int? clientMaxBitrate, User user, string ipAddress) + private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress) { var maxBitrate = clientMaxBitrate; var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0; diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs index 8bddf00d5..963e17724 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs @@ -71,7 +71,8 @@ namespace Jellyfin.Api.Helpers /// <returns>A <see cref="Task"/>.</returns> public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken) { - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token; + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken); + cancellationToken = linkedCancellationTokenSource.Token; try { diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 94856e03e..56585aeab 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -84,7 +84,7 @@ namespace Jellyfin.Api.Helpers authorization.Version, authorization.DeviceId, authorization.Device, - request.HttpContext.GetNormalizedRemoteIp(), + request.HttpContext.GetNormalizedRemoteIp().ToString(), user); if (session == null) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index d20a02cf5..8cffe9c4c 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -41,9 +41,7 @@ namespace Jellyfin.Api.Helpers /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param> @@ -59,16 +57,13 @@ namespace Jellyfin.Api.Helpers ILibraryManager libraryManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, + EncodingHelper encodingHelper, IDlnaManager dlnaManager, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, TranscodingJobType transcodingJobType, CancellationToken cancellationToken) { - EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); // Parse the DLNA time seek header if (!streamingRequest.StartTimeTicks.HasValue) { @@ -121,14 +116,14 @@ namespace Jellyfin.Api.Helpers if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec)) { state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries); - state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i)) + state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec) ?? state.SupportedAudioCodecs.FirstOrDefault(); } if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec)) { state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries); - state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i)) + state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(mediaEncoder.CanEncodeToSubtitleCodec) ?? state.SupportedSubtitleCodecs.FirstOrDefault(); } @@ -297,16 +292,14 @@ namespace Jellyfin.Api.Helpers } } - if (profile == null) - { - profile = dlnaManager.GetDefaultProfile(); - } + profile ??= dlnaManager.GetDefaultProfile(); var audioCodec = state.ActualOutputAudioCodec; if (!state.IsVideoRequest) { - responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildAudioHeader( + responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader( + profile, state.OutputContainer, audioCodec, state.OutputAudioBitrate, @@ -323,7 +316,7 @@ namespace Jellyfin.Api.Helpers responseHeaders.Add( "contentFeatures.dlna.org", - new ContentFeatureBuilder(profile).BuildVideoHeader(state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty); + ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty); } } @@ -508,17 +501,15 @@ namespace Jellyfin.Api.Helpers private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static) { - var headers = request.Headers; - if (!string.IsNullOrWhiteSpace(deviceProfileId)) { state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId); - } - else if (!string.IsNullOrWhiteSpace(deviceProfileId)) - { - var caps = deviceManager.GetCapabilities(deviceProfileId); - state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile; + if (state.DeviceProfile == null) + { + var caps = deviceManager.GetCapabilities(deviceProfileId); + state.DeviceProfile = caps == null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile; + } } var profile = state.DeviceProfile; diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 240d132b1..0879cbd18 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -62,8 +62,7 @@ namespace Jellyfin.Api.Helpers /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> public TranscodingJobHelper( ILogger<TranscodingJobHelper> logger, @@ -73,8 +72,7 @@ namespace Jellyfin.Api.Helpers IServerConfigurationManager serverConfigurationManager, ISessionManager sessionManager, IAuthorizationContext authorizationContext, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, + EncodingHelper encodingHelper, ILoggerFactory loggerFactory) { _logger = logger; @@ -84,8 +82,8 @@ namespace Jellyfin.Api.Helpers _serverConfigurationManager = serverConfigurationManager; _sessionManager = sessionManager; _authorizationContext = authorizationContext; + _encodingHelper = encodingHelper; _loggerFactory = loggerFactory; - _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); DeleteEncodedMediaCache(); diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 67d0a3b5a..c10c34b59 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -15,10 +15,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.3" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.5" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> - <PackageReference Include="Swashbuckle.AspNetCore" Version="6.0.7" /> - <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.0.7" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.4" /> + <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.1.4" /> </ItemGroup> <ItemGroup> @@ -28,7 +28,6 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs index 991dbfc50..f93638898 100644 --- a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs +++ b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs @@ -1,4 +1,7 @@ -namespace Jellyfin.Api.Models.LibraryDtos +using System; +using System.Collections.Generic; + +namespace Jellyfin.Api.Models.LibraryDtos { /// <summary> /// Media Update Info Dto. @@ -6,14 +9,8 @@ public class MediaUpdateInfoDto { /// <summary> - /// Gets or sets media path. - /// </summary> - public string? Path { get; set; } - - /// <summary> - /// Gets or sets media update type. - /// Created, Modified, Deleted. + /// Gets or sets the list of updates. /// </summary> - public string? UpdateType { get; set; } + public IReadOnlyList<MediaUpdateInfoPathDto> Updates { get; set; } = Array.Empty<MediaUpdateInfoPathDto>(); } } diff --git a/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs new file mode 100644 index 000000000..852315b92 --- /dev/null +++ b/Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs @@ -0,0 +1,19 @@ +namespace Jellyfin.Api.Models.LibraryDtos +{ + /// <summary> + /// The media update info path. + /// </summary> + public class MediaUpdateInfoPathDto + { + /// <summary> + /// Gets or sets media path. + /// </summary> + public string? Path { get; set; } + + /// <summary> + /// Gets or sets media update type. + /// Created, Modified, Deleted. + /// </summary> + public string? UpdateType { get; set; } + } +} diff --git a/Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs new file mode 100644 index 000000000..fbd4985f9 --- /dev/null +++ b/Jellyfin.Api/Models/LibraryStructureDto/UpdateMediaPathRequestDto.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using MediaBrowser.Model.Configuration; + +namespace Jellyfin.Api.Models.LibraryStructureDto +{ + /// <summary> + /// Update library options dto. + /// </summary> + public class UpdateMediaPathRequestDto + { + /// <summary> + /// Gets or sets the library name. + /// </summary> + [Required] + public string Name { get; set; } = null!; + + /// <summary> + /// Gets or sets library folder path information. + /// </summary> + [Required] + public MediaPathInfo PathInfo { get; set; } = null!; + } +} diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 588ce717c..8913180e4 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using MediaBrowser.Common.Json.Converters; diff --git a/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs new file mode 100644 index 000000000..2c3a6282f --- /dev/null +++ b/Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs @@ -0,0 +1,30 @@ +using MediaBrowser.Model.Notifications; + +namespace Jellyfin.Api.Models.NotificationDtos +{ + /// <summary> + /// The admin notification dto. + /// </summary> + public class AdminNotificationDto + { + /// <summary> + /// Gets or sets the notification name. + /// </summary> + public string? Name { get; set; } + + /// <summary> + /// Gets or sets the notification description. + /// </summary> + public string? Description { get; set; } + + /// <summary> + /// Gets or sets the notification level. + /// </summary> + public NotificationLevel? NotificationLevel { get; set; } + + /// <summary> + /// Gets or sets the notification url. + /// </summary> + public string? Url { get; set; } + } +} diff --git a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs index 393627435..41f7b169e 100644 --- a/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs +++ b/Jellyfin.Api/Models/UserDtos/AuthenticateUserByName.cs @@ -1,4 +1,6 @@ -namespace Jellyfin.Api.Models.UserDtos +using System; + +namespace Jellyfin.Api.Models.UserDtos { /// <summary> /// The authenticate user by name request body. @@ -18,6 +20,7 @@ /// <summary> /// Gets or sets the sha1-hashed password. /// </summary> + [Obsolete("Send password using pw field")] public string? Password { get; set; } } } diff --git a/Jellyfin.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs index 4e75f4cfd..b7ba30180 100644 --- a/Jellyfin.Data/DayOfWeekHelper.cs +++ b/Jellyfin.Data/DayOfWeekHelper.cs @@ -1,67 +1,21 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; using Jellyfin.Data.Enums; namespace Jellyfin.Data { public static class DayOfWeekHelper { - public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day) + public static DayOfWeek[] GetDaysOfWeek(DynamicDayOfWeek day) { - var days = new List<DayOfWeek>(7); - - if (day == DynamicDayOfWeek.Sunday - || day == DynamicDayOfWeek.Weekend - || day == DynamicDayOfWeek.Everyday) - { - days.Add(DayOfWeek.Sunday); - } - - if (day == DynamicDayOfWeek.Monday - || day == DynamicDayOfWeek.Weekday - || day == DynamicDayOfWeek.Everyday) - { - days.Add(DayOfWeek.Monday); - } - - if (day == DynamicDayOfWeek.Tuesday - || day == DynamicDayOfWeek.Weekday - || day == DynamicDayOfWeek.Everyday) - { - days.Add(DayOfWeek.Tuesday); - } - - if (day == DynamicDayOfWeek.Wednesday - || day == DynamicDayOfWeek.Weekday - || day == DynamicDayOfWeek.Everyday) + return day switch { - days.Add(DayOfWeek.Wednesday); - } - - if (day == DynamicDayOfWeek.Thursday - || day == DynamicDayOfWeek.Weekday - || day == DynamicDayOfWeek.Everyday) - { - days.Add(DayOfWeek.Thursday); - } - - if (day == DynamicDayOfWeek.Friday - || day == DynamicDayOfWeek.Weekday - || day == DynamicDayOfWeek.Everyday) - { - days.Add(DayOfWeek.Friday); - } - - if (day == DynamicDayOfWeek.Saturday - || day == DynamicDayOfWeek.Weekend - || day == DynamicDayOfWeek.Everyday) - { - days.Add(DayOfWeek.Saturday); - } - - return days; + DynamicDayOfWeek.Everyday => new[] { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday }, + DynamicDayOfWeek.Weekday => new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday }, + DynamicDayOfWeek.Weekend => new[] { DayOfWeek.Sunday, DayOfWeek.Saturday }, + _ => new[] { (DayOfWeek)day } + }; } } } diff --git a/Jellyfin.Data/Entities/AccessSchedule.cs b/Jellyfin.Data/Entities/AccessSchedule.cs index 7d1b76a3f..befc4ca02 100644 --- a/Jellyfin.Data/Entities/AccessSchedule.cs +++ b/Jellyfin.Data/Entities/AccessSchedule.cs @@ -1,7 +1,5 @@ using System; -using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; using System.Xml.Serialization; using Jellyfin.Data.Enums; @@ -28,64 +26,37 @@ namespace Jellyfin.Data.Entities } /// <summary> - /// Initializes a new instance of the <see cref="AccessSchedule"/> class. - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected AccessSchedule() - { - } - - /// <summary> - /// Gets or sets the id of this instance. + /// Gets the id of this instance. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [XmlIgnore] - [Key] - [Required] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> - /// Gets or sets the id of the associated user. + /// Gets the id of the associated user. /// </summary> [XmlIgnore] - [Required] - public Guid UserId { get; protected set; } + public Guid UserId { get; private set; } /// <summary> /// Gets or sets the day of week. /// </summary> /// <value>The day of week.</value> - [Required] public DynamicDayOfWeek DayOfWeek { get; set; } /// <summary> /// Gets or sets the start hour. /// </summary> /// <value>The start hour.</value> - [Required] public double StartHour { get; set; } /// <summary> /// Gets or sets the end hour. /// </summary> /// <value>The end hour.</value> - [Required] public double EndHour { get; set; } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="dayOfWeek">The day of the week.</param> - /// <param name="startHour">The start hour.</param> - /// <param name="endHour">The end hour.</param> - /// <param name="userId">The associated user's id.</param> - /// <returns>The newly created instance.</returns> - public static AccessSchedule Create(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId) - { - return new AccessSchedule(dayOfWeek, startHour, endHour, userId); - } } } diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs index e2d5c7187..1d1b86552 100644 --- a/Jellyfin.Data/Entities/ActivityLog.cs +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -18,8 +18,7 @@ namespace Jellyfin.Data.Entities /// <param name="name">The name.</param> /// <param name="type">The type.</param> /// <param name="userId">The user id.</param> - /// <param name="logLevel">The log level.</param> - public ActivityLog(string name, string type, Guid userId, LogLevel logLevel = LogLevel.Information) + public ActivityLog(string name, string type, Guid userId) { if (string.IsNullOrEmpty(name)) { @@ -35,23 +34,14 @@ namespace Jellyfin.Data.Entities Type = type; UserId = userId; DateCreated = DateTime.UtcNow; - LogSeverity = logLevel; + LogSeverity = LogLevel.Information; } /// <summary> - /// Initializes a new instance of the <see cref="ActivityLog"/> class. - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected ActivityLog() - { - } - - /// <summary> - /// Gets or sets the identity of this instance. - /// This is the key in the backing database. + /// Gets the identity of this instance. /// </summary> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the name. @@ -59,7 +49,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required, Max length = 512. /// </remarks> - [Required] [MaxLength(512)] [StringLength(512)] public string Name { get; set; } @@ -72,7 +61,7 @@ namespace Jellyfin.Data.Entities /// </remarks> [MaxLength(512)] [StringLength(512)] - public string Overview { get; set; } + public string? Overview { get; set; } /// <summary> /// Gets or sets the short overview. @@ -82,7 +71,7 @@ namespace Jellyfin.Data.Entities /// </remarks> [MaxLength(512)] [StringLength(512)] - public string ShortOverview { get; set; } + public string? ShortOverview { get; set; } /// <summary> /// Gets or sets the type. @@ -90,7 +79,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required, Max length = 256. /// </remarks> - [Required] [MaxLength(256)] [StringLength(256)] public string Type { get; set; } @@ -111,7 +99,7 @@ namespace Jellyfin.Data.Entities /// </remarks> [MaxLength(256)] [StringLength(256)] - public string ItemId { get; set; } + public string? ItemId { get; set; } /// <summary> /// Gets or sets the date created. This should be in UTC. @@ -131,7 +119,7 @@ namespace Jellyfin.Data.Entities /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs b/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs index 511e3b281..b07b7c731 100644 --- a/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs +++ b/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs @@ -15,32 +15,25 @@ namespace Jellyfin.Data.Entities /// <param name="userId">The user id.</param> /// <param name="itemId">The item id.</param> /// <param name="client">The client.</param> - /// <param name="preferenceKey">The preference key.</param> - /// <param name="preferenceValue">The preference value.</param> - public CustomItemDisplayPreferences(Guid userId, Guid itemId, string client, string preferenceKey, string preferenceValue) + /// <param name="key">The preference key.</param> + /// <param name="value">The preference value.</param> + public CustomItemDisplayPreferences(Guid userId, Guid itemId, string client, string key, string? value) { UserId = userId; ItemId = itemId; Client = client; - Key = preferenceKey; - Value = preferenceValue; + Key = key; + Value = value; } /// <summary> - /// Initializes a new instance of the <see cref="CustomItemDisplayPreferences"/> class. - /// </summary> - protected CustomItemDisplayPreferences() - { - } - - /// <summary> - /// Gets or sets the Id. + /// Gets the Id. /// </summary> /// <remarks> /// Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the user Id. @@ -64,7 +57,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required. Max Length = 32. /// </remarks> - [Required] [MaxLength(32)] [StringLength(32)] public string Client { get; set; } @@ -75,7 +67,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required. /// </remarks> - [Required] public string Key { get; set; } /// <summary> @@ -84,7 +75,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required. /// </remarks> - [Required] - public string Value { get; set; } + public string? Value { get; set; } } } diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Data/Entities/DisplayPreferences.cs index 1a8ca1da3..646961238 100644 --- a/Jellyfin.Data/Entities/DisplayPreferences.cs +++ b/Jellyfin.Data/Entities/DisplayPreferences.cs @@ -1,6 +1,4 @@ -#pragma warning disable CA2227 - -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -30,27 +28,18 @@ namespace Jellyfin.Data.Entities SkipBackwardLength = 10000; ScrollDirection = ScrollDirection.Horizontal; ChromecastVersion = ChromecastVersion.Stable; - DashboardTheme = string.Empty; - TvHome = string.Empty; HomeSections = new HashSet<HomeSection>(); } /// <summary> - /// Initializes a new instance of the <see cref="DisplayPreferences"/> class. - /// </summary> - protected DisplayPreferences() - { - } - - /// <summary> - /// Gets or sets the Id. + /// Gets the Id. /// </summary> /// <remarks> /// Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the user Id. @@ -74,7 +63,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required. Max Length = 32. /// </remarks> - [Required] [MaxLength(32)] [StringLength(32)] public string Client { get; set; } @@ -145,18 +133,18 @@ namespace Jellyfin.Data.Entities /// </summary> [MaxLength(32)] [StringLength(32)] - public string DashboardTheme { get; set; } + public string? DashboardTheme { get; set; } /// <summary> /// Gets or sets the tv home screen. /// </summary> [MaxLength(32)] [StringLength(32)] - public string TvHome { get; set; } + public string? TvHome { get; set; } /// <summary> - /// Gets or sets the home sections. + /// Gets the home sections. /// </summary> - public virtual ICollection<HomeSection> HomeSections { get; protected set; } + public virtual ICollection<HomeSection> HomeSections { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs index 878811e59..14da0bb15 100644 --- a/Jellyfin.Data/Entities/Group.cs +++ b/Jellyfin.Data/Entities/Group.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -33,22 +31,12 @@ namespace Jellyfin.Data.Entities } /// <summary> - /// Initializes a new instance of the <see cref="Group"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Group() - { - } - - /// <summary> - /// Gets or sets the id of this group. + /// Gets the id of this group. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> - public Guid Id { get; protected set; } + public Guid Id { get; private set; } /// <summary> /// Gets or sets the group's name. @@ -56,24 +44,23 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required, Max length = 255. /// </remarks> - [Required] [MaxLength(255)] [StringLength(255)] public string Name { get; set; } /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> - /// Gets or sets a collection containing the group's permissions. + /// Gets a collection containing the group's permissions. /// </summary> - public virtual ICollection<Permission> Permissions { get; protected set; } + public virtual ICollection<Permission> Permissions { get; private set; } /// <summary> - /// Gets or sets a collection containing the group's preferences. + /// Gets a collection containing the group's preferences. /// </summary> - public virtual ICollection<Preference> Preferences { get; protected set; } + public virtual ICollection<Preference> Preferences { get; private set; } /// <inheritdoc/> public bool HasPermission(PermissionKind kind) diff --git a/Jellyfin.Data/Entities/HomeSection.cs b/Jellyfin.Data/Entities/HomeSection.cs index 062046260..e194aa537 100644 --- a/Jellyfin.Data/Entities/HomeSection.cs +++ b/Jellyfin.Data/Entities/HomeSection.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities @@ -10,14 +9,13 @@ namespace Jellyfin.Data.Entities public class HomeSection { /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity. Required. /// </remarks> - [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the Id of the associated display preferences. diff --git a/Jellyfin.Data/Entities/ImageInfo.cs b/Jellyfin.Data/Entities/ImageInfo.cs index ab8452e62..b5c7a1c12 100644 --- a/Jellyfin.Data/Entities/ImageInfo.cs +++ b/Jellyfin.Data/Entities/ImageInfo.cs @@ -20,28 +20,18 @@ namespace Jellyfin.Data.Entities } /// <summary> - /// Initializes a new instance of the <see cref="ImageInfo"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected ImageInfo() - { - } - - /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> - /// Gets or sets the user id. + /// Gets the user id. /// </summary> - public Guid? UserId { get; protected set; } + public Guid? UserId { get; private set; } /// <summary> /// Gets or sets the path of the image. @@ -49,7 +39,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required. /// </remarks> - [Required] [MaxLength(512)] [StringLength(512)] public string Path { get; set; } diff --git a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs index 2b25bb25f..948126d0a 100644 --- a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs +++ b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs @@ -29,20 +29,13 @@ namespace Jellyfin.Data.Entities } /// <summary> - /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class. - /// </summary> - protected ItemDisplayPreferences() - { - } - - /// <summary> - /// Gets or sets the Id. + /// Gets the id. /// </summary> /// <remarks> /// Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the user Id. @@ -66,7 +59,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required. Max Length = 32. /// </remarks> - [Required] [MaxLength(32)] [StringLength(32)] public string Client { get; set; } @@ -106,7 +98,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required. /// </remarks> - [Required] [MaxLength(64)] [StringLength(64)] public string SortBy { get; set; } diff --git a/Jellyfin.Data/Entities/Libraries/Artwork.cs b/Jellyfin.Data/Entities/Libraries/Artwork.cs index 06cd33330..923525fc5 100644 --- a/Jellyfin.Data/Entities/Libraries/Artwork.cs +++ b/Jellyfin.Data/Entities/Libraries/Artwork.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -18,8 +16,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="path">The path.</param> /// <param name="kind">The kind of art.</param> - /// <param name="owner">The owner.</param> - public Artwork(string path, ArtKind kind, IHasArtwork owner) + public Artwork(string path, ArtKind kind) { if (string.IsNullOrEmpty(path)) { @@ -28,28 +25,16 @@ namespace Jellyfin.Data.Entities.Libraries Path = path; Kind = kind; - - owner?.Artwork.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="Artwork"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Artwork() - { } /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the path. @@ -57,7 +42,6 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required, Max length = 65535. /// </remarks> - [Required] [MaxLength(65535)] [StringLength(65535)] public string Path { get; set; } @@ -72,7 +56,7 @@ namespace Jellyfin.Data.Entities.Libraries /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/Book.cs b/Jellyfin.Data/Entities/Libraries/Book.cs index 2e63f75bd..a838686d0 100644 --- a/Jellyfin.Data/Entities/Libraries/Book.cs +++ b/Jellyfin.Data/Entities/Libraries/Book.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System.Collections.Generic; using Jellyfin.Data.Interfaces; @@ -13,18 +11,19 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="Book"/> class. /// </summary> - public Book() + /// <param name="library">The library.</param> + public Book(Library library) : base(library) { BookMetadata = new HashSet<BookMetadata>(); Releases = new HashSet<Release>(); } /// <summary> - /// Gets or sets a collection containing the metadata for this book. + /// Gets a collection containing the metadata for this book. /// </summary> - public virtual ICollection<BookMetadata> BookMetadata { get; protected set; } + public virtual ICollection<BookMetadata> BookMetadata { get; private set; } /// <inheritdoc /> - public virtual ICollection<Release> Releases { get; protected set; } + public virtual ICollection<Release> Releases { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs index 4a3d290f0..4a350d200 100644 --- a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs @@ -1,8 +1,4 @@ -#pragma warning disable CA2227 - -using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities.Libraries @@ -17,41 +13,22 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="title">The title or name of the object.</param> /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="book">The book.</param> - public BookMetadata(string title, string language, Book book) : base(title, language) + public BookMetadata(string title, string language) : base(title, language) { - if (book == null) - { - throw new ArgumentNullException(nameof(book)); - } - - book.BookMetadata.Add(this); - Publishers = new HashSet<Company>(); } /// <summary> - /// Initializes a new instance of the <see cref="BookMetadata"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected BookMetadata() - { - } - - /// <summary> /// Gets or sets the ISBN. /// </summary> public long? Isbn { get; set; } /// <summary> - /// Gets or sets a collection of the publishers for this book. + /// Gets a collection of the publishers for this book. /// </summary> - public virtual ICollection<Company> Publishers { get; protected set; } + public virtual ICollection<Company> Publishers { get; private set; } /// <inheritdoc /> - [NotMapped] public ICollection<Company> Companies => Publishers; } } diff --git a/Jellyfin.Data/Entities/Libraries/Chapter.cs b/Jellyfin.Data/Entities/Libraries/Chapter.cs index f503de379..3d81f713d 100644 --- a/Jellyfin.Data/Entities/Libraries/Chapter.cs +++ b/Jellyfin.Data/Entities/Libraries/Chapter.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -17,8 +15,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="language">ISO-639-3 3-character language codes.</param> /// <param name="startTime">The start time for this chapter.</param> - /// <param name="release">The release.</param> - public Chapter(string language, long startTime, Release release) + public Chapter(string language, long startTime) { if (string.IsNullOrEmpty(language)) { @@ -27,33 +24,16 @@ namespace Jellyfin.Data.Entities.Libraries Language = language; StartTime = startTime; - - if (release == null) - { - throw new ArgumentNullException(nameof(release)); - } - - release.Chapters.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="Chapter"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Chapter() - { } /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the name. @@ -63,7 +43,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Name { get; set; } + public string? Name { get; set; } /// <summary> /// Gets or sets the language. @@ -72,7 +52,6 @@ namespace Jellyfin.Data.Entities.Libraries /// Required, Min length = 3, Max length = 3 /// ISO-639-3 3-character language codes. /// </remarks> - [Required] [MinLength(3)] [MaxLength(3)] [StringLength(3)] @@ -93,7 +72,7 @@ namespace Jellyfin.Data.Entities.Libraries /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; protected set; } + public uint RowVersion { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/Collection.cs b/Jellyfin.Data/Entities/Libraries/Collection.cs index 39eded752..7de601969 100644 --- a/Jellyfin.Data/Entities/Libraries/Collection.cs +++ b/Jellyfin.Data/Entities/Libraries/Collection.cs @@ -1,4 +1,4 @@ -#pragma warning disable CA2227 +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -21,13 +21,13 @@ namespace Jellyfin.Data.Entities.Libraries } /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the name. @@ -37,16 +37,16 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Name { get; set; } + public string? Name { get; set; } /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> - /// Gets or sets a collection containing this collection's items. + /// Gets a collection containing this collection's items. /// </summary> - public virtual ICollection<CollectionItem> Items { get; protected set; } + public virtual ICollection<CollectionItem> Items { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs index f9539964d..0cb4716db 100644 --- a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs +++ b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Interfaces; @@ -13,39 +12,10 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="CollectionItem"/> class. /// </summary> - /// <param name="collection">The collection.</param> - /// <param name="previous">The previous item.</param> - /// <param name="next">The next item.</param> - public CollectionItem(Collection collection, CollectionItem previous, CollectionItem next) - { - if (collection == null) - { - throw new ArgumentNullException(nameof(collection)); - } - - collection.Items.Add(this); - - if (next != null) - { - Next = next; - next.Previous = this; - } - - if (previous != null) - { - Previous = previous; - previous.Next = this; - } - } - - /// <summary> - /// Initializes a new instance of the <see cref="CollectionItem"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected CollectionItem() + /// <param name="libraryItem">The library item.</param> + public CollectionItem(LibraryItem libraryItem) { + LibraryItem = libraryItem; } /// <summary> @@ -59,7 +29,7 @@ namespace Jellyfin.Data.Entities.Libraries /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> /// Gets or sets the library item. @@ -75,7 +45,7 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// TODO check if this properly updated Dependant and has the proper principal relationship. /// </remarks> - public virtual CollectionItem Next { get; set; } + public virtual CollectionItem? Next { get; set; } /// <summary> /// Gets or sets the previous item in the collection. @@ -83,7 +53,7 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// TODO check if this properly updated Dependant and has the proper principal relationship. /// </remarks> - public virtual CollectionItem Previous { get; set; } + public virtual CollectionItem? Previous { get; set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/Company.cs b/Jellyfin.Data/Entities/Libraries/Company.cs index 3b6ed3309..1abbee445 100644 --- a/Jellyfin.Data/Entities/Libraries/Company.cs +++ b/Jellyfin.Data/Entities/Libraries/Company.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -15,49 +13,36 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="Company"/> class. /// </summary> - /// <param name="owner">The owner of this company.</param> - public Company(IHasCompanies owner) + public Company() { - owner?.Companies.Add(this); - CompanyMetadata = new HashSet<CompanyMetadata>(); + ChildCompanies = new HashSet<Company>(); } /// <summary> - /// Initializes a new instance of the <see cref="Company"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Company() - { - } - - /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> - /// Gets or sets a collection containing the metadata. + /// Gets a collection containing the metadata. /// </summary> - public virtual ICollection<CompanyMetadata> CompanyMetadata { get; protected set; } + public virtual ICollection<CompanyMetadata> CompanyMetadata { get; private set; } /// <summary> - /// Gets or sets a collection containing this company's child companies. + /// Gets a collection containing this company's child companies. /// </summary> - public virtual ICollection<Company> ChildCompanies { get; protected set; } + public virtual ICollection<Company> ChildCompanies { get; private set; } /// <inheritdoc /> - [NotMapped] public ICollection<Company> Companies => ChildCompanies; /// <inheritdoc /> diff --git a/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs b/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs index 8aa0486af..a29f08c7f 100644 --- a/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations; namespace Jellyfin.Data.Entities.Libraries @@ -13,21 +12,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="title">The title or name of the object.</param> /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="company">The company.</param> - public CompanyMetadata(string title, string language, Company company) : base(title, language) - { - if (company == null) - { - throw new ArgumentNullException(nameof(company)); - } - - company.CompanyMetadata.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="CompanyMetadata"/> class. - /// </summary> - protected CompanyMetadata() + public CompanyMetadata(string title, string language) : base(title, language) { } @@ -39,7 +24,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(65535)] [StringLength(65535)] - public string Description { get; set; } + public string? Description { get; set; } /// <summary> /// Gets or sets the headquarters. @@ -49,7 +34,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(255)] [StringLength(255)] - public string Headquarters { get; set; } + public string? Headquarters { get; set; } /// <summary> /// Gets or sets the country code. @@ -59,7 +44,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(2)] [StringLength(2)] - public string Country { get; set; } + public string? Country { get; set; } /// <summary> /// Gets or sets the homepage. @@ -69,6 +54,6 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Homepage { get; set; } + public string? Homepage { get; set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/CustomItem.cs b/Jellyfin.Data/Entities/Libraries/CustomItem.cs index 115489c78..e27d01d86 100644 --- a/Jellyfin.Data/Entities/Libraries/CustomItem.cs +++ b/Jellyfin.Data/Entities/Libraries/CustomItem.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System.Collections.Generic; using Jellyfin.Data.Interfaces; @@ -13,18 +11,19 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="CustomItem"/> class. /// </summary> - public CustomItem() + /// <param name="library">The library.</param> + public CustomItem(Library library) : base(library) { CustomItemMetadata = new HashSet<CustomItemMetadata>(); Releases = new HashSet<Release>(); } /// <summary> - /// Gets or sets a collection containing the metadata for this item. + /// Gets a collection containing the metadata for this item. /// </summary> - public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; protected set; } + public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; private set; } /// <inheritdoc /> - public virtual ICollection<Release> Releases { get; protected set; } + public virtual ICollection<Release> Releases { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs b/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs index f0daedfbe..af2393870 100644 --- a/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs @@ -1,5 +1,3 @@ -using System; - namespace Jellyfin.Data.Entities.Libraries { /// <summary> @@ -12,24 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="title">The title or name of the object.</param> /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="item">The item.</param> - public CustomItemMetadata(string title, string language, CustomItem item) : base(title, language) - { - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } - - item.CustomItemMetadata.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="CustomItemMetadata"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected CustomItemMetadata() + public CustomItemMetadata(string title, string language) : base(title, language) { } } diff --git a/Jellyfin.Data/Entities/Libraries/Episode.cs b/Jellyfin.Data/Entities/Libraries/Episode.cs index 0bdc2d764..ce2f0c617 100644 --- a/Jellyfin.Data/Entities/Libraries/Episode.cs +++ b/Jellyfin.Data/Entities/Libraries/Episode.cs @@ -1,6 +1,3 @@ -#pragma warning disable CA2227 - -using System; using System.Collections.Generic; using Jellyfin.Data.Interfaces; @@ -14,41 +11,24 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="Episode"/> class. /// </summary> - /// <param name="season">The season.</param> - public Episode(Season season) + /// <param name="library">The library.</param> + public Episode(Library library) : base(library) { - if (season == null) - { - throw new ArgumentNullException(nameof(season)); - } - - season.Episodes.Add(this); - Releases = new HashSet<Release>(); EpisodeMetadata = new HashSet<EpisodeMetadata>(); } /// <summary> - /// Initializes a new instance of the <see cref="Episode"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Episode() - { - } - - /// <summary> /// Gets or sets the episode number. /// </summary> public int? EpisodeNumber { get; set; } /// <inheritdoc /> - public virtual ICollection<Release> Releases { get; protected set; } + public virtual ICollection<Release> Releases { get; private set; } /// <summary> - /// Gets or sets a collection containing the metadata for this episode. + /// Gets a collection containing the metadata for this episode. /// </summary> - public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; protected set; } + public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs b/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs index 7efb840f0..b0ef11e0f 100644 --- a/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations; namespace Jellyfin.Data.Entities.Libraries @@ -13,24 +12,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="title">The title or name of the object.</param> /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="episode">The episode.</param> - public EpisodeMetadata(string title, string language, Episode episode) : base(title, language) - { - if (episode == null) - { - throw new ArgumentNullException(nameof(episode)); - } - - episode.EpisodeMetadata.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="EpisodeMetadata"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected EpisodeMetadata() + public EpisodeMetadata(string title, string language) : base(title, language) { } @@ -42,7 +24,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Outline { get; set; } + public string? Outline { get; set; } /// <summary> /// Gets or sets the plot. @@ -52,7 +34,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(65535)] [StringLength(65535)] - public string Plot { get; set; } + public string? Plot { get; set; } /// <summary> /// Gets or sets the tagline. @@ -62,6 +44,6 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Tagline { get; set; } + public string? Tagline { get; set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/Genre.cs b/Jellyfin.Data/Entities/Libraries/Genre.cs index 2a2dbd1a5..3b822ee82 100644 --- a/Jellyfin.Data/Entities/Libraries/Genre.cs +++ b/Jellyfin.Data/Entities/Libraries/Genre.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Interfaces; @@ -14,42 +13,19 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the <see cref="Genre"/> class. /// </summary> /// <param name="name">The name.</param> - /// <param name="itemMetadata">The metadata.</param> - public Genre(string name, ItemMetadata itemMetadata) + public Genre(string name) { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - Name = name; - - if (itemMetadata == null) - { - throw new ArgumentNullException(nameof(itemMetadata)); - } - - itemMetadata.Genres.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="Genre"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Genre() - { } /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the name. @@ -57,14 +33,13 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Indexed, Required, Max length = 255. /// </remarks> - [Required] [MaxLength(255)] [StringLength(255)] public string Name { get; set; } /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; protected set; } + public uint RowVersion { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs b/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs index d74330c05..d429a90c6 100644 --- a/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -43,23 +41,13 @@ namespace Jellyfin.Data.Entities.Libraries } /// <summary> - /// Initializes a new instance of the <see cref="ItemMetadata"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to being abstract. - /// </remarks> - protected ItemMetadata() - { - } - - /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the title. @@ -67,7 +55,6 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required, Max length = 1024. /// </remarks> - [Required] [MaxLength(1024)] [StringLength(1024)] public string Title { get; set; } @@ -80,7 +67,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string OriginalTitle { get; set; } + public string? OriginalTitle { get; set; } /// <summary> /// Gets or sets the sort title. @@ -90,7 +77,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string SortTitle { get; set; } + public string? SortTitle { get; set; } /// <summary> /// Gets or sets the language. @@ -99,7 +86,6 @@ namespace Jellyfin.Data.Entities.Libraries /// Required, Min length = 3, Max length = 3. /// ISO-639-3 3-character language codes. /// </remarks> - [Required] [MinLength(3)] [MaxLength(3)] [StringLength(3)] @@ -111,12 +97,12 @@ namespace Jellyfin.Data.Entities.Libraries public DateTimeOffset? ReleaseDate { get; set; } /// <summary> - /// Gets or sets the date added. + /// Gets the date added. /// </summary> /// <remarks> /// Required. /// </remarks> - public DateTime DateAdded { get; protected set; } + public DateTime DateAdded { get; private set; } /// <summary> /// Gets or sets the date modified. @@ -126,37 +112,32 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> public DateTime DateModified { get; set; } - /// <summary> - /// Gets or sets the row version. - /// </summary> - /// <remarks> - /// Required, ConcurrencyToken. - /// </remarks> + /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> - /// Gets or sets a collection containing the person roles for this item. + /// Gets a collection containing the person roles for this item. /// </summary> - public virtual ICollection<PersonRole> PersonRoles { get; protected set; } + public virtual ICollection<PersonRole> PersonRoles { get; private set; } /// <summary> - /// Gets or sets a collection containing the genres for this item. + /// Gets a collection containing the genres for this item. /// </summary> - public virtual ICollection<Genre> Genres { get; protected set; } + public virtual ICollection<Genre> Genres { get; private set; } /// <inheritdoc /> - public virtual ICollection<Artwork> Artwork { get; protected set; } + public virtual ICollection<Artwork> Artwork { get; private set; } /// <summary> - /// Gets or sets a collection containing the ratings for this item. + /// Gets a collection containing the ratings for this item. /// </summary> - public virtual ICollection<Rating> Ratings { get; protected set; } + public virtual ICollection<Rating> Ratings { get; private set; } /// <summary> - /// Gets or sets a collection containing the metadata sources for this item. + /// Gets a collection containing the metadata sources for this item. /// </summary> - public virtual ICollection<MetadataProviderId> Sources { get; protected set; } + public virtual ICollection<MetadataProviderId> Sources { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/Library.cs b/Jellyfin.Data/Entities/Libraries/Library.cs index 4f82a2e2a..0db42a1c7 100644 --- a/Jellyfin.Data/Entities/Libraries/Library.cs +++ b/Jellyfin.Data/Entities/Libraries/Library.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Interfaces; @@ -14,34 +13,21 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the <see cref="Library"/> class. /// </summary> /// <param name="name">The name of the library.</param> - public Library(string name) + /// <param name="path">The path of the library.</param> + public Library(string name, string path) { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - Name = name; + Path = path; } /// <summary> - /// Initializes a new instance of the <see cref="Library"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Library() - { - } - - /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the name. @@ -49,7 +35,6 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required, Max length = 128. /// </remarks> - [Required] [MaxLength(128)] [StringLength(128)] public string Name { get; set; } @@ -60,12 +45,11 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required. /// </remarks> - [Required] public string Path { get; set; } /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/LibraryItem.cs b/Jellyfin.Data/Entities/Libraries/LibraryItem.cs index a9167aa7f..d889b871e 100644 --- a/Jellyfin.Data/Entities/Libraries/LibraryItem.cs +++ b/Jellyfin.Data/Entities/Libraries/LibraryItem.cs @@ -21,29 +21,22 @@ namespace Jellyfin.Data.Entities.Libraries } /// <summary> - /// Initializes a new instance of the <see cref="LibraryItem"/> class. - /// </summary> - protected LibraryItem() - { - } - - /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> - /// Gets or sets the date this library item was added. + /// Gets the date this library item was added. /// </summary> - public DateTime DateAdded { get; protected set; } + public DateTime DateAdded { get; private set; } /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; protected set; } + public uint RowVersion { get; private set; } /// <summary> /// Gets or sets the library of this item. @@ -51,7 +44,6 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required. /// </remarks> - [Required] public virtual Library Library { get; set; } /// <inheritdoc /> diff --git a/Jellyfin.Data/Entities/Libraries/MediaFile.cs b/Jellyfin.Data/Entities/Libraries/MediaFile.cs index 9924d5728..36e1e4777 100644 --- a/Jellyfin.Data/Entities/Libraries/MediaFile.cs +++ b/Jellyfin.Data/Entities/Libraries/MediaFile.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -19,8 +17,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="path">The path relative to the LibraryRoot.</param> /// <param name="kind">The file kind.</param> - /// <param name="release">The release.</param> - public MediaFile(string path, MediaFileKind kind, Release release) + public MediaFile(string path, MediaFileKind kind) { if (string.IsNullOrEmpty(path)) { @@ -30,34 +27,17 @@ namespace Jellyfin.Data.Entities.Libraries Path = path; Kind = kind; - if (release == null) - { - throw new ArgumentNullException(nameof(release)); - } - - release.MediaFiles.Add(this); - MediaFileStreams = new HashSet<MediaFileStream>(); } /// <summary> - /// Initializes a new instance of the <see cref="MediaFile"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected MediaFile() - { - } - - /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the path relative to the library root. @@ -65,7 +45,6 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required, Max length = 65535. /// </remarks> - [Required] [MaxLength(65535)] [StringLength(65535)] public string Path { get; set; } @@ -80,12 +59,12 @@ namespace Jellyfin.Data.Entities.Libraries /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> - /// Gets or sets a collection containing the streams in this file. + /// Gets a collection containing the streams in this file. /// </summary> - public virtual ICollection<MediaFileStream> MediaFileStreams { get; protected set; } + public virtual ICollection<MediaFileStream> MediaFileStreams { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs b/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs index 5b03e260e..e24e73ecb 100644 --- a/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs +++ b/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs @@ -1,4 +1,5 @@ -using System; +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix + using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Interfaces; @@ -14,37 +15,19 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the <see cref="MediaFileStream"/> class. /// </summary> /// <param name="streamNumber">The number of this stream.</param> - /// <param name="mediaFile">The media file.</param> - public MediaFileStream(int streamNumber, MediaFile mediaFile) + public MediaFileStream(int streamNumber) { StreamNumber = streamNumber; - - if (mediaFile == null) - { - throw new ArgumentNullException(nameof(mediaFile)); - } - - mediaFile.MediaFileStreams.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="MediaFileStream"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected MediaFileStream() - { } /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the stream number. @@ -56,7 +39,7 @@ namespace Jellyfin.Data.Entities.Libraries /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs b/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs index a18a612bc..b27196078 100644 --- a/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs +++ b/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs @@ -25,23 +25,13 @@ namespace Jellyfin.Data.Entities.Libraries } /// <summary> - /// Initializes a new instance of the <see cref="MetadataProvider"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected MetadataProvider() - { - } - - /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the name. @@ -49,14 +39,13 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required, Max length = 1024. /// </remarks> - [Required] [MaxLength(1024)] [StringLength(1024)] public string Name { get; set; } /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs index fcfb35bfa..44c198518 100644 --- a/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs +++ b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs @@ -14,8 +14,8 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the <see cref="MetadataProviderId"/> class. /// </summary> /// <param name="providerId">The provider id.</param> - /// <param name="itemMetadata">The metadata entity.</param> - public MetadataProviderId(string providerId, ItemMetadata itemMetadata) + /// <param name="metadataProvider">The metadata provider.</param> + public MetadataProviderId(string providerId, MetadataProvider metadataProvider) { if (string.IsNullOrEmpty(providerId)) { @@ -23,33 +23,17 @@ namespace Jellyfin.Data.Entities.Libraries } ProviderId = providerId; - - if (itemMetadata == null) - { - throw new ArgumentNullException(nameof(itemMetadata)); - } - - itemMetadata.Sources.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="MetadataProviderId"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected MetadataProviderId() - { + MetadataProvider = metadataProvider; } /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the provider id. @@ -57,14 +41,13 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required, Max length = 255. /// </remarks> - [Required] [MaxLength(255)] [StringLength(255)] public string ProviderId { get; set; } /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> /// Gets or sets the metadata provider. diff --git a/Jellyfin.Data/Entities/Libraries/Movie.cs b/Jellyfin.Data/Entities/Libraries/Movie.cs index 08db904fa..499fafd0e 100644 --- a/Jellyfin.Data/Entities/Libraries/Movie.cs +++ b/Jellyfin.Data/Entities/Libraries/Movie.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System.Collections.Generic; using Jellyfin.Data.Interfaces; @@ -13,18 +11,19 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="Movie"/> class. /// </summary> - public Movie() + /// <param name="library">The library.</param> + public Movie(Library library) : base(library) { Releases = new HashSet<Release>(); MovieMetadata = new HashSet<MovieMetadata>(); } /// <inheritdoc /> - public virtual ICollection<Release> Releases { get; protected set; } + public virtual ICollection<Release> Releases { get; private set; } /// <summary> - /// Gets or sets a collection containing the metadata for this movie. + /// Gets a collection containing the metadata for this movie. /// </summary> - public virtual ICollection<MovieMetadata> MovieMetadata { get; protected set; } + public virtual ICollection<MovieMetadata> MovieMetadata { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs index aa1501a5c..44b5f34d7 100644 --- a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs @@ -1,8 +1,5 @@ -#pragma warning disable CA2227 - using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities.Libraries @@ -17,22 +14,9 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="title">The title or name of the movie.</param> /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="movie">The movie.</param> - public MovieMetadata(string title, string language, Movie movie) : base(title, language) + public MovieMetadata(string title, string language) : base(title, language) { Studios = new HashSet<Company>(); - - movie.MovieMetadata.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="MovieMetadata"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected MovieMetadata() - { } /// <summary> @@ -43,7 +27,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Outline { get; set; } + public string? Outline { get; set; } /// <summary> /// Gets or sets the tagline. @@ -53,7 +37,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Tagline { get; set; } + public string? Tagline { get; set; } /// <summary> /// Gets or sets the plot. @@ -63,7 +47,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(65535)] [StringLength(65535)] - public string Plot { get; set; } + public string? Plot { get; set; } /// <summary> /// Gets or sets the country code. @@ -73,15 +57,14 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(2)] [StringLength(2)] - public string Country { get; set; } + public string? Country { get; set; } /// <summary> - /// Gets or sets the studios that produced this movie. + /// Gets the studios that produced this movie. /// </summary> - public virtual ICollection<Company> Studios { get; protected set; } + public virtual ICollection<Company> Studios { get; private set; } /// <inheritdoc /> - [NotMapped] public ICollection<Company> Companies => Studios; } } diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs index 06aff6f45..d6231bbf0 100644 --- a/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs +++ b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System.Collections.Generic; namespace Jellyfin.Data.Entities.Libraries @@ -12,20 +10,21 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="MusicAlbum"/> class. /// </summary> - public MusicAlbum() + /// <param name="library">The library.</param> + public MusicAlbum(Library library) : base(library) { MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>(); Tracks = new HashSet<Track>(); } /// <summary> - /// Gets or sets a collection containing the album metadata. + /// Gets a collection containing the album metadata. /// </summary> - public virtual ICollection<MusicAlbumMetadata> MusicAlbumMetadata { get; protected set; } + public virtual ICollection<MusicAlbumMetadata> MusicAlbumMetadata { get; private set; } /// <summary> - /// Gets or sets a collection containing the tracks. + /// Gets a collection containing the tracks. /// </summary> - public virtual ICollection<Track> Tracks { get; protected set; } + public virtual ICollection<Track> Tracks { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs index 05c0b0374..691f3504f 100644 --- a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -15,22 +13,9 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="title">The title or name of the album.</param> /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="album">The music album.</param> - public MusicAlbumMetadata(string title, string language, MusicAlbum album) : base(title, language) + public MusicAlbumMetadata(string title, string language) : base(title, language) { Labels = new HashSet<Company>(); - - album.MusicAlbumMetadata.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="MusicAlbumMetadata"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected MusicAlbumMetadata() - { } /// <summary> @@ -41,7 +26,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(255)] [StringLength(255)] - public string Barcode { get; set; } + public string? Barcode { get; set; } /// <summary> /// Gets or sets the label number. @@ -51,7 +36,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(255)] [StringLength(255)] - public string LabelNumber { get; set; } + public string? LabelNumber { get; set; } /// <summary> /// Gets or sets the country code. @@ -61,11 +46,11 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(2)] [StringLength(2)] - public string Country { get; set; } + public string? Country { get; set; } /// <summary> - /// Gets or sets a collection containing the labels. + /// Gets a collection containing the labels. /// </summary> - public virtual ICollection<Company> Labels { get; protected set; } + public virtual ICollection<Company> Labels { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/Person.cs b/Jellyfin.Data/Entities/Libraries/Person.cs index af4c87b73..8b67d920d 100644 --- a/Jellyfin.Data/Entities/Libraries/Person.cs +++ b/Jellyfin.Data/Entities/Libraries/Person.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -32,23 +30,13 @@ namespace Jellyfin.Data.Entities.Libraries } /// <summary> - /// Initializes a new instance of the <see cref="Person"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Person() - { - } - - /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the name. @@ -56,7 +44,6 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required, Max length = 1024. /// </remarks> - [Required] [MaxLength(1024)] [StringLength(1024)] public string Name { get; set; } @@ -69,15 +56,15 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(256)] [StringLength(256)] - public string SourceId { get; set; } + public string? SourceId { get; set; } /// <summary> - /// Gets or sets the date added. + /// Gets the date added. /// </summary> /// <remarks> /// Required. /// </remarks> - public DateTime DateAdded { get; protected set; } + public DateTime DateAdded { get; private set; } /// <summary> /// Gets or sets the date modified. @@ -89,12 +76,12 @@ namespace Jellyfin.Data.Entities.Libraries /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> - /// Gets or sets a list of metadata sources for this person. + /// Gets a list of metadata sources for this person. /// </summary> - public virtual ICollection<MetadataProviderId> Sources { get; protected set; } + public virtual ICollection<MetadataProviderId> Sources { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/PersonRole.cs b/Jellyfin.Data/Entities/Libraries/PersonRole.cs index cd38ee83d..7d40bdf44 100644 --- a/Jellyfin.Data/Entities/Libraries/PersonRole.cs +++ b/Jellyfin.Data/Entities/Libraries/PersonRole.cs @@ -1,6 +1,3 @@ -#pragma warning disable CA2227 - -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -18,39 +15,23 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the <see cref="PersonRole"/> class. /// </summary> /// <param name="type">The role type.</param> - /// <param name="itemMetadata">The metadata.</param> - public PersonRole(PersonRoleType type, ItemMetadata itemMetadata) + /// <param name="person">The person.</param> + public PersonRole(PersonRoleType type, Person person) { Type = type; - - if (itemMetadata == null) - { - throw new ArgumentNullException(nameof(itemMetadata)); - } - - itemMetadata.PersonRoles.Add(this); - + Person = person; + Artwork = new HashSet<Artwork>(); Sources = new HashSet<MetadataProviderId>(); } /// <summary> - /// Initializes a new instance of the <see cref="PersonRole"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected PersonRole() - { - } - - /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the name of the person's role. @@ -60,7 +41,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Role { get; set; } + public string? Role { get; set; } /// <summary> /// Gets or sets the person's role type. @@ -72,7 +53,7 @@ namespace Jellyfin.Data.Entities.Libraries /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; protected set; } + public uint RowVersion { get; private set; } /// <summary> /// Gets or sets the person. @@ -80,16 +61,15 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required. /// </remarks> - [Required] public virtual Person Person { get; set; } /// <inheritdoc /> - public virtual ICollection<Artwork> Artwork { get; protected set; } + public virtual ICollection<Artwork> Artwork { get; private set; } /// <summary> - /// Gets or sets a collection containing the metadata sources for this person role. + /// Gets a collection containing the metadata sources for this person role. /// </summary> - public virtual ICollection<MetadataProviderId> Sources { get; protected set; } + public virtual ICollection<MetadataProviderId> Sources { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/Photo.cs b/Jellyfin.Data/Entities/Libraries/Photo.cs index 25562ec96..4b459432b 100644 --- a/Jellyfin.Data/Entities/Libraries/Photo.cs +++ b/Jellyfin.Data/Entities/Libraries/Photo.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System.Collections.Generic; using Jellyfin.Data.Interfaces; @@ -13,18 +11,19 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="Photo"/> class. /// </summary> - public Photo() + /// <param name="library">The library.</param> + public Photo(Library library) : base(library) { PhotoMetadata = new HashSet<PhotoMetadata>(); Releases = new HashSet<Release>(); } /// <summary> - /// Gets or sets a collection containing the photo metadata. + /// Gets a collection containing the photo metadata. /// </summary> - public virtual ICollection<PhotoMetadata> PhotoMetadata { get; protected set; } + public virtual ICollection<PhotoMetadata> PhotoMetadata { get; private set; } /// <inheritdoc /> - public virtual ICollection<Release> Releases { get; protected set; } + public virtual ICollection<Release> Releases { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs b/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs index ffc790b57..6c284307d 100644 --- a/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs @@ -1,5 +1,3 @@ -using System; - namespace Jellyfin.Data.Entities.Libraries { /// <summary> @@ -12,24 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="title">The title or name of the photo.</param> /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="photo">The photo.</param> - public PhotoMetadata(string title, string language, Photo photo) : base(title, language) - { - if (photo == null) - { - throw new ArgumentNullException(nameof(photo)); - } - - photo.PhotoMetadata.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="PhotoMetadata"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected PhotoMetadata() + public PhotoMetadata(string title, string language) : base(title, language) { } } diff --git a/Jellyfin.Data/Entities/Libraries/Rating.cs b/Jellyfin.Data/Entities/Libraries/Rating.cs index 98226cd80..58c8fa49e 100644 --- a/Jellyfin.Data/Entities/Libraries/Rating.cs +++ b/Jellyfin.Data/Entities/Libraries/Rating.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Interfaces; @@ -14,37 +13,19 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the <see cref="Rating"/> class. /// </summary> /// <param name="value">The value.</param> - /// <param name="itemMetadata">The metadata.</param> - public Rating(double value, ItemMetadata itemMetadata) + public Rating(double value) { Value = value; - - if (itemMetadata == null) - { - throw new ArgumentNullException(nameof(itemMetadata)); - } - - itemMetadata.Ratings.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="Rating"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Rating() - { } /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the value. @@ -61,13 +42,13 @@ namespace Jellyfin.Data.Entities.Libraries /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> /// Gets or sets the rating type. /// If this is <c>null</c> it's the internal user rating. /// </summary> - public virtual RatingSource RatingType { get; set; } + public virtual RatingSource? RatingType { get; set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/RatingSource.cs b/Jellyfin.Data/Entities/Libraries/RatingSource.cs index 549f41804..0f3a07324 100644 --- a/Jellyfin.Data/Entities/Libraries/RatingSource.cs +++ b/Jellyfin.Data/Entities/Libraries/RatingSource.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Interfaces; @@ -15,38 +14,20 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="minimumValue">The minimum value.</param> /// <param name="maximumValue">The maximum value.</param> - /// <param name="rating">The rating.</param> - public RatingSource(double minimumValue, double maximumValue, Rating rating) + public RatingSource(double minimumValue, double maximumValue) { MinimumValue = minimumValue; MaximumValue = maximumValue; - - if (rating == null) - { - throw new ArgumentNullException(nameof(rating)); - } - - rating.RatingType = this; - } - - /// <summary> - /// Initializes a new instance of the <see cref="RatingSource"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected RatingSource() - { } /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the name. @@ -56,7 +37,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Name { get; set; } + public string? Name { get; set; } /// <summary> /// Gets or sets the minimum value. @@ -76,12 +57,12 @@ namespace Jellyfin.Data.Entities.Libraries /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> /// Gets or sets the metadata source. /// </summary> - public virtual MetadataProviderId Source { get; set; } + public virtual MetadataProviderId? Source { get; set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/Release.cs b/Jellyfin.Data/Entities/Libraries/Release.cs index b633e08fb..d3d52bf5c 100644 --- a/Jellyfin.Data/Entities/Libraries/Release.cs +++ b/Jellyfin.Data/Entities/Libraries/Release.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -17,8 +15,7 @@ namespace Jellyfin.Data.Entities.Libraries /// Initializes a new instance of the <see cref="Release"/> class. /// </summary> /// <param name="name">The name of this release.</param> - /// <param name="owner">The owner of this release.</param> - public Release(string name, IHasReleases owner) + public Release(string name) { if (string.IsNullOrEmpty(name)) { @@ -27,30 +24,18 @@ namespace Jellyfin.Data.Entities.Libraries Name = name; - owner?.Releases.Add(this); - MediaFiles = new HashSet<MediaFile>(); Chapters = new HashSet<Chapter>(); } /// <summary> - /// Initializes a new instance of the <see cref="Release"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Release() - { - } - - /// <summary> - /// Gets or sets the id. + /// Gets the id. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } /// <summary> /// Gets or sets the name. @@ -58,24 +43,23 @@ namespace Jellyfin.Data.Entities.Libraries /// <remarks> /// Required, Max length = 1024. /// </remarks> - [Required] [MaxLength(1024)] [StringLength(1024)] public string Name { get; set; } /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> - /// Gets or sets a collection containing the media files for this release. + /// Gets a collection containing the media files for this release. /// </summary> - public virtual ICollection<MediaFile> MediaFiles { get; protected set; } + public virtual ICollection<MediaFile> MediaFiles { get; private set; } /// <summary> - /// Gets or sets a collection containing the chapters for this release. + /// Gets a collection containing the chapters for this release. /// </summary> - public virtual ICollection<Chapter> Chapters { get; protected set; } + public virtual ICollection<Chapter> Chapters { get; private set; } /// <inheritdoc /> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Libraries/Season.cs b/Jellyfin.Data/Entities/Libraries/Season.cs index eb6674dbc..fc110b49d 100644 --- a/Jellyfin.Data/Entities/Libraries/Season.cs +++ b/Jellyfin.Data/Entities/Libraries/Season.cs @@ -1,6 +1,3 @@ -#pragma warning disable CA2227 - -using System; using System.Collections.Generic; namespace Jellyfin.Data.Entities.Libraries @@ -13,43 +10,26 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="Season"/> class. /// </summary> - /// <param name="series">The series.</param> - public Season(Series series) + /// <param name="library">The library.</param> + public Season(Library library) : base(library) { - if (series == null) - { - throw new ArgumentNullException(nameof(series)); - } - - series.Seasons.Add(this); - Episodes = new HashSet<Episode>(); SeasonMetadata = new HashSet<SeasonMetadata>(); } /// <summary> - /// Initializes a new instance of the <see cref="Season"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Season() - { - } - - /// <summary> /// Gets or sets the season number. /// </summary> public int? SeasonNumber { get; set; } /// <summary> - /// Gets or sets the season metadata. + /// Gets the season metadata. /// </summary> - public virtual ICollection<SeasonMetadata> SeasonMetadata { get; protected set; } + public virtual ICollection<SeasonMetadata> SeasonMetadata { get; private set; } /// <summary> - /// Gets or sets a collection containing the number of episodes. + /// Gets a collection containing the number of episodes. /// </summary> - public virtual ICollection<Episode> Episodes { get; protected set; } + public virtual ICollection<Episode> Episodes { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs index 7ce79756b..da40a075f 100644 --- a/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations; namespace Jellyfin.Data.Entities.Libraries @@ -13,24 +12,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="title">The title or name of the object.</param> /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="season">The season.</param> - public SeasonMetadata(string title, string language, Season season) : base(title, language) - { - if (season == null) - { - throw new ArgumentNullException(nameof(season)); - } - - season.SeasonMetadata.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="SeasonMetadata"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected SeasonMetadata() + public SeasonMetadata(string title, string language) : base(title, language) { } @@ -42,6 +24,6 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Outline { get; set; } + public string? Outline { get; set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/Series.cs b/Jellyfin.Data/Entities/Libraries/Series.cs index 8c8317d14..0354433e0 100644 --- a/Jellyfin.Data/Entities/Libraries/Series.cs +++ b/Jellyfin.Data/Entities/Libraries/Series.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System; using System.Collections.Generic; @@ -13,9 +11,9 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="Series"/> class. /// </summary> - public Series() + /// <param name="library">The library.</param> + public Series(Library library) : base(library) { - DateAdded = DateTime.UtcNow; Seasons = new HashSet<Season>(); SeriesMetadata = new HashSet<SeriesMetadata>(); } @@ -36,13 +34,13 @@ namespace Jellyfin.Data.Entities.Libraries public DateTime? FirstAired { get; set; } /// <summary> - /// Gets or sets a collection containing the series metadata. + /// Gets a collection containing the series metadata. /// </summary> - public virtual ICollection<SeriesMetadata> SeriesMetadata { get; protected set; } + public virtual ICollection<SeriesMetadata> SeriesMetadata { get; private set; } /// <summary> - /// Gets or sets a collection containing the seasons. + /// Gets a collection containing the seasons. /// </summary> - public virtual ICollection<Season> Seasons { get; protected set; } + public virtual ICollection<Season> Seasons { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs index 877dbfc69..42115802c 100644 --- a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs @@ -1,9 +1,5 @@ -#pragma warning disable CA2227 - -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities.Libraries @@ -18,30 +14,12 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="title">The title or name of the object.</param> /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="series">The series.</param> - public SeriesMetadata(string title, string language, Series series) : base(title, language) + public SeriesMetadata(string title, string language) : base(title, language) { - if (series == null) - { - throw new ArgumentNullException(nameof(series)); - } - - series.SeriesMetadata.Add(this); - Networks = new HashSet<Company>(); } /// <summary> - /// Initializes a new instance of the <see cref="SeriesMetadata"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected SeriesMetadata() - { - } - - /// <summary> /// Gets or sets the outline. /// </summary> /// <remarks> @@ -49,7 +27,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Outline { get; set; } + public string? Outline { get; set; } /// <summary> /// Gets or sets the plot. @@ -59,7 +37,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(65535)] [StringLength(65535)] - public string Plot { get; set; } + public string? Plot { get; set; } /// <summary> /// Gets or sets the tagline. @@ -69,7 +47,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(1024)] [StringLength(1024)] - public string Tagline { get; set; } + public string? Tagline { get; set; } /// <summary> /// Gets or sets the country code. @@ -79,15 +57,14 @@ namespace Jellyfin.Data.Entities.Libraries /// </remarks> [MaxLength(2)] [StringLength(2)] - public string Country { get; set; } + public string? Country { get; set; } /// <summary> - /// Gets or sets a collection containing the networks. + /// Gets a collection containing the networks. /// </summary> - public virtual ICollection<Company> Networks { get; protected set; } + public virtual ICollection<Company> Networks { get; private set; } /// <inheritdoc /> - [NotMapped] public ICollection<Company> Companies => Networks; } } diff --git a/Jellyfin.Data/Entities/Libraries/Track.cs b/Jellyfin.Data/Entities/Libraries/Track.cs index 782bfb5ce..d35400033 100644 --- a/Jellyfin.Data/Entities/Libraries/Track.cs +++ b/Jellyfin.Data/Entities/Libraries/Track.cs @@ -1,6 +1,3 @@ -#pragma warning disable CA2227 - -using System; using System.Collections.Generic; using Jellyfin.Data.Interfaces; @@ -14,41 +11,24 @@ namespace Jellyfin.Data.Entities.Libraries /// <summary> /// Initializes a new instance of the <see cref="Track"/> class. /// </summary> - /// <param name="album">The album.</param> - public Track(MusicAlbum album) + /// <param name="library">The library.</param> + public Track(Library library) : base(library) { - if (album == null) - { - throw new ArgumentNullException(nameof(album)); - } - - album.Tracks.Add(this); - Releases = new HashSet<Release>(); TrackMetadata = new HashSet<TrackMetadata>(); } /// <summary> - /// Initializes a new instance of the <see cref="Track"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected Track() - { - } - - /// <summary> /// Gets or sets the track number. /// </summary> public int? TrackNumber { get; set; } /// <inheritdoc /> - public virtual ICollection<Release> Releases { get; protected set; } + public virtual ICollection<Release> Releases { get; private set; } /// <summary> - /// Gets or sets a collection containing the track metadata. + /// Gets a collection containing the track metadata. /// </summary> - public virtual ICollection<TrackMetadata> TrackMetadata { get; protected set; } + public virtual ICollection<TrackMetadata> TrackMetadata { get; private set; } } } diff --git a/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs b/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs index 321f93bf2..042d2b90d 100644 --- a/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs +++ b/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs @@ -1,5 +1,3 @@ -using System; - namespace Jellyfin.Data.Entities.Libraries { /// <summary> @@ -12,24 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries /// </summary> /// <param name="title">The title or name of the object.</param> /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="track">The track.</param> - public TrackMetadata(string title, string language, Track track) : base(title, language) - { - if (track == null) - { - throw new ArgumentNullException(nameof(track)); - } - - track.TrackMetadata.Add(this); - } - - /// <summary> - /// Initializes a new instance of the <see cref="TrackMetadata"/> class. - /// </summary> - /// <remarks> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </remarks> - protected TrackMetadata() + public TrackMetadata(string title, string language) : base(title, language) { } } diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Data/Entities/Permission.cs index d92e5d9d2..6d2e68077 100644 --- a/Jellyfin.Data/Entities/Permission.cs +++ b/Jellyfin.Data/Entities/Permission.cs @@ -1,3 +1,6 @@ +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix + +using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Enums; @@ -23,29 +26,26 @@ namespace Jellyfin.Data.Entities } /// <summary> - /// Initializes a new instance of the <see cref="Permission"/> class. - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Permission() - { - } - - /// <summary> - /// Gets or sets the id of this permission. + /// Gets the id of this permission. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } + + /// <summary> + /// Gets or sets the id of the associated user. + /// </summary> + public Guid? UserId { get; set; } /// <summary> - /// Gets or sets the type of this permission. + /// Gets the type of this permission. /// </summary> /// <remarks> /// Required. /// </remarks> - public PermissionKind Kind { get; protected set; } + public PermissionKind Kind { get; private set; } /// <summary> /// Gets or sets a value indicating whether the associated user has this permission. @@ -57,7 +57,7 @@ namespace Jellyfin.Data.Entities /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <inheritdoc/> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs index 4efddf2a4..a6ab275d3 100644 --- a/Jellyfin.Data/Entities/Preference.cs +++ b/Jellyfin.Data/Entities/Preference.cs @@ -24,29 +24,26 @@ namespace Jellyfin.Data.Entities } /// <summary> - /// Initializes a new instance of the <see cref="Preference"/> class. - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Preference() - { - } - - /// <summary> - /// Gets or sets the id of this preference. + /// Gets the id of this preference. /// </summary> /// <remarks> /// Identity, Indexed, Required. /// </remarks> [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; protected set; } + public int Id { get; private set; } + + /// <summary> + /// Gets or sets the id of the associated user. + /// </summary> + public Guid? UserId { get; set; } /// <summary> - /// Gets or sets the type of this preference. + /// Gets the type of this preference. /// </summary> /// <remarks> /// Required. /// </remarks> - public PreferenceKind Kind { get; protected set; } + public PreferenceKind Kind { get; private set; } /// <summary> /// Gets or sets the value of this preference. @@ -54,14 +51,13 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required, Max length = 65535. /// </remarks> - [Required] [MaxLength(65535)] [StringLength(65535)] public string Value { get; set; } /// <inheritdoc/> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <inheritdoc/> public void OnSavingChanges() diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 362f3b4eb..e309e54de 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA2227 - using System; using System.Collections.Generic; using System.ComponentModel; @@ -51,6 +49,7 @@ namespace Jellyfin.Data.Entities PasswordResetProviderId = passwordResetProviderId; AccessSchedules = new HashSet<AccessSchedule>(); + DisplayPreferences = new HashSet<DisplayPreferences>(); ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>(); // Groups = new HashSet<Group>(); Permissions = new HashSet<Permission>(); @@ -72,17 +71,6 @@ namespace Jellyfin.Data.Entities PlayDefaultAudioTrack = true; SubtitleMode = SubtitlePlaybackMode.Default; SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups; - - AddDefaultPermissions(); - AddDefaultPreferences(); - } - - /// <summary> - /// Initializes a new instance of the <see cref="User"/> class. - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected User() - { } /// <summary> @@ -100,7 +88,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required, Max length = 255. /// </remarks> - [Required] [MaxLength(255)] [StringLength(255)] public string Username { get; set; } @@ -113,7 +100,7 @@ namespace Jellyfin.Data.Entities /// </remarks> [MaxLength(65535)] [StringLength(65535)] - public string Password { get; set; } + public string? Password { get; set; } /// <summary> /// Gets or sets the user's easy password, or <c>null</c> if none is set. @@ -123,7 +110,7 @@ namespace Jellyfin.Data.Entities /// </remarks> [MaxLength(65535)] [StringLength(65535)] - public string EasyPassword { get; set; } + public string? EasyPassword { get; set; } /// <summary> /// Gets or sets a value indicating whether the user must update their password. @@ -141,7 +128,7 @@ namespace Jellyfin.Data.Entities /// </remarks> [MaxLength(255)] [StringLength(255)] - public string AudioLanguagePreference { get; set; } + public string? AudioLanguagePreference { get; set; } /// <summary> /// Gets or sets the authentication provider id. @@ -149,7 +136,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required, Max length = 255. /// </remarks> - [Required] [MaxLength(255)] [StringLength(255)] public string AuthenticationProviderId { get; set; } @@ -160,7 +146,6 @@ namespace Jellyfin.Data.Entities /// <remarks> /// Required, Max length = 255. /// </remarks> - [Required] [MaxLength(255)] [StringLength(255)] public string PasswordResetProviderId { get; set; } @@ -217,7 +202,7 @@ namespace Jellyfin.Data.Entities /// </remarks> [MaxLength(255)] [StringLength(255)] - public string SubtitleLanguagePreference { get; set; } + public string? SubtitleLanguagePreference { get; set; } /// <summary> /// Gets or sets a value indicating whether missing episodes should be displayed. @@ -312,68 +297,57 @@ namespace Jellyfin.Data.Entities /// Gets or sets the user's profile image. Can be <c>null</c>. /// </summary> // [ForeignKey("UserId")] - public virtual ImageInfo ProfileImage { get; set; } + public virtual ImageInfo? ProfileImage { get; set; } /// <summary> - /// Gets or sets the user's display preferences. + /// Gets the user's display preferences. /// </summary> - /// <remarks> - /// Required. - /// </remarks> - [Required] - public virtual DisplayPreferences DisplayPreferences { get; set; } + public virtual ICollection<DisplayPreferences> DisplayPreferences { get; private set; } /// <summary> /// Gets or sets the level of sync play permissions this user has. /// </summary> public SyncPlayUserAccessType SyncPlayAccess { get; set; } - /// <summary> - /// Gets or sets the row version. - /// </summary> - /// <remarks> - /// Required, Concurrency Token. - /// </remarks> + /// <inheritdoc /> [ConcurrencyCheck] - public uint RowVersion { get; set; } + public uint RowVersion { get; private set; } /// <summary> - /// Gets or sets the list of access schedules this user has. + /// Gets the list of access schedules this user has. /// </summary> - public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; } + public virtual ICollection<AccessSchedule> AccessSchedules { get; private set; } /// <summary> - /// Gets or sets the list of item display preferences. + /// Gets the list of item display preferences. /// </summary> - public virtual ICollection<ItemDisplayPreferences> ItemDisplayPreferences { get; protected set; } + public virtual ICollection<ItemDisplayPreferences> ItemDisplayPreferences { get; private set; } /* /// <summary> - /// Gets or sets the list of groups this user is a member of. + /// Gets the list of groups this user is a member of. /// </summary> - [ForeignKey("Group_Groups_Guid")] - public virtual ICollection<Group> Groups { get; protected set; } + public virtual ICollection<Group> Groups { get; private set; } */ /// <summary> - /// Gets or sets the list of permissions this user has. + /// Gets the list of permissions this user has. /// </summary> [ForeignKey("Permission_Permissions_Guid")] - public virtual ICollection<Permission> Permissions { get; protected set; } + public virtual ICollection<Permission> Permissions { get; private set; } /* /// <summary> - /// Gets or sets the list of provider mappings this user has. + /// Gets the list of provider mappings this user has. /// </summary> - [ForeignKey("ProviderMapping_ProviderMappings_Id")] - public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; } + public virtual ICollection<ProviderMapping> ProviderMappings { get; private set; } */ /// <summary> - /// Gets or sets the list of preferences this user has. + /// Gets the list of preferences this user has. /// </summary> [ForeignKey("Preference_Preferences_Guid")] - public virtual ICollection<Preference> Preferences { get; protected set; } + public virtual ICollection<Preference> Preferences { get; private set; } /// <inheritdoc/> public void OnSavingChanges() @@ -494,18 +468,11 @@ namespace Jellyfin.Data.Entities return Array.IndexOf(GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders), id) != -1; } - private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) - { - var localTime = date.ToLocalTime(); - var hour = localTime.TimeOfDay.TotalHours; - - return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) - && hour >= schedule.StartHour - && hour <= schedule.EndHour; - } - + /// <summary> + /// Initializes the default permissions for a user. Should only be called on user creation. + /// </summary> // TODO: make these user configurable? - private void AddDefaultPermissions() + public void AddDefaultPermissions() { Permissions.Add(new Permission(PermissionKind.IsAdministrator, false)); Permissions.Add(new Permission(PermissionKind.IsDisabled, false)); @@ -530,12 +497,25 @@ namespace Jellyfin.Data.Entities Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false)); } - private void AddDefaultPreferences() + /// <summary> + /// Initializes the default preferences. Should only be called on user creation. + /// </summary> + public void AddDefaultPreferences() { foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast<PreferenceKind>()) { Preferences.Add(new Preference(val, string.Empty)); } } + + private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) + { + var localTime = date.ToLocalTime(); + var hour = localTime.TimeOfDay.TotalHours; + + return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) + && hour >= schedule.StartHour + && hour <= schedule.EndHour; + } } } diff --git a/Jellyfin.Data/Enums/HomeSectionType.cs b/Jellyfin.Data/Enums/HomeSectionType.cs index e597c9431..9bcd097dc 100644 --- a/Jellyfin.Data/Enums/HomeSectionType.cs +++ b/Jellyfin.Data/Enums/HomeSectionType.cs @@ -48,6 +48,11 @@ /// <summary> /// Live TV. /// </summary> - LiveTv = 8 + LiveTv = 8, + + /// <summary> + /// Continue Reading. + /// </summary> + ResumeBook = 9 } } diff --git a/Jellyfin.Data/Interfaces/IHasPermissions.cs b/Jellyfin.Data/Interfaces/IHasPermissions.cs index 3be72259a..85ee12ad7 100644 --- a/Jellyfin.Data/Interfaces/IHasPermissions.cs +++ b/Jellyfin.Data/Interfaces/IHasPermissions.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -namespace Jellyfin.Data +namespace Jellyfin.Data.Interfaces { /// <summary> /// An abstraction representing an entity that has permissions. diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index a8ac45645..3b14d3312 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -5,6 +5,9 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + <Nullable>enable</Nullable> <PublishRepositoryUrl>true</PublishRepositoryUrl> <EmbedUntrackedSources>true</EmbedUntrackedSources> <IncludeSymbols>true</IncludeSymbols> @@ -24,25 +27,19 @@ <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> </ItemGroup> <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.3" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" /> </ItemGroup> <ItemGroup> diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 466a12e67..ee43c2159 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -11,6 +11,8 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> @@ -18,8 +20,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="BlurHashSharp" Version="1.1.1" /> - <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.1" /> + <PackageReference Include="BlurHashSharp" Version="1.2.0" /> + <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" /> <PackageReference Include="SkiaSharp" Version="2.80.2" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2" /> </ItemGroup> @@ -30,16 +32,16 @@ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> </ItemGroup> + <ItemGroup> + <!-- Needed for https://github.com/dotnet/roslyn-analyzers/issues/4382 which is in the SDK yet --> + <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" /> + </ItemGroup> + <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index eab5777d5..8f0fae3be 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -91,9 +91,6 @@ namespace Jellyfin.Drawing.Skia } } - private static bool IsTransparent(SKColor color) - => (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0; - /// <summary> /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>. /// </summary> @@ -111,65 +108,6 @@ namespace Jellyfin.Drawing.Skia }; } - private static bool IsTransparentRow(SKBitmap bmp, int row) - { - for (var i = 0; i < bmp.Width; ++i) - { - if (!IsTransparent(bmp.GetPixel(i, row))) - { - return false; - } - } - - return true; - } - - private static bool IsTransparentColumn(SKBitmap bmp, int col) - { - for (var i = 0; i < bmp.Height; ++i) - { - if (!IsTransparent(bmp.GetPixel(col, i))) - { - return false; - } - } - - return true; - } - - private SKBitmap CropWhiteSpace(SKBitmap bitmap) - { - var topmost = 0; - while (topmost < bitmap.Height && IsTransparentRow(bitmap, topmost)) - { - topmost++; - } - - int bottommost = bitmap.Height; - while (bottommost >= 0 && IsTransparentRow(bitmap, bottommost - 1)) - { - bottommost--; - } - - var leftmost = 0; - while (leftmost < bitmap.Width && IsTransparentColumn(bitmap, leftmost)) - { - leftmost++; - } - - var rightmost = bitmap.Width; - while (rightmost >= 0 && IsTransparentColumn(bitmap, rightmost - 1)) - { - rightmost--; - } - - var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost); - - using var image = SKImage.FromBitmap(bitmap); - using var subset = image.Subset(newRect); - return SKBitmap.FromImage(subset); - } - /// <inheritdoc /> /// <exception cref="ArgumentNullException">The path is null.</exception> /// <exception cref="FileNotFoundException">The path is not valid.</exception> @@ -274,8 +212,8 @@ namespace Jellyfin.Drawing.Skia if (requiresTransparencyHack || forceCleanBitmap) { - using var codec = SKCodec.Create(NormalizePath(path)); - if (codec == null) + using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res); + if (res != SKCodecResult.Success) { origin = GetSKEncodedOrigin(orientation); return null; @@ -312,22 +250,11 @@ namespace Jellyfin.Drawing.Skia return resultBitmap; } - private SKBitmap? GetBitmap(string path, bool cropWhitespace, bool forceAnalyzeBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) - { - if (cropWhitespace) - { - using var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin); - return bitmap == null ? null : CropWhiteSpace(bitmap); - } - - return Decode(path, forceAnalyzeBitmap, orientation, out origin); - } - - private SKBitmap? GetBitmap(string path, bool cropWhitespace, bool autoOrient, ImageOrientation? orientation) + private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation) { if (autoOrient) { - var bitmap = GetBitmap(path, cropWhitespace, true, orientation, out var origin); + var bitmap = Decode(path, true, orientation, out var origin); if (bitmap != null && origin != SKEncodedOrigin.TopLeft) { @@ -340,16 +267,11 @@ namespace Jellyfin.Drawing.Skia return bitmap; } - return GetBitmap(path, cropWhitespace, false, orientation, out _); + return Decode(path, false, orientation, out _); } private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) { - if (origin == SKEncodedOrigin.Default) - { - return bitmap; - } - var needsFlip = origin == SKEncodedOrigin.LeftBottom || origin == SKEncodedOrigin.LeftTop || origin == SKEncodedOrigin.RightBottom @@ -447,7 +369,7 @@ namespace Jellyfin.Drawing.Skia } /// <inheritdoc/> - public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) + public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat) { if (inputPath.Length == 0) { @@ -459,14 +381,14 @@ namespace Jellyfin.Drawing.Skia throw new ArgumentException("String can't be empty.", nameof(outputPath)); } - var skiaOutputFormat = GetImageFormat(selectedOutputFormat); + var skiaOutputFormat = GetImageFormat(outputFormat); var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor); var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer); var blur = options.Blur ?? 0; var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0); - using var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation); + using var bitmap = GetBitmap(inputPath, autoOrient, orientation); if (bitmap == null) { throw new InvalidDataException($"Skia unable to read image {inputPath}"); @@ -474,9 +396,7 @@ namespace Jellyfin.Drawing.Skia var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height); - if (!options.CropWhiteSpace - && options.HasDefaultOptions(inputPath, originalImageSize) - && !autoOrient) + if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient) { // Just spit out the original file if all the options are default return inputPath; diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 91bf0015f..faf814c06 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -1,7 +1,6 @@ #pragma warning disable CA1819 // Properties should not return arrays using System; -using MediaBrowser.Model.Configuration; namespace Jellyfin.Networking.Configuration { diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs index e77b17ba9..8cbe398b0 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs @@ -1,4 +1,3 @@ -using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; namespace Jellyfin.Networking.Configuration diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index cbda74361..63557e91f 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -5,6 +5,8 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> @@ -13,16 +15,11 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 51fcb6d9a..4078fd126 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -165,7 +165,7 @@ namespace Jellyfin.Networking.Manager { foreach (var item in source) { - result.AddItem(item); + result.AddItem(item, false); } } @@ -274,7 +274,7 @@ namespace Jellyfin.Networking.Manager if (_bindExclusions.Count > 0) { // Return all the interfaces except the ones specifically excluded. - return _interfaceAddresses.Exclude(_bindExclusions); + return _interfaceAddresses.Exclude(_bindExclusions, false); } if (individualInterfaces) @@ -285,21 +285,32 @@ namespace Jellyfin.Networking.Manager // No bind address and no exclusions, so listen on all interfaces. Collection<IPObject> result = new Collection<IPObject>(); - if (IsIP4Enabled) + if (IsIP6Enabled && IsIP4Enabled) + { + // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any + result.AddItem(IPAddress.IPv6Any); + } + else if (IsIP4Enabled) { result.AddItem(IPAddress.Any); } - - if (IsIP6Enabled) + else if (IsIP6Enabled) { - result.AddItem(IPAddress.IPv6Any); + // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses. + foreach (var iface in _interfaceAddresses) + { + if (iface.AddressFamily == AddressFamily.InterNetworkV6) + { + result.AddItem(iface.Address); + } + } } return result; } // Remove any excluded bind interfaces. - return _bindAddresses.Exclude(_bindExclusions); + return _bindAddresses.Exclude(_bindExclusions, false); } /// <inheritdoc/> @@ -386,15 +397,26 @@ namespace Jellyfin.Networking.Manager } // Get the first LAN interface address that isn't a loopback. - var interfaces = CreateCollection(_interfaceAddresses - .Exclude(_bindExclusions) - .Where(IsInLocalNetwork) - .OrderBy(p => p.Tag)); + var interfaces = CreateCollection( + _interfaceAddresses + .Exclude(_bindExclusions, false) + .Where(IsInLocalNetwork) + .OrderBy(p => p.Tag)); if (interfaces.Count > 0) { if (haveSource) { + foreach (var intf in interfaces) + { + if (intf.Address.Equals(source.Address)) + { + result = FormatIP6String(intf.Address); + _logger.LogDebug("{Source}: GetBindInterface: Has found matching interface. {Result}", source, result); + return result; + } + } + // Does the request originate in one of the interface subnets? // (For systems with multiple internal network cards, and multiple subnets) foreach (var intf in interfaces) @@ -414,7 +436,7 @@ namespace Jellyfin.Networking.Manager } // There isn't any others, so we'll use the loopback. - result = IsIP6Enabled ? "::" : "127.0.0.1"; + result = IsIP6Enabled ? "::1" : "127.0.0.1"; _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result); return result; } @@ -521,10 +543,10 @@ namespace Jellyfin.Networking.Manager { if (filter == null) { - return _lanSubnets.Exclude(_excludedSubnets).AsNetworks(); + return _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks(); } - return _lanSubnets.Exclude(filter); + return _lanSubnets.Exclude(filter, true); } /// <inheritdoc/> @@ -555,7 +577,7 @@ namespace Jellyfin.Networking.Manager && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { - result.AddItem(iface); + result.AddItem(iface, false); } } @@ -565,6 +587,29 @@ namespace Jellyfin.Networking.Manager return false; } + /// <inheritdoc/> + public bool HasRemoteAccess(IPAddress remoteIp) + { + var config = _configurationManager.GetNetworkConfiguration(); + if (config.EnableRemoteAccess) + { + // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. + // If left blank, all remote addresses will be allowed. + if (RemoteAddressFilter.Count > 0 && !IsInLocalNetwork(remoteIp)) + { + // remoteAddressFilter is a whitelist or blacklist. + return RemoteAddressFilter.ContainsAddress(remoteIp) == !config.IsRemoteIPFilterBlacklist; + } + } + else if (!IsInLocalNetwork(remoteIp)) + { + // Remote not enabled. So everyone should be LAN. + return false; + } + + return true; + } + /// <summary> /// Reloads all settings and re-initialises the instance. /// </summary> @@ -599,8 +644,18 @@ namespace Jellyfin.Networking.Manager var address = IPNetAddress.Parse(parts[0]); var index = int.Parse(parts[1], CultureInfo.InvariantCulture); address.Tag = index; - _interfaceAddresses.AddItem(address); - _interfaceNames.Add(parts[2], Math.Abs(index)); + _interfaceAddresses.AddItem(address, false); + _interfaceNames[parts[2]] = Math.Abs(index); + } + + if (IsIP4Enabled) + { + _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback); + } + + if (IsIP6Enabled) + { + _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback); } } @@ -933,8 +988,8 @@ namespace Jellyfin.Networking.Manager } // Read and parse bind addresses and exclusions, removing ones that don't exist. - _bindAddresses = CreateIPCollection(lanAddresses).Union(_interfaceAddresses); - _bindExclusions = CreateIPCollection(lanAddresses, true).Union(_interfaceAddresses); + _bindAddresses = CreateIPCollection(lanAddresses).ThatAreContainedInNetworks(_interfaceAddresses); + _bindExclusions = CreateIPCollection(lanAddresses, true).ThatAreContainedInNetworks(_interfaceAddresses); _logger.LogInformation("Using bind addresses: {0}", _bindAddresses.AsString()); _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString()); } @@ -1012,12 +1067,12 @@ namespace Jellyfin.Networking.Manager } // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. - _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i))); + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(IsInLocalNetwork)); } _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString()); _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString()); - _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks().AsString()); + _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks().AsString()); } } @@ -1071,7 +1126,7 @@ namespace Jellyfin.Networking.Manager nw.Tag *= -1; } - _interfaceAddresses.AddItem(nw); + _interfaceAddresses.AddItem(nw, false); // Store interface name so we can use the name in Collections. _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; @@ -1092,7 +1147,7 @@ namespace Jellyfin.Networking.Manager nw.Tag *= -1; } - _interfaceAddresses.AddItem(nw); + _interfaceAddresses.AddItem(nw, false); // Store interface name so we can use the name in Collections. _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; @@ -1108,36 +1163,40 @@ namespace Jellyfin.Networking.Manager } #pragma warning restore CA1031 // Do not catch general exception types } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in InitialiseInterfaces."); + } - _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); - _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString()); + // If for some reason we don't have an interface info, resolve our DNS name. + if (_interfaceAddresses.Count == 0) + { + _logger.LogError("No interfaces information available. Resolving DNS name."); + IPHost host = new IPHost(Dns.GetHostName()); + foreach (var a in host.GetAddresses()) + { + _interfaceAddresses.AddItem(a); + } - // If for some reason we don't have an interface info, resolve our DNS name. if (_interfaceAddresses.Count == 0) { - _logger.LogError("No interfaces information available. Resolving DNS name."); - IPHost host = new IPHost(Dns.GetHostName()); - foreach (var a in host.GetAddresses()) - { - _interfaceAddresses.AddItem(a); - } - - if (_interfaceAddresses.Count == 0) - { - _logger.LogWarning("No interfaces information available. Using loopback."); - // Last ditch attempt - use loopback address. - _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback); - if (IsIP6Enabled) - { - _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback); - } - } + _logger.LogWarning("No interfaces information available. Using loopback."); } } - catch (NetworkInformationException ex) + + if (IsIP4Enabled) { - _logger.LogError(ex, "Error in InitialiseInterfaces."); + _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback); } + + if (IsIP6Enabled) + { + _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback); + } + + _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); + _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString()); } } @@ -1206,7 +1265,7 @@ namespace Jellyfin.Networking.Manager private bool MatchesBindInterface(IPObject source, bool isInExternalSubnet, out string result) { result = string.Empty; - var addresses = _bindAddresses.Exclude(_bindExclusions); + var addresses = _bindAddresses.Exclude(_bindExclusions, false); int count = addresses.Count; if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any))) @@ -1291,7 +1350,7 @@ namespace Jellyfin.Networking.Manager result = string.Empty; // Get the first WAN interface address that isn't a loopback. var extResult = _interfaceAddresses - .Exclude(_bindExclusions) + .Exclude(_bindExclusions, false) .Where(p => !IsInLocalNetwork(p)) .OrderBy(p => p.Tag); diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs index 2f9f44ed6..8b0bd84c6 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs @@ -29,20 +29,20 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security } /// <inheritdoc /> - public async Task OnEvent(GenericEventArgs<AuthenticationResult> e) + public async Task OnEvent(GenericEventArgs<AuthenticationResult> eventArgs) { await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"), - e.Argument.User.Name), + eventArgs.Argument.User.Name), "AuthenticationSucceeded", - e.Argument.User.Id) + eventArgs.Argument.User.Id) { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("LabelIpAddressValue"), - e.Argument.SessionInfo.RemoteEndPoint), + eventArgs.Argument.SessionInfo.RemoteEndPoint), }).ConfigureAwait(false); } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs index 0340248bb..aa6015caa 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs @@ -86,7 +86,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session return name; } - private static string? GetPlaybackNotificationType(string mediaType) + private static string GetPlaybackNotificationType(string mediaType) { if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { @@ -98,7 +98,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session return NotificationType.VideoPlayback.ToString(); } - return null; + return "Playback"; } } } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs index 05201a346..cbc9f3017 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs @@ -33,10 +33,10 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System } /// <inheritdoc /> - public async Task OnEvent(TaskCompletionEventArgs e) + public async Task OnEvent(TaskCompletionEventArgs eventArgs) { - var result = e.Result; - var task = e.Task; + var result = eventArgs.Result; + var task = eventArgs.Task; if (task.ScheduledTask is IConfigurableScheduledTask activityTask && !activityTask.IsLogged) @@ -54,14 +54,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System { var vals = new List<string>(); - if (!string.IsNullOrEmpty(e.Result.ErrorMessage)) + if (!string.IsNullOrEmpty(eventArgs.Result.ErrorMessage)) { - vals.Add(e.Result.ErrorMessage); + vals.Add(eventArgs.Result.ErrorMessage); } - if (!string.IsNullOrEmpty(e.Result.LongErrorMessage)) + if (!string.IsNullOrEmpty(eventArgs.Result.LongErrorMessage)) { - vals.Add(e.Result.LongErrorMessage); + vals.Add(eventArgs.Result.LongErrorMessage); } await _activityManager.CreateAsync(new ActivityLog( diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs index 91a30069e..eb7572ac6 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs @@ -30,13 +30,13 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates } /// <inheritdoc /> - public async Task OnEvent(PluginUninstalledEventArgs e) + public async Task OnEvent(PluginUninstalledEventArgs eventArgs) { await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("PluginUninstalledWithName"), - e.Argument.Name), + eventArgs.Argument.Name), NotificationType.PluginUninstalled.ToString(), Guid.Empty)) .ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs index a14911b94..9beb6f2f2 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs @@ -30,12 +30,12 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users } /// <inheritdoc /> - public async Task OnEvent(UserUpdatedEventArgs e) + public async Task OnEvent(UserUpdatedEventArgs eventArgs) { await _sessionManager.SendMessageToUserSessions( - new List<Guid> { e.Argument.Id }, + new List<Guid> { eventArgs.Argument.Id }, SessionMessageType.UserUpdated, - _userManager.GetUserDto(e.Argument), + _userManager.GetUserDto(eventArgs.Argument), CancellationToken.None).ConfigureAwait(false); } } diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 4f24da0ee..2c6a176b6 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -6,6 +6,8 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> @@ -14,7 +16,6 @@ <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> @@ -26,11 +27,13 @@ <ItemGroup> <PackageReference Include="System.Linq.Async" Version="5.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.5" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.5"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.3"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.5"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 39f842354..db648472d 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -149,21 +149,77 @@ namespace Jellyfin.Server.Implementations modelBuilder.HasDefaultSchema("jellyfin"); + // Collations + + modelBuilder.Entity<User>() + .Property(user => user.Username) + .UseCollation("NOCASE"); + + // Delete behavior + + modelBuilder.Entity<User>() + .HasOne(u => u.ProfileImage) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity<User>() + .HasMany(u => u.Permissions) + .WithOne() + .HasForeignKey(p => p.UserId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity<User>() + .HasMany(u => u.Preferences) + .WithOne() + .HasForeignKey(p => p.UserId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity<User>() + .HasMany(u => u.AccessSchedules) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity<User>() + .HasMany(u => u.DisplayPreferences) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity<User>() + .HasMany(u => u.ItemDisplayPreferences) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity<DisplayPreferences>() - .HasIndex(entity => entity.UserId) - .IsUnique(false); + .HasMany(d => d.HomeSections) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + // Indexes + + modelBuilder.Entity<User>() + .HasIndex(entity => entity.Username) + .IsUnique(); modelBuilder.Entity<DisplayPreferences>() .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client }) .IsUnique(); modelBuilder.Entity<CustomItemDisplayPreferences>() - .HasIndex(entity => entity.UserId) - .IsUnique(false); - - modelBuilder.Entity<CustomItemDisplayPreferences>() .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client, entity.Key }) .IsUnique(); + + // Used to get a user's permissions or a specific permission for a user. + // Also prevents multiple values being created for a user. + // Filtered over non-null user ids for when other entities (groups, API keys) get permissions + modelBuilder.Entity<Permission>() + .HasIndex(p => new { p.UserId, p.Kind }) + .HasFilter("[UserId] IS NOT NULL") + .IsUnique(); + + modelBuilder.Entity<Preference>() + .HasIndex(p => new { p.UserId, p.Kind }) + .HasFilter("[UserId] IS NOT NULL") + .IsUnique(); } } } diff --git a/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs new file mode 100644 index 000000000..869676824 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs @@ -0,0 +1,535 @@ +#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; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20210320181425_AddIndexesAndCollations")] + partial class AddIndexesAndCollations + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "5.0.3"); + + 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"); + }); + + 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.ToTable("ActivityLogs"); + }); + + 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") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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.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/20210320181425_AddIndexesAndCollations.cs b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.cs new file mode 100644 index 000000000..506e4ae66 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.cs @@ -0,0 +1,240 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddIndexesAndCollations : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ImageInfos_Users_UserId", + schema: "jellyfin", + table: "ImageInfos"); + + migrationBuilder.DropForeignKey( + name: "FK_Permissions_Users_Permission_Permissions_Guid", + schema: "jellyfin", + table: "Permissions"); + + migrationBuilder.DropForeignKey( + name: "FK_Preferences_Users_Preference_Preferences_Guid", + schema: "jellyfin", + table: "Preferences"); + + migrationBuilder.DropIndex( + name: "IX_Preferences_Preference_Preferences_Guid", + schema: "jellyfin", + table: "Preferences"); + + migrationBuilder.DropIndex( + name: "IX_Permissions_Permission_Permissions_Guid", + schema: "jellyfin", + table: "Permissions"); + + migrationBuilder.DropIndex( + name: "IX_DisplayPreferences_UserId", + schema: "jellyfin", + table: "DisplayPreferences"); + + migrationBuilder.DropIndex( + name: "IX_CustomItemDisplayPreferences_UserId", + schema: "jellyfin", + table: "CustomItemDisplayPreferences"); + + migrationBuilder.AlterColumn<string>( + name: "Username", + schema: "jellyfin", + table: "Users", + type: "TEXT", + maxLength: 255, + nullable: false, + collation: "NOCASE", + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 255); + + migrationBuilder.AddColumn<Guid>( + name: "UserId", + schema: "jellyfin", + table: "Preferences", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn<Guid>( + name: "UserId", + schema: "jellyfin", + table: "Permissions", + type: "TEXT", + nullable: true); + + migrationBuilder.Sql("UPDATE Preferences SET UserId = Preference_Preferences_Guid"); + migrationBuilder.Sql("UPDATE Permissions SET UserId = Permission_Permissions_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + schema: "jellyfin", + table: "Users", + column: "Username", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Preferences_UserId_Kind", + schema: "jellyfin", + table: "Preferences", + columns: new[] { "UserId", "Kind" }, + unique: true, + filter: "[UserId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_UserId_Kind", + schema: "jellyfin", + table: "Permissions", + columns: new[] { "UserId", "Kind" }, + unique: true, + filter: "[UserId] IS NOT NULL"); + + migrationBuilder.AddForeignKey( + name: "FK_ImageInfos_Users_UserId", + schema: "jellyfin", + table: "ImageInfos", + column: "UserId", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Permissions_Users_UserId", + schema: "jellyfin", + table: "Permissions", + column: "UserId", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Preferences_Users_UserId", + schema: "jellyfin", + table: "Preferences", + column: "UserId", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ImageInfos_Users_UserId", + schema: "jellyfin", + table: "ImageInfos"); + + migrationBuilder.DropForeignKey( + name: "FK_Permissions_Users_UserId", + schema: "jellyfin", + table: "Permissions"); + + migrationBuilder.DropForeignKey( + name: "FK_Preferences_Users_UserId", + schema: "jellyfin", + table: "Preferences"); + + migrationBuilder.DropIndex( + name: "IX_Users_Username", + schema: "jellyfin", + table: "Users"); + + migrationBuilder.DropIndex( + name: "IX_Preferences_UserId_Kind", + schema: "jellyfin", + table: "Preferences"); + + migrationBuilder.DropIndex( + name: "IX_Permissions_UserId_Kind", + schema: "jellyfin", + table: "Permissions"); + + migrationBuilder.DropColumn( + name: "UserId", + schema: "jellyfin", + table: "Preferences"); + + migrationBuilder.DropColumn( + name: "UserId", + schema: "jellyfin", + table: "Permissions"); + + migrationBuilder.AlterColumn<string>( + name: "Username", + schema: "jellyfin", + table: "Users", + type: "TEXT", + maxLength: 255, + nullable: false, + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 255, + oldCollation: "NOCASE"); + + migrationBuilder.CreateIndex( + name: "IX_Preferences_Preference_Preferences_Guid", + schema: "jellyfin", + table: "Preferences", + column: "Preference_Preferences_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_Permission_Permissions_Guid", + schema: "jellyfin", + table: "Permissions", + column: "Permission_Permissions_Guid"); + + migrationBuilder.CreateIndex( + name: "IX_DisplayPreferences_UserId", + schema: "jellyfin", + table: "DisplayPreferences", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_CustomItemDisplayPreferences_UserId", + schema: "jellyfin", + table: "CustomItemDisplayPreferences", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_ImageInfos_Users_UserId", + schema: "jellyfin", + table: "ImageInfos", + column: "UserId", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Permissions_Users_Permission_Permissions_Guid", + schema: "jellyfin", + table: "Permissions", + column: "Permission_Permissions_Guid", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Preferences_Users_Preference_Preferences_Guid", + schema: "jellyfin", + table: "Preferences", + column: "Preference_Preferences_Guid", + principalSchema: "jellyfin", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs new file mode 100644 index 000000000..d332d19f2 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs @@ -0,0 +1,520 @@ +#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; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20210407110544_NullableCustomPrefValue")] + partial class NullableCustomPrefValue + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "5.0.3"); + + 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"); + }); + + 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.ToTable("ActivityLogs"); + }); + + 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"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + 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"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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<bool>("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Permission_Permissions_Guid"); + + b.ToTable("Permissions"); + }); + + 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<string>("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Preference_Preferences_Guid"); + + b.ToTable("Preferences"); + }); + + 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"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + 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"); + }); + + 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("Permission_Permissions_Guid"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("Preference_Preferences_Guid"); + }); + + 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/20210407110544_NullableCustomPrefValue.cs b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.cs new file mode 100644 index 000000000..ade68612c --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.cs @@ -0,0 +1,35 @@ +#pragma warning disable CS1591 +// <auto-generated /> +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class NullableCustomPrefValue : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn<string>( + name: "Value", + schema: "jellyfin", + table: "CustomItemDisplayPreferences", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn<string>( + name: "Value", + schema: "jellyfin", + table: "CustomItemDisplayPreferences", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 1614a88ef..286eb7468 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("jellyfin") - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "5.0.3"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -110,13 +110,10 @@ namespace Jellyfin.Server.Implementations.Migrations .HasColumnType("TEXT"); b.Property<string>("Value") - .IsRequired() .HasColumnType("TEXT"); b.HasKey("Id"); - b.HasIndex("UserId"); - b.HasIndex("UserId", "ItemId", "Client", "Key") .IsUnique(); @@ -174,8 +171,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); - b.HasIndex("UserId"); - b.HasIndex("UserId", "ItemId", "Client") .IsUnique(); @@ -289,12 +284,17 @@ namespace Jellyfin.Server.Implementations.Migrations .IsConcurrencyToken() .HasColumnType("INTEGER"); + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + b.Property<bool>("Value") .HasColumnType("INTEGER"); b.HasKey("Id"); - b.HasIndex("Permission_Permissions_Guid"); + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); b.ToTable("Permissions"); }); @@ -315,6 +315,9 @@ namespace Jellyfin.Server.Implementations.Migrations .IsConcurrencyToken() .HasColumnType("INTEGER"); + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + b.Property<string>("Value") .IsRequired() .HasMaxLength(65535) @@ -322,7 +325,9 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasKey("Id"); - b.HasIndex("Preference_Preferences_Guid"); + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); b.ToTable("Preferences"); }); @@ -429,10 +434,14 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property<string>("Username") .IsRequired() .HasMaxLength(255) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .UseCollation("NOCASE"); b.HasKey("Id"); + b.HasIndex("Username") + .IsUnique(); + b.ToTable("Users"); }); @@ -448,8 +457,8 @@ namespace Jellyfin.Server.Implementations.Migrations modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => { b.HasOne("Jellyfin.Data.Entities.User", null) - .WithOne("DisplayPreferences") - .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId") + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); @@ -467,7 +476,8 @@ namespace Jellyfin.Server.Implementations.Migrations { b.HasOne("Jellyfin.Data.Entities.User", null) .WithOne("ProfileImage") - .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId"); + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => @@ -483,14 +493,16 @@ namespace Jellyfin.Server.Implementations.Migrations { b.HasOne("Jellyfin.Data.Entities.User", null) .WithMany("Permissions") - .HasForeignKey("Permission_Permissions_Guid"); + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => { b.HasOne("Jellyfin.Data.Entities.User", null) .WithMany("Preferences") - .HasForeignKey("Preference_Preferences_Guid"); + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => @@ -502,8 +514,7 @@ namespace Jellyfin.Server.Implementations.Migrations { b.Navigation("AccessSchedules"); - b.Navigation("DisplayPreferences") - .IsRequired(); + b.Navigation("DisplayPreferences"); b.Navigation("ItemDisplayPreferences"); diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index 9cc1c3e5e..c99c5e4ef 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -66,7 +66,7 @@ namespace Jellyfin.Server.Implementations.Users else if (string.Equals( spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal), pin.Replace("-", string.Empty, StringComparison.Ordinal), - StringComparison.InvariantCultureIgnoreCase)) + StringComparison.OrdinalIgnoreCase)) { var resetUser = userManager.GetUserByName(spr.UserName) ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index c8a589cab..c89e3c74d 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -1,4 +1,5 @@ #pragma warning disable CA1307 +#pragma warning disable CA1309 using System; using System.Collections.Generic; @@ -35,7 +36,7 @@ namespace Jellyfin.Server.Implementations.Users if (prefs == null) { - prefs = new DisplayPreferences(userId, itemId, client); + prefs = new DisplayPreferences(userId, itemId, client); _dbContext.DisplayPreferences.Add(prefs); } @@ -67,7 +68,7 @@ namespace Jellyfin.Server.Implementations.Users } /// <inheritdoc /> - public IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client) + public Dictionary<string, string?> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client) { return _dbContext.CustomItemDisplayPreferences .AsQueryable() diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 76d1389ca..27d4f40d3 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -144,7 +144,13 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("The new and old names must be different."); } - if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))) + await using var dbContext = _dbProvider.CreateContext(); + + if (await dbContext.Users + .AsQueryable() + .Where(u => u.Username == newName && u.Id != user.Id) + .AnyAsync() + .ConfigureAwait(false)) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, @@ -184,12 +190,15 @@ namespace Jellyfin.Server.Implementations.Users var user = new User( name, - _defaultAuthenticationProvider.GetType().FullName, - _defaultPasswordResetProvider.GetType().FullName) + _defaultAuthenticationProvider.GetType().FullName!, + _defaultPasswordResetProvider.GetType().FullName!) { InternalId = max + 1 }; + user.AddDefaultPermissions(); + user.AddDefaultPreferences(); + _users.Add(user.Id, user); return user; @@ -248,16 +257,6 @@ namespace Jellyfin.Server.Implementations.Users } await using var dbContext = _dbProvider.CreateContext(); - - // Clear all entities related to the user from the database. - if (user.ProfileImage != null) - { - dbContext.Remove(user.ProfileImage); - } - - dbContext.RemoveRange(user.Permissions); - dbContext.RemoveRange(user.Preferences); - dbContext.RemoveRange(user.AccessSchedules); dbContext.Users.Remove(user); await dbContext.SaveChangesAsync().ConfigureAwait(false); _users.Remove(userId); @@ -404,27 +403,18 @@ namespace Jellyfin.Server.Implementations.Users } var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - bool success; - IAuthenticationProvider? authenticationProvider; + var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) + .ConfigureAwait(false); + var authenticationProvider = authResult.authenticationProvider; + var success = authResult.success; - if (user != null) - { - var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) - .ConfigureAwait(false); - authenticationProvider = authResult.authenticationProvider; - success = authResult.success; - } - else + if (user == null) { - var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint) - .ConfigureAwait(false); - authenticationProvider = authResult.authenticationProvider; string updatedUsername = authResult.username; - success = authResult.success; if (success && authenticationProvider != null - && !(authenticationProvider is DefaultAuthenticationProvider)) + && authenticationProvider is not DefaultAuthenticationProvider) { // Trust the username returned by the authentication provider username = updatedUsername; @@ -444,7 +434,7 @@ namespace Jellyfin.Server.Implementations.Users { var providerId = authenticationProvider.GetType().FullName; - if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) + if (providerId != null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase)) { user.AuthenticationProviderId = providerId; await UpdateUserAsync(user).ConfigureAwait(false); diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index ae2fb3999..94c3ca4a9 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -20,6 +20,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -36,18 +37,21 @@ namespace Jellyfin.Server /// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="startupConfig">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param> public CoreAppHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, + IConfiguration startupConfig, IFileSystem fileSystem, IServiceCollection collection) : base( applicationPaths, loggerFactory, options, + startupConfig, fileSystem, collection) { diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 77f6695bb..924b250ce 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -225,14 +225,13 @@ namespace Jellyfin.Server.Extensions .AddJsonOptions(options => { // Update all properties that are set in JsonDefaults - var jsonOptions = JsonDefaults.GetPascalCaseOptions(); + var jsonOptions = JsonDefaults.PascalCaseOptions; // From JsonDefaults options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling; options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented; options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition; options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling; - options.JsonSerializerOptions.PropertyNameCaseInsensitive = jsonOptions.PropertyNameCaseInsensitive; options.JsonSerializerOptions.Converters.Clear(); foreach (var converter in jsonOptions.Converters) @@ -261,15 +260,16 @@ namespace Jellyfin.Server.Extensions { return serviceCollection.AddSwaggerGen(c => { + var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1"; c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", - Version = "v1", + Version = version, Extensions = new Dictionary<string, IOpenApiExtension> { { "x-jellyfin-version", - new OpenApiString(typeof(ApplicationHost).Assembly.GetName().Version?.ToString()) + new OpenApiString(version) } } }); @@ -319,7 +319,7 @@ namespace Jellyfin.Server.Extensions c.OperationFilter<FileResponseFilter>(); c.OperationFilter<FileRequestFilter>(); c.OperationFilter<ParameterObsoleteFilter>(); - c.DocumentFilter<WebsocketModelFilter>(); + c.DocumentFilter<AdditionalModelFilter>(); }); } diff --git a/Jellyfin.Server/Filters/WebsocketModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index 248802857..87a59e0b4 100644 --- a/Jellyfin.Server/Filters/WebsocketModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -1,5 +1,6 @@ using MediaBrowser.Common.Plugins; using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.ApiClient; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; @@ -9,9 +10,9 @@ using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters { /// <summary> - /// Add models used in websocket messaging. + /// Add models not directly used by the API, but used for discovery and websockets. /// </summary> - public class WebsocketModelFilter : IDocumentFilter + public class AdditionalModelFilter : IDocumentFilter { /// <inheritdoc /> public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) @@ -25,6 +26,9 @@ namespace Jellyfin.Server.Filters context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository); context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository); + + context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository); + context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository); } } } diff --git a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs index e54044d0e..b9ce221f5 100644 --- a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs +++ b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using Jellyfin.Api.Attributes; -using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs index 8043989b1..c349e3dca 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// <summary> /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class. /// </summary> - public CamelCaseJsonProfileFormatter() : base(JsonDefaults.GetCamelCaseOptions()) + public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCaseOptions) { SupportedMediaTypes.Clear(); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType)); diff --git a/Jellyfin.Server/Formatters/CssOutputFormatter.cs b/Jellyfin.Server/Formatters/CssOutputFormatter.cs index e8dd48e4e..cfc9d1ad3 100644 --- a/Jellyfin.Server/Formatters/CssOutputFormatter.cs +++ b/Jellyfin.Server/Formatters/CssOutputFormatter.cs @@ -1,5 +1,4 @@ -using System; -using System.Text; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs index d0110b125..0480f5e0e 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -13,7 +13,7 @@ namespace Jellyfin.Server.Formatters /// <summary> /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class. /// </summary> - public PascalCaseJsonProfileFormatter() : base(JsonDefaults.GetPascalCaseOptions()) + public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCaseOptions) { SupportedMediaTypes.Clear(); // Add application/json for default formatter diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index bf4f80669..3496cabe8 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -9,11 +9,14 @@ <AssemblyName>jellyfin</AssemblyName> <OutputType>Exe</OutputType> <TargetFramework>net5.0</TargetFramework> + <ServerGarbageCollection>false</ServerGarbageCollection> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> - <DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + <!-- <DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers> --> </PropertyGroup> <ItemGroup> @@ -26,25 +29,20 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> <PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.3" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.3" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.5" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.5" /> <PackageReference Include="prometheus-net" Version="4.1.1" /> <PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" /> - <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> + <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" /> diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index 525cd9ffe..0afcd61a0 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,9 +1,7 @@ using System.Net; using System.Threading.Tasks; -using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; namespace Jellyfin.Server.Middleware @@ -29,9 +27,8 @@ namespace Jellyfin.Server.Middleware /// </summary> /// <param name="httpContext">The current HTTP context.</param> /// <param name="networkManager">The network manager.</param> - /// <param name="serverConfigurationManager">The server configuration manager.</param> /// <returns>The async task.</returns> - public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) + public async Task Invoke(HttpContext httpContext, INetworkManager networkManager) { if (httpContext.IsLocal()) { @@ -42,32 +39,8 @@ namespace Jellyfin.Server.Middleware var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; - if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) + if (!networkManager.HasRemoteAccess(remoteIp)) { - // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. - // If left blank, all remote addresses will be allowed. - var remoteAddressFilter = networkManager.RemoteAddressFilter; - - if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp)) - { - // remoteAddressFilter is a whitelist or blacklist. - bool isListed = remoteAddressFilter.ContainsAddress(remoteIp); - if (!serverConfigurationManager.GetNetworkConfiguration().IsRemoteIPFilterBlacklist) - { - // Black list, so flip over. - isListed = !isListed; - } - - if (!isListed) - { - // If your name isn't on the list, you arn't coming in. - return; - } - } - } - else if (!networkManager.IsInLocalNetwork(remoteIp)) - { - // Remote not enabled. So everyone should be LAN. return; } diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 8065054a1..67bf24d2a 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -1,9 +1,6 @@ -using System; -using System.Linq; using System.Net; using System.Threading.Tasks; using Jellyfin.Networking.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; diff --git a/Jellyfin.Server/Migrations/MigrationOptions.cs b/Jellyfin.Server/Migrations/MigrationOptions.cs index 816dd9ee7..c9710f1fd 100644 --- a/Jellyfin.Server/Migrations/MigrationOptions.cs +++ b/Jellyfin.Server/Migrations/MigrationOptions.cs @@ -16,9 +16,12 @@ namespace Jellyfin.Server.Migrations Applied = new List<(Guid Id, string Name)>(); } +// .Net xml serializer can't handle interfaces +#pragma warning disable CA1002 // Do not expose generic lists /// <summary> /// Gets the list of applied migration routine names. /// </summary> public List<(Guid Id, string Name)> Applied { get; } +#pragma warning restore CA1002 } } diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 305660ae6..cf938ab8c 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -40,15 +40,15 @@ namespace Jellyfin.Server.Migrations .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m)) .OfType<IMigrationRoutine>() .ToArray(); - var migrationOptions = ((IConfigurationManager)host.ServerConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey); + var migrationOptions = ((IConfigurationManager)host.ConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey); - if (!host.ServerConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0) + if (!host.ConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0) { // If startup wizard is not finished, this is a fresh install. // Don't run any migrations, just mark all of them as applied. logger.LogInformation("Marking all known migrations as applied because this is a fresh install"); migrationOptions.Applied.AddRange(migrations.Where(m => !m.PerformOnNewInstall).Select(m => (m.Id, m.Name))); - host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); + host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); } var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); @@ -77,7 +77,7 @@ namespace Jellyfin.Server.Migrations // Mark the migration as completed logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name); migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); - host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); + host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); } } diff --git a/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs b/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs index 2521d9952..6343c422d 100644 --- a/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs +++ b/Jellyfin.Server/Migrations/Routines/AddPeopleQueryIndex.cs @@ -41,9 +41,9 @@ namespace Jellyfin.Server.Migrations.Routines var databasePath = Path.Join(_serverApplicationPaths.DataPath, DbFilename); using var connection = SQLite3.Open(databasePath, ConnectionFlags.ReadWrite, null); _logger.LogInformation("Creating index idx_TypedBaseItemsUserDataKeyType"); - connection.Execute("CREATE INDEX idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);"); + connection.Execute("CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type);"); _logger.LogInformation("Creating index idx_PeopleNameListOrder"); - connection.Execute("CREATE INDEX idx_PeopleNameListOrder ON People(Name, ListOrder);"); + connection.Execute("CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder);"); } } -}
\ No newline at end of file +} diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs index 6821630db..ee4f8b0ba 100644 --- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs +++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs @@ -75,7 +75,7 @@ namespace Jellyfin.Server.Migrations.Routines { var existingConfigJson = JToken.Parse(File.ReadAllText(oldConfigPath)); return _defaultConfigHistory - .Select(historicalConfigText => JToken.Parse(historicalConfigText)) + .Select(JToken.Parse) .Any(historicalConfigJson => JToken.DeepEquals(existingConfigJson, historicalConfigJson)); } } diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs index bf0225e98..378e88e25 100644 --- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs +++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs @@ -1,6 +1,5 @@ using System; using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Configuration; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 07829c696..e25d29122 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -126,13 +126,13 @@ namespace Jellyfin.Server.Migrations.Routines ShowSidebar = dto.ShowSidebar, ScrollDirection = dto.ScrollDirection, ChromecastVersion = chromecastVersion, - SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) - ? int.Parse(length, CultureInfo.InvariantCulture) + SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) && int.TryParse(length, out var skipForwardLength) + ? skipForwardLength : 30000, - SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) - ? int.Parse(length, CultureInfo.InvariantCulture) + SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && !string.IsNullOrEmpty(length) && int.TryParse(length, out var skipBackwardLength) + ? skipBackwardLength : 10000, - EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) + EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled) ? bool.Parse(enabled) : true, DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty, diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 33f039c39..a15a38177 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -1,7 +1,5 @@ using System; -using System.Globalization; using System.IO; -using System.Linq; using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Serialization; using Jellyfin.Data.Entities; @@ -76,7 +74,7 @@ namespace Jellyfin.Server.Migrations.Routines foreach (var entry in queryResult) { - UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.GetOptions()); + UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.Options); if (mockup == null) { continue; @@ -104,7 +102,7 @@ namespace Jellyfin.Server.Migrations.Routines _ => policy.LoginAttemptsBeforeLockout }; - var user = new User(mockup.Name, policy.AuthenticationProviderId, policy.PasswordResetProviderId) + var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!) { Id = entry[1].ReadGuidFromBlob(), InternalId = entry[0].ToInt64(), diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index f05cdfe9b..c10b2ddb3 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -12,12 +12,10 @@ using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; -using Jellyfin.Api.Controllers; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -164,6 +162,7 @@ namespace Jellyfin.Server appPaths, _loggerFactory, options, + startupConfig, new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), serviceCollection); @@ -172,7 +171,7 @@ namespace Jellyfin.Server // If hosting the web client, validate the client content path if (startupConfig.HostWebClient()) { - string? webContentPath = appHost.ServerConfigurationManager.ApplicationPaths.WebPath; + string? webContentPath = appHost.ConfigurationManager.ApplicationPaths.WebPath; if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) { throw new InvalidOperationException( @@ -198,11 +197,11 @@ namespace Jellyfin.Server } catch { - _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in system.xml and try again."); + _logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again."); throw; } - await appHost.RunStartupTasksAsync().ConfigureAwait(false); + await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false); stopWatch.Stop(); @@ -221,7 +220,7 @@ namespace Jellyfin.Server } finally { - appHost?.Dispose(); + appHost.Dispose(); } if (_restartOnShutdown) @@ -280,7 +279,7 @@ namespace Jellyfin.Server bool flagged = false; foreach (IPObject netAdd in addresses) { - _logger.LogInformation("Kestrel listening on {0}", netAdd); + _logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd); options.Listen(netAdd.Address, appHost.HttpPort); if (appHost.ListenWithHttps) { @@ -622,7 +621,7 @@ namespace Jellyfin.Server string commandLineArgsString; if (options.RestartArgs != null) { - commandLineArgsString = options.RestartArgs ?? string.Empty; + commandLineArgsString = options.RestartArgs; } else { diff --git a/Jellyfin.Server/Properties/AssemblyInfo.cs b/Jellyfin.Server/Properties/AssemblyInfo.cs index 7abf298b1..fe2d5c5f9 100644 --- a/Jellyfin.Server/Properties/AssemblyInfo.cs +++ b/Jellyfin.Server/Properties/AssemblyInfo.cs @@ -21,4 +21,4 @@ using System.Runtime.InteropServices; // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] -[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")] +[assembly: InternalsVisibleTo("Jellyfin.Server.Tests")] diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index f27d28658..60cdc2f6f 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,5 +1,9 @@ +using System; +using System.Net; +using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; +using System.Text; using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; @@ -67,6 +71,12 @@ namespace Jellyfin.Server var acceptJsonHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json, 1.0); var acceptXmlHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml, 0.9); var acceptAnyHeader = new MediaTypeWithQualityHeaderValue("*/*", 0.8); + Func<IServiceProvider, HttpMessageHandler> defaultHttpClientHandlerDelegate = (_) => new SocketsHttpHandler() + { + AutomaticDecompression = DecompressionMethods.All, + RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8 + }; + services .AddHttpClient(NamedClient.Default, c => { @@ -75,7 +85,7 @@ namespace Jellyfin.Server c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); }) - .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler()); + .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); services.AddHttpClient(NamedClient.MusicBrainz, c => { @@ -84,7 +94,7 @@ namespace Jellyfin.Server c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader); c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader); }) - .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler()); + .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); services.AddHealthChecks() .AddDbContextCheck<JellyfinDb>(); diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index b63434092..a1cecc8c6 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -1,10 +1,7 @@ -using System; using System.Collections.Generic; using CommandLine; using Emby.Server.Implementations; -using Emby.Server.Implementations.EntryPoints; using Emby.Server.Implementations.Udp; -using Emby.Server.Implementations.Updates; using MediaBrowser.Controller.Extensions; namespace Jellyfin.Server @@ -77,7 +74,7 @@ namespace Jellyfin.Server /// <inheritdoc /> [Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")] - public Uri? PublishedServerUrl { get; set; } + public string? PublishedServerUrl { get; set; } /// <summary> /// Gets the command line options as a dictionary that can be used in the .NET configuration system. @@ -94,7 +91,7 @@ namespace Jellyfin.Server if (PublishedServerUrl != null) { - config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl.ToString()); + config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl); } if (FFmpegPath != null) diff --git a/Jellyfin.sln b/Jellyfin.sln index d83013dab..8626a4b1b 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -1,4 +1,5 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30503.244 MinimumVisualStudioVersion = 10.0.40219.1 @@ -68,13 +69,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Model.Tests", "tests\Jellyfin.Model.Tests\Jellyfin.Model.Tests.csproj", "{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Tests", "tests\Jellyfin.Server.Tests\Jellyfin.Server.Tests.csproj", "{3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Model.Tests", "tests\Jellyfin.Model.Tests\Jellyfin.Model.Tests.csproj", "{FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Integration.Tests", "tests\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj", "{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -190,10 +195,6 @@ Global {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU - {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -206,6 +207,22 @@ Global {FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D}.Release|Any CPU.Build.0 = Release|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU + {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Release|Any CPU.Build.0 = Release|Any CPU + {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Release|Any CPU.Build.0 = Release|Any CPU + {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -217,10 +234,12 @@ Global {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {30922383-D513-4F4D-B890-A940B57FA353} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/MediaBrowser.Common/Configuration/ConfigurationStore.cs b/MediaBrowser.Common/Configuration/ConfigurationStore.cs index d31d45e4c..050ab1ab5 100644 --- a/MediaBrowser.Common/Configuration/ConfigurationStore.cs +++ b/MediaBrowser.Common/Configuration/ConfigurationStore.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; namespace MediaBrowser.Common.Configuration diff --git a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs index 344aecf53..2df87d879 100644 --- a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs +++ b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 57c654667..1370e6d79 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace MediaBrowser.Common.Configuration { /// <summary> diff --git a/MediaBrowser.Common/Crc32.cs b/MediaBrowser.Common/Crc32.cs new file mode 100644 index 000000000..599eb4c99 --- /dev/null +++ b/MediaBrowser.Common/Crc32.cs @@ -0,0 +1,89 @@ +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Common +{ + public static class Crc32 + { + private static readonly uint[] _crcTable = + { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, + 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, + 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, + 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, + 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, + 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, + 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, + 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, + 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, + 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, + 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, + 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, + 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, + 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, + 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, + 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, + 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, + 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, + 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, + 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, + 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, + 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, + 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, + 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + }; + + public static uint Compute(ReadOnlySpan<byte> bytes) + { + var crc = 0xffffffff; + var len = bytes.Length; + for (var i = 0; i < len; i++) + { + crc = (crc >> 8) ^ _crcTable[(bytes[i] ^ crc) & 0xff]; + } + + return ~crc; + } + } +} diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs index 3e2eae1c8..0e2065302 100644 --- a/MediaBrowser.Common/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Text; namespace MediaBrowser.Common.Cryptography @@ -30,6 +29,16 @@ namespace MediaBrowser.Common.Cryptography public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary<string, string> parameters) { + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + + if (id.Length == 0) + { + throw new ArgumentException("String can't be empty", nameof(id)); + } + Id = id; _hash = hash; _salt = salt; @@ -59,58 +68,109 @@ namespace MediaBrowser.Common.Cryptography /// <value>Return the hashed password.</value> public ReadOnlySpan<byte> Hash => _hash; - public static PasswordHash Parse(string hashString) + public static PasswordHash Parse(ReadOnlySpan<char> hashString) { - // The string should at least contain the hash function and the hash itself - string[] splitted = hashString.Split('$'); - if (splitted.Length < 3) + if (hashString.IsEmpty) + { + throw new ArgumentException("String can't be empty", nameof(hashString)); + } + + if (hashString[0] != '$') { - throw new ArgumentException("String doesn't contain enough segments", nameof(hashString)); + throw new FormatException("Hash string must start with a $"); } - // Start at 1, the first index shouldn't contain any data - int index = 1; + // Ignore first $ + hashString = hashString[1..]; - // Name of the hash function - string id = splitted[index++]; + int nextSegment = hashString.IndexOf('$'); + if (hashString.IsEmpty || nextSegment == 0) + { + throw new FormatException("Hash string must contain a valid id"); + } + else if (nextSegment == -1) + { + return new PasswordHash(hashString.ToString(), Array.Empty<byte>()); + } + + ReadOnlySpan<char> id = hashString[..nextSegment]; + hashString = hashString[(nextSegment + 1)..]; + Dictionary<string, string>? parameters = null; + + nextSegment = hashString.IndexOf('$'); // Optional parameters - Dictionary<string, string> parameters = new Dictionary<string, string>(); - if (splitted[index].IndexOf('=', StringComparison.Ordinal) != -1) + ReadOnlySpan<char> parametersSpan = nextSegment == -1 ? hashString : hashString[..nextSegment]; + if (parametersSpan.Contains('=')) { - foreach (string paramset in splitted[index++].Split(',')) + while (!parametersSpan.IsEmpty) { - if (string.IsNullOrEmpty(paramset)) + ReadOnlySpan<char> parameter; + int index = parametersSpan.IndexOf(','); + if (index == -1) + { + parameter = parametersSpan; + parametersSpan = ReadOnlySpan<char>.Empty; + } + else { - continue; + parameter = parametersSpan[..index]; + parametersSpan = parametersSpan[(index + 1)..]; } - string[] fields = paramset.Split('='); - if (fields.Length != 2) + int splitIndex = parameter.IndexOf('='); + if (splitIndex == -1 || splitIndex == 0 || splitIndex == parameter.Length - 1) { - throw new InvalidDataException($"Malformed parameter in password hash string {paramset}"); + throw new FormatException("Malformed parameter in password hash string"); } - parameters.Add(fields[0], fields[1]); + (parameters ??= new Dictionary<string, string>()).Add( + parameter[..splitIndex].ToString(), + parameter[(splitIndex + 1)..].ToString()); + } + + if (nextSegment == -1) + { + // parameters can't be null here + return new PasswordHash(id.ToString(), Array.Empty<byte>(), Array.Empty<byte>(), parameters!); } + + hashString = hashString[(nextSegment + 1)..]; + nextSegment = hashString.IndexOf('$'); + } + + if (nextSegment == 0) + { + throw new FormatException("Hash string contains an empty segment"); } byte[] hash; byte[] salt; - // Check if the string also contains a salt - if (splitted.Length - index == 2) + if (nextSegment == -1) { - salt = Convert.FromHexString(splitted[index++]); - hash = Convert.FromHexString(splitted[index++]); + salt = Array.Empty<byte>(); + hash = Convert.FromHexString(hashString); } else { - salt = Array.Empty<byte>(); - hash = Convert.FromHexString(splitted[index++]); + salt = Convert.FromHexString(hashString[..nextSegment]); + hashString = hashString[(nextSegment + 1)..]; + nextSegment = hashString.IndexOf('$'); + if (nextSegment != -1) + { + throw new FormatException("Hash string contains too many segments"); + } + + if (hashString.IsEmpty) + { + throw new FormatException("Hash segment is empty"); + } + + hash = Convert.FromHexString(hashString); } - return new PasswordHash(id, hash, salt, parameters); + return new PasswordHash(id.ToString(), hash, salt, parameters ?? new Dictionary<string, string>()); } private void SerializeParameters(StringBuilder stringBuilder) @@ -147,8 +207,13 @@ namespace MediaBrowser.Common.Cryptography .Append(Convert.ToHexString(_salt)); } - return str.Append('$') - .Append(Convert.ToHexString(_hash)).ToString(); + if (_hash.Length != 0) + { + str.Append('$') + .Append(Convert.ToHexString(_hash)); + } + + return str.ToString(); } } } diff --git a/MediaBrowser.Common/Events/EventHelper.cs b/MediaBrowser.Common/Events/EventHelper.cs index c9d3226ac..a9cf86fbc 100644 --- a/MediaBrowser.Common/Events/EventHelper.cs +++ b/MediaBrowser.Common/Events/EventHelper.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Common.Events /// <param name="sender">The sender.</param> /// <param name="args">The <see cref="EventArgs" /> instance containing the event data.</param> /// <param name="logger">The logger.</param> - public static void QueueEventIfNotNull(EventHandler handler, object sender, EventArgs args, ILogger logger) + public static void QueueEventIfNotNull(EventHandler? handler, object sender, EventArgs args, ILogger logger) { if (handler != null) { @@ -43,7 +43,7 @@ namespace MediaBrowser.Common.Events /// <param name="sender">The sender.</param> /// <param name="args">The args.</param> /// <param name="logger">The logger.</param> - public static void QueueEventIfNotNull<T>(EventHandler<T> handler, object sender, T args, ILogger logger) + public static void QueueEventIfNotNull<T>(EventHandler<T>? handler, object sender, T args, ILogger logger) { if (handler != null) { diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index 40020093b..08964420e 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Security.Cryptography; using System.Text; diff --git a/MediaBrowser.Common/Extensions/CopyToExtensions.cs b/MediaBrowser.Common/Extensions/CopyToExtensions.cs index 94bf7c740..2ecbc6539 100644 --- a/MediaBrowser.Common/Extensions/CopyToExtensions.cs +++ b/MediaBrowser.Common/Extensions/CopyToExtensions.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Collections.Generic; namespace MediaBrowser.Common.Extensions diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs index 19fa95480..1e5877c84 100644 --- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Common.Extensions { return (context.Connection.LocalIpAddress == null && context.Connection.RemoteIpAddress == null) - || context.Connection.LocalIpAddress.Equals(context.Connection.RemoteIpAddress); + || Equals(context.Connection.LocalIpAddress, context.Connection.RemoteIpAddress); } /// <summary> @@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Extensions /// </summary> /// <param name="context">The HTTP context.</param> /// <returns>The remote caller IP address.</returns> - public static string GetNormalizedRemoteIp(this HttpContext context) + public static IPAddress GetNormalizedRemoteIp(this HttpContext context) { // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) var ip = context.Connection.RemoteIpAddress ?? IPAddress.Loopback; @@ -35,7 +35,7 @@ namespace MediaBrowser.Common.Extensions ip = ip.MapToIPv4(); } - return ip.ToString(); + return ip; } } } diff --git a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs index 258bd6662..48e758ee4 100644 --- a/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs +++ b/MediaBrowser.Common/Extensions/MethodNotAllowedException.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; namespace MediaBrowser.Common.Extensions diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs index 2f52ba196..c74787122 100644 --- a/MediaBrowser.Common/Extensions/ProcessExtensions.cs +++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Diagnostics; using System.Threading; diff --git a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs index 7c7bdaa92..95802a462 100644 --- a/MediaBrowser.Common/Extensions/RateLimitExceededException.cs +++ b/MediaBrowser.Common/Extensions/RateLimitExceededException.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs index ebac9d8e6..22130c5a1 100644 --- a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs +++ b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; namespace MediaBrowser.Common.Extensions diff --git a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs index 6f0ea9bd5..2604abf85 100644 --- a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs +++ b/MediaBrowser.Common/Extensions/ShuffleExtensions.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; diff --git a/MediaBrowser.Common/Extensions/SplitStringExtensions.cs b/MediaBrowser.Common/Extensions/SplitStringExtensions.cs new file mode 100644 index 000000000..9c9108495 --- /dev/null +++ b/MediaBrowser.Common/Extensions/SplitStringExtensions.cs @@ -0,0 +1,95 @@ +/* +MIT License + +Copyright (c) 2019 Gérald Barré + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +#pragma warning disable CS1591 +#pragma warning disable CA1034 +using System; +using System.Diagnostics.Contracts; +using System.Runtime.InteropServices; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Extension class for splitting lines without unnecessary allocations. + /// </summary> + public static class SplitStringExtensions + { + /// <summary> + /// Creates a new string split enumerator. + /// </summary> + /// <param name="str">The string to split.</param> + /// <param name="separator">The separator to split on.</param> + /// <returns>The enumerator struct.</returns> + [Pure] + public static SplitEnumerator SpanSplit(this string str, char separator) => new (str.AsSpan(), separator); + + /// <summary> + /// Creates a new span split enumerator. + /// </summary> + /// <param name="str">The span to split.</param> + /// <param name="separator">The separator to split on.</param> + /// <returns>The enumerator struct.</returns> + [Pure] + public static SplitEnumerator Split(this ReadOnlySpan<char> str, char separator) => new (str, separator); + + [StructLayout(LayoutKind.Auto)] + public ref struct SplitEnumerator + { + private readonly char _separator; + private ReadOnlySpan<char> _str; + + public SplitEnumerator(ReadOnlySpan<char> str, char separator) + { + _str = str; + _separator = separator; + Current = default; + } + + public ReadOnlySpan<char> Current { get; private set; } + + public readonly SplitEnumerator GetEnumerator() => this; + + public bool MoveNext() + { + if (_str.Length == 0) + { + return false; + } + + var span = _str; + var index = span.IndexOf(_separator); + if (index == -1) + { + _str = ReadOnlySpan<char>.Empty; + Current = span; + return true; + } + + Current = span.Slice(0, index); + _str = span[(index + 1)..]; + return true; + } + } + } +} diff --git a/MediaBrowser.Common/Extensions/StreamExtensions.cs b/MediaBrowser.Common/Extensions/StreamExtensions.cs index cd77be7b2..5cbf57d98 100644 --- a/MediaBrowser.Common/Extensions/StreamExtensions.cs +++ b/MediaBrowser.Common/Extensions/StreamExtensions.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Collections.Generic; using System.IO; using System.Linq; @@ -35,11 +33,11 @@ namespace MediaBrowser.Common.Extensions } /// <summary> - /// Reads all lines in the <see cref="StreamReader" />. + /// Reads all lines in the <see cref="TextReader" />. /// </summary> - /// <param name="reader">The <see cref="StreamReader" /> to read from.</param> + /// <param name="reader">The <see cref="TextReader" /> to read from.</param> /// <returns>All lines in the stream.</returns> - public static IEnumerable<string> ReadAllLines(this StreamReader reader) + public static IEnumerable<string> ReadAllLines(this TextReader reader) { string? line; while ((line = reader.ReadLine()) != null) @@ -47,5 +45,19 @@ namespace MediaBrowser.Common.Extensions yield return line; } } + + /// <summary> + /// Reads all lines in the <see cref="TextReader" />. + /// </summary> + /// <param name="reader">The <see cref="TextReader" /> to read from.</param> + /// <returns>All lines in the stream.</returns> + public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader) + { + string? line; + while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) + { + yield return line; + } + } } } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index ddcf2ac17..46d93e494 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Reflection; @@ -10,7 +12,7 @@ namespace MediaBrowser.Common /// </summary> /// <param name="type">Type to create.</param> /// <returns>New instance of type <param>type</param>.</returns> - public delegate object CreationDelegate(Type type); + public delegate object CreationDelegateFactory(Type type); /// <summary> /// An interface to be implemented by the applications hosting a kernel. @@ -112,7 +114,7 @@ namespace MediaBrowser.Common /// <param name="defaultFunc">Delegate function that gets called to create the object.</param> /// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param> /// <returns><see cref="IReadOnlyCollection{T}" />.</returns> - IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true); + IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true); /// <summary> /// Gets the export types. diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs index 38a7e1d20..127a41a06 100644 --- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs @@ -9,67 +9,16 @@ namespace MediaBrowser.Common.Json.Converters /// Convert comma delimited string to array of type. /// </summary> /// <typeparam name="T">Type to convert to.</typeparam> - public class JsonCommaDelimitedArrayConverter<T> : JsonConverter<T[]> + public sealed class JsonCommaDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T> { - private readonly TypeConverter _typeConverter; - /// <summary> /// Initializes a new instance of the <see cref="JsonCommaDelimitedArrayConverter{T}"/> class. /// </summary> - public JsonCommaDelimitedArrayConverter() + public JsonCommaDelimitedArrayConverter() : base() { - _typeConverter = TypeDescriptor.GetConverter(typeof(T)); } /// <inheritdoc /> - public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - var stringEntries = reader.GetString()?.Split(',', StringSplitOptions.RemoveEmptyEntries); - if (stringEntries == null || stringEntries.Length == 0) - { - return Array.Empty<T>(); - } - - var parsedValues = new object[stringEntries.Length]; - var convertedCount = 0; - for (var i = 0; i < stringEntries.Length; i++) - { - try - { - parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim()); - convertedCount++; - } - catch (FormatException) - { - // TODO log when upgraded to .Net6 - // https://github.com/dotnet/runtime/issues/42975 - // _logger.LogDebug(e, "Error converting value."); - } - } - - var typedValues = new T[convertedCount]; - var typedValueIndex = 0; - for (var i = 0; i < stringEntries.Length; i++) - { - if (parsedValues[i] != null) - { - typedValues.SetValue(parsedValues[i], typedValueIndex); - typedValueIndex++; - } - } - - return typedValues; - } - - return JsonSerializer.Deserialize<T[]>(ref reader, options); - } - - /// <inheritdoc /> - public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value, options); - } + protected override char Delimiter => ','; } } diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs index 24ed3ea19..de41348dd 100644 --- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs +++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs @@ -19,10 +19,10 @@ namespace MediaBrowser.Common.Json.Converters } /// <inheritdoc /> - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; - return (JsonConverter)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType)); + return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType)); } } } diff --git a/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs new file mode 100644 index 000000000..b691798c9 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs @@ -0,0 +1,81 @@ +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// <summary> + /// Convert delimited string to array of type. + /// </summary> + /// <typeparam name="T">Type to convert to.</typeparam> + public abstract class JsonDelimitedArrayConverter<T> : JsonConverter<T[]?> + { + private readonly TypeConverter _typeConverter; + + /// <summary> + /// Initializes a new instance of the <see cref="JsonDelimitedArrayConverter{T}"/> class. + /// </summary> + protected JsonDelimitedArrayConverter() + { + _typeConverter = TypeDescriptor.GetConverter(typeof(T)); + } + + /// <summary> + /// Gets the array delimiter. + /// </summary> + protected virtual char Delimiter { get; } + + /// <inheritdoc /> + public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + // GetString can't return null here because we already handled it above + var stringEntries = reader.GetString()?.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries); + if (stringEntries == null || stringEntries.Length == 0) + { + return Array.Empty<T>(); + } + + var parsedValues = new object[stringEntries.Length]; + var convertedCount = 0; + for (var i = 0; i < stringEntries.Length; i++) + { + try + { + parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim()); + convertedCount++; + } + catch (FormatException) + { + // TODO log when upgraded to .Net6 + // https://github.com/dotnet/runtime/issues/42975 + // _logger.LogDebug(e, "Error converting value."); + } + } + + var typedValues = new T[convertedCount]; + var typedValueIndex = 0; + for (var i = 0; i < stringEntries.Length; i++) + { + if (parsedValues[i] != null) + { + typedValues.SetValue(parsedValues[i], typedValueIndex); + typedValueIndex++; + } + } + + return typedValues; + } + + return JsonSerializer.Deserialize<T[]>(ref reader, options); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + } +} diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs index d5b54e3ca..e2a3d798a 100644 --- a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs +++ b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs @@ -18,10 +18,10 @@ namespace MediaBrowser.Common.Json.Converters } /// <inheritdoc /> - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { var structType = typeToConvert.GenericTypeArguments[0]; - return (JsonConverter)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType)); + return (JsonConverter?)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType)); } } -}
\ No newline at end of file +} diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs index cb3d83f58..3d97a9de5 100644 --- a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Json.Converters return (int?)converter.ConvertFromString(str); } - return JsonSerializer.Deserialize<int?>(ref reader, options); + return JsonSerializer.Deserialize<int>(ref reader, options); } /// <inheritdoc /> diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs index 6a8790374..77cf46b70 100644 --- a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs @@ -7,15 +7,21 @@ namespace MediaBrowser.Common.Json.Converters /// <summary> /// Converts a string <c>N/A</c> to <c>string.Empty</c>. /// </summary> - public class JsonOmdbNotAvailableStringConverter : JsonConverter<string> + public class JsonOmdbNotAvailableStringConverter : JsonConverter<string?> { /// <inheritdoc /> - public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + if (reader.TokenType == JsonTokenType.String) { - var str = reader.GetString(); - if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase)) + // GetString can't return null here because we already handled it above + var str = reader.GetString()!; + if (str.Equals("N/A", StringComparison.OrdinalIgnoreCase)) { return null; } @@ -23,11 +29,11 @@ namespace MediaBrowser.Common.Json.Converters return str; } - return JsonSerializer.Deserialize<string>(ref reader, options); + return JsonSerializer.Deserialize<string?>(ref reader, options); } /// <inheritdoc /> - public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) { writer.WriteStringValue(value); } diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs index 377db1a44..a8f6cfbec 100644 --- a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs @@ -9,67 +9,16 @@ namespace MediaBrowser.Common.Json.Converters /// Convert Pipe delimited string to array of type. /// </summary> /// <typeparam name="T">Type to convert to.</typeparam> - public class JsonPipeDelimitedArrayConverter<T> : JsonConverter<T[]> + public sealed class JsonPipeDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T> { - private readonly TypeConverter _typeConverter; - /// <summary> /// Initializes a new instance of the <see cref="JsonPipeDelimitedArrayConverter{T}"/> class. /// </summary> - public JsonPipeDelimitedArrayConverter() + public JsonPipeDelimitedArrayConverter() : base() { - _typeConverter = TypeDescriptor.GetConverter(typeof(T)); } /// <inheritdoc /> - public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - var stringEntries = reader.GetString()?.Split('|', StringSplitOptions.RemoveEmptyEntries); - if (stringEntries == null || stringEntries.Length == 0) - { - return Array.Empty<T>(); - } - - var parsedValues = new object[stringEntries.Length]; - var convertedCount = 0; - for (var i = 0; i < stringEntries.Length; i++) - { - try - { - parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim()); - convertedCount++; - } - catch (FormatException) - { - // TODO log when upgraded to .Net6 - // https://github.com/dotnet/runtime/issues/42975 - // _logger.LogDebug(e, "Error converting value."); - } - } - - var typedValues = new T[convertedCount]; - var typedValueIndex = 0; - for (var i = 0; i < stringEntries.Length; i++) - { - if (parsedValues[i] != null) - { - typedValues.SetValue(parsedValues[i], typedValueIndex); - typedValueIndex++; - } - } - - return typedValues; - } - - return JsonSerializer.Deserialize<T[]>(ref reader, options); - } - - /// <inheritdoc /> - public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value, options); - } + protected override char Delimiter => '|'; } } diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs index 5e77223ef..1bebc49ec 100644 --- a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs +++ b/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs @@ -19,10 +19,10 @@ namespace MediaBrowser.Common.Json.Converters } /// <inheritdoc /> - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; - return (JsonConverter)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType)); + return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType)); } } } diff --git a/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs b/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs new file mode 100644 index 000000000..6cd980e48 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Buffers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// <summary> + /// Converter to allow the serializer to read strings. + /// </summary> + public class JsonStringConverter : JsonConverter<string?> + { + /// <inheritdoc /> + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Null => null, + JsonTokenType.String => reader.GetString(), + _ => GetRawValue(reader) + }; + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + + private static string GetRawValue(Utf8JsonReader reader) + { + var utf8Bytes = reader.HasValueSequence + ? reader.ValueSequence.ToArray() + : reader.ValueSpan; + return Encoding.UTF8.GetString(utf8Bytes); + } + } +} diff --git a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs b/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs index 37e6f64e3..81c093c54 100644 --- a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs @@ -7,11 +7,14 @@ namespace MediaBrowser.Common.Json.Converters /// <summary> /// Converts a Version object or value to/from JSON. /// </summary> + /// <remarks> + /// Required to send <see cref="Version"/> as a string instead of an object. + /// </remarks> public class JsonVersionConverter : JsonConverter<Version> { /// <inheritdoc /> public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => new Version(reader.GetString()); + => new Version(reader.GetString()!); // Will throw ArgumentNullException on null /// <inheritdoc /> public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 2ef24a884..405d6125f 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -39,7 +39,8 @@ namespace MediaBrowser.Common.Json new JsonStringEnumConverter(), new JsonNullableStructConverterFactory(), new JsonBoolNumberConverter(), - new JsonDateTimeConverter() + new JsonDateTimeConverter(), + new JsonStringConverter() } }; @@ -61,7 +62,7 @@ namespace MediaBrowser.Common.Json /// If the defaults must be modified the author must use the copy constructor. /// </remarks> /// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns> - public static JsonSerializerOptions GetOptions() + public static JsonSerializerOptions Options => _jsonSerializerOptions; /// <summary> @@ -72,7 +73,7 @@ namespace MediaBrowser.Common.Json /// If the defaults must be modified the author must use the copy constructor. /// </remarks> /// <returns>The camelCase <see cref="JsonSerializerOptions" /> options.</returns> - public static JsonSerializerOptions GetCamelCaseOptions() + public static JsonSerializerOptions CamelCaseOptions => _camelCaseJsonSerializerOptions; /// <summary> @@ -83,7 +84,7 @@ namespace MediaBrowser.Common.Json /// If the defaults must be modified the author must use the copy constructor. /// </remarks> /// <returns>The PascalCase <see cref="JsonSerializerOptions" /> options.</returns> - public static JsonSerializerOptions GetPascalCaseOptions() + public static JsonSerializerOptions PascalCaseOptions => _pascalCaseJsonSerializerOptions; } } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index e469436a9..0299a8456 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -14,7 +14,7 @@ </PropertyGroup> <ItemGroup> - <FrameworkReference Include="Microsoft.AspNetCore.App"/> + <FrameworkReference Include="Microsoft.AspNetCore.App" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> </ItemGroup> @@ -33,6 +33,9 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> <PublishRepositoryUrl>true</PublishRepositoryUrl> <EmbedUntrackedSources>true</EmbedUntrackedSources> <IncludeSymbols>true</IncludeSymbols> @@ -46,16 +49,11 @@ <!-- Code analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo"> <_Parameter1>Jellyfin.Common.Tests</_Parameter1> diff --git a/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs b/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs deleted file mode 100644 index f1c5f2477..000000000 --- a/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Net; -using System.Net.Http; - -namespace MediaBrowser.Common.Net -{ - /// <summary> - /// Default http client handler. - /// </summary> - public class DefaultHttpClientHandler : HttpClientHandler - { - /// <summary> - /// Initializes a new instance of the <see cref="DefaultHttpClientHandler"/> class. - /// </summary> - public DefaultHttpClientHandler() - { - AutomaticDecompression = DecompressionMethods.All; - } - } -} diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index b6c390d23..b93939730 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -1,10 +1,8 @@ -#nullable enable using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Net; using System.Net.NetworkInformation; -using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Common.Net @@ -229,5 +227,12 @@ namespace MediaBrowser.Common.Net /// <param name="filter">Optional filter for the list.</param> /// <returns>Returns a filtered list of LAN addresses.</returns> Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null); + + /// <summary> + /// Checks to see if <paramref name="remoteIp"/> has access. + /// </summary> + /// <param name="remoteIp">IP Address of client.</param> + /// <returns><b>True</b> if has access, otherwise <b>false</b>.</returns> + bool HasRemoteAccess(IPAddress remoteIp); } } diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs index 4a7c70190..5db8817ee 100644 --- a/MediaBrowser.Common/Net/IPHost.cs +++ b/MediaBrowser.Common/Net/IPHost.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Diagnostics; using System.Linq; @@ -135,7 +134,7 @@ namespace MediaBrowser.Common.Net } // See if it's an IPv6 with port address e.g. [::1] or [::1]:120. - int i = host.IndexOf("]", StringComparison.OrdinalIgnoreCase); + int i = host.IndexOf(']', StringComparison.Ordinal); if (i != -1) { return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); @@ -389,8 +388,8 @@ namespace MediaBrowser.Common.Net /// <inheritdoc/> protected override IPObject CalculateNetworkAddress() { - var netAddr = NetworkAddressOf(this[0], PrefixLength); - return new IPNetAddress(netAddr.Address, netAddr.PrefixLength); + var (address, prefixLength) = NetworkAddressOf(this[0], PrefixLength); + return new IPNetAddress(address, prefixLength); } /// <summary> @@ -400,13 +399,10 @@ namespace MediaBrowser.Common.Net private bool ResolveHost() { // When was the last time we resolved? - if (_lastResolved == null) - { - _lastResolved = DateTime.UtcNow; - } + _lastResolved ??= DateTime.UtcNow; // If we haven't resolved before, or our timer has run out... - if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved?.AddMinutes(Timeout))) + if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved.Value.AddMinutes(Timeout))) { _lastResolved = DateTime.UtcNow; ResolveHostInternal().GetAwaiter().GetResult(); @@ -427,7 +423,7 @@ namespace MediaBrowser.Common.Net // Resolves the host name - so save a DNS lookup. if (string.Equals(HostName, "localhost", StringComparison.OrdinalIgnoreCase)) { - _addresses = new IPAddress[] { new IPAddress(Ipv4Loopback), new IPAddress(Ipv6Loopback) }; + _addresses = new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback }; return; } diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs index 5fab52eac..f6e3971bf 100644 --- a/MediaBrowser.Common/Net/IPNetAddress.cs +++ b/MediaBrowser.Common/Net/IPNetAddress.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Net; using System.Net.Sockets; @@ -38,7 +37,7 @@ namespace MediaBrowser.Common.Net /// <summary> /// IP6Loopback address host. /// </summary> - public static readonly IPNetAddress IP6Loopback = IPNetAddress.Parse("::1"); + public static readonly IPNetAddress IP6Loopback = new IPNetAddress(IPAddress.IPv6Loopback); /// <summary> /// Object's IP address. @@ -113,7 +112,7 @@ namespace MediaBrowser.Common.Net } // Is it a network? - string[] tokens = addr.Split("/"); + string[] tokens = addr.Split('/'); if (tokens.Length == 2) { @@ -171,8 +170,8 @@ namespace MediaBrowser.Common.Net address = address.MapToIPv4(); } - var altAddress = NetworkAddressOf(address, PrefixLength); - return NetworkAddress.Address.Equals(altAddress.Address) && NetworkAddress.PrefixLength >= altAddress.PrefixLength; + var (altAddress, altPrefix) = NetworkAddressOf(address, PrefixLength); + return NetworkAddress.Address.Equals(altAddress) && NetworkAddress.PrefixLength >= altPrefix; } /// <inheritdoc/> @@ -196,8 +195,8 @@ namespace MediaBrowser.Common.Net return NetworkAddress.PrefixLength <= netaddrObj.PrefixLength; } - var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength); - return NetworkAddress.Address.Equals(altAddress.Address); + var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength).address; + return NetworkAddress.Address.Equals(altAddress); } return false; @@ -216,11 +215,11 @@ namespace MediaBrowser.Common.Net } /// <inheritdoc/> - public override bool Equals(IPAddress address) + public override bool Equals(IPAddress ip) { - if (address != null && !address.Equals(IPAddress.None) && !Address.Equals(IPAddress.None)) + if (ip != null && !ip.Equals(IPAddress.None) && !Address.Equals(IPAddress.None)) { - return address.Equals(Address); + return ip.Equals(Address); } return false; @@ -270,8 +269,8 @@ namespace MediaBrowser.Common.Net /// <inheritdoc/> protected override IPObject CalculateNetworkAddress() { - var value = NetworkAddressOf(_address, PrefixLength); - return new IPNetAddress(value.Address, value.PrefixLength); + var (address, prefixLength) = NetworkAddressOf(_address, PrefixLength); + return new IPNetAddress(address, prefixLength); } } } diff --git a/MediaBrowser.Common/Net/IPObject.cs b/MediaBrowser.Common/Net/IPObject.cs index 69cd57f8a..2612268fd 100644 --- a/MediaBrowser.Common/Net/IPObject.cs +++ b/MediaBrowser.Common/Net/IPObject.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Net; using System.Net.Sockets; @@ -11,16 +10,6 @@ namespace MediaBrowser.Common.Net public abstract class IPObject : IEquatable<IPObject> { /// <summary> - /// IPv6 Loopback address. - /// </summary> - protected static readonly byte[] Ipv6Loopback = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }; - - /// <summary> - /// IPv4 Loopback address. - /// </summary> - protected static readonly byte[] Ipv4Loopback = { 127, 0, 0, 1 }; - - /// <summary> /// The network address of this object. /// </summary> private IPObject? _networkAddress; @@ -64,7 +53,7 @@ namespace MediaBrowser.Common.Net /// <param name="address">IP Address to convert.</param> /// <param name="prefixLength">Subnet prefix.</param> /// <returns>IPAddress.</returns> - public static (IPAddress Address, byte PrefixLength) NetworkAddressOf(IPAddress address, byte prefixLength) + public static (IPAddress address, byte prefixLength) NetworkAddressOf(IPAddress address, byte prefixLength) { if (address == null) { @@ -78,7 +67,7 @@ namespace MediaBrowser.Common.Net if (IsLoopback(address)) { - return (Address: address, PrefixLength: prefixLength); + return (address, prefixLength); } // An ip address is just a list of bytes, each one representing a segment on the network. @@ -110,7 +99,7 @@ namespace MediaBrowser.Common.Net } // Return the network address for the prefix. - return (Address: new IPAddress(addressBytes), PrefixLength: prefixLength); + return (new IPAddress(addressBytes), prefixLength); } /// <summary> diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 9c1a0cf49..264bfacb4 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -27,9 +27,11 @@ namespace MediaBrowser.Common.Net /// </summary> /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> /// <param name="item">Item to add.</param> - public static void AddItem(this Collection<IPObject> source, IPObject item) + /// <param name="itemsAreNetworks">If <c>true</c> the values are treated as subnets. + /// If <b>false</b> items are addresses.</param> + public static void AddItem(this Collection<IPObject> source, IPObject item, bool itemsAreNetworks = true) { - if (!source.ContainsAddress(item)) + if (!source.ContainsAddress(item) || !itemsAreNetworks) { source.Add(item); } @@ -190,8 +192,9 @@ namespace MediaBrowser.Common.Net /// </summary> /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> /// <param name="excludeList">Items to exclude.</param> + /// <param name="isNetwork">Collection is a network collection.</param> /// <returns>A new collection, with the items excluded.</returns> - public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList) + public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList, bool isNetwork) { if (source.Count == 0 || excludeList == null) { @@ -216,7 +219,7 @@ namespace MediaBrowser.Common.Net if (!found) { - results.AddItem(outer); + results.AddItem(outer, isNetwork); } } @@ -229,7 +232,7 @@ namespace MediaBrowser.Common.Net /// <param name="source">The <see cref="Collection{IPObject}"/>.</param> /// <param name="target">Collection to compare with.</param> /// <returns>A collection containing all the matches.</returns> - public static Collection<IPObject> Union(this Collection<IPObject> source, Collection<IPObject> target) + public static Collection<IPObject> ThatAreContainedInNetworks(this Collection<IPObject> source, Collection<IPObject> target) { if (source.Count == 0) { diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index 7b162c0e1..8972089a8 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.IO; using System.Reflection; @@ -50,7 +52,7 @@ namespace MediaBrowser.Common.Plugins /// Gets a value indicating whether the plugin can be uninstalled. /// </summary> public bool CanUninstall => !Path.GetDirectoryName(AssemblyFilePath) - .Equals(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), StringComparison.InvariantCulture); + .Equals(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), StringComparison.Ordinal); /// <summary> /// Gets the plugin info. diff --git a/MediaBrowser.Common/Plugins/BasePluginOfT.cs b/MediaBrowser.Common/Plugins/BasePluginOfT.cs index d5c780851..8a6d28e0f 100644 --- a/MediaBrowser.Common/Plugins/BasePluginOfT.cs +++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs @@ -1,4 +1,6 @@ +#nullable disable #pragma warning disable SA1649 // File name should match first type name + using System; using System.IO; using System.Runtime.InteropServices; @@ -39,29 +41,27 @@ namespace MediaBrowser.Common.Plugins { ApplicationPaths = applicationPaths; XmlSerializer = xmlSerializer; - if (this is IPluginAssembly assemblyPlugin) - { - var assembly = GetType().Assembly; - var assemblyName = assembly.GetName(); - var assemblyFilePath = assembly.Location; - var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath)); - if (!Directory.Exists(dataFolderPath) && Version != null) - { - // Try again with the version number appended to the folder name. - dataFolderPath = dataFolderPath + "_" + Version.ToString(); - } + var assembly = GetType().Assembly; + var assemblyName = assembly.GetName(); + var assemblyFilePath = assembly.Location; + + var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath)); + if (!Directory.Exists(dataFolderPath) && Version != null) + { + // Try again with the version number appended to the folder name. + dataFolderPath = dataFolderPath + "_" + Version.ToString(); + } - assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version); + SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version); - var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true); - if (idAttributes.Length > 0) - { - var attribute = (GuidAttribute)idAttributes[0]; - var assemblyId = new Guid(attribute.Value); + var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true); + if (idAttributes.Length > 0) + { + var attribute = (GuidAttribute)idAttributes[0]; + var assemblyId = new Guid(attribute.Value); - assemblyPlugin.SetId(assemblyId); - } + SetId(assemblyId); } } @@ -107,10 +107,7 @@ namespace MediaBrowser.Common.Plugins { lock (_configurationSyncLock) { - if (_configuration == null) - { - _configuration = LoadConfiguration(); - } + _configuration ??= LoadConfiguration(); } } diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs index b2ba1179c..01e0a536d 100644 --- a/MediaBrowser.Common/Plugins/IPlugin.cs +++ b/MediaBrowser.Common/Plugins/IPlugin.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using MediaBrowser.Model.Plugins; diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs index fc2fcb517..176bcbbd5 100644 --- a/MediaBrowser.Common/Plugins/IPluginManager.cs +++ b/MediaBrowser.Common/Plugins/IPluginManager.cs @@ -1,9 +1,8 @@ -#nullable enable - using System; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; +using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Updates; using Microsoft.Extensions.DependencyInjection; @@ -17,7 +16,7 @@ namespace MediaBrowser.Common.Plugins /// <summary> /// Gets the Plugins. /// </summary> - IList<LocalPlugin> Plugins { get; } + IReadOnlyList<LocalPlugin> Plugins { get; } /// <summary> /// Creates the plugins. @@ -51,8 +50,9 @@ namespace MediaBrowser.Common.Plugins /// <param name="packageInfo">The <see cref="PackageInfo"/> used to generate a manifest.</param> /// <param name="version">Version to be installed.</param> /// <param name="path">The path where to save the manifest.</param> + /// <param name="status">Initial status of the plugin.</param> /// <returns>True if successful.</returns> - Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path); + Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status); /// <summary> /// Imports plugin details from a folder. diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs index 23b6cfa81..4c8e2d504 100644 --- a/MediaBrowser.Common/Plugins/LocalPlugin.cs +++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using MediaBrowser.Model.Plugins; @@ -22,7 +21,7 @@ namespace MediaBrowser.Common.Plugins public LocalPlugin(string path, bool isSupported, PluginManifest manifest) { Path = path; - DllFiles = new List<string>(); + DllFiles = Array.Empty<string>(); _supported = isSupported; Manifest = manifest; } @@ -59,9 +58,9 @@ namespace MediaBrowser.Common.Plugins public string Path { get; } /// <summary> - /// Gets the list of dll files for this plugin. + /// Gets or sets the list of dll files for this plugin. /// </summary> - public List<string> DllFiles { get; } + public IReadOnlyList<string> DllFiles { get; set; } /// <summary> /// Gets or sets the instance of this plugin. diff --git a/MediaBrowser.Common/Plugins/PluginManifest.cs b/MediaBrowser.Common/Plugins/PluginManifest.cs index 4c724f694..2910dbe14 100644 --- a/MediaBrowser.Common/Plugins/PluginManifest.cs +++ b/MediaBrowser.Common/Plugins/PluginManifest.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Text.Json.Serialization; using MediaBrowser.Model.Plugins; diff --git a/MediaBrowser.Common/Progress/ActionableProgress.cs b/MediaBrowser.Common/Progress/ActionableProgress.cs index d5bcd5be9..0ba46ea3b 100644 --- a/MediaBrowser.Common/Progress/ActionableProgress.cs +++ b/MediaBrowser.Common/Progress/ActionableProgress.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +#pragma warning disable CA1003 using System; @@ -13,9 +14,9 @@ namespace MediaBrowser.Common.Progress /// <summary> /// The _actions. /// </summary> - private Action<T> _action; + private Action<T>? _action; - public event EventHandler<T> ProgressChanged; + public event EventHandler<T>? ProgressChanged; /// <summary> /// Registers the action. diff --git a/MediaBrowser.Common/Progress/SimpleProgress.cs b/MediaBrowser.Common/Progress/SimpleProgress.cs index d75675bf1..7071f2bc3 100644 --- a/MediaBrowser.Common/Progress/SimpleProgress.cs +++ b/MediaBrowser.Common/Progress/SimpleProgress.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +#pragma warning disable CA1003 using System; @@ -6,7 +7,7 @@ namespace MediaBrowser.Common.Progress { public class SimpleProgress<T> : IProgress<T> { - public event EventHandler<T> ProgressChanged; + public event EventHandler<T>? ProgressChanged; public void Report(T value) { diff --git a/MediaBrowser.Common/Providers/ProviderIdParsers.cs b/MediaBrowser.Common/Providers/ProviderIdParsers.cs new file mode 100644 index 000000000..33d09ed38 --- /dev/null +++ b/MediaBrowser.Common/Providers/ProviderIdParsers.cs @@ -0,0 +1,123 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MediaBrowser.Common.Providers +{ + /// <summary> + /// Parsers for provider ids. + /// </summary> + public static class ProviderIdParsers + { + private const int ImdbMinNumbers = 7; + private const int ImdbMaxNumbers = 8; + private const string ImdbPrefix = "tt"; + + /// <summary> + /// Parses an IMDb id from a string. + /// </summary> + /// <param name="text">The text to parse.</param> + /// <param name="imdbId">The parsed IMDb id.</param> + /// <returns>True if parsing was successful, false otherwise.</returns> + public static bool TryFindImdbId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> imdbId) + { + // imdb id is at least 9 chars (tt + 7 numbers) + while (text.Length >= 2 + ImdbMinNumbers) + { + var ttPos = text.IndexOf(ImdbPrefix); + if (ttPos == -1) + { + imdbId = default; + return false; + } + + text = text.Slice(ttPos); + var i = 2; + var limit = Math.Min(text.Length, ImdbMaxNumbers + 2); + for (; i < limit; i++) + { + var c = text[i]; + if (!IsDigit(c)) + { + break; + } + } + + // skip if more than 8 digits + 2 chars for tt + if (i <= ImdbMaxNumbers + 2 && i >= ImdbMinNumbers + 2) + { + imdbId = text.Slice(0, i); + return true; + } + + text = text.Slice(i); + } + + imdbId = default; + return false; + } + + /// <summary> + /// Parses an TMDb id from a movie url. + /// </summary> + /// <param name="text">The text with the url to parse.</param> + /// <param name="tmdbId">The parsed TMDb id.</param> + /// <returns>True if parsing was successful, false otherwise.</returns> + public static bool TryFindTmdbMovieId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tmdbId) + => TryFindProviderId(text, "themoviedb.org/movie/", out tmdbId); + + /// <summary> + /// Parses an TMDb id from a series url. + /// </summary> + /// <param name="text">The text with the url to parse.</param> + /// <param name="tmdbId">The parsed TMDb id.</param> + /// <returns>True if parsing was successful, false otherwise.</returns> + public static bool TryFindTmdbSeriesId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tmdbId) + => TryFindProviderId(text, "themoviedb.org/tv/", out tmdbId); + + /// <summary> + /// Parses an TVDb id from a url. + /// </summary> + /// <param name="text">The text with the url to parse.</param> + /// <param name="tvdbId">The parsed TVDb id.</param> + /// <returns>True if parsing was successful, false otherwise.</returns> + public static bool TryFindTvdbId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tvdbId) + => TryFindProviderId(text, "thetvdb.com/?tab=series&id=", out tvdbId); + + private static bool TryFindProviderId(ReadOnlySpan<char> text, ReadOnlySpan<char> searchString, [NotNullWhen(true)] out ReadOnlySpan<char> providerId) + { + var searchPos = text.IndexOf(searchString); + if (searchPos == -1) + { + providerId = default; + return false; + } + + text = text.Slice(searchPos + searchString.Length); + + int i = 0; + for (; i < text.Length; i++) + { + var c = text[i]; + + if (!IsDigit(c)) + { + break; + } + } + + if (i >= 1) + { + providerId = text.Slice(0, i); + return true; + } + + providerId = default; + return false; + } + + private static bool IsDigit(char c) + { + return c >= '0' && c <= '9'; + } + } +} diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index 0844c2d79..c2a28e0a2 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs index adf336313..f4f759955 100644 --- a/MediaBrowser.Common/Updates/InstallationEventArgs.cs +++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using MediaBrowser.Model.Updates; diff --git a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs index 46f10c84f..d37146195 100644 --- a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs +++ b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs index 4249a9a66..635e4eb3d 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Controller.Session; diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index ecdffa2eb..a56d3c822 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs index 6729b9115..8c9d1baf8 100644 --- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs +++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index 31dd95402..68119cfed 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -1,7 +1,8 @@ +#nullable disable + using System; using System.Linq; using System.Threading; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs index e1f5d05a6..b2b36c040 100644 --- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs @@ -1,4 +1,5 @@ -using System; +#nullable disable + using System.Threading; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index b2315bda4..26c64e0da 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs index 476992cbd..fa7aff647 100644 --- a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs +++ b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Channels/ChannelItemResult.cs b/MediaBrowser.Controller/Channels/ChannelItemResult.cs index cee7b2003..8e937852f 100644 --- a/MediaBrowser.Controller/Channels/ChannelItemResult.cs +++ b/MediaBrowser.Controller/Channels/ChannelItemResult.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs index 32469d4d7..53a73d62a 100644 --- a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs +++ b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 namespace MediaBrowser.Controller.Channels diff --git a/MediaBrowser.Controller/Channels/IChannel.cs b/MediaBrowser.Controller/Channels/IChannel.cs index 2c0eadf95..01bf8d5c8 100644 --- a/MediaBrowser.Controller/Channels/IChannel.cs +++ b/MediaBrowser.Controller/Channels/IChannel.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs index ddae7dbd3..4c5626338 100644 --- a/MediaBrowser.Controller/Channels/IChannelManager.cs +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Channels/IHasCacheKey.cs b/MediaBrowser.Controller/Channels/IHasCacheKey.cs index bf895a0ec..9fae43033 100644 --- a/MediaBrowser.Controller/Channels/IHasCacheKey.cs +++ b/MediaBrowser.Controller/Channels/IHasCacheKey.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 namespace MediaBrowser.Controller.Channels diff --git a/MediaBrowser.Controller/Channels/ISearchableChannel.cs b/MediaBrowser.Controller/Channels/ISearchableChannel.cs index b627ca1c2..b58446fc4 100644 --- a/MediaBrowser.Controller/Channels/ISearchableChannel.cs +++ b/MediaBrowser.Controller/Channels/ISearchableChannel.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs index 137f5d095..152c653dc 100644 --- a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs +++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs index 7e9bb28ed..0d837faca 100644 --- a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs +++ b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs index f6037d05e..94e7541f8 100644 --- a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs +++ b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Collections/CollectionEvents.cs b/MediaBrowser.Controller/Collections/CollectionEvents.cs index ce59b4ada..821318ffc 100644 --- a/MediaBrowser.Controller/Collections/CollectionEvents.cs +++ b/MediaBrowser.Controller/Collections/CollectionEvents.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs index a6991e2ea..46bc37e7f 100644 --- a/MediaBrowser.Controller/Collections/ICollectionManager.cs +++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs index 43ad04dba..44e2c45dd 100644 --- a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs +++ b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 8f0872dba..ef17c8fb3 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs index dc2d5a356..b51dc255c 100644 --- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs +++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 770c6dc2d..800f7a8bb 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 #nullable enable diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 935a79031..9bfead8b3 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 #nullable enable @@ -61,7 +63,7 @@ namespace MediaBrowser.Controller.Drawing string GetImageCacheTag(BaseItem item, ChapterInfo info); - string GetImageCacheTag(User user); + string? GetImageCacheTag(User user); /// <summary> /// Processes the image. diff --git a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs index fe0465d0d..f06bbe4d0 100644 --- a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 namespace MediaBrowser.Controller.Drawing diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs index 87c28d577..204175ed5 100644 --- a/MediaBrowser.Controller/Drawing/ImageHelper.cs +++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs @@ -1,75 +1,20 @@ +#nullable disable + #pragma warning disable CS1591 +#nullable enable -using System; -using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Drawing { public static class ImageHelper { - public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions? originalImageSize) - { - if (originalImageSize.HasValue) - { - // Determine the output size based on incoming parameters - var newSize = DrawingUtils.Resize(originalImageSize.Value, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0); - - return newSize; - } - - return GetSizeEstimate(options); - } - - private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options) - { - if (options.Width.HasValue && options.Height.HasValue) - { - return new ImageDimensions(options.Width.Value, options.Height.Value); - } - - double aspect = GetEstimatedAspectRatio(options.Image.Type, options.Item); - - int? width = options.Width ?? options.MaxWidth; - - if (width.HasValue) - { - int heightValue = Convert.ToInt32((double)width.Value / aspect); - return new ImageDimensions(width.Value, heightValue); - } - - var height = options.Height ?? options.MaxHeight ?? 200; - int widthValue = Convert.ToInt32(aspect * height); - return new ImageDimensions(widthValue, height); - } - - private static double GetEstimatedAspectRatio(ImageType type, BaseItem item) + public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions originalImageSize) { - switch (type) - { - case ImageType.Art: - case ImageType.Backdrop: - case ImageType.Chapter: - case ImageType.Screenshot: - case ImageType.Thumb: - return 1.78; - case ImageType.Banner: - return 5.4; - case ImageType.Box: - case ImageType.BoxRear: - case ImageType.Disc: - case ImageType.Menu: - case ImageType.Profile: - return 1; - case ImageType.Logo: - return 2.58; - case ImageType.Primary: - double defaultPrimaryImageAspectRatio = item.GetDefaultPrimaryImageAspectRatio(); - return defaultPrimaryImageAspectRatio > 0 ? defaultPrimaryImageAspectRatio : 2.0 / 3; - default: - return 1; - } + // Determine the output size based on incoming parameters + var newSize = DrawingUtils.Resize(originalImageSize, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0); + newSize = DrawingUtils.ResizeFill(newSize, options.FillWidth, options.FillHeight); + return newSize; } } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index 22105b7d7..11e663301 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -24,8 +26,6 @@ namespace MediaBrowser.Controller.Drawing public int ImageIndex { get; set; } - public bool CropWhiteSpace { get; set; } - public int? Width { get; set; } public int? Height { get; set; } @@ -34,6 +34,10 @@ namespace MediaBrowser.Controller.Drawing public int? MaxHeight { get; set; } + public int? FillWidth { get; set; } + + public int? FillHeight { get; set; } + public int Quality { get; set; } public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; } @@ -95,6 +99,11 @@ namespace MediaBrowser.Controller.Drawing return false; } + if (sizeValue.Width > FillWidth || sizeValue.Height > FillHeight) + { + return false; + } + return true; } @@ -106,7 +115,6 @@ namespace MediaBrowser.Controller.Drawing PercentPlayed.Equals(0) && !UnplayedCount.HasValue && !Blur.HasValue && - !CropWhiteSpace && string.IsNullOrEmpty(BackgroundColor) && string.IsNullOrEmpty(ForegroundLayer); } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs index d3a2b4dbf..b036425ab 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs index 46f58ec15..591cc53d1 100644 --- a/MediaBrowser.Controller/Drawing/ImageStream.cs +++ b/MediaBrowser.Controller/Drawing/ImageStream.cs @@ -12,7 +12,7 @@ namespace MediaBrowser.Controller.Drawing /// Gets or sets the stream. /// </summary> /// <value>The stream.</value> - public Stream Stream { get; set; } + public Stream? Stream { get; set; } /// <summary> /// Gets or sets the format. diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index 356783750..758e841a7 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index 988557f42..7f4bbead0 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 6ebea5f44..f1944a7d3 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -120,8 +122,7 @@ namespace MediaBrowser.Controller.Entities var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { - FileInfo = FileSystem.GetDirectoryInfo(path), - Path = path + FileInfo = FileSystem.GetDirectoryInfo(path) }; // Gather child folder and files diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 8220464b3..4c2b7cb7c 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs index f6d3cd6cc..1625c748a 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -1,6 +1,10 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.Entities.Audio { @@ -23,15 +27,7 @@ namespace MediaBrowser.Controller.Entities.Audio public static IEnumerable<string> GetAllArtists<T>(this T item) where T : IHasArtist, IHasAlbumArtist { - foreach (var i in item.AlbumArtists) - { - yield return i; - } - - foreach (var i in item.Artists) - { - yield return i; - } + return item.AlbumArtists.Concat(item.Artists).DistinctNames(); } } } diff --git a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs index ac4dd1688..db60c3071 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 namespace MediaBrowser.Controller.Entities.Audio diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 9a33ad9d7..610bce4f5 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 8a9bb12c7..6101d3016 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index f0c076108..b07d47ffd 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs index f4bd851e1..405284622 100644 --- a/MediaBrowser.Controller/Entities/AudioBook.cs +++ b/MediaBrowser.Controller/Entities/AudioBook.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 53d45261e..ca5213273 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -106,15 +108,10 @@ namespace MediaBrowser.Controller.Entities { get { - if (_themeSongIds == null) - { - _themeSongIds = GetExtras() - .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong) - .Select(song => song.Id) - .ToArray(); - } - - return _themeSongIds; + return _themeSongIds ??= GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong) + .Select(song => song.Id) + .ToArray(); } private set @@ -128,15 +125,10 @@ namespace MediaBrowser.Controller.Entities { get { - if (_themeVideoIds == null) - { - _themeVideoIds = GetExtras() - .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo) - .Select(song => song.Id) - .ToArray(); - } - - return _themeVideoIds; + return _themeVideoIds ??= GetExtras() + .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo) + .Select(song => song.Id) + .ToArray(); } private set @@ -2324,7 +2316,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => i.IsLocalFile) .Select(i => System.IO.Path.GetDirectoryName(i.Path)) .Distinct(StringComparer.OrdinalIgnoreCase) - .SelectMany(i => directoryService.GetFilePaths(i)) + .SelectMany(directoryService.GetFilePaths) .ToList(); var deletedImages = ImageInfos diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index c65477d39..c39b18891 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -1,5 +1,9 @@ +#nullable disable + +#nullable enable #pragma warning disable CS1591 +using System; using System.Linq; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -64,9 +68,19 @@ namespace MediaBrowser.Controller.Entities /// <param name="source">The source object.</param> /// <param name="dest">The destination object.</param> public static void DeepCopy<T, TU>(this T source, TU dest) - where T : BaseItem - where TU : BaseItem + where T : BaseItem + where TU : BaseItem { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (dest == null) + { + throw new ArgumentNullException(nameof(dest)); + } + var destProps = typeof(TU).GetProperties().Where(x => x.CanWrite).ToList(); foreach (var sourceProp in typeof(T).GetProperties()) @@ -99,8 +113,8 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="source">The source object.</param> public static TU DeepCopy<T, TU>(this T source) - where T : BaseItem - where TU : BaseItem, new() + where T : BaseItem + where TU : BaseItem, new() { var dest = new TU(); source.DeepCopy(dest); diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs index ef5a5a734..1bd25042f 100644 --- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs +++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Text.Json.Serialization; diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 55945283c..3d0370248 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 65fd1654c..a86da29ce 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -26,7 +28,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class CollectionFolder : Folder, ICollectionFolder { - private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; public static IXmlSerializer XmlSerializer { get; set; } public static IServerApplicationHost ApplicationHost { get; set; } @@ -61,7 +63,6 @@ namespace MediaBrowser.Controller.Entities try { var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions; - if (result == null) { return new LibraryOptions(); @@ -271,7 +272,6 @@ namespace MediaBrowser.Controller.Entities var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { FileInfo = FileSystem.GetDirectoryInfo(path), - Path = path, Parent = GetParent() as Folder, CollectionType = CollectionType }; @@ -355,9 +355,7 @@ namespace MediaBrowser.Controller.Entities if (result.Count == 0) { - var folder = LibraryManager.FindByPath(path, true) as Folder; - - if (folder != null) + if (LibraryManager.FindByPath(path, true) is Folder folder) { result.Add(folder); } diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs index 3a34c668c..244cc00be 100644 --- a/MediaBrowser.Controller/Entities/Extensions.cs +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Linq; using MediaBrowser.Common.Extensions; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index cac5026f7..a59f5c6e4 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1,8 +1,9 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Text.Json.Serialization; @@ -1434,9 +1435,14 @@ namespace MediaBrowser.Controller.Entities var linkedChildren = LinkedChildren; foreach (var i in linkedChildren) { - if (i.ItemId.HasValue && i.ItemId.Value == itemId) + if (i.ItemId.HasValue) { - return true; + if (i.ItemId.Value == itemId) + { + return true; + } + + continue; } var child = GetLinkedChild(i); @@ -1764,20 +1770,15 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } - }); - - double unplayedCount = unplayedQueryResult.TotalRecordCount; + }).TotalRecordCount; - dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount; + dto.UnplayedItemCount = unplayedQueryResult; - if (itemDto != null && itemDto.RecursiveItemCount.HasValue) + if (itemDto?.RecursiveItemCount > 0) { - if (itemDto.RecursiveItemCount.Value > 0) - { - var unplayedPercentage = (unplayedCount / itemDto.RecursiveItemCount.Value) * 100; - dto.PlayedPercentage = 100 - unplayedPercentage; - dto.Played = dto.PlayedPercentage.Value >= 100; - } + var unplayedPercentage = ((double)unplayedQueryResult / itemDto.RecursiveItemCount.Value) * 100; + dto.PlayedPercentage = 100 - unplayedPercentage; + dto.Played = dto.PlayedPercentage.Value >= 100; } else { diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 74a170204..7987f38a0 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs index b84a9fa6f..2304570fd 100644 --- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs index d7d007668..3aeb7468f 100644 --- a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs +++ b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace MediaBrowser.Controller.Entities { /// <summary> diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs index 13226b234..14459624e 100644 --- a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs +++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace MediaBrowser.Controller.Entities { /// <summary> diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs index 0f612262a..98c3b3edf 100644 --- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs index f747b5149..f80f7c304 100644 --- a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs +++ b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Model.LiveTv; diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs index 5444f1f52..64d769d5b 100644 --- a/MediaBrowser.Controller/Entities/IHasSeries.cs +++ b/MediaBrowser.Controller/Entities/IHasSeries.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs index 6a350212b..f317a02ff 100644 --- a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs +++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs index d1f6f2b7e..2bd9ded33 100644 --- a/MediaBrowser.Controller/Entities/IHasTrailers.cs +++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 270217356..c06021029 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 5b96a5af6..b2d6a4609 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index 570d8eec0..ea8555dbf 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index 8e0f721e7..01c0a9339 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 05e4229ca..74e84288d 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -217,8 +219,7 @@ namespace MediaBrowser.Controller.Entities.Movies private IEnumerable<BaseItem> FlattenItems(BaseItem item, List<Guid> expandedFolders) { - var boxset = item as BoxSet; - if (boxset != null) + if (item is BoxSet boxset) { if (!expandedFolders.Contains(item.Id)) { diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 8b67aaccc..64d60c2e9 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index b278a0142..f42e7723c 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index 1f3758a73..687ce1ec8 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -100,23 +100,5 @@ namespace MediaBrowser.Controller.Entities existing.SetProviderId(id.Key, id.Value); } } - - public static bool ContainsPerson(List<PersonInfo> people, string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - foreach (var i in people) - { - if (string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } } } diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index c4fcb0267..d9ff55362 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs index 4ff9b0955..fb79323f8 100644 --- a/MediaBrowser.Controller/Entities/PersonInfo.cs +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index 2fc66176f..3312a0e3e 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Text.Json.Serialization; @@ -24,8 +26,7 @@ namespace MediaBrowser.Controller.Entities var parents = GetParents(); foreach (var parent in parents) { - var photoAlbum = parent as PhotoAlbum; - if (photoAlbum != null) + if (parent is PhotoAlbum photoAlbum) { return photoAlbum; } diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs index 50f1655f3..7e4ec1830 100644 --- a/MediaBrowser.Controller/Entities/Share.cs +++ b/MediaBrowser.Controller/Entities/Share.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index 9018ddb75..ae1d10447 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index dc12fbbea..2724bd9b3 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -99,7 +101,15 @@ namespace MediaBrowser.Controller.Entities.TV take--; } - list.InsertRange(0, seriesUserDataKeys.Take(take).Select(i => i + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000"))); + var newList = seriesUserDataKeys.GetRange(0, take); + var suffix = ParentIndexNumber.Value.ToString("000", CultureInfo.InvariantCulture) + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture); + for (int i = 0; i < take; i++) + { + newList[i] = newList[i] + suffix; + } + + newList.AddRange(list); + list = newList; } return list; diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 93bdd6e70..ad3e0fe8d 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -1,7 +1,10 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Entities; @@ -56,7 +59,15 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; if (series != null) { - list.InsertRange(0, series.GetUserDataKeys().Select(i => i + (IndexNumber ?? 0).ToString("000"))); + var newList = series.GetUserDataKeys(); + var suffix = (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); + for (int i = 0; i < newList.Count; i++) + { + newList[i] = newList[i] + suffix; + } + + newList.AddRange(list); + list = newList; } return list; diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index a410c1b66..ded825abc 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -169,14 +171,12 @@ namespace MediaBrowser.Controller.Entities.TV { var list = base.GetUserDataKeys(); - var key = this.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(key)) + if (this.TryGetProviderId(MetadataProvider.Imdb, out var key)) { list.Insert(0, key); } - key = this.GetProviderId(MetadataProvider.Tvdb); - if (!string.IsNullOrEmpty(key)) + if (this.TryGetProviderId(MetadataProvider.Tvdb, out key)) { list.Insert(0, key); } @@ -208,7 +208,7 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; query.IncludeItemTypes = new[] { nameof(Season) }; - query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(); + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; if (user != null && !user.DisplayMissingEpisodes) { @@ -228,7 +228,7 @@ namespace MediaBrowser.Controller.Entities.TV query.SeriesPresentationUniqueKey = seriesKey; if (query.OrderBy.Count == 0) { - query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(); + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; } if (query.IncludeItemTypes.Length == 0) @@ -254,7 +254,7 @@ namespace MediaBrowser.Controller.Entities.TV AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, IncludeItemTypes = new[] { nameof(Episode), nameof(Season) }, - OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }; @@ -318,20 +318,13 @@ namespace MediaBrowser.Controller.Entities.TV cancellationToken.ThrowIfCancellationRequested(); - var skipItem = false; - - var episode = item as Episode; - - if (episode != null + bool skipItem = item is Episode episode && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh && !refreshOptions.ReplaceAllMetadata && episode.IsMissingEpisode && episode.LocationType == LocationType.Virtual && episode.PremiereDate.HasValue - && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30) - { - skipItem = true; - } + && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30; if (!skipItem) { @@ -365,7 +358,7 @@ namespace MediaBrowser.Controller.Entities.TV AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey, SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null, IncludeItemTypes = new[] { nameof(Episode) }, - OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }; if (user != null) diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 9ae8ad708..b086b5906 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs index db63c42e4..f60359c01 100644 --- a/MediaBrowser.Controller/Entities/UserItemData.cs +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 7f7224ae0..e492740ed 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index b1da4d64c..0dfde2766 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -75,10 +77,7 @@ namespace MediaBrowser.Controller.Entities public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { - if (query == null) - { - query = new InternalItemsQuery(user); - } + query ??= new InternalItemsQuery(user); query.EnableTotalRecordCount = false; var result = GetItemList(query); diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 4e33a6bbd..15a4573c2 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -217,8 +219,7 @@ namespace MediaBrowser.Controller.Entities private QueryResult<BaseItem> GetMovieLatest(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); - + query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.Recursive = true; query.Parent = parent; query.SetUser(user); @@ -230,7 +231,7 @@ namespace MediaBrowser.Controller.Entities private QueryResult<BaseItem> GetMovieResume(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.IsResumable = true; query.Recursive = true; query.Parent = parent; @@ -327,8 +328,7 @@ namespace MediaBrowser.Controller.Entities private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); - + query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.Recursive = true; query.Parent = parent; query.SetUser(user); @@ -356,7 +356,7 @@ namespace MediaBrowser.Controller.Entities private QueryResult<BaseItem> GetTvResume(Folder parent, User user, InternalItemsQuery query) { - query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(); + query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) }; query.IsResumable = true; query.Recursive = true; query.Parent = parent; diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 6320b01b8..723027a88 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index b2e4d307a..4d84a151a 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Events/IEventConsumer.cs b/MediaBrowser.Controller/Events/IEventConsumer.cs index 5c4ab5d8d..93005134a 100644 --- a/MediaBrowser.Controller/Events/IEventConsumer.cs +++ b/MediaBrowser.Controller/Events/IEventConsumer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace MediaBrowser.Controller.Events diff --git a/MediaBrowser.Controller/Events/IEventManager.cs b/MediaBrowser.Controller/Events/IEventManager.cs index a1f40b3a6..074e3f1fe 100644 --- a/MediaBrowser.Controller/Events/IEventManager.cs +++ b/MediaBrowser.Controller/Events/IEventManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace MediaBrowser.Controller.Events diff --git a/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs b/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs index 46d7e5a17..3a331ad00 100644 --- a/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs +++ b/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Events; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Session; namespace MediaBrowser.Controller.Events.Session diff --git a/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs b/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs index aab19cc46..deeaaf55d 100644 --- a/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs +++ b/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Events; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Session; namespace MediaBrowser.Controller.Events.Session diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs index b06046c05..0dd8b0dbf 100644 --- a/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs +++ b/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Events; +using Jellyfin.Data.Events; using MediaBrowser.Model.Updates; namespace MediaBrowser.Controller.Events.Updates diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs index dfadc9f61..c1d503a7e 100644 --- a/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs +++ b/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Events; +using Jellyfin.Data.Events; using MediaBrowser.Model.Updates; namespace MediaBrowser.Controller.Events.Updates diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs index 045a60027..7a9866834 100644 --- a/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs +++ b/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Events; +using Jellyfin.Data.Events; using MediaBrowser.Model.Updates; namespace MediaBrowser.Controller.Events.Updates diff --git a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs index a111e6d82..0f27be9bb 100644 --- a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs +++ b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs @@ -1,5 +1,4 @@ using Jellyfin.Data.Events; -using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Plugins; namespace MediaBrowser.Controller.Events.Updates diff --git a/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs index 661ca066a..b078e06dc 100644 --- a/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs +++ b/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Events; +using Jellyfin.Data.Events; using MediaBrowser.Model.Updates; namespace MediaBrowser.Controller.Events.Updates diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs index 182c8ef65..8441a3171 100644 --- a/MediaBrowser.Controller/Extensions/StringExtensions.cs +++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs index 041eeea62..1678d5067 100644 --- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs +++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using Jellyfin.Data.Entities; @@ -48,7 +50,7 @@ namespace MediaBrowser.Controller /// <param name="itemId">The item id.</param> /// <param name="client">The client string.</param> /// <returns>The dictionary of custom item display preferences.</returns> - IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client); + Dictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client); /// <summary> /// Sets the custom item display preference for the user and client. diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 6378625e8..094923842 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -1,12 +1,11 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Net; -using System.Threading; -using System.Threading.Tasks; using MediaBrowser.Common; -using MediaBrowser.Common.Plugins; using MediaBrowser.Model.System; using Microsoft.AspNetCore.Http; @@ -55,7 +54,7 @@ namespace MediaBrowser.Controller /// <summary> /// Gets the configured published server url. /// </summary> - Uri PublishedServerUrl { get; } + string PublishedServerUrl { get; } /// <summary> /// Gets the system info. diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index be57d6bca..1890dbb36 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Common.Configuration; diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs index d45493d40..3bb1bd9a0 100644 --- a/MediaBrowser.Controller/Library/IIntroProvider.cs +++ b/MediaBrowser.Controller/Library/IIntroProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 80fcf71f3..782e15398 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -467,6 +469,15 @@ namespace MediaBrowser.Controller.Library void UpdatePeople(BaseItem item, List<PersonInfo> people); /// <summary> + /// Asynchronously updates the people. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="people">The people.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The async task.</returns> + Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken); + + /// <summary> /// Gets the item ids. /// </summary> /// <param name="query">The query.</param> diff --git a/MediaBrowser.Controller/Library/ILiveStream.cs b/MediaBrowser.Controller/Library/ILiveStream.cs index ff25be657..85d866de5 100644 --- a/MediaBrowser.Controller/Library/ILiveStream.cs +++ b/MediaBrowser.Controller/Library/ILiveStream.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Threading; diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index 21c6ef2af..d3d85a056 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Library/IMetadataSaver.cs b/MediaBrowser.Controller/Library/IMetadataSaver.cs index 027cc5b40..5fbfad881 100644 --- a/MediaBrowser.Controller/Library/IMetadataSaver.cs +++ b/MediaBrowser.Controller/Library/IMetadataSaver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs index d12f008e7..5329841bf 100644 --- a/MediaBrowser.Controller/Library/IMusicManager.cs +++ b/MediaBrowser.Controller/Library/IMusicManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index c6a83e4dc..58499e853 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 6e267834b..c95b0ea32 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Library/IUserViewManager.cs b/MediaBrowser.Controller/Library/IUserViewManager.cs index 8d541e8b6..46004e42f 100644 --- a/MediaBrowser.Controller/Library/IUserViewManager.cs +++ b/MediaBrowser.Controller/Library/IUserViewManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Library/IntroInfo.cs b/MediaBrowser.Controller/Library/IntroInfo.cs index 283cc631c..90786786b 100644 --- a/MediaBrowser.Controller/Library/IntroInfo.cs +++ b/MediaBrowser.Controller/Library/IntroInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs index 1798a4fad..a37dc7af1 100644 --- a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs +++ b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index 12a311dc3..0e2d8fb02 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -14,14 +16,14 @@ namespace MediaBrowser.Controller.Library /// These are arguments relating to the file system that are collected once and then referred to /// whenever needed. Primarily for entity resolution. /// </summary> - public class ItemResolveArgs : EventArgs + public class ItemResolveArgs { /// <summary> /// The _app paths. /// </summary> private readonly IServerApplicationPaths _appPaths; - public IDirectoryService DirectoryService { get; private set; } + private LibraryOptions _libraryOptions; /// <summary> /// Initializes a new instance of the <see cref="ItemResolveArgs" /> class. @@ -34,17 +36,18 @@ namespace MediaBrowser.Controller.Library DirectoryService = directoryService; } + public IDirectoryService DirectoryService { get; } + /// <summary> /// Gets the file system children. /// </summary> /// <value>The file system children.</value> public FileSystemMetadata[] FileSystemChildren { get; set; } - public LibraryOptions LibraryOptions { get; set; } - - public LibraryOptions GetLibraryOptions() + public LibraryOptions LibraryOptions { - return LibraryOptions ?? (LibraryOptions = Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent)); + get => _libraryOptions ??= Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent); + set => _libraryOptions = value; } /// <summary> @@ -60,10 +63,10 @@ namespace MediaBrowser.Controller.Library public FileSystemMetadata FileInfo { get; set; } /// <summary> - /// Gets or sets the path. + /// Gets the path. /// </summary> /// <value>The path.</value> - public string Path { get; set; } + public string Path => FileInfo.FullName; /// <summary> /// Gets a value indicating whether this instance is directory. @@ -139,7 +142,7 @@ namespace MediaBrowser.Controller.Library /// Adds the additional location. /// </summary> /// <param name="path">The path.</param> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c> or empty.</exception> public void AddAdditionalLocation(string path) { if (string.IsNullOrEmpty(path)) @@ -147,11 +150,7 @@ namespace MediaBrowser.Controller.Library throw new ArgumentException("The path was empty or null.", nameof(path)); } - if (AdditionalLocations == null) - { - AdditionalLocations = new List<string>(); - } - + AdditionalLocations ??= new List<string>(); AdditionalLocations.Add(path); } @@ -175,7 +174,7 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="name">The name.</param> /// <returns>FileSystemInfo.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception> public FileSystemMetadata GetFileSystemEntryByName(string name) { if (string.IsNullOrEmpty(name)) diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs index 9581603f0..7bc8fa5ab 100644 --- a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs +++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs new file mode 100644 index 000000000..41cfcae16 --- /dev/null +++ b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs @@ -0,0 +1,17 @@ +#nullable disable + +#pragma warning disable CS1591 + +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Controller.Library +{ + public static class MetadataConfigurationExtensions + { + public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config) + { + return config.GetConfiguration<MetadataConfiguration>("metadata"); + } + } +} diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs index f16304db0..a6be6c0d3 100644 --- a/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs +++ b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs @@ -14,18 +14,10 @@ namespace MediaBrowser.Controller.Library { new ConfigurationStore { - Key = "metadata", - ConfigurationType = typeof(MetadataConfiguration) + Key = "metadata", + ConfigurationType = typeof(MetadataConfiguration) } }; } } - - public static class MetadataConfigurationExtensions - { - public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config) - { - return config.GetConfiguration<MetadataConfiguration>("metadata"); - } - } } diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs index 1c90bb4e0..29bfeca09 100644 --- a/MediaBrowser.Controller/Library/NameExtensions.cs +++ b/MediaBrowser.Controller/Library/NameExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 using System; @@ -10,6 +9,10 @@ namespace MediaBrowser.Controller.Library { public static class NameExtensions { + public static IEnumerable<string> DistinctNames(this IEnumerable<string> names) + => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase) + .Select(x => x.First()); + private static string RemoveDiacritics(string? name) { if (name == null) @@ -19,9 +22,5 @@ namespace MediaBrowser.Controller.Library return name.RemoveDiacritics(); } - - public static IEnumerable<string> DistinctNames(this IEnumerable<string> names) - => names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()); } } diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs index a2be3a42a..609336ec4 100644 --- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs +++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs index ac372bceb..2138fef58 100644 --- a/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs +++ b/MediaBrowser.Controller/Library/PlaybackStartEventArgs.cs @@ -1,4 +1,4 @@ -namespace MediaBrowser.Controller.Library +namespace MediaBrowser.Controller.Library { /// <summary> /// An event that occurs when playback is started. diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs index 5efdc6a48..8f42d3706 100644 --- a/MediaBrowser.Controller/Library/Profiler.cs +++ b/MediaBrowser.Controller/Library/Profiler.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Diagnostics; using System.Globalization; diff --git a/MediaBrowser.Controller/Library/SearchHintInfo.cs b/MediaBrowser.Controller/Library/SearchHintInfo.cs index 897c2b7f4..de7806adc 100644 --- a/MediaBrowser.Controller/Library/SearchHintInfo.cs +++ b/MediaBrowser.Controller/Library/SearchHintInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs index a3aa6019e..968338dc6 100644 --- a/MediaBrowser.Controller/Library/TVUtils.cs +++ b/MediaBrowser.Controller/Library/TVUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace MediaBrowser.Controller.Library { @@ -12,7 +13,8 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="day">The day.</param> /// <returns>List{DayOfWeek}.</returns> - public static DayOfWeek[] GetAirDays(string day) + [return: NotNullIfNotNull("day")] + public static DayOfWeek[]? GetAirDays(string? day) { if (!string.IsNullOrEmpty(day)) { diff --git a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs index cd9109753..bfe433c97 100644 --- a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs +++ b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs index 44bd38b54..a55fd670d 100644 --- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Model.LiveTv; @@ -46,6 +48,12 @@ namespace MediaBrowser.Controller.LiveTv public ChannelType ChannelType { get; set; } /// <summary> + /// Gets or sets the group of the channel. + /// </summary> + /// <value>The group of the channel.</value> + public string ChannelGroup { get; set; } + + /// <summary> /// Supply the image path if it can be accessed directly from the file system. /// </summary> /// <value>The image path.</value> diff --git a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs index 038ff2eae..2bd4b20e8 100644 --- a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs +++ b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 54495c1c4..c28e0426b 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index 3ca1d165e..897f263f3 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs index abca8f239..7dced9f5e 100644 --- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index ec933caf3..51e56f4b5 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 43af495dd..d9634a731 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs index b62974904..eb3babc18 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs index 739978e7c..aa5eb59d1 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index f9f559ee9..4a977c5cc 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs index 69190694f..00135afa8 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs index 847c0ea8c..0b943c939 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs index 1343ecd98..1bb649a99 100644 --- a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs index 1b8f41db6..728387c56 100644 --- a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #nullable enable #pragma warning disable CS1591 diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs index aa5170617..e54dc967c 100644 --- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs index 2759b314f..1c1a4417d 100644 --- a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs +++ b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 namespace MediaBrowser.Controller.LiveTv diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 6b1c096ac..37ce35fc2 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -34,6 +34,9 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> <PublishRepositoryUrl>true</PublishRepositoryUrl> <EmbedUntrackedSources>true</EmbedUntrackedSources> <IncludeSymbols>true</IncludeSymbols> @@ -47,14 +50,9 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e5877a484..97cb8d63b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -10,8 +12,6 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; @@ -27,9 +27,7 @@ namespace MediaBrowser.Controller.MediaEncoding private static readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private static readonly string[] _videoProfiles = new[] { @@ -44,14 +42,10 @@ namespace MediaBrowser.Controller.MediaEncoding public EncodingHelper( IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration) + ISubtitleEncoder subtitleEncoder) { _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; _subtitleEncoder = subtitleEncoder; - _configuration = configuration; } public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) @@ -313,6 +307,12 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } + // ISO files don't have an ffmpeg format + if (string.Equals(container, "iso", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + return container; } @@ -541,6 +541,8 @@ namespace MediaBrowser.Controller.MediaEncoding .Append(encodingOptions.VaapiDevice) .Append(' '); } + + arg.Append("-autorotate 0 "); } if (state.IsVideoRequest @@ -585,6 +587,8 @@ namespace MediaBrowser.Controller.MediaEncoding .Append("-init_hw_device qsv@va ") .Append("-hwaccel_output_format vaapi "); } + + arg.Append("-autorotate 0 "); } } @@ -592,7 +596,7 @@ namespace MediaBrowser.Controller.MediaEncoding && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecDecoder) { - arg.Append("-hwaccel_output_format cuda "); + arg.Append("-hwaccel_output_format cuda -autorotate 0 "); } if (state.IsVideoRequest diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index dacd6dea6..1e13382b7 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -9,7 +11,6 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; using MediaBrowser.Model.Session; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs index 1f3abe8f4..88de5b292 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs index fbc827534..c38e7ec3b 100644 --- a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs +++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.IO; diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs index 15a2580af..773547872 100644 --- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs +++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 5cbb57990..d3260280a 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -7,7 +9,6 @@ using System.Threading.Tasks; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.System; diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs index 6ebf7f159..3fb2c47e1 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.IO; diff --git a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs index e7b4c8c15..044ba6d33 100644 --- a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 namespace MediaBrowser.Controller.MediaEncoding diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index cc8820f39..aa5e2c403 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -1,9 +1,10 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Globalization; using System.IO; -using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs index 281d50372..841e7b287 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -1,11 +1,5 @@ #pragma warning disable CS1591 -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using MediaBrowser.Model.IO; - namespace MediaBrowser.Controller.MediaEncoding { /// <summary> diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs index 2cb04bdc4..1dd8bcf31 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Model.Dlna; diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs index 93573e08e..2452b25ab 100644 --- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using Jellyfin.Data.Entities; diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 163a9c8f8..855467e8e 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs index 04b2e13e8..d15c6d318 100644 --- a/MediaBrowser.Controller/Net/IAuthService.cs +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -1,5 +1,3 @@ -#nullable enable - using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index e87f3bca6..5e9fce550 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 #nullable enable diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs index c6347133a..f0d0b45a0 100644 --- a/MediaBrowser.Controller/Net/SecurityException.cs +++ b/MediaBrowser.Controller/Net/SecurityException.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; namespace MediaBrowser.Controller.Net diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs index be0b3ddc3..6f7ebf156 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Model.Net; namespace MediaBrowser.Controller.Net diff --git a/MediaBrowser.Controller/Notifications/INotificationManager.cs b/MediaBrowser.Controller/Notifications/INotificationManager.cs index 08d9bc12a..7caba1097 100644 --- a/MediaBrowser.Controller/Notifications/INotificationManager.cs +++ b/MediaBrowser.Controller/Notifications/INotificationManager.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Controller.Notifications /// <returns>Task.</returns> Task SendNotification(NotificationRequest request, CancellationToken cancellationToken); - Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken); + Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken); /// <summary> /// Adds the parts. diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs index fa947220a..535c08795 100644 --- a/MediaBrowser.Controller/Notifications/INotificationService.cs +++ b/MediaBrowser.Controller/Notifications/INotificationService.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Threading; diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs index d768abfe7..4be0e09ae 100644 --- a/MediaBrowser.Controller/Notifications/UserNotification.cs +++ b/MediaBrowser.Controller/Notifications/UserNotification.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 45c6805f0..56fb36af2 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -153,8 +155,7 @@ namespace MediaBrowser.Controller.Persistence /// <summary> /// Updates the inherited values. /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - void UpdateInheritedValues(CancellationToken cancellationToken); + void UpdateInheritedValues(); int GetCount(InternalItemsQuery query); diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs index 81ba513ce..6f5f02123 100644 --- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs +++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; using System.Threading; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index e8b7be7e2..a80c11643 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -1,8 +1,11 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text.Json.Serialization; using System.Threading; @@ -13,7 +16,6 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Playlists @@ -43,7 +45,8 @@ namespace MediaBrowser.Controller.Playlists public static bool IsPlaylistFile(string path) { - return System.IO.Path.HasExtension(path); + // The path will sometimes be a directory and "Path.HasExtension" returns true if the name contains a '.' (dot). + return System.IO.Path.HasExtension(path) && !Directory.Exists(path); } [JsonIgnore] @@ -125,10 +128,7 @@ namespace MediaBrowser.Controller.Playlists private List<BaseItem> GetPlayableItems(User user, InternalItemsQuery query) { - if (query == null) - { - query = new InternalItemsQuery(user); - } + query ??= new InternalItemsQuery(user); query.IsFolder = false; @@ -162,7 +162,7 @@ namespace MediaBrowser.Controller.Playlists Recursive = true, IncludeItemTypes = new[] { nameof(Audio) }, GenreIds = new[] { musicGenre.Id }, - OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }); } @@ -174,7 +174,7 @@ namespace MediaBrowser.Controller.Playlists Recursive = true, IncludeItemTypes = new[] { nameof(Audio) }, ArtistIds = new[] { musicArtist.Id }, - OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, DtoOptions = options }); } diff --git a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs b/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs deleted file mode 100644 index bf15fe040..000000000 --- a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs +++ /dev/null @@ -1,22 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; -using System.Reflection; - -namespace MediaBrowser.Controller.Plugins -{ - public interface ILocalizablePlugin - { - Stream GetDictionary(string culture); - } - - public static class LocalizablePluginHelper - { - public static Stream GetDictionary(Assembly assembly, string manifestPrefix, string culture) - { - // Find all dictionaries using GetManifestResourceNames, start start with the prefix - // Return the one for the culture if exists, otherwise return the default - return null; - } - } -} diff --git a/MediaBrowser.Controller/Providers/BookInfo.cs b/MediaBrowser.Controller/Providers/BookInfo.cs index cce0a25fc..3055c5d87 100644 --- a/MediaBrowser.Controller/Providers/BookInfo.cs +++ b/MediaBrowser.Controller/Providers/BookInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 namespace MediaBrowser.Controller.Providers diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 16fd1d42b..291a26883 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -12,11 +12,11 @@ namespace MediaBrowser.Controller.Providers { private readonly IFileSystem _fileSystem; - private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new ConcurrentDictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new (StringComparer.Ordinal); - private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new ConcurrentDictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new (StringComparer.Ordinal); - private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new ConcurrentDictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new (StringComparer.Ordinal); public DirectoryService(IFileSystem fileSystem) { @@ -43,18 +43,17 @@ namespace MediaBrowser.Controller.Providers return list; } - public FileSystemMetadata GetFile(string path) + public FileSystemMetadata? GetFile(string path) { - var result = _fileCache.GetOrAdd(path, p => + if (!_fileCache.TryGetValue(path, out var result)) { - var file = _fileSystem.GetFileInfo(p); - return file != null && file.Exists ? file : null; - }); - - if (result == null) - { - // lets not store null results in the cache - _fileCache.TryRemove(path, out _); + var file = _fileSystem.GetFileInfo(path); + var res = file != null && file.Exists ? file : null; + if (res != null) + { + result = res; + _fileCache.TryAdd(path, result); + } } return result; diff --git a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs index 006174be8..66fddb0e3 100644 --- a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs +++ b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index a4c8dab7e..341bf6936 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs index f06481c7a..9cee06a4c 100644 --- a/MediaBrowser.Controller/Providers/IDirectoryService.cs +++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Controller.Providers List<FileSystemMetadata> GetFiles(string path); - FileSystemMetadata GetFile(string path); + FileSystemMetadata? GetFile(string path); IReadOnlyList<string> GetFilePaths(string path); diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs index 5e38446bc..e2dbef2bc 100644 --- a/MediaBrowser.Controller/Providers/IExternalId.cs +++ b/MediaBrowser.Controller/Providers/IExternalId.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs index c129eddb3..f78bd6ddf 100644 --- a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs @@ -10,6 +10,6 @@ namespace MediaBrowser.Controller.Providers /// </summary> public interface ILocalImageProvider : IImageProvider { - List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); + IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); } } diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 2f5b1d4a3..b4d91f396 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -91,8 +93,11 @@ namespace MediaBrowser.Controller.Providers /// <summary> /// Adds the metadata providers. /// </summary> - void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders, - IEnumerable<IMetadataSaver> savers, + void AddParts( + IEnumerable<IImageProvider> imageProviders, + IEnumerable<IMetadataService> metadataServices, + IEnumerable<IMetadataProvider> metadataProviders, + IEnumerable<IMetadataSaver> metadataSavers, IEnumerable<IExternalId> externalIds); /// <summary> diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs index ee8f5b860..de1631dcf 100644 --- a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; diff --git a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs index 9592baa7c..e401ed211 100644 --- a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs @@ -3,7 +3,6 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Net; namespace MediaBrowser.Controller.Providers { diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs index 9fc379f04..81a22affb 100644 --- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs index b50def043..b8dd416a2 100644 --- a/MediaBrowser.Controller/Providers/ItemInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -14,8 +16,7 @@ namespace MediaBrowser.Controller.Providers ContainingFolderPath = item.ContainingFolderPath; IsInMixedFolder = item.IsInMixedFolder; - var video = item as Video; - if (video != null) + if (item is Video video) { VideoType = video.VideoType; IsPlaceHolder = video.IsPlaceHolder; diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs index b777cc1d3..f16669a78 100644 --- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Providers/LocalImageInfo.cs b/MediaBrowser.Controller/Providers/LocalImageInfo.cs index 41801862f..a8e70e6d0 100644 --- a/MediaBrowser.Controller/Providers/LocalImageInfo.cs +++ b/MediaBrowser.Controller/Providers/LocalImageInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index b92b83701..5afc358ba 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -45,10 +47,7 @@ namespace MediaBrowser.Controller.Providers if (copy.RefreshPaths != null && copy.RefreshPaths.Length > 0) { - if (RefreshPaths == null) - { - RefreshPaths = Array.Empty<string>(); - } + RefreshPaths ??= Array.Empty<string>(); RefreshPaths = copy.RefreshPaths.ToArray(); } diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index 1c695cafa..8b0967a6e 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -1,24 +1,30 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Globalization; using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Providers { public class MetadataResult<T> { - public List<LocalImageInfo> Images { get; set; } - - public List<UserItemData> UserDataList { get; set; } - public MetadataResult() { Images = new List<LocalImageInfo>(); + RemoteImages = new List<(string url, ImageType type)>(); ResultLanguage = "en"; } + public List<LocalImageInfo> Images { get; set; } + + public List<(string url, ImageType type)> RemoteImages { get; set; } + + public List<UserItemData> UserDataList { get; set; } + public List<PersonInfo> People { get; set; } public bool HasMetadata { get; set; } @@ -33,10 +39,7 @@ namespace MediaBrowser.Controller.Providers public void AddPerson(PersonInfo p) { - if (People == null) - { - People = new List<PersonInfo>(); - } + People ??= new List<PersonInfo>(); PeopleHelper.AddPerson(People, p); } @@ -50,16 +53,15 @@ namespace MediaBrowser.Controller.Providers { People = new List<PersonInfo>(); } - - People.Clear(); + else + { + People.Clear(); + } } public UserItemData GetOrAddUserData(string userId) { - if (UserDataList == null) - { - UserDataList = new List<UserItemData>(); - } + UserDataList ??= new List<UserItemData>(); UserItemData userData = null; diff --git a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs index 0b927f6eb..322320abd 100644 --- a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs +++ b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs index 9653bc1c4..d830231cf 100644 --- a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs +++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Providers/SongInfo.cs b/MediaBrowser.Controller/Providers/SongInfo.cs index 58f76dca9..c90717a2e 100644 --- a/MediaBrowser.Controller/Providers/SongInfo.cs +++ b/MediaBrowser.Controller/Providers/SongInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs index 959a2d771..c4e709c24 100644 --- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs +++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using MediaBrowser.Model.QuickConnect; diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs index 67acdd9a3..e77593a03 100644 --- a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -11,22 +13,22 @@ namespace MediaBrowser.Controller.Resolvers where T : BaseItem, new() { /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public virtual ResolverPriority Priority => ResolverPriority.First; + + /// <summary> /// Resolves the specified args. /// </summary> /// <param name="args">The args.</param> /// <returns>`0.</returns> - protected virtual T Resolve(ItemResolveArgs args) + public virtual T Resolve(ItemResolveArgs args) { return null; } /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public virtual ResolverPriority Priority => ResolverPriority.First; - - /// <summary> /// Sets initial values on the newly resolved item. /// </summary> /// <param name="item">The item.</param> diff --git a/MediaBrowser.Controller/Security/AuthenticationInfo.cs b/MediaBrowser.Controller/Security/AuthenticationInfo.cs index efac9273e..b4b242f1b 100644 --- a/MediaBrowser.Controller/Security/AuthenticationInfo.cs +++ b/MediaBrowser.Controller/Security/AuthenticationInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs index c5f3da0b1..3af6a525c 100644 --- a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs +++ b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs index 883b74165..1dd69ccd8 100644 --- a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs +++ b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Model.Devices; diff --git a/MediaBrowser.Controller/Session/AuthenticationRequest.cs b/MediaBrowser.Controller/Session/AuthenticationRequest.cs index cc321fd22..647c75e66 100644 --- a/MediaBrowser.Controller/Session/AuthenticationRequest.cs +++ b/MediaBrowser.Controller/Session/AuthenticationRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -12,6 +14,7 @@ namespace MediaBrowser.Controller.Session public string Password { get; set; } + [Obsolete("Send full password in Password field")] public string PasswordSha1 { get; set; } public string App { get; set; } diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index bc4ccd44c..6bc39d6f4 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 6c06dcad5..7eda49c60 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Session/SessionEventArgs.cs b/MediaBrowser.Controller/Session/SessionEventArgs.cs index 097e32eae..269fe7dc4 100644 --- a/MediaBrowser.Controller/Session/SessionEventArgs.cs +++ b/MediaBrowser.Controller/Session/SessionEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index d09852870..5da3783bf 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs index 70cb9eebe..4d9b98889 100644 --- a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs +++ b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs @@ -1,7 +1,5 @@ #pragma warning disable CS1591 -#nullable enable - using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs index 6d03d97ae..bd47db39a 100644 --- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.Sorting diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index 88467814c..aa6ec513f 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index feb26bc10..9e661cbe4 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -1,8 +1,9 @@ +#nullable disable + #pragma warning disable CS1591 using System; using System.Collections.Generic; -using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs index a633262de..326348d58 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs b/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs index ce8141219..c782f5796 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs +++ b/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs index a86b05778..85b3e6fbd 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs +++ b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.IO; diff --git a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs index 7d3c20e8f..0f7c47e76 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs +++ b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs index e7395b136..3d3e44da0 100644 --- a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs +++ b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Threading; diff --git a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs index c97fd7044..3891ac0a6 100644 --- a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs +++ b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Sync/ISyncProvider.cs b/MediaBrowser.Controller/Sync/ISyncProvider.cs index 950cc73e8..ea20014c7 100644 --- a/MediaBrowser.Controller/Sync/ISyncProvider.cs +++ b/MediaBrowser.Controller/Sync/ISyncProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs index a626738fb..7eac52299 100644 --- a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs +++ b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs index 5fb982e85..7e7e759a5 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupMember.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Controller.Session; namespace MediaBrowser.Controller.SyncPlay diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs index e3de22db3..91a13fb28 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay.PlaybackRequests; diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs index 12ce6c8f8..6b5a7cdf9 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/IdleGroupState.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay.PlaybackRequests; diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs index fba8ba9e2..b9786ddb0 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Threading; using MediaBrowser.Controller.Session; diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs index 9797b247c..cb1cadf0b 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Threading; using MediaBrowser.Controller.Session; diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs index 507573653..a0c38b309 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Threading; using MediaBrowser.Controller.Session; diff --git a/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs b/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs index 201f29952..9045063ee 100644 --- a/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/IGroupPlaybackRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Controller/SyncPlay/IGroupState.cs b/MediaBrowser.Controller/SyncPlay/IGroupState.cs index 95ee09985..0666a62a8 100644 --- a/MediaBrowser.Controller/SyncPlay/IGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/IGroupState.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay.PlaybackRequests; diff --git a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs index aa263638a..de26c7d9e 100644 --- a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs +++ b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs index 1c954828c..a6999a12c 100644 --- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs index 4090f65b9..ef496c103 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/AbstractPlaybackRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs index 11cc99fcd..d188114c3 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/BufferGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Threading; using MediaBrowser.Controller.Session; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs index 64ef791ed..464c81dfd 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/IgnoreWaitGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs index 9cd8da566..be314e807 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/MovePlaylistItemGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Threading; using MediaBrowser.Controller.Session; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs index e0ae0deb7..679076239 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/NextItemGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Threading; using MediaBrowser.Controller.Session; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs index 2869b35f7..7ee18a366 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PauseGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs index 8ef3b2030..beab655c5 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PingGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs index 16f9b4087..05ff262c1 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PlayGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs index 166ee0800..3e34b6ce4 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/PreviousItemGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Threading; using MediaBrowser.Controller.Session; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs index d4af63b6d..0f91476de 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/QueueGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs index 74f01cbea..b1f0bd360 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/ReadyGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Threading; using MediaBrowser.Controller.Session; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs index 47c06c222..689145293 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs index ecaa689ae..196113374 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SeekGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs index c3451703e..44df127a6 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetPlaylistItemGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Threading; using MediaBrowser.Controller.Session; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs index 51011672e..d250eab56 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetRepeatModeGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs index d7b2504b4..5034e992e 100644 --- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/SetShuffleModeGroupRequest.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Threading; using MediaBrowser.Controller.Session; using MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs index fdec29417..b8ae9f3ff 100644 --- a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs +++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.LocalMetadata/BaseXmlProvider.cs b/MediaBrowser.LocalMetadata/BaseXmlProvider.cs index b036a2cc8..9f8e921b4 100644 --- a/MediaBrowser.LocalMetadata/BaseXmlProvider.cs +++ b/MediaBrowser.LocalMetadata/BaseXmlProvider.cs @@ -91,7 +91,7 @@ namespace MediaBrowser.LocalMetadata /// <param name="info">Item inf.</param> /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> /// <returns>The file system metadata.</returns> - protected abstract FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService); + protected abstract FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService); /// <inheritdoc /> public bool HasChanged(BaseItem item, IDirectoryService directoryService) diff --git a/MediaBrowser.LocalMetadata/Images/CollectionFolderLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/CollectionFolderLocalImageProvider.cs index 556bb6a0e..b6189bcdd 100644 --- a/MediaBrowser.LocalMetadata/Images/CollectionFolderLocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/CollectionFolderLocalImageProvider.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.LocalMetadata.Images } /// <inheritdoc /> - public List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService) + public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService) { var collectionFolder = (CollectionFolder)item; diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs index 393ad2efb..bc62ca4d5 100644 --- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs @@ -39,9 +39,13 @@ namespace MediaBrowser.LocalMetadata.Images } /// <inheritdoc /> - public List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService) + public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService) { var parentPath = Path.GetDirectoryName(item.Path); + if (parentPath == null) + { + return Enumerable.Empty<LocalImageInfo>(); + } var parentPathFiles = directoryService.GetFiles(parentPath); diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs index 509b5d700..10d691b3e 100644 --- a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -69,13 +70,13 @@ namespace MediaBrowser.LocalMetadata.Images } /// <inheritdoc /> - public List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService) + public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService) { var path = item.GetInternalMetadataPath(); if (!Directory.Exists(path)) { - return new List<LocalImageInfo>(); + return Enumerable.Empty<LocalImageInfo>(); } try @@ -85,7 +86,7 @@ namespace MediaBrowser.LocalMetadata.Images catch (IOException ex) { _logger.LogError(ex, "Error while getting images for {Library}", item.Name); - return new List<LocalImageInfo>(); + return Enumerable.Empty<LocalImageInfo>(); } } } diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 84c3ed8b0..7ad8c24e8 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -108,7 +108,7 @@ namespace MediaBrowser.LocalMetadata.Images { if (!item.IsFileProtocol) { - return new List<FileSystemMetadata>(); + return Enumerable.Empty<FileSystemMetadata>(); } var path = item.ContainingFolderPath; @@ -116,7 +116,7 @@ namespace MediaBrowser.LocalMetadata.Images // Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs... if (!Directory.Exists(path)) { - return Array.Empty<FileSystemMetadata>(); + return Enumerable.Empty<FileSystemMetadata>(); } if (includeDirectories) @@ -133,7 +133,7 @@ namespace MediaBrowser.LocalMetadata.Images } /// <inheritdoc /> - public List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService) + public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService) { var files = GetFiles(item, true, directoryService).ToList(); @@ -151,7 +151,7 @@ namespace MediaBrowser.LocalMetadata.Images /// <param name="path">The images path.</param> /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> /// <returns>The local image info.</returns> - public List<LocalImageInfo> GetImages(BaseItem item, string path, IDirectoryService directoryService) + public IEnumerable<LocalImageInfo> GetImages(BaseItem item, string path, IDirectoryService directoryService) { return GetImages(item, new[] { path }, directoryService); } @@ -163,7 +163,7 @@ namespace MediaBrowser.LocalMetadata.Images /// <param name="paths">The image paths.</param> /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> /// <returns>The local image info.</returns> - public List<LocalImageInfo> GetImages(BaseItem item, IEnumerable<string> paths, IDirectoryService directoryService) + public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IEnumerable<string> paths, IDirectoryService directoryService) { IEnumerable<FileSystemMetadata> files = paths.SelectMany(i => _fileSystem.GetFiles(i, BaseItem.SupportedImageExtensions, true, false)); @@ -181,9 +181,7 @@ namespace MediaBrowser.LocalMetadata.Images { if (supportParentSeriesFiles) { - var season = item as Season; - - if (season != null) + if (item is Season season) { PopulateSeasonImagesFromSeriesFolder(season, images, directoryService); } diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 3ce9ff4cc..eb2077a5f 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -16,6 +16,8 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> @@ -24,14 +26,9 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index b0afb834b..32e5ac761 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -468,8 +468,7 @@ namespace MediaBrowser.LocalMetadata.Parsers { var val = reader.ReadElementContentAsString(); - var hasDisplayOrder = item as IHasDisplayOrder; - if (hasDisplayOrder != null) + if (item is IHasDisplayOrder hasDisplayOrder) { if (!string.IsNullOrWhiteSpace(val)) { @@ -1275,8 +1274,8 @@ namespace MediaBrowser.LocalMetadata.Parsers // Only split by comma if there is no pipe in the string // We have to be careful to not split names like Matthew, Jr. - var separator = value.IndexOf('|', StringComparison.Ordinal) == -1 - && value.IndexOf(';', StringComparison.Ordinal) == -1 ? new[] { ',' } : new[] { '|', ';' }; + var separator = !value.Contains('|', StringComparison.Ordinal) + && !value.Contains(';', StringComparison.Ordinal) ? new[] { ',' } : new[] { '|', ';' }; value = value.Trim().Trim(separator); diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs index ff846830b..7df800971 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.LocalMetadata.Parsers } /// <inheritdoc /> - protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<BoxSet> item) + protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<BoxSet> itemResult) { switch (reader.Name) { @@ -33,7 +33,7 @@ namespace MediaBrowser.LocalMetadata.Parsers { using (var subReader = reader.ReadSubtree()) { - FetchFromCollectionItemsNode(subReader, item); + FetchFromCollectionItemsNode(subReader, itemResult); } } else @@ -44,7 +44,7 @@ namespace MediaBrowser.LocalMetadata.Parsers break; default: - base.FetchDataFromXmlNode(reader, item); + base.FetchDataFromXmlNode(reader, itemResult); break; } } diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index 78c0fa8ad..b84307cb2 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -23,9 +23,9 @@ namespace MediaBrowser.LocalMetadata.Parsers } /// <inheritdoc /> - protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Playlist> result) + protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Playlist> itemResult) { - var item = result.Item; + var item = itemResult.Item; switch (reader.Name) { @@ -53,7 +53,7 @@ namespace MediaBrowser.LocalMetadata.Parsers break; default: - base.FetchDataFromXmlNode(reader, result); + base.FetchDataFromXmlNode(reader, itemResult); break; } } diff --git a/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs b/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs index cc705a9df..ea123bbf2 100644 --- a/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs +++ b/MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.LocalMetadata.Providers } /// <inheritdoc /> - protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) + protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService) { return directoryService.GetFile(Path.Combine(info.Path, "collection.xml")); } diff --git a/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs b/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs index 36f3048ad..899035652 100644 --- a/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs +++ b/MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs @@ -39,7 +39,7 @@ namespace MediaBrowser.LocalMetadata.Providers } /// <inheritdoc /> - protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) + protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService) { return directoryService.GetFile(PlaylistXmlSaver.GetSavePath(info.Path)); } diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index a337521c6..98ed3dcf7 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Text; using System.Threading; using System.Xml; -using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -37,7 +36,7 @@ namespace MediaBrowser.LocalMetadata.Savers /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{BaseXmlSaver}"/> interface.</param> - public BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger<BaseXmlSaver> logger) + protected BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger<BaseXmlSaver> logger) { FileSystem = fileSystem; ConfigurationManager = configurationManager; @@ -133,7 +132,8 @@ namespace MediaBrowser.LocalMetadata.Savers // On Windows, savint the file will fail if the file is hidden or readonly FileSystem.SetAttributes(path, false, false); - using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) { stream.CopyTo(filestream); } @@ -296,8 +296,7 @@ namespace MediaBrowser.LocalMetadata.Savers writer.WriteEndElement(); } - var hasDisplayOrder = item as IHasDisplayOrder; - if (hasDisplayOrder != null && !string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder)) + if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder)) { writer.WriteElementString("DisplayOrder", hasDisplayOrder.DisplayOrder); } @@ -312,8 +311,7 @@ namespace MediaBrowser.LocalMetadata.Savers writer.WriteElementString("ProductionYear", item.ProductionYear.Value.ToString(_usCulture)); } - var hasAspectRatio = item as IHasAspectRatio; - if (hasAspectRatio != null) + if (item is IHasAspectRatio hasAspectRatio) { if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio)) { @@ -420,20 +418,17 @@ namespace MediaBrowser.LocalMetadata.Savers writer.WriteEndElement(); } - var boxset = item as BoxSet; - if (boxset != null) + if (item is BoxSet boxset) { AddLinkedChildren(boxset, writer, "CollectionItems", "CollectionItem"); } - var playlist = item as Playlist; - if (playlist != null && !Playlist.IsPlaylistFile(playlist.Path)) + if (item is Playlist playlist && !Playlist.IsPlaylistFile(playlist.Path)) { AddLinkedChildren(playlist, writer, "PlaylistItems", "PlaylistItem"); } - var hasShares = item as IHasShares; - if (hasShares != null) + if (item is IHasShares hasShares) { AddShares(hasShares, writer); } @@ -541,10 +536,5 @@ namespace MediaBrowser.LocalMetadata.Savers writer.WriteEndElement(); } - - private bool IsPersonType(PersonInfo person, string type) - { - return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); - } } } diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs index 4a54b677d..ef9943722 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -71,7 +71,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo _impl.FullName, new[] { searchPattern }, false, - searchOption.HasFlag(System.IO.SearchOption.AllDirectories)).ToArray(), + (searchOption & System.IO.SearchOption.AllDirectories) == System.IO.SearchOption.AllDirectories).ToArray(), x => new BdInfoFileInfo(x)); } diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs index 9108d9649..6ebaa4fff 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs @@ -61,33 +61,25 @@ namespace MediaBrowser.MediaEncoding.BdInfo foreach (var stream in playlist.SortedStreams) { - var videoStream = stream as TSVideoStream; - - if (videoStream != null) + if (stream is TSVideoStream videoStream) { AddVideoStream(mediaStreams, videoStream); continue; } - var audioStream = stream as TSAudioStream; - - if (audioStream != null) + if (stream is TSAudioStream audioStream) { AddAudioStream(mediaStreams, audioStream); continue; } - var textStream = stream as TSTextStream; - - if (textStream != null) + if (stream is TSTextStream textStream) { AddSubtitleStream(mediaStreams, textStream); continue; } - var graphicsStream = stream as TSGraphicsStream; - - if (graphicsStream != null) + if (stream is TSGraphicsStream graphicsStream) { AddSubtitleStream(mediaStreams, graphicsStream); } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index a52019384..62c0c0bb1 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -16,7 +16,6 @@ using MediaBrowser.Common.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -53,7 +52,6 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly IServerConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; - private readonly Lazy<EncodingHelper> _encodingHelperFactory; private readonly string _startupOptionFFmpegPath; private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2); @@ -77,16 +75,14 @@ namespace MediaBrowser.MediaEncoding.Encoder IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, - Lazy<EncodingHelper> encodingHelperFactory, IConfiguration config) { _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; _localization = localization; - _encodingHelperFactory = encodingHelperFactory; _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty; - _jsonSerializerOptions = JsonDefaults.GetOptions(); + _jsonSerializerOptions = JsonDefaults.Options; } /// <inheritdoc /> @@ -370,7 +366,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) { var prefix = "file"; - if (mediaSource.VideoType == VideoType.BluRay || mediaSource.VideoType == VideoType.Iso) + if (mediaSource.VideoType == VideoType.BluRay) { prefix = "bluray"; } @@ -448,7 +444,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (result == null || (result.Streams == null && result.Format == null)) { - throw new Exception("ffprobe failed - streams and format are both null."); + throw new FfmpegException("ffprobe failed - streams and format are both null."); } if (result.Streams != null) @@ -571,32 +567,18 @@ namespace MediaBrowser.MediaEncoding.Encoder // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar. // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar - var vf = string.Empty; - - if (threedFormat.HasValue) + var vf = threedFormat switch { - switch (threedFormat.Value) - { - case Video3DFormat.HalfSideBySide: - vf = "-vf crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1"; - // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not. - break; - case Video3DFormat.FullSideBySide: - vf = "-vf crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1"; - // fsbs crop width in half,set the display aspect,crop out any black bars we may have made - break; - case Video3DFormat.HalfTopAndBottom: - vf = "-vf crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1"; - // htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made - break; - case Video3DFormat.FullTopAndBottom: - vf = "-vf crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1"; - // ftab crop heigt in half, set the display aspect,crop out any black bars we may have made - break; - default: - break; - } - } + // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not. + Video3DFormat.HalfSideBySide => "-vf crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + // fsbs crop width in half,set the display aspect,crop out any black bars we may have made + Video3DFormat.FullSideBySide => "-vf crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + // htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made + Video3DFormat.HalfTopAndBottom => "-vf crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + // ftab crop heigt in half, set the display aspect,crop out any black bars we may have made + Video3DFormat.FullTopAndBottom => "-vf crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", + _ => string.Empty + }; var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; @@ -604,7 +586,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (enableHdrExtraction) { string tonemapFilters = "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p"; - if (string.IsNullOrEmpty(vf)) + if (vf.Length == 0) { vf = "-vf " + tonemapFilters; } @@ -633,35 +615,11 @@ namespace MediaBrowser.MediaEncoding.Encoder var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads); - var probeSizeArgument = string.Empty; - var analyzeDurationArgument = string.Empty; - - if (!string.IsNullOrWhiteSpace(probeSizeArgument)) - { - args = probeSizeArgument + " " + args; - } - - if (!string.IsNullOrWhiteSpace(analyzeDurationArgument)) - { - args = analyzeDurationArgument + " " + args; - } - if (offset.HasValue) { args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args; } - if (videoStream != null) - { - /* fix - var decoder = encodinghelper.GetHardwareAcceleratedVideoDecoder(VideoType.VideoFile, videoStream, GetEncodingOptions()); - if (!string.IsNullOrWhiteSpace(decoder)) - { - args = decoder + " " + args; - } - */ - } - if (!string.IsNullOrWhiteSpace(container)) { var inputFormat = EncodingHelper.GetInputFormat(container); @@ -723,7 +681,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogError(msg); - throw new Exception(msg); + throw new FfmpegException(msg); } return tempExtractPath; @@ -770,30 +728,6 @@ namespace MediaBrowser.MediaEncoding.Encoder var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, threads); - var probeSizeArgument = string.Empty; - var analyzeDurationArgument = string.Empty; - - if (!string.IsNullOrWhiteSpace(probeSizeArgument)) - { - args = probeSizeArgument + " " + args; - } - - if (!string.IsNullOrWhiteSpace(analyzeDurationArgument)) - { - args = analyzeDurationArgument + " " + args; - } - - if (videoStream != null) - { - /* fix - var decoder = encodinghelper.GetHardwareAcceleratedVideoDecoder(VideoType.VideoFile, videoStream, GetEncodingOptions()); - if (!string.IsNullOrWhiteSpace(decoder)) - { - args = decoder + " " + args; - } - */ - } - if (!string.IsNullOrWhiteSpace(container)) { var inputFormat = EncodingHelper.GetInputFormat(container); @@ -872,7 +806,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogError(msg); - throw new Exception(msg); + throw new FfmpegException(msg); } } } diff --git a/MediaBrowser.MediaEncoding/FfmpegException.cs b/MediaBrowser.MediaEncoding/FfmpegException.cs new file mode 100644 index 000000000..1697fd33a --- /dev/null +++ b/MediaBrowser.MediaEncoding/FfmpegException.cs @@ -0,0 +1,39 @@ +using System; + +namespace MediaBrowser.MediaEncoding +{ + /// <summary> + /// Represents errors that occur during interaction with FFmpeg. + /// </summary> + public class FfmpegException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="FfmpegException"/> class. + /// </summary> + public FfmpegException() + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="FfmpegException"/> class with a specified error message. + /// </summary> + /// <param name="message">The message that describes the error.</param> + public FfmpegException(string message) : base(message) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="FfmpegException"/> class with a specified error message and a + /// reference to the inner exception that is the cause of this exception. + /// </summary> + /// <param name="message">The error message that explains the reason for the exception.</param> + /// <param name="innerException"> + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if + /// no inner exception is specified. + /// </param> + public FfmpegException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 3d6b4f98a..39fb0b47c 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -10,6 +10,8 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> @@ -30,13 +32,8 @@ <PackageReference Include="UTF.Unknown" Version="2.3.0" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs index b2d4db894..da37687e8 100644 --- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs +++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; namespace MediaBrowser.MediaEncoding.Probing { @@ -85,12 +86,14 @@ namespace MediaBrowser.MediaEncoding.Probing { var val = GetDictionaryValue(tags, key); - if (!string.IsNullOrEmpty(val)) + if (string.IsNullOrEmpty(val)) { - if (DateTime.TryParse(val, out var i)) - { - return i.ToUniversalTime(); - } + return null; + } + + if (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out var i)) + { + return i.ToUniversalTime(); } return null; diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs index 8a7c032c5..7b7744163 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -using MediaBrowser.Common.Json.Converters; namespace MediaBrowser.MediaEncoding.Probing { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index b9cb49cf2..884ec0a29 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -29,7 +29,7 @@ namespace MediaBrowser.MediaEncoding.Probing private readonly ILogger _logger; private readonly ILocalizationManager _localization; - private List<string> _splitWhiteList = null; + private string[] _splitWhiteList; public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization) { @@ -37,6 +37,8 @@ namespace MediaBrowser.MediaEncoding.Probing _localization = localization; } + private IReadOnlyList<string> SplitWhitelist => _splitWhiteList ??= new string[] { "AC/DC" }; + public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol) { var info = new MediaInfo @@ -57,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Probing .Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec)) .ToList(); - info.MediaAttachments = internalStreams.Select(s => GetMediaAttachment(s)) + info.MediaAttachments = internalStreams.Select(GetMediaAttachment) .Where(i => i != null) .ToList(); @@ -131,6 +133,7 @@ namespace MediaBrowser.MediaEncoding.Probing info.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ?? + FFProbeHelpers.GetDictionaryDateTime(tags, "date_released") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "date"); if (isAudio) @@ -640,7 +643,7 @@ namespace MediaBrowser.MediaEncoding.Probing } // Filter out junk - if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && streamInfo.CodecTagString.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1) + if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && !streamInfo.CodecTagString.Contains("[0]", StringComparison.OrdinalIgnoreCase)) { stream.CodecTag = streamInfo.CodecTagString; } @@ -1186,43 +1189,28 @@ namespace MediaBrowser.MediaEncoding.Probing FetchStudios(audio, tags, "label"); // These support mulitple values, but for now we only store the first. - var mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")); - if (mb == null) - { - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMARTISTID")); - } + var mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")) + ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMARTISTID")); audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb); - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id")); - if (mb == null) - { - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ARTISTID")); - } + mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id")) + ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ARTISTID")); audio.SetProviderId(MetadataProvider.MusicBrainzArtist, mb); - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id")); - if (mb == null) - { - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMID")); - } + mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id")) + ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMID")); audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, mb); - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id")); - if (mb == null) - { - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASEGROUPID")); - } + mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id")) + ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASEGROUPID")); audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb); - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id")); - if (mb == null) - { - mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASETRACKID")); - } + mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id")) + ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASETRACKID")); audio.SetProviderId(MetadataProvider.MusicBrainzTrack, mb); } @@ -1268,7 +1256,7 @@ namespace MediaBrowser.MediaEncoding.Probing var artistsFound = new List<string>(); - foreach (var whitelistArtist in GetSplitWhitelist()) + foreach (var whitelistArtist in SplitWhitelist) { var originalVal = val; val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase); @@ -1287,19 +1275,6 @@ namespace MediaBrowser.MediaEncoding.Probing return artistsFound; } - private IEnumerable<string> GetSplitWhitelist() - { - if (_splitWhiteList == null) - { - _splitWhiteList = new List<string> - { - "AC/DC" - }; - } - - return _splitWhiteList; - } - /// <summary> /// Gets the studios from the tags collection. /// </summary> @@ -1500,11 +1475,23 @@ namespace MediaBrowser.MediaEncoding.Probing } else { - throw new Exception(); // Switch to default parsing + // Switch to default parsing + if (subtitle.Contains('.', StringComparison.Ordinal)) + { + // skip the comment, keep the subtitle + description = string.Join('.', subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first + } + else + { + description = subtitle.Trim(); // Clean up whitespaces and save it + } } } - catch // Default parsing + catch (Exception ex) { + _logger.LogError(ex, "Error while parsing subtitle field"); + + // Default parsing if (subtitle.Contains('.', StringComparison.Ordinal)) { // skip the comment, keep the subtitle diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index d19538730..39bec8da1 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -25,7 +25,7 @@ using UtfUnknown; namespace MediaBrowser.MediaEncoding.Subtitles { - public class SubtitleEncoder : ISubtitleEncoder + public sealed class SubtitleEncoder : ISubtitleEncoder { private readonly ILogger<SubtitleEncoder> _logger; private readonly IApplicationPaths _appPaths; @@ -484,7 +484,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { _logger.LogError("ffmpeg subtitle conversion failed for {Path}", inputPath); - throw new Exception( + throw new FfmpegException( string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath)); } @@ -637,7 +637,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles _logger.LogError(msg); - throw new Exception(msg); + throw new FfmpegException(msg); } else { @@ -677,7 +677,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!string.Equals(text, newText, StringComparison.Ordinal)) { - using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None)) using (var writer = new StreamWriter(fileStream, encoding)) { await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs b/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs index fcc90a1f7..f9f474586 100644 --- a/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs +++ b/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs @@ -1,32 +1,43 @@ -#nullable disable -#pragma warning disable CS1591 - namespace MediaBrowser.Model.ApiClient { + /// <summary> + /// The server discovery info model. + /// </summary> public class ServerDiscoveryInfo { /// <summary> - /// Gets or sets the address. + /// Initializes a new instance of the <see cref="ServerDiscoveryInfo"/> class. + /// </summary> + /// <param name="address">The server address.</param> + /// <param name="id">The server id.</param> + /// <param name="name">The server name.</param> + /// <param name="endpointAddress">The endpoint address.</param> + public ServerDiscoveryInfo(string address, string id, string name, string? endpointAddress = null) + { + Address = address; + Id = id; + Name = name; + EndpointAddress = endpointAddress; + } + + /// <summary> + /// Gets the address. /// </summary> - /// <value>The address.</value> - public string Address { get; set; } + public string Address { get; } /// <summary> - /// Gets or sets the server identifier. + /// Gets the server identifier. /// </summary> - /// <value>The server identifier.</value> - public string Id { get; set; } + public string Id { get; } /// <summary> - /// Gets or sets the name. + /// Gets the name. /// </summary> - /// <value>The name.</value> - public string Name { get; set; } + public string Name { get; } /// <summary> - /// Gets or sets the endpoint address. + /// Gets the endpoint address. /// </summary> - /// <value>The endpoint address.</value> - public string EndpointAddress { get; set; } + public string? EndpointAddress { get; } } } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index a9b280301..365bbeef6 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Model.Configuration EnableDecodingColorDepth10Vp9 = true; EnableEnhancedNvdecDecoder = true; EnableHardwareEncoding = true; - AllowHevcEncoding = true; + AllowHevcEncoding = false; EnableSubtitleExtraction = true; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; } diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index d83c8f2f3..740966088 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CS1591 using System; @@ -9,25 +8,15 @@ namespace MediaBrowser.Model.Dlna { public class ContainerProfile { - public ContainerProfile() - { - Conditions = Array.Empty<ProfileCondition>(); - } - [XmlAttribute("type")] public DlnaProfileType Type { get; set; } - public ProfileCondition[] Conditions { get; set; } + public ProfileCondition[]? Conditions { get; set; } = Array.Empty<ProfileCondition>(); [XmlAttribute("container")] - public string Container { get; set; } - - public string[] GetContainers() - { - return SplitValue(Container); - } + public string Container { get; set; } = string.Empty; - public static string[] SplitValue(string value) + public static string[] SplitValue(string? value) { if (string.IsNullOrEmpty(value)) { @@ -37,14 +26,14 @@ namespace MediaBrowser.Model.Dlna return value.Split(',', StringSplitOptions.RemoveEmptyEntries); } - public bool ContainsContainer(string container) + public bool ContainsContainer(string? container) { - var containers = GetContainers(); + var containers = SplitValue(Container); return ContainsContainer(containers, container); } - public static bool ContainsContainer(string profileContainers, string inputContainer) + public static bool ContainsContainer(string? profileContainers, string? inputContainer) { var isNegativeList = false; if (profileContainers != null && profileContainers.StartsWith('-')) @@ -56,46 +45,30 @@ namespace MediaBrowser.Model.Dlna return ContainsContainer(SplitValue(profileContainers), isNegativeList, inputContainer); } - public static bool ContainsContainer(string[] profileContainers, string inputContainer) + public static bool ContainsContainer(string[]? profileContainers, string? inputContainer) { return ContainsContainer(profileContainers, false, inputContainer); } - public static bool ContainsContainer(string[] profileContainers, bool isNegativeList, string inputContainer) + public static bool ContainsContainer(string[]? profileContainers, bool isNegativeList, string? inputContainer) { - if (profileContainers.Length == 0) + if (profileContainers == null || profileContainers.Length == 0) { + // Empty profiles always support all containers/codecs return true; } - if (isNegativeList) - { - var allInputContainers = SplitValue(inputContainer); + var allInputContainers = SplitValue(inputContainer); - foreach (var container in allInputContainers) - { - if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase)) - { - return false; - } - } - - return true; - } - else + foreach (var container in allInputContainers) { - var allInputContainers = SplitValue(inputContainer); - - foreach (var container in allInputContainers) + if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase)) { - if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase)) - { - return true; - } + return !isNegativeList; } - - return false; } + + return isNegativeList; } } } diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index ec106f105..600a44157 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -10,14 +10,8 @@ namespace MediaBrowser.Model.Dlna { public class ContentFeatureBuilder { - private readonly DeviceProfile _profile; - - public ContentFeatureBuilder(DeviceProfile profile) - { - _profile = profile; - } - - public string BuildImageHeader( + public static string BuildImageHeader( + DeviceProfile profile, string container, int? width, int? height, @@ -38,27 +32,31 @@ namespace MediaBrowser.Model.Dlna ";DLNA.ORG_FLAGS={0}", DlnaMaps.FlagsToString(flagValue)); - ResponseProfile mediaProfile = _profile.GetImageMediaProfile( - container, - width, - height); - if (string.IsNullOrEmpty(orgPn)) { + ResponseProfile mediaProfile = profile.GetImageMediaProfile( + container, + width, + height); + orgPn = mediaProfile?.OrgPn; + + if (string.IsNullOrEmpty(orgPn)) + { + orgPn = GetImageOrgPnValue(container, width, height); + } } if (string.IsNullOrEmpty(orgPn)) { - orgPn = GetImageOrgPnValue(container, width, height); + return orgOp.TrimStart(';') + orgCi + dlnaflags; } - string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn; - - return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); + return "DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags; } - public string BuildAudioHeader( + public static string BuildAudioHeader( + DeviceProfile profile, string container, string audioCodec, int? audioBitrate, @@ -94,7 +92,7 @@ namespace MediaBrowser.Model.Dlna ";DLNA.ORG_FLAGS={0}", DlnaMaps.FlagsToString(flagValue)); - ResponseProfile mediaProfile = _profile.GetAudioMediaProfile( + ResponseProfile mediaProfile = profile.GetAudioMediaProfile( container, audioCodec, audioChannels, @@ -109,12 +107,16 @@ namespace MediaBrowser.Model.Dlna orgPn = GetAudioOrgPnValue(container, audioBitrate, audioSampleRate, audioChannels); } - string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn; + if (string.IsNullOrEmpty(orgPn)) + { + return orgOp.TrimStart(';') + orgCi + dlnaflags; + } - return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); + return "DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags; } - public List<string> BuildVideoHeader( + public static List<string> BuildVideoHeader( + DeviceProfile profile, string container, string videoCodec, string audioCodec, @@ -163,7 +165,7 @@ namespace MediaBrowser.Model.Dlna ";DLNA.ORG_FLAGS={0}", DlnaMaps.FlagsToString(flagValue)); - ResponseProfile mediaProfile = _profile.GetVideoMediaProfile( + ResponseProfile mediaProfile = profile.GetVideoMediaProfile( container, audioCodec, videoCodec, @@ -192,9 +194,9 @@ namespace MediaBrowser.Model.Dlna } else { - foreach (string s in GetVideoOrgPnValue(container, videoCodec, audioCodec, width, height, timestamp)) + foreach (var s in GetVideoOrgPnValue(container, videoCodec, audioCodec, width, height, timestamp)) { - orgPnValues.Add(s); + orgPnValues.Add(s.ToString()); break; } } @@ -203,20 +205,20 @@ namespace MediaBrowser.Model.Dlna foreach (string orgPn in orgPnValues) { - string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn; - - var value = (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); - - contentFeatureList.Add(value); + if (string.IsNullOrEmpty(orgPn)) + { + contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags); + continue; + } + else + { + contentFeatureList.Add("DLNA.ORG_PN=" + orgPn + orgCi + dlnaflags); + } } if (orgPnValues.Count == 0) { - string contentFeatures = string.Empty; - - var value = (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); - - contentFeatureList.Add(value); + contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags); } return contentFeatureList; @@ -224,19 +226,14 @@ namespace MediaBrowser.Model.Dlna private static string GetImageOrgPnValue(string container, int? width, int? height) { - MediaFormatProfile? format = new MediaFormatProfileResolver() - .ResolveImageFormat( - container, - width, - height); + MediaFormatProfile? format = MediaFormatProfileResolver.ResolveImageFormat(container, width, height); return format.HasValue ? format.Value.ToString() : null; } private static string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels) { - MediaFormatProfile? format = new MediaFormatProfileResolver() - .ResolveAudioFormat( + MediaFormatProfile? format = MediaFormatProfileResolver.ResolveAudioFormat( container, audioBitrate, audioSampleRate, @@ -245,9 +242,9 @@ namespace MediaBrowser.Model.Dlna return format.HasValue ? format.Value.ToString() : null; } - private static string[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp) + private static MediaFormatProfile[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp) { - return new MediaFormatProfileResolver().ResolveVideoFormat(container, videoCodec, audioCodec, width, height, timestamp); + return MediaFormatProfileResolver.ResolveVideoFormat(container, videoCodec, audioCodec, width, height, timestamp); } } } diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index ff5186658..feb3d880e 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -1,6 +1,6 @@ -#nullable disable #pragma warning disable CA1819 // Properties should not return arrays using System; +using System.ComponentModel; using System.Linq; using System.Xml.Serialization; using MediaBrowser.Model.MediaInfo; @@ -8,226 +8,219 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna { /// <summary> - /// Defines the <see cref="DeviceProfile" />. + /// A <see cref="DeviceProfile" /> represents a set of metadata which determines which content a certain device is able to play. + /// <br/> + /// Specifically, it defines the supported <see cref="ContainerProfiles">containers</see> and + /// <see cref="CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels) + /// the device is able to direct play (without transcoding or remuxing), + /// as well as which <see cref="TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't. /// </summary> [XmlRoot("Profile")] public class DeviceProfile { /// <summary> - /// Initializes a new instance of the <see cref="DeviceProfile"/> class. + /// Gets or sets the name of this device profile. /// </summary> - public DeviceProfile() - { - DirectPlayProfiles = Array.Empty<DirectPlayProfile>(); - TranscodingProfiles = Array.Empty<TranscodingProfile>(); - ResponseProfiles = Array.Empty<ResponseProfile>(); - CodecProfiles = Array.Empty<CodecProfile>(); - ContainerProfiles = Array.Empty<ContainerProfile>(); - SubtitleProfiles = Array.Empty<SubtitleProfile>(); - - XmlRootAttributes = Array.Empty<XmlAttribute>(); - - SupportedMediaTypes = "Audio,Photo,Video"; - MaxStreamingBitrate = 8000000; - MaxStaticBitrate = 8000000; - MusicStreamingTranscodingBitrate = 128000; - } - - /// <summary> - /// Gets or sets the Name. - /// </summary> - public string Name { get; set; } + public string? Name { get; set; } /// <summary> /// Gets or sets the Id. /// </summary> [XmlIgnore] - public string Id { get; set; } + public string? Id { get; set; } /// <summary> /// Gets or sets the Identification. /// </summary> - public DeviceIdentification Identification { get; set; } + public DeviceIdentification? Identification { get; set; } /// <summary> - /// Gets or sets the FriendlyName. + /// Gets or sets the friendly name of the device profile, which can be shown to users. /// </summary> - public string FriendlyName { get; set; } + public string? FriendlyName { get; set; } /// <summary> - /// Gets or sets the Manufacturer. + /// Gets or sets the manufacturer of the device which this profile represents. /// </summary> - public string Manufacturer { get; set; } + public string? Manufacturer { get; set; } /// <summary> - /// Gets or sets the ManufacturerUrl. + /// Gets or sets an url for the manufacturer of the device which this profile represents. /// </summary> - public string ManufacturerUrl { get; set; } + public string? ManufacturerUrl { get; set; } /// <summary> - /// Gets or sets the ModelName. + /// Gets or sets the model name of the device which this profile represents. /// </summary> - public string ModelName { get; set; } + public string? ModelName { get; set; } /// <summary> - /// Gets or sets the ModelDescription. + /// Gets or sets the model description of the device which this profile represents. /// </summary> - public string ModelDescription { get; set; } + public string? ModelDescription { get; set; } /// <summary> - /// Gets or sets the ModelNumber. + /// Gets or sets the model number of the device which this profile represents. /// </summary> - public string ModelNumber { get; set; } + public string? ModelNumber { get; set; } /// <summary> /// Gets or sets the ModelUrl. /// </summary> - public string ModelUrl { get; set; } + public string? ModelUrl { get; set; } /// <summary> - /// Gets or sets the SerialNumber. + /// Gets or sets the serial number of the device which this profile represents. /// </summary> - public string SerialNumber { get; set; } + public string? SerialNumber { get; set; } /// <summary> /// Gets or sets a value indicating whether EnableAlbumArtInDidl. /// </summary> + [DefaultValue(false)] public bool EnableAlbumArtInDidl { get; set; } /// <summary> /// Gets or sets a value indicating whether EnableSingleAlbumArtLimit. /// </summary> + [DefaultValue(false)] public bool EnableSingleAlbumArtLimit { get; set; } /// <summary> /// Gets or sets a value indicating whether EnableSingleSubtitleLimit. /// </summary> + [DefaultValue(false)] public bool EnableSingleSubtitleLimit { get; set; } /// <summary> /// Gets or sets the SupportedMediaTypes. /// </summary> - public string SupportedMediaTypes { get; set; } + public string SupportedMediaTypes { get; set; } = "Audio,Photo,Video"; /// <summary> /// Gets or sets the UserId. /// </summary> - public string UserId { get; set; } + public string? UserId { get; set; } /// <summary> /// Gets or sets the AlbumArtPn. /// </summary> - public string AlbumArtPn { get; set; } + public string? AlbumArtPn { get; set; } /// <summary> /// Gets or sets the MaxAlbumArtWidth. /// </summary> - public int MaxAlbumArtWidth { get; set; } + public int? MaxAlbumArtWidth { get; set; } /// <summary> /// Gets or sets the MaxAlbumArtHeight. /// </summary> - public int MaxAlbumArtHeight { get; set; } + public int? MaxAlbumArtHeight { get; set; } /// <summary> - /// Gets or sets the MaxIconWidth. + /// Gets or sets the maximum allowed width of embedded icons. /// </summary> public int? MaxIconWidth { get; set; } /// <summary> - /// Gets or sets the MaxIconHeight. + /// Gets or sets the maximum allowed height of embedded icons. /// </summary> public int? MaxIconHeight { get; set; } /// <summary> - /// Gets or sets the MaxStreamingBitrate. + /// Gets or sets the maximum allowed bitrate for all streamed content. /// </summary> - public int? MaxStreamingBitrate { get; set; } + public int? MaxStreamingBitrate { get; set; } = 8000000; /// <summary> - /// Gets or sets the MaxStaticBitrate. + /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files). /// </summary> - public int? MaxStaticBitrate { get; set; } + public int? MaxStaticBitrate { get; set; } = 8000000; /// <summary> - /// Gets or sets the MusicStreamingTranscodingBitrate. + /// Gets or sets the maximum allowed bitrate for transcoded music streams. /// </summary> - public int? MusicStreamingTranscodingBitrate { get; set; } + public int? MusicStreamingTranscodingBitrate { get; set; } = 128000; /// <summary> - /// Gets or sets the MaxStaticMusicBitrate. + /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files. /// </summary> - public int? MaxStaticMusicBitrate { get; set; } + public int? MaxStaticMusicBitrate { get; set; } = 8000000; /// <summary> /// Gets or sets the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace. /// </summary> - public string SonyAggregationFlags { get; set; } + public string? SonyAggregationFlags { get; set; } /// <summary> /// Gets or sets the ProtocolInfo. /// </summary> - public string ProtocolInfo { get; set; } + public string? ProtocolInfo { get; set; } /// <summary> /// Gets or sets the TimelineOffsetSeconds. /// </summary> + [DefaultValue(0)] public int TimelineOffsetSeconds { get; set; } /// <summary> /// Gets or sets a value indicating whether RequiresPlainVideoItems. /// </summary> + [DefaultValue(false)] public bool RequiresPlainVideoItems { get; set; } /// <summary> /// Gets or sets a value indicating whether RequiresPlainFolders. /// </summary> + [DefaultValue(false)] public bool RequiresPlainFolders { get; set; } /// <summary> /// Gets or sets a value indicating whether EnableMSMediaReceiverRegistrar. /// </summary> + [DefaultValue(false)] public bool EnableMSMediaReceiverRegistrar { get; set; } /// <summary> /// Gets or sets a value indicating whether IgnoreTranscodeByteRangeRequests. /// </summary> + [DefaultValue(false)] public bool IgnoreTranscodeByteRangeRequests { get; set; } /// <summary> /// Gets or sets the XmlRootAttributes. /// </summary> - public XmlAttribute[] XmlRootAttributes { get; set; } + public XmlAttribute[] XmlRootAttributes { get; set; } = Array.Empty<XmlAttribute>(); /// <summary> /// Gets or sets the direct play profiles. /// </summary> - public DirectPlayProfile[] DirectPlayProfiles { get; set; } + public DirectPlayProfile[] DirectPlayProfiles { get; set; } = Array.Empty<DirectPlayProfile>(); /// <summary> /// Gets or sets the transcoding profiles. /// </summary> - public TranscodingProfile[] TranscodingProfiles { get; set; } + public TranscodingProfile[] TranscodingProfiles { get; set; } = Array.Empty<TranscodingProfile>(); /// <summary> - /// Gets or sets the ContainerProfiles. + /// Gets or sets the container profiles. /// </summary> - public ContainerProfile[] ContainerProfiles { get; set; } + public ContainerProfile[] ContainerProfiles { get; set; } = Array.Empty<ContainerProfile>(); /// <summary> - /// Gets or sets the CodecProfiles. + /// Gets or sets the codec profiles. /// </summary> - public CodecProfile[] CodecProfiles { get; set; } + public CodecProfile[] CodecProfiles { get; set; } = Array.Empty<CodecProfile>(); /// <summary> /// Gets or sets the ResponseProfiles. /// </summary> - public ResponseProfile[] ResponseProfiles { get; set; } + public ResponseProfile[] ResponseProfiles { get; set; } = Array.Empty<ResponseProfile>(); /// <summary> - /// Gets or sets the SubtitleProfiles. + /// Gets or sets the subtitle profiles. /// </summary> - public SubtitleProfile[] SubtitleProfiles { get; set; } + public SubtitleProfile[] SubtitleProfiles { get; set; } = Array.Empty<SubtitleProfile>(); /// <summary> /// The GetSupportedMediaTypes. @@ -244,13 +237,13 @@ namespace MediaBrowser.Model.Dlna /// <param name="container">The container.</param> /// <param name="audioCodec">The audio Codec.</param> /// <returns>A <see cref="TranscodingProfile"/>.</returns> - public TranscodingProfile GetAudioTranscodingProfile(string container, string audioCodec) + public TranscodingProfile? GetAudioTranscodingProfile(string? container, string? audioCodec) { container = (container ?? string.Empty).TrimStart('.'); foreach (var i in TranscodingProfiles) { - if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Audio) + if (i.Type != DlnaProfileType.Audio) { continue; } @@ -278,13 +271,13 @@ namespace MediaBrowser.Model.Dlna /// <param name="audioCodec">The audio Codec.</param> /// <param name="videoCodec">The video Codec.</param> /// <returns>The <see cref="TranscodingProfile"/>.</returns> - public TranscodingProfile GetVideoTranscodingProfile(string container, string audioCodec, string videoCodec) + public TranscodingProfile? GetVideoTranscodingProfile(string? container, string? audioCodec, string? videoCodec) { container = (container ?? string.Empty).TrimStart('.'); foreach (var i in TranscodingProfiles) { - if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Video) + if (i.Type != DlnaProfileType.Video) { continue; } @@ -299,7 +292,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!string.Equals(videoCodec, i.VideoCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(videoCodec, i.VideoCodec, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -320,7 +313,7 @@ namespace MediaBrowser.Model.Dlna /// <param name="audioSampleRate">The audio sample rate.</param> /// <param name="audioBitDepth">The audio bit depth.</param> /// <returns>The <see cref="ResponseProfile"/>.</returns> - public ResponseProfile GetAudioMediaProfile(string container, string audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth) + public ResponseProfile? GetAudioMediaProfile(string container, string? audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth) { foreach (var i in ResponseProfiles) { @@ -384,7 +377,7 @@ namespace MediaBrowser.Model.Dlna /// <param name="width">The width.</param> /// <param name="height">The height.</param> /// <returns>The <see cref="ResponseProfile"/>.</returns> - public ResponseProfile GetImageMediaProfile(string container, int? width, int? height) + public ResponseProfile? GetImageMediaProfile(string container, int? width, int? height) { foreach (var i in ResponseProfiles) { @@ -442,10 +435,10 @@ namespace MediaBrowser.Model.Dlna /// <param name="videoCodecTag">The video Codec tag.</param> /// <param name="isAvc">True if Avc.</param> /// <returns>The <see cref="ResponseProfile"/>.</returns> - public ResponseProfile GetVideoMediaProfile( + public ResponseProfile? GetVideoMediaProfile( string container, - string audioCodec, - string videoCodec, + string? audioCodec, + string? videoCodec, int? width, int? height, int? bitDepth, diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs index 88cb83991..fa3ad098f 100644 --- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs +++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs @@ -1,6 +1,6 @@ -#nullable disable #pragma warning disable CS1591 +using System.ComponentModel.DataAnnotations; using System.Xml.Serialization; namespace MediaBrowser.Model.Dlna @@ -8,13 +8,13 @@ namespace MediaBrowser.Model.Dlna public class DirectPlayProfile { [XmlAttribute("container")] - public string Container { get; set; } + public string? Container { get; set; } [XmlAttribute("audioCodec")] - public string AudioCodec { get; set; } + public string? AudioCodec { get; set; } [XmlAttribute("videoCodec")] - public string VideoCodec { get; set; } + public string? VideoCodec { get; set; } [XmlAttribute("type")] public DlnaProfileType Type { get; set; } diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs index f61b8d59e..7ce248509 100644 --- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs +++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs @@ -9,16 +9,9 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna { - public class MediaFormatProfileResolver + public static class MediaFormatProfileResolver { - public string[] ResolveVideoFormat(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) - { - return ResolveVideoFormatInternal(container, videoCodec, audioCodec, width, height, timestampType) - .Select(i => i.ToString()) - .ToArray(); - } - - private MediaFormatProfile[] ResolveVideoFormatInternal(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) + public static MediaFormatProfile[] ResolveVideoFormat(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) { if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase)) { @@ -84,7 +77,7 @@ namespace MediaBrowser.Model.Dlna return Array.Empty<MediaFormatProfile>(); } - private MediaFormatProfile[] ResolveVideoMPEG2TSFormat(string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) + private static MediaFormatProfile[] ResolveVideoMPEG2TSFormat(string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType) { string suffix = string.Empty; @@ -209,12 +202,12 @@ namespace MediaBrowser.Model.Dlna return Array.Empty<MediaFormatProfile>(); } - private MediaFormatProfile ValueOf(string value) + private static MediaFormatProfile ValueOf(string value) { return (MediaFormatProfile)Enum.Parse(typeof(MediaFormatProfile), value, true); } - private MediaFormatProfile? ResolveVideoMP4Format(string videoCodec, string audioCodec, int? width, int? height) + private static MediaFormatProfile? ResolveVideoMP4Format(string videoCodec, string audioCodec, int? width, int? height) { if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { @@ -287,7 +280,7 @@ namespace MediaBrowser.Model.Dlna return null; } - private MediaFormatProfile? ResolveVideo3GPFormat(string videoCodec, string audioCodec) + private static MediaFormatProfile? ResolveVideo3GPFormat(string videoCodec, string audioCodec) { if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { @@ -317,7 +310,7 @@ namespace MediaBrowser.Model.Dlna return null; } - private MediaFormatProfile? ResolveVideoASFFormat(string videoCodec, string audioCodec, int? width, int? height) + private static MediaFormatProfile? ResolveVideoASFFormat(string videoCodec, string audioCodec, int? width, int? height) { if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase) && (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "wmapro", StringComparison.OrdinalIgnoreCase))) @@ -371,7 +364,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public MediaFormatProfile? ResolveAudioFormat(string container, int? bitrate, int? frequency, int? channels) + public static MediaFormatProfile? ResolveAudioFormat(string container, int? bitrate, int? frequency, int? channels) { if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase)) { @@ -413,7 +406,7 @@ namespace MediaBrowser.Model.Dlna return null; } - private MediaFormatProfile ResolveAudioASFFormat(int? bitrate) + private static MediaFormatProfile ResolveAudioASFFormat(int? bitrate) { if (bitrate.HasValue && bitrate.Value <= 193) { @@ -423,7 +416,7 @@ namespace MediaBrowser.Model.Dlna return MediaFormatProfile.WMA_FULL; } - private MediaFormatProfile? ResolveAudioLPCMFormat(int? frequency, int? channels) + private static MediaFormatProfile? ResolveAudioLPCMFormat(int? frequency, int? channels) { if (frequency.HasValue && channels.HasValue) { @@ -453,7 +446,7 @@ namespace MediaBrowser.Model.Dlna return MediaFormatProfile.LPCM16_48_STEREO; } - private MediaFormatProfile ResolveAudioMP4Format(int? bitrate) + private static MediaFormatProfile ResolveAudioMP4Format(int? bitrate) { if (bitrate.HasValue && bitrate.Value <= 320) { @@ -463,7 +456,7 @@ namespace MediaBrowser.Model.Dlna return MediaFormatProfile.AAC_ISO; } - private MediaFormatProfile ResolveAudioADTSFormat(int? bitrate) + private static MediaFormatProfile ResolveAudioADTSFormat(int? bitrate) { if (bitrate.HasValue && bitrate.Value <= 320) { @@ -473,7 +466,7 @@ namespace MediaBrowser.Model.Dlna return MediaFormatProfile.AAC_ADTS; } - public MediaFormatProfile? ResolveImageFormat(string container, int? width, int? height) + public static MediaFormatProfile? ResolveImageFormat(string container, int? width, int? height) { if (string.Equals(container, "jpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "jpg", StringComparison.OrdinalIgnoreCase)) @@ -499,7 +492,7 @@ namespace MediaBrowser.Model.Dlna return null; } - private MediaFormatProfile ResolveImageJPGFormat(int? width, int? height) + private static MediaFormatProfile ResolveImageJPGFormat(int? width, int? height) { if (width.HasValue && height.HasValue) { @@ -524,7 +517,7 @@ namespace MediaBrowser.Model.Dlna return MediaFormatProfile.JPEG_SM; } - private MediaFormatProfile ResolveImagePNGFormat(int? width, int? height) + private static MediaFormatProfile ResolveImagePNGFormat(int? width, int? height) { if (width.HasValue && height.HasValue) { diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index bf33691c7..f4c69fe8f 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -297,7 +297,7 @@ namespace MediaBrowser.Model.Dlna int? inputAudioSampleRate = audioStream?.SampleRate; int? inputAudioBitDepth = audioStream?.BitDepth; - if (directPlayMethods.Count() > 0) + if (directPlayMethods.Any()) { string audioCodec = audioStream?.Codec; @@ -514,6 +514,8 @@ namespace MediaBrowser.Model.Dlna private static List<TranscodeReason> GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles) { + var mediaType = videoStream == null ? DlnaProfileType.Audio : DlnaProfileType.Video; + var containerSupported = false; var audioSupported = false; var videoSupported = false; @@ -521,7 +523,7 @@ namespace MediaBrowser.Model.Dlna foreach (var profile in directPlayProfiles) { // Check container type - if (profile.SupportsContainer(item.Container)) + if (profile.Type == mediaType && profile.SupportsContainer(item.Container)) { containerSupported = true; @@ -674,7 +676,7 @@ namespace MediaBrowser.Model.Dlna var videoStream = item.VideoStream; - // TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough + // TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, options, PlayMethod.DirectPlay); var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, options, PlayMethod.DirectStream); bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1); @@ -1017,14 +1019,15 @@ namespace MediaBrowser.Model.Dlna } DeviceProfile profile = options.Profile; + string container = mediaSource.Container; // See if it can be direct played DirectPlayProfile directPlay = null; - foreach (var i in profile.DirectPlayProfiles) + foreach (var p in profile.DirectPlayProfiles) { - if (i.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(i, mediaSource, videoStream, audioStream)) + if (p.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(p, container, videoStream, audioStream)) { - directPlay = i; + directPlay = p; break; } } @@ -1032,23 +1035,23 @@ namespace MediaBrowser.Model.Dlna if (directPlay == null) { _logger.LogInformation( - "Profile: {0}, No video direct play profiles found for {1} with codec {2}", - profile?.Name ?? "Unknown Profile", - mediaSource?.Path ?? "Unknown path", - videoStream?.Codec ?? "Unknown codec"); + "Container: {Container}, Video: {Video}, Audio: {Audio} cannot be direct played by profile: {Profile} for path: {Path}", + container, + videoStream?.Codec ?? "no video", + audioStream?.Codec ?? "no audio", + profile.Name ?? "unknown profile", + mediaSource.Path ?? "unknown path"); return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles)); } - string container = mediaSource.Container; - var conditions = new List<ProfileCondition>(); - foreach (var i in profile.ContainerProfiles) + foreach (var p in profile.ContainerProfiles) { - if (i.Type == DlnaProfileType.Video - && i.ContainsContainer(container)) + if (p.Type == DlnaProfileType.Video + && p.ContainsContainer(container)) { - foreach (var c in i.Conditions) + foreach (var c in p.Conditions) { conditions.Add(c); } @@ -1896,10 +1899,10 @@ namespace MediaBrowser.Model.Dlna return true; } - private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream) + private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, string container, MediaStream videoStream, MediaStream audioStream) { // Check container type - if (!profile.SupportsContainer(item.Container)) + if (!profile.SupportsContainer(container)) { return false; } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 29da5d9e7..252872847 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -630,10 +630,8 @@ namespace MediaBrowser.Model.Dlna continue; } - // Be careful, IsDirectStream==true by default (Static != false or not in query). - // See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true. if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) && - string.Equals(pair.Value, "true", StringComparison.OrdinalIgnoreCase)) + string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs index f05e31047..214578a85 100644 --- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -1,6 +1,7 @@ -#nullable disable #pragma warning disable CS1591 +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Xml.Serialization; namespace MediaBrowser.Model.Dlna @@ -8,47 +9,56 @@ namespace MediaBrowser.Model.Dlna public class TranscodingProfile { [XmlAttribute("container")] - public string Container { get; set; } + public string Container { get; set; } = string.Empty; [XmlAttribute("type")] public DlnaProfileType Type { get; set; } [XmlAttribute("videoCodec")] - public string VideoCodec { get; set; } + public string VideoCodec { get; set; } = string.Empty; [XmlAttribute("audioCodec")] - public string AudioCodec { get; set; } + public string AudioCodec { get; set; } = string.Empty; [XmlAttribute("protocol")] - public string Protocol { get; set; } + public string Protocol { get; set; } = string.Empty; + [DefaultValue(false)] [XmlAttribute("estimateContentLength")] public bool EstimateContentLength { get; set; } + [DefaultValue(false)] [XmlAttribute("enableMpegtsM2TsMode")] public bool EnableMpegtsM2TsMode { get; set; } + [DefaultValue(TranscodeSeekInfo.Auto)] [XmlAttribute("transcodeSeekInfo")] public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + [DefaultValue(false)] [XmlAttribute("copyTimestamps")] public bool CopyTimestamps { get; set; } + [DefaultValue(EncodingContext.Streaming)] [XmlAttribute("context")] public EncodingContext Context { get; set; } + [DefaultValue(false)] [XmlAttribute("enableSubtitlesInManifest")] public bool EnableSubtitlesInManifest { get; set; } [XmlAttribute("maxAudioChannels")] - public string MaxAudioChannels { get; set; } + public string? MaxAudioChannels { get; set; } + [DefaultValue(0)] [XmlAttribute("minSegments")] public int MinSegments { get; set; } + [DefaultValue(0)] [XmlAttribute("segmentLength")] public int SegmentLength { get; set; } + [DefaultValue(false)] [XmlAttribute("breakOnNonKeyFrames")] public bool BreakOnNonKeyFrames { get; set; } diff --git a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs index d71013f01..987a3a908 100644 --- a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs +++ b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs @@ -16,5 +16,7 @@ namespace MediaBrowser.Model.Dlna public IPAddress LocalIpAddress { get; set; } public int LocalPort { get; set; } + + public IPAddress RemoteIpAddress { get; set; } } } diff --git a/MediaBrowser.Model/Drawing/DrawingUtils.cs b/MediaBrowser.Model/Drawing/DrawingUtils.cs index 1512c5233..556792768 100644 --- a/MediaBrowser.Model/Drawing/DrawingUtils.cs +++ b/MediaBrowser.Model/Drawing/DrawingUtils.cs @@ -58,6 +58,52 @@ namespace MediaBrowser.Model.Drawing } /// <summary> + /// Scale down to fill box. + /// Returns original size if both width and height are null or zero. + /// </summary> + /// <param name="size">The original size object.</param> + /// <param name="fillWidth">A new fixed width, if desired.</param> + /// <param name="fillHeight">A new fixed height, if desired.</param> + /// <returns>A new size object or size.</returns> + public static ImageDimensions ResizeFill( + ImageDimensions size, + int? fillWidth, + int? fillHeight) + { + // Return original size if input is invalid. + if ((fillWidth == null || fillWidth == 0) + && (fillHeight == null || fillHeight == 0)) + { + return size; + } + + if (fillWidth == null || fillWidth == 0) + { + fillWidth = 1; + } + + if (fillHeight == null || fillHeight == 0) + { + fillHeight = 1; + } + + double widthRatio = size.Width / (double)fillWidth; + double heightRatio = size.Height / (double)fillHeight; + double scaleRatio = Math.Min(widthRatio, heightRatio); + + // Clamp to current size. + if (scaleRatio < 1) + { + return size; + } + + int newWidth = Convert.ToInt32(Math.Ceiling(size.Width / scaleRatio)); + int newHeight = Convert.ToInt32(Math.Ceiling(size.Height / scaleRatio)); + + return new ImageDimensions(newWidth, newHeight); + } + + /// <summary> /// Gets the new width. /// </summary> /// <param name="currentHeight">Height of the current.</param> diff --git a/MediaBrowser.Model/Dto/NameIdPair.cs b/MediaBrowser.Model/Dto/NameIdPair.cs index 7f18b4502..31516947f 100644 --- a/MediaBrowser.Model/Dto/NameIdPair.cs +++ b/MediaBrowser.Model/Dto/NameIdPair.cs @@ -1,8 +1,6 @@ #nullable disable #pragma warning disable CS1591 -using System; - namespace MediaBrowser.Model.Dto { public class NameIdPair diff --git a/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs b/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs new file mode 100644 index 000000000..7c627f0e3 --- /dev/null +++ b/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs @@ -0,0 +1,29 @@ +#nullable disable +// THIS IS A HACK +// TODO: @bond Move to separate project + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Model.Entities +{ + /// <summary> + /// Converts an object to a lowercase string. + /// </summary> + /// <typeparam name="T">The object type.</typeparam> + public class JsonLowerCaseConverter<T> : JsonConverter<T> + { + /// <inheritdoc /> + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize<T>(ref reader, options); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString().ToLowerInvariant()); + } + } +} diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index ade9d7e8d..e644c9ba7 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -163,7 +163,7 @@ namespace MediaBrowser.Model.Entities foreach (var tag in attributes) { // Keep Tags that are not already in Title. - if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + if (!Title.Contains(tag, StringComparison.OrdinalIgnoreCase)) { result.Append(" - ").Append(tag); } @@ -202,7 +202,7 @@ namespace MediaBrowser.Model.Entities foreach (var tag in attributes) { // Keep Tags that are not already in Title. - if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + if (!Title.Contains(tag, StringComparison.OrdinalIgnoreCase)) { result.Append(" - ").Append(tag); } @@ -522,9 +522,9 @@ namespace MediaBrowser.Model.Entities // sub = external .sub file - return codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) == -1 && - codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) == -1 && - codec.IndexOf("dvbsub", StringComparison.OrdinalIgnoreCase) == -1 && + return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase) && + !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase) && + !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase) && !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase) && !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase); } diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs index 4aff6e3a4..ce4b0ec92 100644 --- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs +++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs @@ -10,13 +10,40 @@ namespace MediaBrowser.Model.Entities public static class ProviderIdsExtensions { /// <summary> + /// Checks if this instance has an id for the given provider. + /// </summary> + /// <param name="instance">The instance.</param> + /// <param name="name">The of the provider name.</param> + /// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns> + public static bool HasProviderId(this IHasProviderIds instance, string name) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + return instance.TryGetProviderId(name, out _); + } + + /// <summary> + /// Checks if this instance has an id for the given provider. + /// </summary> + /// <param name="instance">The instance.</param> + /// <param name="provider">The provider.</param> + /// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns> + public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider) + { + return instance.HasProviderId(provider.ToString()); + } + + /// <summary> /// Gets a provider id. /// </summary> /// <param name="instance">The instance.</param> /// <param name="name">The name.</param> /// <param name="id">The provider id.</param> /// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns> - public static bool TryGetProviderId(this IHasProviderIds instance, string name, [MaybeNullWhen(false)] out string id) + public static bool TryGetProviderId(this IHasProviderIds instance, string name, [NotNullWhen(true)] out string? id) { if (instance == null) { @@ -29,7 +56,15 @@ namespace MediaBrowser.Model.Entities return false; } - return instance.ProviderIds.TryGetValue(name, out id); + var foundProviderId = instance.ProviderIds.TryGetValue(name, out id); + // This occurs when searching with Identify (and possibly in other places) + if (string.IsNullOrEmpty(id)) + { + id = null; + foundProviderId = false; + } + + return foundProviderId; } /// <summary> @@ -39,7 +74,7 @@ namespace MediaBrowser.Model.Entities /// <param name="provider">The provider.</param> /// <param name="id">The provider id.</param> /// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns> - public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [MaybeNullWhen(false)] out string id) + public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [NotNullWhen(true)] out string? id) { return instance.TryGetProviderId(provider.ToString(), out id); } @@ -88,10 +123,7 @@ namespace MediaBrowser.Model.Entities else { // Ensure it exists - if (instance.ProviderIds == null) - { - instance.ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } + instance.ProviderIds ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); instance.ProviderIds[name] = value; } diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs index ea3df3726..8fed392b9 100644 --- a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs +++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using System; +using System.Text.Json.Serialization; using MediaBrowser.Model.Configuration; namespace MediaBrowser.Model.Entities @@ -35,6 +36,7 @@ namespace MediaBrowser.Model.Entities /// Gets or sets the type of the collection. /// </summary> /// <value>The type of the collection.</value> + [JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))] public CollectionTypeOptions? CollectionType { get; set; } public LibraryOptions LibraryOptions { get; set; } diff --git a/MediaBrowser.Model/IO/FileSystemMetadata.cs b/MediaBrowser.Model/IO/FileSystemMetadata.cs index 118c78e80..fb74886bf 100644 --- a/MediaBrowser.Model/IO/FileSystemMetadata.cs +++ b/MediaBrowser.Model/IO/FileSystemMetadata.cs @@ -38,12 +38,6 @@ namespace MediaBrowser.Model.IO public long Length { get; set; } /// <summary> - /// Gets or sets the name of the directory. - /// </summary> - /// <value>The name of the directory.</value> - public string DirectoryName { get; set; } - - /// <summary> /// Gets or sets the last write time UTC. /// </summary> /// <value>The last write time UTC.</value> diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index ef08ecec6..e5c26430a 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -118,13 +118,6 @@ namespace MediaBrowser.Model.IO bool ContainsSubPath(string parentPath, string path); /// <summary> - /// Determines whether [is root path] [the specified path]. - /// </summary> - /// <param name="path">The path.</param> - /// <returns><c>true</c> if [is root path] [the specified path]; otherwise, <c>false</c>.</returns> - bool IsRootPath(string path); - - /// <summary> /// Normalizes the path. /// </summary> /// <param name="path">The path.</param> diff --git a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs index 7d4bbb2d0..05576a0f8 100644 --- a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs +++ b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs @@ -1,9 +1,6 @@ #nullable disable #pragma warning disable CS1591 -using System; -using MediaBrowser.Model.Dto; - namespace MediaBrowser.Model.LiveTv { public class TunerHostInfo diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index b6d916913..4db99f0b0 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -19,7 +19,8 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> - <LangVersion>latest</LangVersion> + <!-- <AnalysisMode>AllEnabledByDefault</AnalysisMode> --> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> <PublishRepositoryUrl>true</PublishRepositoryUrl> <EmbedUntrackedSources>true</EmbedUntrackedSources> <IncludeSymbols>true</IncludeSymbols> @@ -35,7 +36,7 @@ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> <PackageReference Include="System.Globalization" Version="4.3.0" /> - <PackageReference Include="System.Text.Json" Version="5.0.1" /> + <PackageReference Include="System.Text.Json" Version="5.0.2" /> </ItemGroup> <ItemGroup> @@ -44,7 +45,6 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <!-- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> --> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> @@ -53,8 +53,4 @@ <ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs index 94bb5d6e3..09beb2ef7 100644 --- a/MediaBrowser.Model/Notifications/NotificationOptions.cs +++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs @@ -5,8 +5,6 @@ using System; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.Users; namespace MediaBrowser.Model.Notifications { @@ -95,16 +93,17 @@ namespace MediaBrowser.Model.Notifications { NotificationOption opt = GetOptions(notificationType); - return opt == null || - !opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase); + return opt == null + || !opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase); } public bool IsEnabledToMonitorUser(string type, Guid userId) { NotificationOption opt = GetOptions(type); - return opt != null && opt.Enabled && - !opt.DisabledMonitorUsers.Contains(userId.ToString(string.Empty), StringComparer.OrdinalIgnoreCase); + return opt != null + && opt.Enabled + && !opt.DisabledMonitorUsers.Contains(userId.ToString("N"), StringComparer.OrdinalIgnoreCase); } public bool IsEnabledToSendToUser(string type, string userId, User user) diff --git a/MediaBrowser.Model/Session/MessageCommand.cs b/MediaBrowser.Model/Session/MessageCommand.cs index 09abfbb3f..cc9db8e6c 100644 --- a/MediaBrowser.Model/Session/MessageCommand.cs +++ b/MediaBrowser.Model/Session/MessageCommand.cs @@ -1,12 +1,15 @@ #nullable disable #pragma warning disable CS1591 +using System.ComponentModel.DataAnnotations; + namespace MediaBrowser.Model.Session { public class MessageCommand { public string Header { get; set; } + [Required(AllowEmptyStrings = false)] public string Text { get; set; } public long? TimeoutMs { get; set; } diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 9dd87aef5..fb1d4f490 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -172,7 +172,9 @@ namespace MediaBrowser.Providers.Manager SetImagePath(item, type, imageIndex, savedPaths[0]); // Delete the current path - if (currentImageIsLocalFile && !savedPaths.Contains(currentImagePath, StringComparer.OrdinalIgnoreCase)) + if (currentImageIsLocalFile + && !savedPaths.Contains(currentImagePath, StringComparer.OrdinalIgnoreCase) + && (saveLocally || currentImagePath.Contains(_config.ApplicationPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase))) { var currentPath = currentImagePath; @@ -261,7 +263,8 @@ namespace MediaBrowser.Providers.Manager _fileSystem.SetAttributes(path, false, false); - await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index ffc6889fa..4471a25b2 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -469,6 +469,7 @@ namespace MediaBrowser.Providers.Manager try { using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await _providerManager.SaveImage( diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 8b3ca17ca..401c7e99f 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -201,7 +201,7 @@ namespace MediaBrowser.Providers.Manager } // Save to database - await SaveItemAsync(metadataResult, libraryOptions, updateType, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false); } await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false); @@ -216,66 +216,18 @@ namespace MediaBrowser.Providers.Manager lookupInfo.Year = result.ProductionYear; } - protected async Task SaveItemAsync(MetadataResult<TItemType> result, LibraryOptions libraryOptions, ItemUpdateType reason, CancellationToken cancellationToken) + protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken) { if (result.Item.SupportsPeople && result.People != null) { var baseItem = result.Item; - LibraryManager.UpdatePeople(baseItem, result.People); - await SavePeopleMetadataAsync(result.People, libraryOptions, cancellationToken).ConfigureAwait(false); + await LibraryManager.UpdatePeopleAsync(baseItem, result.People, cancellationToken).ConfigureAwait(false); } await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); } - private async Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken) - { - var personsToSave = new List<BaseItem>(); - - foreach (var person in people) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl)) - { - var itemUpdateType = ItemUpdateType.MetadataDownload; - var saveEntity = false; - var personEntity = LibraryManager.GetPerson(person.Name); - foreach (var id in person.ProviderIds) - { - if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)) - { - personEntity.SetProviderId(id.Key, id.Value); - saveEntity = true; - } - } - - if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary)) - { - personEntity.SetImage( - new ItemImageInfo - { - Path = person.ImageUrl, - Type = ImageType.Primary - }, - 0); - - saveEntity = true; - itemUpdateType = ItemUpdateType.ImageUpdate; - } - - if (saveEntity) - { - personsToSave.Add(personEntity); - await LibraryManager.RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); - } - } - } - - LibraryManager.CreateItems(personsToSave, null, CancellationToken.None); - } - protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { item.AfterMetadataRefresh(); @@ -329,8 +281,7 @@ namespace MediaBrowser.Providers.Manager return true; } - var folder = item as Folder; - if (folder != null) + if (item is Folder folder) { return folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks; } @@ -384,8 +335,7 @@ namespace MediaBrowser.Providers.Manager private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IList<BaseItem> children) { - var folder = item as Folder; - if (folder != null && folder.SupportsCumulativeRunTimeTicks) + if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks) { long ticks = 0; @@ -473,7 +423,7 @@ namespace MediaBrowser.Providers.Manager if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue) || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1)) { - updateType = updateType | ItemUpdateType.MetadataEdit; + updateType |= ItemUpdateType.MetadataEdit; } return updateType; @@ -493,7 +443,7 @@ namespace MediaBrowser.Providers.Manager if (currentList.Length != item.Genres.Length || !currentList.OrderBy(i => i).SequenceEqual(item.Genres.OrderBy(i => i), StringComparer.OrdinalIgnoreCase)) { - updateType = updateType | ItemUpdateType.MetadataEdit; + updateType |= ItemUpdateType.MetadataEdit; } } @@ -514,7 +464,7 @@ namespace MediaBrowser.Providers.Manager if (currentList.Length != item.Studios.Length || !currentList.OrderBy(i => i).SequenceEqual(item.Studios.OrderBy(i => i), StringComparer.OrdinalIgnoreCase)) { - updateType = updateType | ItemUpdateType.MetadataEdit; + updateType |= ItemUpdateType.MetadataEdit; } } @@ -529,7 +479,7 @@ namespace MediaBrowser.Providers.Manager { if (item.UpdateRatingToItems(children)) { - updateType = updateType | ItemUpdateType.MetadataEdit; + updateType |= ItemUpdateType.MetadataEdit; } } @@ -686,7 +636,7 @@ namespace MediaBrowser.Providers.Manager var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken) .ConfigureAwait(false); - refreshResult.UpdateType = refreshResult.UpdateType | remoteResult.UpdateType; + refreshResult.UpdateType |= remoteResult.UpdateType; refreshResult.ErrorMessage = remoteResult.ErrorMessage; refreshResult.Failures += remoteResult.Failures; } @@ -706,9 +656,15 @@ namespace MediaBrowser.Providers.Manager if (localItem.HasMetadata) { + foreach (var remoteImage in localItem.RemoteImages) + { + await ProviderManager.SaveImage(item, remoteImage.url, remoteImage.type, null, cancellationToken).ConfigureAwait(false); + refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; + } + if (imageService.MergeImages(item, localItem.Images)) { - refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.ImageUpdate; + refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; } if (localItem.UserDataList != null) @@ -717,7 +673,7 @@ namespace MediaBrowser.Providers.Manager } MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true); - refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport; + refreshResult.UpdateType |= ItemUpdateType.MetadataImport; // Only one local provider allowed per item if (item.IsLocked || localItem.Item.IsLocked || IsFullLocalMetadata(localItem.Item)) @@ -749,7 +705,7 @@ namespace MediaBrowser.Providers.Manager var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken) .ConfigureAwait(false); - refreshResult.UpdateType = refreshResult.UpdateType | remoteResult.UpdateType; + refreshResult.UpdateType |= remoteResult.UpdateType; refreshResult.ErrorMessage = remoteResult.ErrorMessage; refreshResult.Failures += remoteResult.Failures; } @@ -845,7 +801,7 @@ namespace MediaBrowser.Providers.Manager MergeData(result, temp, Array.Empty<MetadataField>(), false, false); MergeNewData(temp.Item, id); - refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataDownload; + refreshResult.UpdateType |= ItemUpdateType.MetadataDownload; } else { diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 913f14d9b..dd497845d 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -25,7 +25,6 @@ using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; using Priority_Queue; @@ -60,8 +59,8 @@ namespace MediaBrowser.Providers.Manager private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>(); private IMetadataProvider[] _metadataProviders = Array.Empty<IMetadataProvider>(); - private IEnumerable<IMetadataSaver> _savers; - private IExternalId[] _externalIds; + private IMetadataSaver[] _savers = Array.Empty<IMetadataSaver>(); + private IExternalId[] _externalIds = Array.Empty<IExternalId>(); private bool _isProcessingRefreshQueue; private bool _disposed; @@ -125,7 +124,7 @@ namespace MediaBrowser.Providers.Manager _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray(); _savers = metadataSavers - .Where(i => !(i is IConfigurableProvider configurable) || configurable.IsEnabled) + .Where(i => i is not IConfigurableProvider configurable || configurable.IsEnabled) .ToArray(); } @@ -168,7 +167,7 @@ namespace MediaBrowser.Providers.Manager throw new HttpRequestException("Invalid image received.", null, response.StatusCode); } - var contentType = response.Content.Headers.ContentType.MediaType; + var contentType = response.Content.Headers.ContentType?.MediaType; // Workaround for tvheadend channel icons // TODO: Isolate this hack into the tvh plugin @@ -242,6 +241,7 @@ namespace MediaBrowser.Providers.Manager languages.Add(preferredLanguage); } + // TODO include [query.IncludeAllLanguages] as an argument to the providers var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -869,14 +869,14 @@ namespace MediaBrowser.Providers.Manager } } } - catch (Exception) +#pragma warning disable CA1031 // do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // do not catch general exception types { - // Logged at lower levels + _logger.LogError(ex, "Provider {ProviderName} failed to retrieve search results", provider.Name); } } - // _logger.LogDebug("Returning search results {0}", _json.SerializeToString(resultList)); - return resultList; } @@ -1021,26 +1021,26 @@ namespace MediaBrowser.Providers.Manager // TODO: Need to hunt down the conditions for this happening _activeRefreshes.AddOrUpdate( id, - (_) => throw new Exception( + (_) => throw new InvalidOperationException( string.Format( CultureInfo.InvariantCulture, "Cannot update refresh progress of item '{0}' ({1}) because a refresh for this item is not running", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture))), - (_, __) => progress); + (_, _) => progress); RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress))); } /// <inheritdoc/> - public void QueueRefresh(Guid id, MetadataRefreshOptions options, RefreshPriority priority) + public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority) { if (_disposed) { return; } - _refreshQueue.Enqueue(new Tuple<Guid, MetadataRefreshOptions>(id, options), (int)priority); + _refreshQueue.Enqueue(new Tuple<Guid, MetadataRefreshOptions>(itemId, options), (int)priority); lock (_refreshQueueLock) { @@ -1073,17 +1073,16 @@ namespace MediaBrowser.Providers.Manager try { var item = libraryManager.GetItemById(refreshItem.Item1); - if (item != null) + if (item == null) { - // Try to throttle this a little bit. - await Task.Delay(100, cancellationToken).ConfigureAwait(false); + continue; + } - var task = item is MusicArtist artist - ? RefreshArtist(artist, refreshItem.Item2, cancellationToken) - : RefreshItem(item, refreshItem.Item2, cancellationToken); + var task = item is MusicArtist artist + ? RefreshArtist(artist, refreshItem.Item2, cancellationToken) + : RefreshItem(item, refreshItem.Item2, cancellationToken); - await task.ConfigureAwait(false); - } + await task.ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 071a149db..cdb07a15d 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -19,10 +19,10 @@ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> - <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" /> <PackageReference Include="PlaylistsNET" Version="1.1.3" /> - <PackageReference Include="TMDbLib" Version="1.7.3-alpha" /> + <PackageReference Include="TMDbLib" Version="1.8.1" /> </ItemGroup> <PropertyGroup> @@ -30,20 +30,17 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors> + <AnalysisMode Condition=" '$(Configuration)' == 'Debug'">AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - <ItemGroup> <None Remove="Plugins\AudioDb\Configuration\config.html" /> <EmbeddedResource Include="Plugins\AudioDb\Configuration\config.html" /> diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index 64ad1bddf..03e45fb86 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs @@ -137,9 +137,7 @@ namespace MediaBrowser.Providers.MediaInfo return false; } - var audio = item as Audio; - - return audio != null; + return item is Audio; } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 74849a522..f049cc81f 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -111,10 +111,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - if (streamFileNames == null) - { - streamFileNames = Array.Empty<string>(); - } + streamFileNames ??= Array.Empty<string>(); mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs index 912aedb0d..44ab5aa5b 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs @@ -172,9 +172,7 @@ namespace MediaBrowser.Providers.MediaInfo SubtitleFetcherOrder = subtitleFetcherOrder }; - var episode = video as Episode; - - if (episode != null) + if (video is Episode episode) { request.IndexNumberEnd = episode.IndexNumberEnd; request.SeriesName = episode.SeriesName; diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index c36c3af6a..30af6710a 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -154,9 +154,7 @@ namespace MediaBrowser.Providers.MediaInfo return false; } - var video = item as Video; - - if (video != null && !video.IsPlaceHolder && video.IsCompleteMedia) + if (item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia) { return true; } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs index cd9e47743..85a28747f 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.AudioDb { @@ -22,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { private readonly IServerConfigurationManager _config; private readonly IHttpClientFactory _httpClientFactory; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; public AudioDbAlbumImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory) { diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs index f463a3566..25bb3f9ce 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs @@ -19,7 +19,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Music; namespace MediaBrowser.Providers.Plugins.AudioDb @@ -29,7 +28,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; private readonly IHttpClientFactory _httpClientFactory; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; public static AudioDbAlbumProvider Current; @@ -171,7 +170,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true); await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs index 36700d191..db8536cc9 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.AudioDb { @@ -22,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb { private readonly IServerConfigurationManager _config; private readonly IHttpClientFactory _httpClientFactory; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; public AudioDbArtistImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory) { diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs index 7a15adb8e..cbb61fa35 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs @@ -18,7 +18,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Music; namespace MediaBrowser.Providers.Plugins.AudioDb @@ -31,7 +30,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; private readonly IHttpClientFactory _httpClientFactory; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; public AudioDbArtistProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory) { @@ -155,7 +154,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb Directory.CreateDirectory(Path.GetDirectoryName(path)); - await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true); await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 97fcbfb6f..428b0ded1 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb _configurationManager = configurationManager; _appHost = appHost; - _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions()); + _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options); _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter()); _jsonOptions.Converters.Add(new JsonOmdbNotAvailableInt32Converter()); } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index e3301ff32..46d303890 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -6,9 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; -using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; @@ -39,7 +37,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb _configurationManager = configurationManager; _appHost = appHost; - _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions()); + _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options); _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter()); _jsonOptions.Converters.Add(new JsonOmdbNotAvailableInt32Converter()); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index df1e12240..5ad61c567 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -58,7 +58,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets var language = item.GetPreferredMetadataLanguage(); - var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false); + // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here + var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, null, null, cancellationToken).ConfigureAwait(false); if (collection?.Images == null) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index fcd8e614c..ca1af6c49 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; @@ -19,11 +20,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets { private readonly IHttpClientFactory _httpClientFactory; private readonly TmdbClientManager _tmdbClientManager; + private readonly ILibraryManager _libraryManager; - public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) + public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager, ILibraryManager libraryManager) { _httpClientFactory = httpClientFactory; _tmdbClientManager = tmdbClientManager; + _libraryManager = libraryManager; } public string Name => TmdbUtils.ProviderName; @@ -83,7 +86,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets // We don't already have an Id, need to fetch it if (tmdbId <= 0) { - var searchResults = await _tmdbClientManager.SearchCollectionAsync(id.Name, language, cancellationToken).ConfigureAwait(false); + // ParseName is required here. + // Caller provides the filename with extension stripped and NOT the parsed filename + var parsedName = _libraryManager.ParseName(id.Name); + var cleanedName = TmdbUtils.CleanName(parsedName.Name); + var searchResults = await _tmdbClientManager.SearchCollectionAsync(cleanedName, language, cancellationToken).ConfigureAwait(false); if (searchResults != null && searchResults.Count > 0) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs index dac9e961c..f34d689c1 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs @@ -73,8 +73,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies return Enumerable.Empty<RemoteImageInfo>(); } + // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here var movie = await _tmdbClientManager - .GetMovieAsync(movieTmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken) + .GetMovieAsync(movieTmdbId, null, null, cancellationToken) .ConfigureAwait(false); if (movie?.Images == null) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 9a3e3d5fa..d22c1b50a 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -7,8 +7,6 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using TMDbLib.Objects.Find; -using TMDbLib.Objects.Search; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -16,6 +14,8 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; +using TMDbLib.Objects.Find; +using TMDbLib.Objects.Search; namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { @@ -140,7 +140,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies // ParseName is required here. // Caller provides the filename with extension stripped and NOT the parsed filename var parsedName = _libraryManager.ParseName(info.Name); - var searchResults = await _tmdbClientManager.SearchMovieAsync(parsedName.Name, parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + var cleanedName = TmdbUtils.CleanName(parsedName.Name); + var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); if (searchResults.Count > 0) { @@ -148,6 +149,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies } } + if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId)) + { + var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + if (movieResultFromImdbId?.MovieResults.Count > 0) + { + tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(); + } + } + if (string.IsNullOrEmpty(tmdbId)) { return new MetadataResult<Movie>(); @@ -165,6 +175,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var movie = new Movie { Name = movieResult.Title ?? movieResult.OriginalTitle, + OriginalTitle = movieResult.OriginalTitle, Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture), Tagline = movieResult.Tagline, ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray() diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index 3f57c4bc4..bf42ceade 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -49,37 +49,36 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var person = (Person)item; - var personTmdbId = Convert.ToInt32(person.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - if (personTmdbId > 0) + if (!person.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId)) { - var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false); - if (personResult?.Images?.Profiles == null) - { - return Enumerable.Empty<RemoteImageInfo>(); - } + return Enumerable.Empty<RemoteImageInfo>(); + } - var remoteImages = new List<RemoteImageInfo>(); - var language = item.GetPreferredMetadataLanguage(); + var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), cancellationToken).ConfigureAwait(false); + if (personResult?.Images?.Profiles == null) + { + return Enumerable.Empty<RemoteImageInfo>(); + } - for (var i = 0; i < personResult.Images.Profiles.Count; i++) - { - var image = personResult.Images.Profiles[i]; - remoteImages.Add(new RemoteImageInfo - { - ProviderName = Name, - Type = ImageType.Primary, - Width = image.Width, - Height = image.Height, - Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language), - Url = _tmdbClientManager.GetProfileUrl(image.FilePath) - }); - } + var remoteImages = new RemoteImageInfo[personResult.Images.Profiles.Count]; + var language = item.GetPreferredMetadataLanguage(); - return remoteImages.OrderByLanguageDescending(language); + for (var i = 0; i < personResult.Images.Profiles.Count; i++) + { + var image = personResult.Images.Profiles[i]; + remoteImages[i] = new RemoteImageInfo + { + ProviderName = Name, + Type = ImageType.Primary, + Width = image.Width, + Height = image.Height, + Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language), + Url = _tmdbClientManager.GetProfileUrl(image.FilePath) + }; } - return Enumerable.Empty<RemoteImageInfo>(); + return remoteImages.OrderByLanguageDescending(language); } public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 4384c203e..1757c8267 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -30,11 +30,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) { - var personTmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture); - - if (personTmdbId <= 0) + if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId)) { - var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false); + var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), cancellationToken).ConfigureAwait(false); if (personResult != null) { @@ -51,19 +49,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People } result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture)); - result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId); + if (!string.IsNullOrEmpty(personResult.ExternalIds.ImdbId)) + { + result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId); + } return new[] { result }; } } - // TODO why? Because of the old rate limit? - if (searchInfo.IsAutomated) - { - // Don't hammer moviedb searching by name - return Enumerable.Empty<RemoteSearchResult>(); - } - var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false); var remoteSearchResults = new List<RemoteSearchResult>(); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index 3b7a0b254..ba18c542f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -63,8 +63,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var language = item.GetPreferredMetadataLanguage(); + // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here var episodeResult = await _tmdbClientManager - .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken) + .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken) .ConfigureAwait(false); var stills = episodeResult?.Images?.Stills; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 93998a110..8ec8f6464 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV } var episodeResult = await _tmdbClientManager - .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) .ConfigureAwait(false); if (episodeResult == null) @@ -111,24 +111,31 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var item = new Episode { - Name = info.Name, IndexNumber = info.IndexNumber, ParentIndexNumber = info.ParentIndexNumber, - IndexNumberEnd = info.IndexNumberEnd + IndexNumberEnd = info.IndexNumberEnd, + Name = episodeResult.Name, + PremiereDate = episodeResult.AirDate, + ProductionYear = episodeResult.AirDate?.Year, + Overview = episodeResult.Overview, + CommunityRating = Convert.ToSingle(episodeResult.VoteAverage) }; - if (!string.IsNullOrEmpty(episodeResult.ExternalIds?.TvdbId)) + var externalIds = episodeResult.ExternalIds; + if (!string.IsNullOrEmpty(externalIds?.TvdbId)) { - item.SetProviderId(MetadataProvider.Tvdb, episodeResult.ExternalIds.TvdbId); + item.SetProviderId(MetadataProvider.Tvdb, externalIds.TvdbId); } - item.PremiereDate = episodeResult.AirDate; - item.ProductionYear = episodeResult.AirDate?.Year; - - item.Name = episodeResult.Name; - item.Overview = episodeResult.Overview; + if (!string.IsNullOrEmpty(externalIds?.ImdbId)) + { + item.SetProviderId(MetadataProvider.Imdb, externalIds.ImdbId); + } - item.CommunityRating = Convert.ToSingle(episodeResult.VoteAverage); + if (!string.IsNullOrEmpty(externalIds?.TvrageId)) + { + item.SetProviderId(MetadataProvider.TvRage, externalIds.TvrageId); + } if (episodeResult.Videos?.Results != null) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index f4ed480ae..0d23c7872 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -52,8 +52,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var language = item.GetPreferredMetadataLanguage(); + // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here var seasonResult = await _tmdbClientManager - .GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken) + .GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, null, null, cancellationToken) .ConfigureAwait(false); var posters = seasonResult?.Images?.Posters; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index d0c6b8b88..326c116b3 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -54,13 +54,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (string.IsNullOrEmpty(tmdbId)) { - return null; + return Enumerable.Empty<RemoteImageInfo>(); } var language = item.GetPreferredMetadataLanguage(); + // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here var series = await _tmdbClientManager - .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken) + .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), null, null, cancellationToken) .ConfigureAwait(false); if (series?.Images == null) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 942c85b90..496e1ae25 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; @@ -22,15 +23,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder { private readonly IHttpClientFactory _httpClientFactory; + private readonly ILibraryManager _libraryManager; private readonly TmdbClientManager _tmdbClientManager; public TmdbSeriesProvider( + ILibraryManager libraryManager, IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager) { + _libraryManager = libraryManager; _httpClientFactory = httpClientFactory; _tmdbClientManager = tmdbClientManager; - Current = this; } public string Name => TmdbUtils.ProviderName; @@ -38,13 +41,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV // After TheTVDB public int Order => 1; - internal static TmdbSeriesProvider Current { get; private set; } - public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) { - var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb); - - if (!string.IsNullOrEmpty(tmdbId)) + if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId)) { var series = await _tmdbClientManager .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, cancellationToken) @@ -58,9 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV } } - var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb); - - if (!string.IsNullOrEmpty(imdbId)) + if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out var imdbId)) { var findResult = await _tmdbClientManager .FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, cancellationToken) @@ -81,9 +78,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV } } - var tvdbId = searchInfo.GetProviderId(MetadataProvider.Tvdb); - - if (!string.IsNullOrEmpty(tvdbId)) + if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)) { var findResult = await _tmdbClientManager .FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, cancellationToken) @@ -104,7 +99,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV } } - var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken) + var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken: cancellationToken) .ConfigureAwait(false); var remoteResults = new RemoteSearchResult[tvSearchResults.Count]; @@ -170,40 +165,32 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var tmdbId = info.GetProviderId(MetadataProvider.Tmdb); - if (string.IsNullOrEmpty(tmdbId)) + if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId)) { - var imdbId = info.GetProviderId(MetadataProvider.Imdb); - - if (!string.IsNullOrEmpty(imdbId)) + var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + if (searchResult?.TvResults.Count > 0) { - var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (searchResult != null) - { - tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture); - } + tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture); } } - if (string.IsNullOrEmpty(tmdbId)) + if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)) { - var tvdbId = info.GetProviderId(MetadataProvider.Tvdb); - - if (!string.IsNullOrEmpty(tvdbId)) + var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + if (searchResult?.TvResults.Count > 0) { - var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (searchResult != null) - { - tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture); - } + tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture); } } if (string.IsNullOrEmpty(tmdbId)) { result.QueriedById = false; - var searchResults = await _tmdbClientManager.SearchSeriesAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + // ParseName is required here. + // Caller provides the filename with extension stripped and NOT the parsed filename + var parsedName = _libraryManager.ParseName(info.Name); + var cleanedName = TmdbUtils.CleanName(parsedName.Name); + var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false); if (searchResults.Count > 0) { @@ -211,32 +198,34 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV } } - if (!string.IsNullOrEmpty(tmdbId)) + if (string.IsNullOrEmpty(tmdbId)) { - cancellationToken.ThrowIfCancellationRequested(); + return result; + } - var tvShow = await _tmdbClientManager - .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) - .ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); - result = new MetadataResult<Series> - { - Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode), - ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage - }; + var tvShow = await _tmdbClientManager + .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken) + .ConfigureAwait(false); - foreach (var person in GetPersons(tvShow)) - { - result.AddPerson(person); - } + result = new MetadataResult<Series> + { + Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode), + ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage + }; - result.HasMetadata = result.Item != null; + foreach (var person in GetPersons(tvShow)) + { + result.AddPerson(person); } + result.HasMetadata = result.Item != null; + return result; } - private Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode) + private static Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode) { var series = new Series { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index 2dc5cd55d..05e5d3ced 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -125,7 +125,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb tmdbId, language: TmdbUtils.NormalizeLanguage(language), includeImageLanguage: imageLanguages, - extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings, + extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups, cancellationToken: cancellationToken).ConfigureAwait(false); if (series != null) @@ -137,6 +137,56 @@ namespace MediaBrowser.Providers.Plugins.Tmdb } /// <summary> + /// Gets a tv show episode group from the TMDb API based on the show id and the display order. + /// </summary> + /// <param name="tvShowId">The tv show's TMDb id.</param> + /// <param name="displayOrder">The display order.</param> + /// <param name="language">The tv show's language.</param> + /// <param name="imageLanguages">A comma-separated list of image languages.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The TMDb tv show episode group information or null if not found.</returns> + private async Task<TvGroupCollection> GetSeriesGroupAsync(int tvShowId, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken) + { + TvGroupType? groupType = + string.Equals(displayOrder, "absolute", StringComparison.Ordinal) ? TvGroupType.Absolute : + string.Equals(displayOrder, "dvd", StringComparison.Ordinal) ? TvGroupType.DVD : + null; + + if (groupType == null) + { + return null; + } + + var key = $"group-{tvShowId.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; + if (_memoryCache.TryGetValue(key, out TvGroupCollection group)) + { + return group; + } + + await EnsureClientConfigAsync().ConfigureAwait(false); + + var series = await GetSeriesAsync(tvShowId, language, imageLanguages, cancellationToken).ConfigureAwait(false); + var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id; + + if (episodeGroupId == null) + { + return null; + } + + group = await _tmDbClient.GetTvEpisodeGroupsAsync( + episodeGroupId, + language: TmdbUtils.NormalizeLanguage(language), + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (group != null) + { + _memoryCache.Set(key, group, TimeSpan.FromHours(CacheDurationInHours)); + } + + return group; + } + + /// <summary> /// Gets a tv season from the TMDb API based on the tv show's TMDb id. /// </summary> /// <param name="tvShowId">The tv season's TMDb id.</param> @@ -177,13 +227,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="tvShowId">The tv show's TMDb id.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="episodeNumber">The episode number.</param> + /// <param name="displayOrder">The display order.</param> /// <param name="language">The episode's language.</param> /// <param name="imageLanguages">A comma-separated list of image languages.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb tv episode information or null if not found.</returns> - public async Task<TvEpisode> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string language, string imageLanguages, CancellationToken cancellationToken) + public async Task<TvEpisode> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string language, string imageLanguages, CancellationToken cancellationToken) { - var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{language}"; + var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; if (_memoryCache.TryGetValue(key, out TvEpisode episode)) { return episode; @@ -191,6 +242,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb await EnsureClientConfigAsync().ConfigureAwait(false); + var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken); + if (group != null) + { + var season = group.Groups.Find(s => s.Order == seasonNumber); + // Episode order starts at 0 + var ep = season?.Episodes.Find(e => e.Order == episodeNumber - 1); + if (ep != null) + { + seasonNumber = ep.SeasonNumber; + episodeNumber = ep.EpisodeNumber; + } + } + episode = await _tmDbClient.GetTvEpisodeAsync( tvShowId, seasonNumber, @@ -278,9 +342,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// </summary> /// <param name="name">The name of the tv show.</param> /// <param name="language">The tv show's language.</param> + /// <param name="year">The year the tv show first aired.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb tv show information.</returns> - public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, CancellationToken cancellationToken) + public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default) { var key = $"searchseries-{name}-{language}"; if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series)) @@ -291,7 +356,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb await EnsureClientConfigAsync().ConfigureAwait(false); var searchResults = await _tmDbClient - .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken) + .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken) .ConfigureAwait(false); if (searchResults.Results.Count > 0) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 0e8a5baab..2498ce9c4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using MediaBrowser.Model.Entities; using TMDbLib.Objects.General; @@ -12,6 +13,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// </summary> public static class TmdbUtils { + private static readonly Regex _nonWords = new (@"[\W_]+", RegexOptions.Compiled); + /// <summary> /// URL of the TMDB instance to use. /// </summary> @@ -43,25 +46,36 @@ namespace MediaBrowser.Providers.Plugins.Tmdb }; /// <summary> + /// Cleans the name according to TMDb requirements. + /// </summary> + /// <param name="name">The name of the entity.</param> + /// <returns>The cleaned name.</returns> + public static string CleanName(string name) + { + // TMDb expects a space separated list of words make sure that is the case + return _nonWords.Replace(name, " "); + } + + /// <summary> /// Maps the TMDB provided roles for crew members to Jellyfin roles. /// </summary> /// <param name="crew">Crew member to map against the Jellyfin person types.</param> /// <returns>The Jellyfin person type.</returns> public static string MapCrewToPersonType(Crew crew) { - if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase) - && crew.Job.Contains("director", StringComparison.InvariantCultureIgnoreCase)) + if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) + && crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase)) { return PersonType.Director; } - if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase) - && crew.Job.Contains("producer", StringComparison.InvariantCultureIgnoreCase)) + if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) + && crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase)) { return PersonType.Producer; } - if (crew.Department.Equals("writing", StringComparison.InvariantCultureIgnoreCase)) + if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)) { return PersonType.Writer; } diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index 5fcf6d9aa..5ec9a02cb 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -177,13 +178,11 @@ namespace MediaBrowser.Providers.Studios { var lines = new List<string>(); - while (!reader.EndOfStream) + foreach (var line in reader.ReadAllLines()) { - var text = reader.ReadLine(); - - if (!string.IsNullOrWhiteSpace(text)) + if (!string.IsNullOrWhiteSpace(line)) { - lines.Add(text); + lines.Add(line); } } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 47e9d5ee8..bf0c853ae 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -205,18 +205,36 @@ namespace MediaBrowser.Providers.Subtitles if (saveInMediaFolder) { - savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName)); + var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName)); + // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path."); + if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)) + { + savePaths.Add(mediaFolderPath); + } } - savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); + var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); + + // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path."); + if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) + { + savePaths.Add(internalPath); + } - await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); + if (savePaths.Count > 0) + { + await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); + } + else + { + _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid."); + } } } private async Task TrySaveToFiles(Stream stream, List<string> savePaths) { - Exception exceptionToThrow = null; + List<Exception> exs = null; foreach (var savePath in savePaths) { @@ -228,7 +246,8 @@ namespace MediaBrowser.Providers.Subtitles { Directory.CreateDirectory(Path.GetDirectoryName(savePath)); - using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, FileStreamBufferSize, true)) + // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . + using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true)) { await stream.CopyToAsync(fs).ConfigureAwait(false); } @@ -237,10 +256,7 @@ namespace MediaBrowser.Providers.Subtitles } catch (Exception ex) { - if (exceptionToThrow == null) - { - exceptionToThrow = ex; - } + (exs ??= new List<Exception>()).Add(ex); } finally { @@ -250,9 +266,9 @@ namespace MediaBrowser.Providers.Subtitles stream.Position = 0; } - if (exceptionToThrow != null) + if (exs != null) { - throw exceptionToThrow; + throw new AggregateException(exs); } } diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index 40f06c731..2904b40ec 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -20,18 +20,15 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 6f164caf3..302c93f0b 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -6,11 +6,12 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using System.Threading; using System.Xml; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Providers; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -27,6 +28,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers private readonly IConfigurationManager _config; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; + private readonly IDirectoryService _directoryService; private Dictionary<string, string> _validProviderIds; /// <summary> @@ -37,12 +39,14 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public BaseNfoParser( ILogger logger, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) { Logger = logger; _config = config; @@ -50,6 +54,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers _validProviderIds = new Dictionary<string, string>(); _userManager = userManager; _userDataManager = userDataManager; + _directoryService = directoryService; } protected CultureInfo UsCulture { get; } = new CultureInfo("en-US"); @@ -63,8 +68,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected virtual bool SupportsUrlAfterClosingXmlTag => false; - protected virtual string MovieDbParserSearchString => "themoviedb.org/movie/"; - /// <summary> /// Fetches metadata for an item from one xml file. /// </summary> @@ -181,8 +184,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers } else { - // If the file is just an Imdb url, handle that - + // If the file is just provider urls, handle that ParseProviderLinks(item.Item, xml); return; @@ -221,50 +223,29 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected void ParseProviderLinks(T item, string xml) { - // Look for a match for the Regex pattern "tt" followed by 7 or 8 digits - var m = Regex.Match(xml, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); - if (m.Success) + if (ProviderIdParsers.TryFindImdbId(xml, out var imdbId)) { - item.SetProviderId(MetadataProvider.Imdb, m.Value); + item.SetProviderId(MetadataProvider.Imdb, imdbId.ToString()); } - // Support Tmdb - // https://www.themoviedb.org/movie/30287-fallo - var srch = MovieDbParserSearchString; - var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase); - - if (index != -1) + if (item is Movie) { - var tmdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/'); - index = tmdbId.IndexOf('-'); - if (index != -1) + if (ProviderIdParsers.TryFindTmdbMovieId(xml, out var tmdbId)) { - tmdbId = tmdbId.Slice(0, index); - } - - if (!tmdbId.IsEmpty - && !tmdbId.IsWhiteSpace() - && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - item.SetProviderId(MetadataProvider.Tmdb, value.ToString(UsCulture)); + item.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString()); } } if (item is Series) { - srch = "thetvdb.com/?tab=series&id="; - - index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase); + if (ProviderIdParsers.TryFindTmdbSeriesId(xml, out var tmdbId)) + { + item.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString()); + } - if (index != -1) + if (ProviderIdParsers.TryFindTvdbId(xml, out var tvdbId)) { - var tvdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/'); - if (!tvdbId.IsEmpty - && !tvdbId.IsWhiteSpace() - && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - item.SetProviderId(MetadataProvider.Tvdb, value.ToString(UsCulture)); - } + item.SetProviderId(MetadataProvider.Tvdb, tvdbId.ToString()); } } } @@ -681,6 +662,21 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } + case "ratings": + { + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchFromRatingsNode(subtree, item); + } + else + { + reader.Read(); + } + + break; + } + case "aired": case "formed": case "premiered": @@ -785,6 +781,64 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } + case "thumb": + { + var artType = reader.GetAttribute("aspect"); + var val = reader.ReadElementContentAsString(); + + // skip: + // - empty aspect tag + // - empty uri + // - tag containing '.' because we can't set images for seasons, episodes or movie sets within series or movies + if (string.IsNullOrEmpty(artType) || string.IsNullOrEmpty(val) || artType.Contains('.', StringComparison.Ordinal)) + { + break; + } + + ImageType imageType = GetImageType(artType); + + if (!Uri.TryCreate(val, UriKind.Absolute, out var uri)) + { + Logger.LogError("Image location {Path} specified in nfo file for {ItemName} is not a valid URL or file path.", val, item.Name); + break; + } + + if (uri.IsFile) + { + // only allow one item of each type + if (itemResult.Images.Any(x => x.Type == imageType)) + { + break; + } + + var fileSystemMetadata = _directoryService.GetFile(val); + // non existing file returns null + if (fileSystemMetadata == null || !fileSystemMetadata.Exists) + { + Logger.LogWarning("Artwork file {Path} specified in nfo file for {ItemName} does not exist.", uri, item.Name); + break; + } + + itemResult.Images.Add(new LocalImageInfo() + { + FileInfo = fileSystemMetadata, + Type = imageType + }); + } + else + { + // only allow one item of each type + if (itemResult.RemoteImages.Any(x => x.type == imageType)) + { + break; + } + + itemResult.RemoteImages.Add((uri.ToString(), imageType)); + } + + break; + } + default: string readerName = reader.Name; if (_validProviderIds.TryGetValue(readerName, out string? providerIdValue)) @@ -1041,6 +1095,92 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } + private void FetchFromRatingsNode(XmlReader reader, T item) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "rating": + { + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + + var ratingName = reader.GetAttribute("name"); + + using var subtree = reader.ReadSubtree(); + FetchFromRatingNode(subtree, item, ratingName); + + break; + } + + default: + reader.Skip(); + break; + } + } + else + { + reader.Read(); + } + } + } + + private void FetchFromRatingNode(XmlReader reader, T item, string? ratingName) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "value": + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (float.TryParse(val, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var ratingValue)) + { + // if ratingName contains tomato --> assume critic rating + if (ratingName != null && + ratingName.Contains("tomato", StringComparison.OrdinalIgnoreCase) && + !ratingName.Contains("audience", StringComparison.OrdinalIgnoreCase)) + { + item.CriticRating = ratingValue; + } + else + { + item.CommunityRating = ratingValue; + } + } + } + + break; + default: + reader.Skip(); + break; + } + } + else + { + reader.Read(); + } + } + } + /// <summary> /// Gets the persons from XML node. /// </summary> @@ -1168,11 +1308,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// <returns>IEnumerable{System.String}.</returns> private IEnumerable<string> SplitNames(string value) { - value = value ?? string.Empty; - // Only split by comma if there is no pipe in the string // We have to be careful to not split names like Matthew, Jr. - var separator = value.IndexOf('|', StringComparison.Ordinal) == -1 && value.IndexOf(';', StringComparison.Ordinal) == -1 + var separator = !value.Contains('|', StringComparison.Ordinal) && !value.Contains(';', StringComparison.Ordinal) ? new[] { ',' } : new[] { '|', ';' }; @@ -1180,5 +1318,25 @@ namespace MediaBrowser.XbmcMetadata.Parsers return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : value.Split(separator, StringSplitOptions.RemoveEmptyEntries); } + + /// <summary> + /// Parses the ImageType from the nfo aspect property. + /// </summary> + /// <param name="aspect">The nfo aspect property.</param> + /// <returns>The image type.</returns> + private static ImageType GetImageType(string aspect) + { + return aspect switch + { + "banner" => ImageType.Banner, + "clearlogo" => ImageType.Logo, + "discart" => ImageType.Disc, + "landscape" => ImageType.Thumb, + "clearart" => ImageType.Art, + "fanart" => ImageType.Backdrop, + // unknown type (including "poster") --> primary + _ => ImageType.Primary, + }; + } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index eb93148c6..6b1607530 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -25,13 +25,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public EpisodeNfoParser( ILogger logger, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) - : base(logger, config, providerManager, userManager, userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) + : base(logger, config, providerManager, userManager, userDataManager, directoryService) { } diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index 2d0eb8433..e51055725 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -25,13 +25,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="DirectoryService"/> interface.</param> public MovieNfoParser( ILogger logger, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) - : base(logger, config, providerManager, userManager, userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) + : base(logger, config, providerManager, userManager, userDataManager, directoryService) { } @@ -47,12 +49,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers { case "id": { + // get ids from attributes string? imdbId = reader.GetAttribute("IMDB"); string? tmdbId = reader.GetAttribute("TMDB"); - if (string.IsNullOrWhiteSpace(imdbId)) + // read id from content + var contentId = reader.ReadElementContentAsString(); + if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId)) { - imdbId = reader.ReadElementContentAsString(); + imdbId = contentId; + } + else if (string.IsNullOrEmpty(tmdbId)) + { + tmdbId = contentId; } if (!string.IsNullOrWhiteSpace(imdbId)) diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs index bd2607bd8..2f5fd40e2 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs @@ -21,13 +21,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="DirectoryService"/> interface.</param> public SeasonNfoParser( ILogger logger, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) - : base(logger, config, providerManager, userManager, userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) + : base(logger, config, providerManager, userManager, userDataManager, directoryService) { } diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index fbab8b521..2c893ac9f 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -22,13 +22,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public SeriesNfoParser( ILogger logger, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) - : base(logger, config, providerManager, userManager, userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) + : base(logger, config, providerManager, userManager, userDataManager, directoryService) { } @@ -36,9 +38,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected override bool SupportsUrlAfterClosingXmlTag => true; /// <inheritdoc /> - protected override string MovieDbParserSearchString => "themoviedb.org/tv/"; - - /// <inheritdoc /> protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Series> itemResult) { var item = itemResult.Item; diff --git a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs index 24f127411..f7a85a07e 100644 --- a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs @@ -20,6 +20,7 @@ namespace MediaBrowser.XbmcMetadata.Providers private readonly IProviderManager _providerManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; + private readonly IDirectoryService _directoryService; /// <summary> /// Initializes a new instance of the <see cref="AlbumNfoProvider"/> class. @@ -30,13 +31,15 @@ namespace MediaBrowser.XbmcMetadata.Providers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public AlbumNfoProvider( ILogger<AlbumNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) : base(fileSystem) { _logger = logger; @@ -44,16 +47,17 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; _userManager = userManager; _userDataManager = userDataManager; + _directoryService = directoryService; } /// <inheritdoc /> protected override void Fetch(MetadataResult<MusicAlbum> result, string path, CancellationToken cancellationToken) { - new BaseNfoParser<MusicAlbum>(_logger, _config, _providerManager, _userManager, _userDataManager).Fetch(result, path, cancellationToken); + new BaseNfoParser<MusicAlbum>(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken); } /// <inheritdoc /> - protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) + protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService) => directoryService.GetFile(Path.Combine(info.Path, "album.nfo")); } } diff --git a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs index fac28ab59..2b563b7fa 100644 --- a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs @@ -20,6 +20,7 @@ namespace MediaBrowser.XbmcMetadata.Providers private readonly IProviderManager _providerManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; + private readonly IDirectoryService _directoryService; /// <summary> /// Initializes a new instance of the <see cref="ArtistNfoProvider"/> class. @@ -30,13 +31,15 @@ namespace MediaBrowser.XbmcMetadata.Providers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public ArtistNfoProvider( IFileSystem fileSystem, ILogger<ArtistNfoProvider> logger, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) : base(fileSystem) { _logger = logger; @@ -44,16 +47,17 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; _userManager = userManager; _userDataManager = userDataManager; + _directoryService = directoryService; } /// <inheritdoc /> protected override void Fetch(MetadataResult<MusicArtist> result, string path, CancellationToken cancellationToken) { - new BaseNfoParser<MusicArtist>(_logger, _config, _providerManager, _userManager, _userDataManager).Fetch(result, path, cancellationToken); + new BaseNfoParser<MusicArtist>(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken); } /// <inheritdoc /> - protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) + protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService) => directoryService.GetFile(Path.Combine(info.Path, "artist.nfo")); } } diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs index af722748b..8574be3f3 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs @@ -21,14 +21,16 @@ namespace MediaBrowser.XbmcMetadata.Providers private readonly IProviderManager _providerManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; + private readonly IDirectoryService _directoryService; - public BaseVideoNfoProvider( + protected BaseVideoNfoProvider( ILogger<BaseVideoNfoProvider<T>> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) : base(fileSystem) { _logger = logger; @@ -36,6 +38,7 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; _userManager = userManager; _userDataManager = userDataManager; + _directoryService = directoryService; } /// <inheritdoc /> @@ -45,10 +48,12 @@ namespace MediaBrowser.XbmcMetadata.Providers { Item = result.Item }; - new MovieNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager).Fetch(tmpItem, path, cancellationToken); + new MovieNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(tmpItem, path, cancellationToken); result.Item = (T)tmpItem.Item; result.People = tmpItem.People; + result.Images = tmpItem.Images; + result.RemoteImages = tmpItem.RemoteImages; if (tmpItem.UserDataList != null) { diff --git a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs index 7233f99dc..f6c5877b4 100644 --- a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs @@ -20,6 +20,7 @@ namespace MediaBrowser.XbmcMetadata.Providers private readonly IProviderManager _providerManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; + private readonly IDirectoryService _directoryService; /// <summary> /// Initializes a new instance of the <see cref="EpisodeNfoProvider"/> class. @@ -30,13 +31,15 @@ namespace MediaBrowser.XbmcMetadata.Providers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public EpisodeNfoProvider( ILogger<EpisodeNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) : base(fileSystem) { _logger = logger; @@ -44,16 +47,17 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; _userManager = userManager; _userDataManager = userDataManager; + _directoryService = directoryService; } /// <inheritdoc /> protected override void Fetch(MetadataResult<Episode> result, string path, CancellationToken cancellationToken) { - new EpisodeNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager).Fetch(result, path, cancellationToken); + new EpisodeNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken); } /// <inheritdoc /> - protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) + protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService) { var path = Path.ChangeExtension(info.Path, ".nfo"); diff --git a/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs index 811d39a9d..cdbc5a918 100644 --- a/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs @@ -21,14 +21,16 @@ namespace MediaBrowser.XbmcMetadata.Providers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public MovieNfoProvider( ILogger<MovieNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) - : base(logger, fileSystem, config, providerManager, userManager, userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) + : base(logger, fileSystem, config, providerManager, userManager, userDataManager, directoryService) { } } diff --git a/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs index 09df509ee..9d1f3e61d 100644 --- a/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/MusicVideoNfoProvider.cs @@ -21,14 +21,16 @@ namespace MediaBrowser.XbmcMetadata.Providers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public MusicVideoNfoProvider( ILogger<MusicVideoNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) - : base(logger, fileSystem, config, providerManager, userManager, userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) + : base(logger, fileSystem, config, providerManager, userManager, userDataManager, directoryService) { } } diff --git a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs index 8f0ed6df7..d01abadf6 100644 --- a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs @@ -20,6 +20,7 @@ namespace MediaBrowser.XbmcMetadata.Providers private readonly IProviderManager _providerManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; + private readonly IDirectoryService _directoryService; /// <summary> /// Initializes a new instance of the <see cref="SeasonNfoProvider"/> class. @@ -30,13 +31,15 @@ namespace MediaBrowser.XbmcMetadata.Providers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public SeasonNfoProvider( ILogger<SeasonNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) : base(fileSystem) { _logger = logger; @@ -44,16 +47,17 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; _userManager = userManager; _userDataManager = userDataManager; + _directoryService = directoryService; } /// <inheritdoc /> protected override void Fetch(MetadataResult<Season> result, string path, CancellationToken cancellationToken) { - new SeasonNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager).Fetch(result, path, cancellationToken); + new SeasonNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken); } /// <inheritdoc /> - protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) + protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService) => directoryService.GetFile(Path.Combine(info.Path, "season.nfo")); } } diff --git a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs index 3e496dc58..002b94463 100644 --- a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs @@ -20,6 +20,7 @@ namespace MediaBrowser.XbmcMetadata.Providers private readonly IProviderManager _providerManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; + private readonly IDirectoryService _directoryService; /// <summary> /// Initializes a new instance of the <see cref="SeriesNfoProvider"/> class. @@ -30,13 +31,15 @@ namespace MediaBrowser.XbmcMetadata.Providers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public SeriesNfoProvider( ILogger<SeriesNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) : base(fileSystem) { _logger = logger; @@ -44,16 +47,17 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; _userManager = userManager; _userDataManager = userDataManager; + _directoryService = directoryService; } /// <inheritdoc /> protected override void Fetch(MetadataResult<Series> result, string path, CancellationToken cancellationToken) { - new SeriesNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager).Fetch(result, path, cancellationToken); + new SeriesNfoParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken); } /// <inheritdoc /> - protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) + protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService) => directoryService.GetFile(Path.Combine(info.Path, "tvshow.nfo")); } } diff --git a/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs index 4717d81e6..93b1be62f 100644 --- a/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/VideoNfoProvider.cs @@ -21,14 +21,16 @@ namespace MediaBrowser.XbmcMetadata.Providers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> public VideoNfoProvider( ILogger<VideoNfoProvider> logger, IFileSystem fileSystem, IConfigurationManager config, IProviderManager providerManager, IUserManager userManager, - IUserDataManager userDataManager) - : base(logger, fileSystem, config, providerManager, userManager, userDataManager) + IUserDataManager userDataManager, + IDirectoryService directoryService) + : base(logger, fileSystem, config, providerManager, userManager, userDataManager, directoryService) { } } diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs index c22f77dcd..2385e7048 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs @@ -96,18 +96,16 @@ namespace MediaBrowser.XbmcMetadata.Savers } /// <inheritdoc /> - protected override List<string> GetTagsUsed(BaseItem item) + protected override IEnumerable<string> GetTagsUsed(BaseItem item) { - var list = base.GetTagsUsed(item); - list.AddRange( - new string[] - { - "track", - "artist", - "albumartist" - }); + foreach (var tag in base.GetTagsUsed(item)) + { + yield return tag; + } - return list; + yield return "track"; + yield return "artist"; + yield return "albumartist"; } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index 6365cdecb..71b58cddb 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -88,16 +88,15 @@ namespace MediaBrowser.XbmcMetadata.Savers } /// <inheritdoc /> - protected override List<string> GetTagsUsed(BaseItem item) + protected override IEnumerable<string> GetTagsUsed(BaseItem item) { - var list = base.GetTagsUsed(item); - list.AddRange(new string[] + foreach (var tag in base.GetTagsUsed(item)) { - "album", - "disbanded" - }); + yield return tag; + } - return list; + yield return "album"; + yield return "disbanded"; } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 0edab3787..3be35e2d9 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -166,19 +166,16 @@ namespace MediaBrowser.XbmcMetadata.Savers /// <inheritdoc /> public abstract bool IsEnabledFor(BaseItem item, ItemUpdateType updateType); - protected virtual List<string> GetTagsUsed(BaseItem item) + protected virtual IEnumerable<string> GetTagsUsed(BaseItem item) { - var list = new List<string>(); foreach (var providerKey in item.ProviderIds.Keys) { var providerIdTagName = GetTagForProviderKey(providerKey); if (!_commonTags.Contains(providerIdTagName)) { - list.Add(providerIdTagName); + yield return providerIdTagName; } } - - return list; } /// <inheritdoc /> @@ -261,7 +258,7 @@ namespace MediaBrowser.XbmcMetadata.Savers AddMediaInfo(hasMediaSources, writer); } - var tagsUsed = GetTagsUsed(item); + var tagsUsed = GetTagsUsed(item).ToList(); try { @@ -351,10 +348,7 @@ namespace MediaBrowser.XbmcMetadata.Savers } var scanType = stream.IsInterlaced ? "interlaced" : "progressive"; - if (!string.IsNullOrEmpty(scanType)) - { - writer.WriteElementString("scantype", scanType); - } + writer.WriteElementString("scantype", scanType); if (stream.Channels.HasValue) { @@ -968,7 +962,7 @@ namespace MediaBrowser.XbmcMetadata.Savers => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); - private void AddCustomTags(string path, List<string> xmlTagsUsed, XmlWriter writer, ILogger<BaseNfoSaver> logger) + private void AddCustomTags(string path, IReadOnlyCollection<string> xmlTagsUsed, XmlWriter writer, ILogger<BaseNfoSaver> logger) { var settings = new XmlReaderSettings() { diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs index 5d3d17893..62f80e81b 100644 --- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs @@ -111,24 +111,23 @@ namespace MediaBrowser.XbmcMetadata.Savers } /// <inheritdoc /> - protected override List<string> GetTagsUsed(BaseItem item) + protected override IEnumerable<string> GetTagsUsed(BaseItem item) { - var list = base.GetTagsUsed(item); - list.AddRange(new string[] + foreach (var tag in base.GetTagsUsed(item)) { - "aired", - "season", - "episode", - "episodenumberend", - "airsafter_season", - "airsbefore_episode", - "airsbefore_season", - "displayseason", - "displayepisode", - "showtitle" - }); - - return list; + yield return tag; + } + + yield return "aired"; + yield return "season"; + yield return "episode"; + yield return "episodenumberend"; + yield return "airsafter_season"; + yield return "airsbefore_episode"; + yield return "airsbefore_season"; + yield return "displayseason"; + yield return "displayepisode"; + yield return "showtitle"; } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index 841121735..412e8031b 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -123,18 +123,17 @@ namespace MediaBrowser.XbmcMetadata.Savers } /// <inheritdoc /> - protected override List<string> GetTagsUsed(BaseItem item) + protected override IEnumerable<string> GetTagsUsed(BaseItem item) { - var list = base.GetTagsUsed(item); - list.AddRange(new string[] + foreach (var tag in base.GetTagsUsed(item)) { - "album", - "artist", - "set", - "id" - }); + yield return tag; + } - return list; + yield return "album"; + yield return "artist"; + yield return "set"; + yield return "id"; } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs index 925a230bd..b9d73ba82 100644 --- a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs @@ -72,15 +72,14 @@ namespace MediaBrowser.XbmcMetadata.Savers } /// <inheritdoc /> - protected override List<string> GetTagsUsed(BaseItem item) + protected override IEnumerable<string> GetTagsUsed(BaseItem item) { - var list = base.GetTagsUsed(item); - list.AddRange(new string[] + foreach (var tag in base.GetTagsUsed(item)) { - "seasonnumber" - }); + yield return tag; + } - return list; + yield return "seasonnumber"; } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs index 42285db76..083f22e5d 100644 --- a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs @@ -90,20 +90,19 @@ namespace MediaBrowser.XbmcMetadata.Savers } /// <inheritdoc /> - protected override List<string> GetTagsUsed(BaseItem item) + protected override IEnumerable<string> GetTagsUsed(BaseItem item) { - var list = base.GetTagsUsed(item); - list.AddRange(new string[] + foreach (var tag in base.GetTagsUsed(item)) { - "id", - "episodeguide", - "season", - "episode", - "status", - "displayorder" - }); + yield return tag; + } - return list; + yield return "id"; + yield return "episodeguide"; + yield return "season"; + yield return "episode"; + yield return "status"; + yield return "displayorder"; } } } @@ -29,9 +29,6 @@ <a href="https://features.jellyfin.org"> <img alt="Submit Feature Requests" src="https://img.shields.io/badge/fider-vote%20on%20features-success.svg"/> </a> -<a href="https://forum.jellyfin.org"> -<img alt="Discuss on our Forum" src="https://img.shields.io/discourse/https/forum.jellyfin.org/users.svg"/> -</a> <a href="https://matrix.to/#/+jellyfin:matrix.org"> <img alt="Chat on Matrix" src="https://img.shields.io/matrix/jellyfin:matrix.org.svg?logo=matrix"/> </a> @@ -85,6 +82,8 @@ Before the project can be built, you must first install the [.NET 5.0 SDK](https Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET Core development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2017) and [Visual Studio Code](https://code.visualstudio.com/Download). +[ffmpeg](https://github.com/jellyfin/jellyfin-ffmpeg) will also need to be installed. + ### Cloning the Repository After dependencies are installed you will need to clone a local copy of this repository. If you just want to run the server from source you can clone this repository directly, but if you are intending to contribute code changes to the project, you should [set up your own fork](https://jellyfin.org/docs/general/contributing/development.html#set-up-your-copy-of-the-repo) of the repository. The following example shows how you can clone the repository directly over HTTPS. diff --git a/RSSDP/DeviceAvailableEventArgs.cs b/RSSDP/DeviceAvailableEventArgs.cs index b7d22a7df..04b14c4dc 100644 --- a/RSSDP/DeviceAvailableEventArgs.cs +++ b/RSSDP/DeviceAvailableEventArgs.cs @@ -8,7 +8,7 @@ namespace Rssdp /// </summary> public sealed class DeviceAvailableEventArgs : EventArgs { - public IPAddress LocalIpAddress { get; set; } + public IPAddress RemoteIpAddress { get; set; } private readonly DiscoveredSsdpDevice _DiscoveredDevice; diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 8f1f0fa61..f448ad38b 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -415,10 +415,7 @@ namespace Rssdp.Infrastructure { lock (_SendSocketSynchroniser) { - if (_sendSockets == null) - { - _sendSockets = CreateSocketAndListenForResponsesAsync(); - } + _sendSockets ??= CreateSocketAndListenForResponsesAsync(); } } } diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index bfad6de97..188e298e2 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -97,6 +97,11 @@ namespace Rssdp.Infrastructure private async void OnBroadcastTimerCallback(object state) { + if (IsDisposed) + { + return; + } + StartListeningForNotifications(); RemoveExpiredDevicesFromCache(); @@ -180,8 +185,6 @@ namespace Rssdp.Infrastructure /// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> ty is true.</exception> public void StartListeningForNotifications() { - ThrowIfDisposed(); - _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived; _CommunicationsServer.RequestReceived += CommsServer_RequestReceived; _CommunicationsServer.BeginListeningForBroadcasts(); @@ -208,7 +211,7 @@ namespace Rssdp.Infrastructure /// Raises the <see cref="DeviceAvailable"/> event. /// </summary> /// <seealso cref="DeviceAvailable"/> - protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress localIpAddress) + protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IpAddress) { if (this.IsDisposed) { @@ -220,7 +223,7 @@ namespace Rssdp.Infrastructure { handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) { - LocalIpAddress = localIpAddress + RemoteIpAddress = IpAddress }); } } @@ -286,7 +289,7 @@ namespace Rssdp.Infrastructure } } - private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress localIpAddress) + private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress IpAddress) { bool isNewDevice = false; lock (_Devices) @@ -304,17 +307,17 @@ namespace Rssdp.Infrastructure } } - DeviceFound(device, isNewDevice, localIpAddress); + DeviceFound(device, isNewDevice, IpAddress); } - private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress localIpAddress) + private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IpAddress) { if (!NotificationTypeMatchesFilter(device)) { return; } - OnDeviceAvailable(device, isNewDevice, localIpAddress); + OnDeviceAvailable(device, isNewDevice, IpAddress); } private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device) @@ -347,13 +350,13 @@ namespace Rssdp.Infrastructure return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken); } - private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress localIpAddress) + private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress IpAddress) { if (!message.IsSuccessStatusCode) { return; } - + var location = GetFirstHeaderUriValue("Location", message); if (location != null) { @@ -367,11 +370,11 @@ namespace Rssdp.Infrastructure ResponseHeaders = message.Headers }; - AddOrUpdateDiscoveredDevice(device, localIpAddress); + AddOrUpdateDiscoveredDevice(device, IpAddress); } } - private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress localIpAddress) + private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IpAddress) { if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) { @@ -381,7 +384,7 @@ namespace Rssdp.Infrastructure var notificationType = GetFirstHeaderStringValue("NTS", message); if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) { - ProcessAliveNotification(message, localIpAddress); + ProcessAliveNotification(message, IpAddress); } else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) { @@ -389,7 +392,7 @@ namespace Rssdp.Infrastructure } } - private void ProcessAliveNotification(HttpRequestMessage message, IPAddress localIpAddress) + private void ProcessAliveNotification(HttpRequestMessage message, IPAddress IpAddress) { var location = GetFirstHeaderUriValue("Location", message); if (location != null) @@ -404,7 +407,7 @@ namespace Rssdp.Infrastructure ResponseHeaders = message.Headers }; - AddOrUpdateDiscoveredDevice(device, localIpAddress); + AddOrUpdateDiscoveredDevice(device, IpAddress); } } @@ -515,11 +518,6 @@ namespace Rssdp.Infrastructure private void RemoveExpiredDevicesFromCache() { - if (this.IsDisposed) - { - return; - } - DiscoveredSsdpDevice[] expiredDevices = null; lock (_Devices) { @@ -630,7 +628,7 @@ namespace Rssdp.Infrastructure private void CommsServer_RequestReceived(object sender, RequestReceivedEventArgs e) { - ProcessNotificationMessage(e.Message, e.LocalIpAddress); + ProcessNotificationMessage(e.Message, e.ReceivedFrom.Address); } } } diff --git a/apiclient/.openapi-generator-ignore b/apiclient/.openapi-generator-ignore deleted file mode 100644 index f3802cf54..000000000 --- a/apiclient/.openapi-generator-ignore +++ /dev/null @@ -1,2 +0,0 @@ -# Prevent generator from creating these files: -git_push.sh diff --git a/apiclient/templates/typescript/axios/generate.sh b/apiclient/templates/typescript/axios/generate.sh deleted file mode 100644 index 9599f85db..000000000 --- a/apiclient/templates/typescript/axios/generate.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -artifactsDirectory="${1}" - -java -jar openapi-generator-cli.jar generate \ - --input-spec ${artifactsDirectory}/openapispec/openapi.json \ - --generator-name typescript-axios \ - --output ./apiclient/generated/typescript/axios \ - --template-dir ./apiclient/templates/typescript/axios \ - --ignore-file-override ./apiclient/.openapi-generator-ignore \ - --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",modelPackage="models",apiPackage="api",npmName="axios" diff --git a/apiclient/templates/typescript/axios/package.mustache b/apiclient/templates/typescript/axios/package.mustache deleted file mode 100644 index 7bfab08cb..000000000 --- a/apiclient/templates/typescript/axios/package.mustache +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@jellyfin/client-axios", - "version": "10.7.0{{snapshotVersion}}", - "description": "Jellyfin api client using axios", - "author": "Jellyfin Contributors", - "keywords": [ - "axios", - "typescript", - "jellyfin" - ], - "license": "GPL-3.0-only", - "main": "./dist/index.js", - "typings": "./dist/index.d.ts", - "scripts": { - "build": "tsc --outDir dist/", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "axios": "^0.19.2" - }, - "devDependencies": { - "@types/node": "^12.11.5", - "typescript": "^3.6.4" - }{{#npmRepository}},{{/npmRepository}} -{{#npmRepository}} - "publishConfig": { - "registry": "{{npmRepository}}" - } -{{/npmRepository}} -} diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin index 7cbfa88ee..9ebaf2bd8 100644 --- a/debian/conf/jellyfin +++ b/debian/conf/jellyfin @@ -33,6 +33,11 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg" # [OPTIONAL] run Jellyfin without the web app #JELLYFIN_NOWEBAPP_OPT="--nowebclient" +# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC) +# 0 = Workstation +# 1 = Server +#COMPlus_gcServer=1 + # # SysV init/Upstart options # diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64 index 428072613..ec0321f47 100644 --- a/deployment/Dockerfile.debian.amd64 +++ b/deployment/Dockerfile.debian.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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.debian.arm64 b/deployment/Dockerfile.debian.arm64 index b540efc09..8fd5ddb93 100644 --- a/deployment/Dockerfile.debian.arm64 +++ b/deployment/Dockerfile.debian.arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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.debian.armhf b/deployment/Dockerfile.debian.armhf index 426ce02fc..14615d19f 100644 --- a/deployment/Dockerfile.debian.armhf +++ b/deployment/Dockerfile.debian.armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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 2549f25ee..137e56ecf 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -13,9 +13,7 @@ RUN dnf update -y \ && dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd # Install DotNET SDK -RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc \ - && curl -o /etc/yum.repos.d/microsoft-prod.repo https://packages.microsoft.com/config/fedora/$(rpm -E %fedora)/prod.repo \ - && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} +RUN dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} # Create symlinks and directories RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.amd64 /build.sh \ diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64 index 3b91515f3..1f6ca1558 100644 --- a/deployment/Dockerfile.linux.amd64 +++ b/deployment/Dockerfile.linux.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl index 2ca9072ba..6af5d8baf 100644 --- a/deployment/Dockerfile.linux.amd64-musl +++ b/deployment/Dockerfile.linux.amd64-musl @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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.linux.arm64 b/deployment/Dockerfile.linux.arm64 index 03efd306d..15b59e29d 100644 --- a/deployment/Dockerfile.linux.arm64 +++ b/deployment/Dockerfile.linux.arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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.linux.armhf b/deployment/Dockerfile.linux.armhf index 585572204..71a0fda21 100644 --- a/deployment/Dockerfile.linux.armhf +++ b/deployment/Dockerfile.linux.armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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.macos b/deployment/Dockerfile.macos index b37afdcfb..9291bcbb9 100644 --- a/deployment/Dockerfile.macos +++ b/deployment/Dockerfile.macos @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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.portable b/deployment/Dockerfile.portable index 686b20197..e98ba74f8 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -15,7 +15,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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 3513bf8ec..d1fd8818e 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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 5acdf0d17..8e79d417c 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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 42f757d05..627caa95a 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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.windows.amd64 b/deployment/Dockerfile.windows.amd64 index 6ed1193fb..5723abcae 100644 --- a/deployment/Dockerfile.windows.amd64 +++ b/deployment/Dockerfile.windows.amd64 @@ -15,7 +15,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/a2052604-de46-4cd4-8256-9bc222537d32/a798771950904eaf91c0c37c58f516e1/dotnet-sdk-5.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-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/fedora/jellyfin.env b/fedora/jellyfin.env index bf64acd3f..56b7a3558 100644 --- a/fedora/jellyfin.env +++ b/fedora/jellyfin.env @@ -35,3 +35,7 @@ JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh" # [OPTIONAL] run Jellyfin without the web app #JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp" +# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC) +# 0 = Workstation +# 1 = Server +#COMPlus_gcServer=1 diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 81337390c..19c0a08b2 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -38,7 +38,7 @@ <Rule Id="SA1633" Action="None" /> </Rules> - <Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design"> + <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design"> <!-- error on CA2016: Forward the CancellationToken parameter to methods that take one or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token --> <Rule Id="CA2016" Action="Error" /> @@ -53,6 +53,8 @@ <Rule Id="CA1716" Action="Info" /> <!-- disable warning CA1720: Identifiers should not contain type names --> <Rule Id="CA1720" Action="Info" /> + <!-- disable warning CA1805: Do not initialize unnecessarily --> + <Rule Id="CA1805" Action="Info" /> <!-- disable warning CA1812: internal class that is apparently never instantiated. If so, remove the code from the assembly. If this class is intended to contain only static members, make it static --> @@ -61,7 +63,11 @@ <Rule Id="CA1822" Action="Info" /> <!-- disable warning CA2000: Dispose objects before losing scope --> <Rule Id="CA2000" Action="Info" /> + <!-- disable warning CA5394: Do not use insecure randomness --> + <Rule Id="CA5394" Action="Info" /> + <!-- disable warning CA1014: Mark assemblies with CLSCompliantAttribute --> + <Rule Id="CA1014" Action="Info" /> <!-- disable warning CA1054: Change the type of parameter url from string to System.Uri --> <Rule Id="CA1054" Action="None" /> <!-- disable warning CA1055: URI return values should not be strings --> @@ -72,5 +78,7 @@ <Rule Id="CA1303" Action="None" /> <!-- disable warning CA1308: Normalize strings to uppercase --> <Rule Id="CA1308" Action="None" /> + <!-- disable warning CA2101: Specify marshaling for P/Invoke string arguments --> + <Rule Id="CA2101" Action="None" /> </Rules> </RuleSet> diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index ee20cc573..de03aa5f5 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -128,6 +128,8 @@ namespace Jellyfin.Api.Tests.Auth { var authorizationInfo = _fixture.Create<AuthorizationInfo>(); authorizationInfo.User = _fixture.Create<User>(); + authorizationInfo.User.AddDefaultPermissions(); + authorizationInfo.User.AddDefaultPreferences(); authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin); authorizationInfo.IsApiKey = false; diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs index 09ffa8468..5b3d784ff 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; @@ -41,7 +42,7 @@ namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed) { _networkManagerMock - .Setup(n => n.IsInLocalNetwork(It.IsAny<string>())) + .Setup(n => n.IsInLocalNetwork(It.IsAny<IPAddress>())) .Returns(isInLocalNetwork); TestHelpers.SetupConfigurationManager(_configurationManagerMock, true); diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 873ff0ab4..397b863b7 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -10,15 +10,17 @@ <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> - <PackageReference Include="AutoFixture" Version="4.15.0" /> - <PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" /> - <PackageReference Include="AutoFixture.Xunit2" Version="4.15.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.3" /> + <PackageReference Include="AutoFixture" Version="4.17.0" /> + <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> + <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.5" /> <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> @@ -27,22 +29,14 @@ <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" /> - </ItemGroup> - - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - - <ItemGroup> - <EmbeddedResource Include="TestPage.html" /> + <ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" /> + <ProjectReference Include="../../Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj" /> </ItemGroup> </Project> diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs index f27cdf7b6..f9bca4146 100644 --- a/tests/Jellyfin.Api.Tests/TestHelpers.cs +++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs @@ -26,8 +26,11 @@ namespace Jellyfin.Api.Tests { var user = new User( "jellyfin", - typeof(DefaultAuthenticationProvider).FullName, - typeof(DefaultPasswordResetProvider).FullName); + typeof(DefaultAuthenticationProvider).FullName!, + typeof(DefaultPasswordResetProvider).FullName!); + + user.AddDefaultPermissions(); + user.AddDefaultPreferences(); // Set administrator flag. user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase)); diff --git a/tests/Jellyfin.Common.Tests/Crc32Tests.cs b/tests/Jellyfin.Common.Tests/Crc32Tests.cs new file mode 100644 index 000000000..e95a2867f --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Crc32Tests.cs @@ -0,0 +1,33 @@ +using System; +using System.Text; +using MediaBrowser.Common; +using Xunit; + +namespace Jellyfin.Common.Tests +{ + public static class Crc32Tests + { + [Fact] + public static void Compute_Empty_Zero() + { + Assert.Equal<uint>(0, Crc32.Compute(Array.Empty<byte>())); + } + + [Theory] + [InlineData(0x414fa339, "The quick brown fox jumps over the lazy dog")] + public static void Compute_Valid_Success(uint expected, string data) + { + Assert.Equal(expected, Crc32.Compute(Encoding.UTF8.GetBytes(data))); + } + + [Theory] + [InlineData(0x414fa339, "54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F67")] + [InlineData(0x190a55ad, "0000000000000000000000000000000000000000000000000000000000000000")] + [InlineData(0xff6cab0b, "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")] + [InlineData(0x91267e8a, "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F")] + public static void Compute_ValidHex_Success(uint expected, string data) + { + Assert.Equal(expected, Crc32.Compute(Convert.FromHexString(data))); + } + } +} diff --git a/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs new file mode 100644 index 000000000..e6c325bac --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Cryptography; +using Xunit; + +namespace Jellyfin.Common.Tests.Cryptography +{ + public static class PasswordHashTests + { + [Fact] + public static void Ctor_Null_ThrowsArgumentNullException() + { + Assert.Throws<ArgumentNullException>(() => new PasswordHash(null!, Array.Empty<byte>())); + } + + [Fact] + public static void Ctor_Empty_ThrowsArgumentException() + { + Assert.Throws<ArgumentException>(() => new PasswordHash(string.Empty, Array.Empty<byte>())); + } + + public static IEnumerable<object[]> Parse_Valid_TestData() + { + // Id + yield return new object[] + { + "$PBKDF2", + new PasswordHash("PBKDF2", Array.Empty<byte>()) + }; + + // Id + parameter + yield return new object[] + { + "$PBKDF2$iterations=1000", + new PasswordHash( + "PBKDF2", + Array.Empty<byte>(), + Array.Empty<byte>(), + new Dictionary<string, string>() + { + { "iterations", "1000" }, + }) + }; + + // Id + parameters + yield return new object[] + { + "$PBKDF2$iterations=1000,m=120", + new PasswordHash( + "PBKDF2", + Array.Empty<byte>(), + Array.Empty<byte>(), + new Dictionary<string, string>() + { + { "iterations", "1000" }, + { "m", "120" } + }) + }; + + // Id + hash + yield return new object[] + { + "$PBKDF2$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D", + new PasswordHash( + "PBKDF2", + Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"), + Array.Empty<byte>(), + new Dictionary<string, string>()) + }; + + // Id + salt + hash + yield return new object[] + { + "$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D", + new PasswordHash( + "PBKDF2", + Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"), + Convert.FromHexString("69F420"), + new Dictionary<string, string>()) + }; + + // Id + parameter + hash + yield return new object[] + { + "$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D", + new PasswordHash( + "PBKDF2", + Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"), + Array.Empty<byte>(), + new Dictionary<string, string>() + { + { "iterations", "1000" } + }) + }; + + // Id + parameters + hash + yield return new object[] + { + "$PBKDF2$iterations=1000,m=120$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D", + new PasswordHash( + "PBKDF2", + Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"), + Array.Empty<byte>(), + new Dictionary<string, string>() + { + { "iterations", "1000" }, + { "m", "120" } + }) + }; + + // Id + parameters + salt + hash + yield return new object[] + { + "$PBKDF2$iterations=1000,m=120$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D", + new PasswordHash( + "PBKDF2", + Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"), + Convert.FromHexString("69F420"), + new Dictionary<string, string>() + { + { "iterations", "1000" }, + { "m", "120" } + }) + }; + } + + [Theory] + [MemberData(nameof(Parse_Valid_TestData))] + public static void Parse_Valid_Success(string passwordHashString, PasswordHash expected) + { + var passwordHash = PasswordHash.Parse(passwordHashString); + Assert.Equal(expected.Id, passwordHash.Id); + Assert.Equal(expected.Parameters, passwordHash.Parameters); + Assert.Equal(expected.Salt.ToArray(), passwordHash.Salt.ToArray()); + Assert.Equal(expected.Hash.ToArray(), passwordHash.Hash.ToArray()); + Assert.Equal(expected.ToString(), passwordHash.ToString()); + } + + [Theory] + [InlineData("$PBKDF2")] + [InlineData("$PBKDF2$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + [InlineData("$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + [InlineData("$PBKDF2$iterations=1000,m=120$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + [InlineData("$PBKDF2$iterations=1000,m=120$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + [InlineData("$PBKDF2$iterations=1000,m=120")] + public static void ToString_Roundtrip_Success(string passwordHash) + { + Assert.Equal(passwordHash, PasswordHash.Parse(passwordHash).ToString()); + } + + [Fact] + public static void Parse_Null_ThrowsArgumentException() + { + Assert.Throws<ArgumentException>(() => PasswordHash.Parse(null)); + } + + [Fact] + public static void Parse_Empty_ThrowsArgumentException() + { + Assert.Throws<ArgumentException>(() => PasswordHash.Parse(string.Empty)); + } + + [Theory] + [InlineData("$")] // No id + [InlineData("$$")] // Empty segments + [InlineData("PBKDF2$")] // Doesn't start with $ + [InlineData("$PBKDF2$$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Empty segment + [InlineData("$PBKDF2$iterations=1000$$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Empty salt segment + [InlineData("$PBKDF2$iterations=1000$69F420$")] // Empty hash segment + [InlineData("$PBKDF2$=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter + [InlineData("$PBKDF2$=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter + [InlineData("$PBKDF2$iterations=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter + [InlineData("$PBKDF2$iterations=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Ends on $ + [InlineData("$PBKDF2$iterations=$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Extra segment + [InlineData("$PBKDF2$iterations=$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$anotherone")] // Extra segment + [InlineData("$PBKDF2$iterations=$invalidstalt$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid salt + [InlineData("$PBKDF2$iterations=$69F420$invalid hash")] // Invalid hash + [InlineData("$PBKDF2$69F420$")] // Empty hash + public static void Parse_InvalidFormat_ThrowsFormatException(string passwordHash) + { + Assert.Throws<FormatException>(() => PasswordHash.Parse(passwordHash)); + } + } +} diff --git a/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs b/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs new file mode 100644 index 000000000..9903409fa --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Extensions; +using Xunit; + +namespace Jellyfin.Common.Tests.Extensions +{ + public static class CopyToExtensionsTests + { + public static IEnumerable<object[]> CopyTo_Valid_Correct_TestData() + { + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } }; + yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } }; + } + + [Theory] + [MemberData(nameof(CopyTo_Valid_Correct_TestData))] + public static void CopyTo_Valid_Correct<T>(IReadOnlyList<T> source, IList<T> destination, int index, IList<T> expected) + { + source.CopyTo(destination, index); + Assert.Equal(expected, destination); + } + + public static IEnumerable<object[]> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData() + { + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 }; + yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 }; + yield return new object[] { new[] { 0, 1, 2 }, Array.Empty<int>(), 0 }; + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 }; + yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 }; + } + + [Theory] + [MemberData(nameof(CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData))] + public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException<T>(IReadOnlyList<T> source, IList<T> destination, int index) + { + Assert.Throws<ArgumentOutOfRangeException>(() => source.CopyTo(destination, index)); + } + } +} diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 278f34109..8018b2966 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -10,10 +10,12 @@ <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> @@ -21,7 +23,6 @@ <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> @@ -32,8 +33,4 @@ <ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs index 0d2bdd1af..ca300401d 100644 --- a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs +++ b/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System; +using System.Text.Json; using System.Text.Json.Serialization; using Jellyfin.Common.Tests.Models; using MediaBrowser.Model.Session; @@ -9,6 +10,27 @@ namespace Jellyfin.Common.Tests.Json public static class JsonCommaDelimitedArrayTests { [Fact] + public static void Deserialize_String_Null_Success() + { + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": null }", options); + Assert.Null(value?.Value); + } + + [Fact] + public static void Deserialize_Empty_Success() + { + var desiredValue = new GenericBodyArrayModel<string> + { + Value = Array.Empty<string>() + }; + + var options = new JsonSerializerOptions(); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": """" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] public static void Deserialize_String_Valid_Success() { var desiredValue = new GenericBodyArrayModel<string> @@ -49,6 +71,34 @@ namespace Jellyfin.Common.Tests.Json } [Fact] + public static void Deserialize_GenericCommandType_EmptyEntry_Success() + { + var desiredValue = new GenericBodyArrayModel<GeneralCommandType> + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public static void Deserialize_GenericCommandType_Invalid_Success() + { + var desiredValue = new GenericBodyArrayModel<GeneralCommandType> + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new JsonStringEnumConverter()); + var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", options); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] public static void Deserialize_GenericCommandType_Space_Valid_Success() { var desiredValue = new GenericBodyArrayModel<GeneralCommandType> diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs index 1e1cde957..dbfad3c2f 100644 --- a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs +++ b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Text.Json; using MediaBrowser.Common.Json.Converters; using Xunit; diff --git a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs index 22bc7afb9..cb3b66c4c 100644 --- a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs +++ b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Text.Json; using MediaBrowser.Common.Json.Converters; using Xunit; diff --git a/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs index faed086a1..efe8063a0 100644 --- a/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs +++ b/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs @@ -39,6 +39,15 @@ namespace Jellyfin.Common.Tests.Json } [Theory] + [InlineData("\"8\"", 8)] + [InlineData("8", 8)] + public void Deserialize_NullableInt_Success(string input, int? expected) + { + var result = JsonSerializer.Deserialize<int?>(input, _options); + Assert.Equal(result, expected); + } + + [Theory] [InlineData("\"N/A\"")] [InlineData("null")] public void Deserialization_To_Nullable_String_Shoud_Be_Null(string input) @@ -48,21 +57,11 @@ namespace Jellyfin.Common.Tests.Json } [Theory] - [InlineData("\"8\"", 8)] - [InlineData("8", 8)] - public void Deserialize_Int_Success(string input, int expected) - { - var result = JsonSerializer.Deserialize<int>(input, _options); - Assert.Equal(result, expected); - } - - [Fact] - public void Deserialize_Normal_String_Success() + [InlineData("\"Jellyfin\"", "Jellyfin")] + public void Deserialize_Normal_String_Success(string input, string expected) { - const string Input = "\"Jellyfin\""; - const string Expected = "Jellyfin"; - var result = JsonSerializer.Deserialize<string>(Input, _options); - Assert.Equal(Expected, result); + var result = JsonSerializer.Deserialize<string?>(input, _options); + Assert.Equal(expected, result); } [Fact] diff --git a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs new file mode 100644 index 000000000..fd77694b3 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using MediaBrowser.Common.Json.Converters; +using Xunit; + +namespace Jellyfin.Common.Tests.Json +{ + public class JsonStringConverterTests + { + private readonly JsonSerializerOptions _jsonSerializerOptions + = new () + { + Converters = + { + new JsonStringConverter() + } + }; + + [Theory] + [InlineData("\"test\"", "test")] + [InlineData("123", "123")] + [InlineData("123.45", "123.45")] + [InlineData("true", "true")] + [InlineData("false", "false")] + public void Deserialize_String_Valid_Success(string input, string output) + { + var deserialized = JsonSerializer.Deserialize<string>(input, _jsonSerializerOptions); + Assert.Equal(deserialized, output); + } + + [Fact] + public void Deserialize_Int32asInt32_Valid_Success() + { + const string? input = "123"; + const int output = 123; + var deserialized = JsonSerializer.Deserialize<int>(input, _jsonSerializerOptions); + Assert.Equal(deserialized, output); + } + } +}
\ No newline at end of file diff --git a/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs new file mode 100644 index 000000000..f2cefdbf8 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs @@ -0,0 +1,36 @@ +using System; +using System.Text.Json; +using MediaBrowser.Common.Json.Converters; +using Xunit; + +namespace Jellyfin.Common.Tests.Json +{ + public class JsonVersionConverterTests + { + private readonly JsonSerializerOptions _options; + + public JsonVersionConverterTests() + { + _options = new JsonSerializerOptions(); + _options.Converters.Add(new JsonVersionConverter()); + } + + [Fact] + public void Deserialize_Version_Success() + { + var input = "\"1.025.222\""; + var output = new Version(1, 25, 222); + var deserializedInput = JsonSerializer.Deserialize<Version>(input, _options); + Assert.Equal(output, deserializedInput); + } + + [Fact] + public void Serialize_Version_Success() + { + var input = new Version(1, 09, 59); + var output = "\"1.9.59\""; + var serializedInput = JsonSerializer.Serialize(input, _options); + Assert.Equal(output, serializedInput); + } + } +} diff --git a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs deleted file mode 100644 index c4422bd10..000000000 --- a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using MediaBrowser.Common; -using MediaBrowser.Common.Cryptography; -using Xunit; - -namespace Jellyfin.Common.Tests -{ - public class PasswordHashTests - { - [Theory] - [InlineData( - "$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D", - "PBKDF2", - "", - "62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] - public void ParseTest(string passwordHash, string id, string salt, string hash) - { - var pass = PasswordHash.Parse(passwordHash); - Assert.Equal(id, pass.Id); - Assert.Equal(salt, Convert.ToHexString(pass.Salt)); - Assert.Equal(hash, Convert.ToHexString(pass.Hash)); - } - - [Theory] - [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] - public void ToStringTest(string passwordHash) - { - Assert.Equal(passwordHash, PasswordHash.Parse(passwordHash).ToString()); - } - } -} diff --git a/tests/Jellyfin.Common.Tests/Providers/ProviderIdParserTests.cs b/tests/Jellyfin.Common.Tests/Providers/ProviderIdParserTests.cs new file mode 100644 index 000000000..ef9d31cc1 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Providers/ProviderIdParserTests.cs @@ -0,0 +1,85 @@ +using System; +using MediaBrowser.Common.Providers; +using Xunit; + +namespace Jellyfin.Common.Tests.Providers +{ + public class ProviderIdParserTests + { + [Theory] + [InlineData("tt1234567", "tt1234567")] + [InlineData("tt12345678", "tt12345678")] + [InlineData("https://www.imdb.com/title/tt1234567", "tt1234567")] + [InlineData("https://www.imdb.com/title/tt12345678", "tt12345678")] + [InlineData(@"multiline\nhttps://www.imdb.com/title/tt1234567", "tt1234567")] + [InlineData(@"multiline\nhttps://www.imdb.com/title/tt12345678", "tt12345678")] + [InlineData("tt1234567tt7654321", "tt1234567")] + [InlineData("tt12345678tt7654321", "tt12345678")] + [InlineData("tt123456789", "tt12345678")] + public void FindImdbId_Valid_Success(string text, string expected) + { + Assert.True(ProviderIdParsers.TryFindImdbId(text, out ReadOnlySpan<char> parsedId)); + Assert.Equal(expected, parsedId.ToString()); + } + + [Theory] + [InlineData("tt123456")] + [InlineData("https://www.imdb.com/title/tt123456")] + [InlineData("Jellyfin")] + public void FindImdbId_Invalid_Success(string text) + { + Assert.False(ProviderIdParsers.TryFindImdbId(text, out _)); + } + + [Theory] + [InlineData("https://www.themoviedb.org/movie/30287-fallo", "30287")] + [InlineData("themoviedb.org/movie/30287", "30287")] + public void FindTmdbMovieId_Valid_Success(string text, string expected) + { + Assert.True(ProviderIdParsers.TryFindTmdbMovieId(text, out ReadOnlySpan<char> parsedId)); + Assert.Equal(expected, parsedId.ToString()); + } + + [Theory] + [InlineData("https://www.themoviedb.org/movie/fallo-30287")] + [InlineData("https://www.themoviedb.org/tv/1668-friends")] + public void FindTmdbMovieId_Invalid_Success(string text) + { + Assert.False(ProviderIdParsers.TryFindTmdbMovieId(text, out _)); + } + + [Theory] + [InlineData("https://www.themoviedb.org/tv/1668-friends", "1668")] + [InlineData("themoviedb.org/tv/1668", "1668")] + public void FindTmdbSeriesId_Valid_Success(string text, string expected) + { + Assert.True(ProviderIdParsers.TryFindTmdbSeriesId(text, out ReadOnlySpan<char> parsedId)); + Assert.Equal(expected, parsedId.ToString()); + } + + [Theory] + [InlineData("https://www.themoviedb.org/tv/friends-1668")] + [InlineData("https://www.themoviedb.org/movie/30287-fallo")] + public void FindTmdbSeriesId_Invalid_Success(string text) + { + Assert.False(ProviderIdParsers.TryFindTmdbSeriesId(text, out _)); + } + + [Theory] + [InlineData("https://www.thetvdb.com/?tab=series&id=121361", "121361")] + [InlineData("thetvdb.com/?tab=series&id=121361", "121361")] + public void FindTvdbId_Valid_Success(string text, string expected) + { + Assert.True(ProviderIdParsers.TryFindTvdbId(text, out ReadOnlySpan<char> parsedId)); + Assert.Equal(expected, parsedId.ToString()); + } + + [Theory] + [InlineData("thetvdb.com/?tab=series&id=Jellyfin121361")] + [InlineData("https://www.themoviedb.org/tv/1668-friends")] + public void FindTvdbId_Invalid_Success(string text) + { + Assert.False(ProviderIdParsers.TryFindTvdbId(text, out _)); + } + } +} diff --git a/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs new file mode 100644 index 000000000..feffb50e8 --- /dev/null +++ b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs @@ -0,0 +1,200 @@ +using System.Linq; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; +using Moq; +using Xunit; + +namespace Jellyfin.Controller.Tests +{ + public class DirectoryServiceTests + { + private const string LowerCasePath = "/music/someartist"; + private const string UpperCasePath = "/music/SOMEARTIST"; + + private static readonly FileSystemMetadata[] _lowerCaseFileSystemMetadata = + { + new () + { + FullName = LowerCasePath + "/Artwork", + IsDirectory = true + }, + new () + { + FullName = LowerCasePath + "/Some Other Folder", + IsDirectory = true + }, + new () + { + FullName = LowerCasePath + "/Song 2.mp3", + IsDirectory = false + }, + new () + { + FullName = LowerCasePath + "/Song 3.mp3", + IsDirectory = false + } + }; + + private static readonly FileSystemMetadata[] _upperCaseFileSystemMetadata = + { + new () + { + FullName = UpperCasePath + "/Lyrics", + IsDirectory = true + }, + new () + { + FullName = UpperCasePath + "/Song 1.mp3", + IsDirectory = false + } + }; + + [Fact] + public void GetFileSystemEntries_GivenPathsWithDifferentCasing_CachesAll() + { + var fileSystemMock = new Mock<IFileSystem>(); + fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata); + fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata); + var directoryService = new DirectoryService(fileSystemMock.Object); + + var upperCaseResult = directoryService.GetFileSystemEntries(UpperCasePath); + var lowerCaseResult = directoryService.GetFileSystemEntries(LowerCasePath); + + Assert.Equal(_upperCaseFileSystemMetadata, upperCaseResult); + Assert.Equal(_lowerCaseFileSystemMetadata, lowerCaseResult); + } + + [Fact] + public void GetFiles_GivenPathsWithDifferentCasing_ReturnsCorrectFiles() + { + var fileSystemMock = new Mock<IFileSystem>(); + fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata); + fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata); + var directoryService = new DirectoryService(fileSystemMock.Object); + + var upperCaseResult = directoryService.GetFiles(UpperCasePath); + var lowerCaseResult = directoryService.GetFiles(LowerCasePath); + + Assert.Equal(_upperCaseFileSystemMetadata.Where(f => !f.IsDirectory), upperCaseResult); + Assert.Equal(_lowerCaseFileSystemMetadata.Where(f => !f.IsDirectory), lowerCaseResult); + } + + [Fact] + public void GetFile_GivenFilePathsWithDifferentCasing_ReturnsCorrectFile() + { + const string lowerCasePath = "/music/someartist/song 1.mp3"; + var lowerCaseFileSystemMetadata = new FileSystemMetadata + { + FullName = lowerCasePath, + Exists = true + }; + const string upperCasePath = "/music/SOMEARTIST/SONG 1.mp3"; + var upperCaseFileSystemMetadata = new FileSystemMetadata + { + FullName = upperCasePath, + Exists = false + }; + var fileSystemMock = new Mock<IFileSystem>(); + fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata); + fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata); + var directoryService = new DirectoryService(fileSystemMock.Object); + + var lowerCaseResult = directoryService.GetFile(lowerCasePath); + var upperCaseResult = directoryService.GetFile(upperCasePath); + + Assert.Equal(lowerCaseFileSystemMetadata, lowerCaseResult); + Assert.Null(upperCaseResult); + } + + [Fact] + public void GetFile_GivenCachedPath_ReturnsCachedFile() + { + const string path = "/music/someartist/song 1.mp3"; + var cachedFileSystemMetadata = new FileSystemMetadata + { + FullName = path, + Exists = true + }; + var newFileSystemMetadata = new FileSystemMetadata + { + FullName = "/music/SOMEARTIST/song 1.mp3", + Exists = true + }; + + var fileSystemMock = new Mock<IFileSystem>(); + fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == path))).Returns(cachedFileSystemMetadata); + var directoryService = new DirectoryService(fileSystemMock.Object); + + var result = directoryService.GetFile(path); + fileSystemMock.Setup(f => f.GetFileInfo(It.Is<string>(x => x == path))).Returns(newFileSystemMetadata); + var secondResult = directoryService.GetFile(path); + + Assert.Equal(cachedFileSystemMetadata, result); + Assert.Equal(cachedFileSystemMetadata, secondResult); + } + + [Fact] + public void GetFilePaths_GivenCachedFilePathWithoutClear_ReturnsOnlyCachedPaths() + { + const string path = "/music/someartist"; + + var cachedPaths = new[] + { + "/music/someartist/song 1.mp3", + "/music/someartist/song 2.mp3", + "/music/someartist/song 3.mp3", + "/music/someartist/song 4.mp3", + }; + var newPaths = new[] + { + "/music/someartist/song 5.mp3", + "/music/someartist/song 6.mp3", + "/music/someartist/song 7.mp3", + "/music/someartist/song 8.mp3", + }; + + var fileSystemMock = new Mock<IFileSystem>(); + fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths); + var directoryService = new DirectoryService(fileSystemMock.Object); + + var result = directoryService.GetFilePaths(path); + fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths); + var secondResult = directoryService.GetFilePaths(path); + + Assert.Equal(cachedPaths, result); + Assert.Equal(cachedPaths, secondResult); + } + + [Fact] + public void GetFilePaths_GivenCachedFilePathWithClear_ReturnsNewPaths() + { + const string path = "/music/someartist"; + + var cachedPaths = new[] + { + "/music/someartist/song 1.mp3", + "/music/someartist/song 2.mp3", + "/music/someartist/song 3.mp3", + "/music/someartist/song 4.mp3", + }; + var newPaths = new[] + { + "/music/someartist/song 5.mp3", + "/music/someartist/song 6.mp3", + "/music/someartist/song 7.mp3", + "/music/someartist/song 8.mp3", + }; + + var fileSystemMock = new Mock<IFileSystem>(); + fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths); + var directoryService = new DirectoryService(fileSystemMock.Object); + + var result = directoryService.GetFilePaths(path); + fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths); + var secondResult = directoryService.GetFilePaths(path, true); + + Assert.Equal(cachedPaths, result); + Assert.Equal(newPaths, secondResult); + } + } +} diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index b02a68a3d..ad1627698 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -10,10 +10,13 @@ <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> @@ -21,7 +24,6 @@ <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> @@ -31,8 +33,4 @@ <ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs b/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs new file mode 100644 index 000000000..668bd8f87 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/DlnaManagerTests.cs @@ -0,0 +1,131 @@ +using Emby.Dlna; +using Emby.Dlna.PlayTo; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Jellyfin.Dlna.Tests +{ + public class DlnaManagerTests + { + private DlnaManager GetManager() + { + var xmlSerializer = new Mock<IXmlSerializer>(); + var fileSystem = new Mock<IFileSystem>(); + var appPaths = new Mock<IApplicationPaths>(); + var loggerFactory = new Mock<ILoggerFactory>(); + var appHost = new Mock<IServerApplicationHost>(); + + return new DlnaManager(xmlSerializer.Object, fileSystem.Object, appPaths.Object, loggerFactory.Object, appHost.Object); + } + + [Fact] + public void IsMatch_GivenMatchingName_ReturnsTrue() + { + var device = new DeviceInfo() + { + Name = "My Device", + Manufacturer = "LG Electronics", + ManufacturerUrl = "http://www.lge.com", + ModelDescription = "LG WebOSTV DMRplus", + ModelName = "LG TV", + ModelNumber = "1.0", + }; + + var profile = new DeviceProfile() + { + Name = "Test Profile", + FriendlyName = "My Device", + Manufacturer = "LG Electronics", + ManufacturerUrl = "http://www.lge.com", + ModelDescription = "LG WebOSTV DMRplus", + ModelName = "LG TV", + ModelNumber = "1.0", + Identification = new () + { + FriendlyName = "My Device", + Manufacturer = "LG Electronics", + ManufacturerUrl = "http://www.lge.com", + ModelDescription = "LG WebOSTV DMRplus", + ModelName = "LG TV", + ModelNumber = "1.0", + } + }; + + var profile2 = new DeviceProfile() + { + Name = "Test Profile", + FriendlyName = "My Device", + Identification = new DeviceIdentification() + { + FriendlyName = "My Device", + } + }; + + var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile2.Identification); + var deviceMatch2 = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification); + + Assert.True(deviceMatch); + Assert.True(deviceMatch2); + } + + [Fact] + public void IsMatch_GivenNamesAndManufacturersDoNotMatch_ReturnsFalse() + { + var device = new DeviceInfo() + { + Name = "My Device", + Manufacturer = "JVC" + }; + + var profile = new DeviceProfile() + { + Name = "Test Profile", + FriendlyName = "My Device", + Manufacturer = "LG Electronics", + ManufacturerUrl = "http://www.lge.com", + ModelDescription = "LG WebOSTV DMRplus", + ModelName = "LG TV", + ModelNumber = "1.0", + Identification = new () + { + FriendlyName = "My Device", + Manufacturer = "LG Electronics", + ManufacturerUrl = "http://www.lge.com", + ModelDescription = "LG WebOSTV DMRplus", + ModelName = "LG TV", + ModelNumber = "1.0", + } + }; + + var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification); + + Assert.False(deviceMatch); + } + + [Fact] + public void IsMatch_GivenNamesAndRegExMatch_ReturnsTrue() + { + var device = new DeviceInfo() + { + Name = "My Device" + }; + + var profile = new DeviceProfile() + { + Name = "Test Profile", + FriendlyName = "My .*", + Identification = new () + }; + + var deviceMatch = GetManager().IsMatch(device.ToDeviceIdentification(), profile.Identification); + + Assert.True(deviceMatch); + } + } +} diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj index 850db1c75..f7c21f072 100644 --- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj +++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj @@ -5,10 +5,13 @@ <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> @@ -16,7 +19,6 @@ <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> @@ -26,8 +28,4 @@ <ProjectReference Include="../../Emby.Dlna/Emby.Dlna.csproj" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs index c39ef0ce9..415682e85 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs @@ -16,7 +16,7 @@ namespace Jellyfin.MediaEncoding.Tests var path = Path.Join("Test Data", fileName); using (var stream = File.OpenRead(path)) { - await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.GetOptions()).ConfigureAwait(false); + await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false); } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index e729dbb09..8321d0255 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -10,6 +10,8 @@ <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> @@ -19,7 +21,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> @@ -27,7 +29,6 @@ <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> @@ -37,8 +38,4 @@ <ProjectReference Include="../../MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs new file mode 100644 index 000000000..69e2aa437 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -0,0 +1,56 @@ +using System.IO; +using System.Text.Json; +using MediaBrowser.Common.Json; +using MediaBrowser.MediaEncoding.Probing; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Jellyfin.MediaEncoding.Tests.Probing +{ + public class ProbeResultNormalizerTests + { + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), null); + + [Fact] + public void GetMediaInfo_MetaData_Success() + { + var bytes = File.ReadAllBytes("Test Data/Probing/some_matadata.json"); + var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions); + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/some_matadata.mkv", MediaProtocol.File); + + Assert.Single(res.MediaStreams); + + Assert.NotNull(res.VideoStream); + Assert.Equal("4:3", res.VideoStream.AspectRatio); + Assert.Equal(25f, res.VideoStream.AverageFrameRate); + Assert.Equal(8, res.VideoStream.BitDepth); + Assert.Equal(69432, res.VideoStream.BitRate); + Assert.Equal("h264", res.VideoStream.Codec); + Assert.Equal("1/50", res.VideoStream.CodecTimeBase); + Assert.Equal(240, res.VideoStream.Height); + Assert.Equal(320, res.VideoStream.Width); + Assert.Equal(0, res.VideoStream.Index); + Assert.False(res.VideoStream.IsAnamorphic); + Assert.True(res.VideoStream.IsAVC); + Assert.True(res.VideoStream.IsDefault); + Assert.False(res.VideoStream.IsExternal); + Assert.False(res.VideoStream.IsForced); + Assert.False(res.VideoStream.IsInterlaced); + Assert.False(res.VideoStream.IsTextSubtitleStream); + Assert.Equal(13d, res.VideoStream.Level); + Assert.Equal("4", res.VideoStream.NalLengthSize); + Assert.Equal("yuv444p", res.VideoStream.PixelFormat); + Assert.Equal("High 4:4:4 Predictive", res.VideoStream.Profile); + Assert.Equal(25f, res.VideoStream.RealFrameRate); + Assert.Equal(1, res.VideoStream.RefFrames); + Assert.Equal("1/1000", res.VideoStream.TimeBase); + Assert.Equal(MediaStreamType.Video, res.VideoStream.Type); + + Assert.Empty(res.Chapters); + Assert.Equal("Just color bars", res.Overview); + } + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs index 5033d1de9..5db80c300 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs @@ -13,38 +13,11 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests { public class SsaParserTests { - // commonly shared invariant value between tests, assumes default format order - private const string InvariantDialoguePrefix = "[Events]\nDialogue: ,0:00:00.00,0:00:00.01,,,,,,,"; - private readonly SsaParser _parser = new SsaParser(new NullLogger<AssParser>()); [Theory] - [InlineData("[EvEnTs]\nDialogue: ,0:00:00.00,0:00:00.01,,,,,,,text", "text")] // label casing insensitivity - [InlineData("[Events]\n,0:00:00.00,0:00:00.01,,,,,,,labelless dialogue", "labelless dialogue")] // no "Dialogue:" label, it is optional - // TODO: Fix upstream - // [InlineData("[Events]\nFormat: Text, Start, End, Layer, Effect, Style\nDialogue: reordered text,0:00:00.00,0:00:00.01", "reordered text")] // reordered formats - [InlineData(InvariantDialoguePrefix + "Cased TEXT", "Cased TEXT")] // preserve text casing - [InlineData(InvariantDialoguePrefix + " text ", " text ")] // do not trim text - [InlineData(InvariantDialoguePrefix + "text, more text", "text, more text")] // append excess dialogue values (> 10) to text - [InlineData(InvariantDialoguePrefix + "start {\\fnFont Name}text{\\fn} end", "start <font face=\"Font Name\">text</font> end")] // font name - [InlineData(InvariantDialoguePrefix + "start {\\fs10}text{\\fs} end", "start <font size=\"10\">text</font> end")] // font size - [InlineData(InvariantDialoguePrefix + "start {\\c&H112233}text{\\c} end", "start <font color=\"#332211\">text</font> end")] // color - // TODO: Fix upstream - // [InlineData(InvariantDialoguePrefix + "start {\\1c&H112233}text{\\1c} end", "start <font color=\"#332211\">text</font> end")] // primay color - // [InlineData(InvariantDialoguePrefix + "start {\\fnFont Name}text1 {\\fs10}text2{\\fs}{\\fn} {\\1c&H112233}text3{\\1c} end", "start <font face=\"Font Name\">text1 <font size=\"10\">text2</font></font> <font color=\"#332211\">text3</font> end")] // nested formatting - public void Parse(string ssa, string expectedText) - { - using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa))) - { - SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, CancellationToken.None); - SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[0]; - Assert.Equal(expectedText, actual.Text); - } - } - - [Theory] [MemberData(nameof(Parse_MultipleDialogues_TestData))] - public void Parse_MultipleDialogues(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents) + public void Parse_MultipleDialogues_Success(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents) { using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa))) { diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/some_matadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/some_matadata.json new file mode 100644 index 000000000..720fc5c8f --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/some_matadata.json @@ -0,0 +1,74 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "High 4:4:4 Predictive", + "codec_type": "video", + "codec_time_base": "1/50", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 320, + "height": 240, + "coded_width": 320, + "coded_height": 240, + "closed_captions": 0, + "has_b_frames": 2, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "4:3", + "pix_fmt": "yuv444p", + "level": 13, + "chroma_location": "left", + "field_order": "progressive", + "refs": 1, + "is_avc": "true", + "nal_length_size": "4", + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "bits_per_raw_sample": "8", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "ENCODER": "Lavc57.107.100 libx264", + "DURATION": "00:00:01.000000000" + } + } + ], + "chapters": [ + + ], + "format": { + "filename": "some_metadata.mkv", + "nb_streams": 1, + "nb_programs": 0, + "format_name": "matroska,webm", + "format_long_name": "Matroska / WebM", + "start_time": "0.000000", + "duration": "1.000000", + "size": "8679", + "bit_rate": "69432", + "probe_score": 100, + "tags": { + "DESCRIPTION": "Just color bars", + "ARCHIVAL": "yes", + "PRESERVE_THIS": "okay", + "ENCODER": "Lavf57.83.100" + } + } +} diff --git a/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs b/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs new file mode 100644 index 000000000..cca056c28 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Dlna/ContainerProfileTests.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Model.Dlna; +using Xunit; + +namespace Jellyfin.Model.Tests.Dlna +{ + public class ContainerProfileTests + { + private readonly ContainerProfile _emptyContainerProfile = new ContainerProfile(); + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("mp4")] + public void ContainsContainer_EmptyContainerProfile_True(string? containers) + { + Assert.True(_emptyContainerProfile.ContainsContainer(containers)); + } + } +} diff --git a/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs b/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs new file mode 100644 index 000000000..955d296cc --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs @@ -0,0 +1,70 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using MediaBrowser.Model.Entities; +using Xunit; + +namespace Jellyfin.Model.Tests.Entities +{ + public class JsonLowerCaseConverterTests + { + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() + { + Converters = + { + new JsonStringEnumConverter() + } + }; + + [Theory] + [InlineData(null, "{\"CollectionType\":null}")] + [InlineData(CollectionTypeOptions.Movies, "{\"CollectionType\":\"movies\"}")] + [InlineData(CollectionTypeOptions.MusicVideos, "{\"CollectionType\":\"musicvideos\"}")] + public void Serialize_CollectionTypeOptions_Correct(CollectionTypeOptions? collectionType, string expected) + { + Assert.Equal(expected, JsonSerializer.Serialize(new TestContainer(collectionType), _jsonOptions)); + } + + [Theory] + [InlineData("{\"CollectionType\":null}", null)] + [InlineData("{\"CollectionType\":\"movies\"}", CollectionTypeOptions.Movies)] + [InlineData("{\"CollectionType\":\"musicvideos\"}", CollectionTypeOptions.MusicVideos)] + public void Deserialize_CollectionTypeOptions_Correct(string json, CollectionTypeOptions? result) + { + var res = JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions); + Assert.NotNull(res); + Assert.Equal(result, res!.CollectionType); + } + + [Theory] + [InlineData(null)] + [InlineData(CollectionTypeOptions.Movies)] + [InlineData(CollectionTypeOptions.MusicVideos)] + public void RoundTrip_CollectionTypeOptions_Correct(CollectionTypeOptions? value) + { + var res = JsonSerializer.Deserialize<TestContainer>(JsonSerializer.Serialize(new TestContainer(value), _jsonOptions), _jsonOptions); + Assert.NotNull(res); + Assert.Equal(value, res!.CollectionType); + } + + [Theory] + [InlineData("{\"CollectionType\":null}")] + [InlineData("{\"CollectionType\":\"movies\"}")] + [InlineData("{\"CollectionType\":\"musicvideos\"}")] + public void RoundTrip_String_Correct(string json) + { + var res = JsonSerializer.Serialize(JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions), _jsonOptions); + Assert.Equal(json, res); + } + + private class TestContainer + { + public TestContainer(CollectionTypeOptions? collectionType) + { + CollectionType = collectionType; + } + + [JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))] + public CollectionTypeOptions? CollectionType { get; set; } + } + } +} diff --git a/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs index c1a1525ba..a1ace8476 100644 --- a/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Entities/ProviderIdsExtensionsTests.cs @@ -10,6 +10,53 @@ namespace Jellyfin.Model.Tests.Entities private const string ExampleImdbId = "tt0113375"; [Fact] + public void HasProviderId_NullInstance_ThrowsArgumentNullException() + { + Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.HasProviderId(null!, MetadataProvider.Imdb)); + } + + [Fact] + public void HasProviderId_NullProvider_False() + { + var nullProvider = new ProviderIdsExtensionsTestsObject + { + ProviderIds = null! + }; + + Assert.False(nullProvider.HasProviderId(MetadataProvider.Imdb)); + } + + [Fact] + public void HasProviderId_NullName_ThrowsArgumentNullException() + { + Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensionsTestsObject.Empty.HasProviderId(null!)); + } + + [Fact] + public void HasProviderId_NotFoundName_False() + { + Assert.False(ProviderIdsExtensionsTestsObject.Empty.HasProviderId(MetadataProvider.Imdb)); + } + + [Fact] + public void HasProviderId_FoundName_True() + { + var provider = new ProviderIdsExtensionsTestsObject(); + provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId; + + Assert.True(provider.HasProviderId(MetadataProvider.Imdb)); + } + + [Fact] + public void HasProviderId_FoundNameEmptyValue_False() + { + var provider = new ProviderIdsExtensionsTestsObject(); + provider.ProviderIds[MetadataProvider.Imdb.ToString()] = string.Empty; + + Assert.False(provider.HasProviderId(MetadataProvider.Imdb)); + } + + [Fact] public void GetProviderId_NullInstance_ThrowsArgumentNullException() { Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.GetProviderId(null!, MetadataProvider.Imdb)); @@ -30,7 +77,7 @@ namespace Jellyfin.Model.Tests.Entities [Fact] public void GetProviderId_NullProvider_Null() { - var nullProvider = new ProviderIdsExtensionsTestsObject() + var nullProvider = new ProviderIdsExtensionsTestsObject { ProviderIds = null! }; @@ -47,7 +94,7 @@ namespace Jellyfin.Model.Tests.Entities [Fact] public void TryGetProviderId_NullProvider_False() { - var nullProvider = new ProviderIdsExtensionsTestsObject() + var nullProvider = new ProviderIdsExtensionsTestsObject { ProviderIds = null! }; @@ -75,6 +122,16 @@ namespace Jellyfin.Model.Tests.Entities } [Fact] + public void TryGetProviderId_FoundNameEmptyValue_False() + { + var provider = new ProviderIdsExtensionsTestsObject(); + provider.ProviderIds[MetadataProvider.Imdb.ToString()] = string.Empty; + + Assert.False(provider.TryGetProviderId(MetadataProvider.Imdb, out var id)); + Assert.Null(id); + } + + [Fact] public void SetProviderId_NullInstance_ThrowsArgumentNullException() { Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.SetProviderId(null!, MetadataProvider.Imdb, ExampleImdbId)); @@ -108,7 +165,7 @@ namespace Jellyfin.Model.Tests.Entities [Fact] public void SetProviderId_NullProvider_Success() { - var nullProvider = new ProviderIdsExtensionsTestsObject() + var nullProvider = new ProviderIdsExtensionsTestsObject { ProviderIds = null! }; @@ -120,7 +177,7 @@ namespace Jellyfin.Model.Tests.Entities [Fact] public void SetProviderId_NullProviderAndEmptyName_Success() { - var nullProvider = new ProviderIdsExtensionsTestsObject() + var nullProvider = new ProviderIdsExtensionsTestsObject { ProviderIds = null! }; diff --git a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs index 51633e157..5864a0509 100644 --- a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs +++ b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs @@ -1,4 +1,3 @@ -using System; using MediaBrowser.Model.Extensions; using Xunit; diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj index b6d2c63bd..c5b51ef76 100644 --- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -5,10 +5,12 @@ <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> @@ -16,7 +18,6 @@ <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> @@ -26,8 +27,4 @@ <ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs index e5768b620..d9e77dd2e 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using Emby.Naming.AudioBook; using Emby.Naming.Common; diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs index b3257ace3..53b35c2d6 100644 --- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using Emby.Naming.AudioBook; using Emby.Naming.Common; @@ -10,7 +9,7 @@ namespace Jellyfin.Naming.Tests.AudioBook { private readonly NamingOptions _namingOptions = new NamingOptions(); - public static IEnumerable<object[]> GetResolveFileTestData() + public static IEnumerable<object[]> Resolve_ValidFileNameTestData() { yield return new object[] { @@ -36,7 +35,7 @@ namespace Jellyfin.Naming.Tests.AudioBook } [Theory] - [MemberData(nameof(GetResolveFileTestData))] + [MemberData(nameof(Resolve_ValidFileNameTestData))] public void Resolve_ValidFileName_Success(AudioBookFileInfo expectedResult) { var result = new AudioBookResolver(_namingOptions).Resolve(expectedResult.Path); diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 99185c975..ebb134fc3 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -8,12 +8,14 @@ <PropertyGroup> <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> - <Nullable>enable</Nullable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> @@ -25,14 +27,9 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs index f3abacb4f..2446660f3 100644 --- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs @@ -1,4 +1,3 @@ -using System; using Emby.Naming.Common; using Emby.Naming.Subtitles; using Xunit; diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs index fde06c5a1..a720bdade 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs @@ -7,18 +7,13 @@ namespace Jellyfin.Naming.Tests.Video { public sealed class CleanStringTests { - private readonly NamingOptions _namingOptions = new NamingOptions(); + private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions()); [Theory] [InlineData("Super movie 480p.mp4", "Super movie")] [InlineData("Super movie 480p 2001.mp4", "Super movie")] [InlineData("Super movie [480p].mp4", "Super movie")] [InlineData("480 Super movie [tmdbid=12345].mp4", "480 Super movie")] - [InlineData("Super movie(2009).mp4", "Super movie(2009).mp4")] - [InlineData("Run lola run (lola rennt) (2009).mp4", "Run lola run (lola rennt) (2009).mp4")] - [InlineData(@"American.Psycho.mkv", "American.Psycho.mkv")] - [InlineData(@"American Psycho.mkv", "American Psycho.mkv")] - [InlineData(@"[rec].mkv", "[rec].mkv")] [InlineData("Crouching.Tiger.Hidden.Dragon.4k.mkv", "Crouching.Tiger.Hidden.Dragon")] [InlineData("Crouching.Tiger.Hidden.Dragon.UltraHD.mkv", "Crouching.Tiger.Hidden.Dragon")] [InlineData("Crouching.Tiger.Hidden.Dragon.UHD.mkv", "Crouching.Tiger.Hidden.Dragon")] @@ -29,17 +24,25 @@ namespace Jellyfin.Naming.Tests.Video [InlineData("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")] [InlineData("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")] // FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")] - public void CleanStringTest(string input, string expectedName) + public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName) { - if (new VideoResolver(_namingOptions).TryCleanString(input, out ReadOnlySpan<char> newName)) - { - // TODO: compare spans when XUnit supports it - Assert.Equal(expectedName, newName.ToString()); - } - else - { - Assert.Equal(expectedName, input); - } + Assert.True(_videoResolver.TryCleanString(input, out ReadOnlySpan<char> newName)); + // TODO: compare spans when XUnit supports it + Assert.Equal(expectedName, newName.ToString()); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("Super movie(2009).mp4")] + [InlineData("[rec].mkv")] + [InlineData("American.Psycho.mkv")] + [InlineData("American Psycho.mkv")] + [InlineData("Run lola run (lola rennt) (2009).mp4")] + public void CleanStringTest_DoesntNeedCleaning_False(string? input) + { + Assert.False(_videoResolver.TryCleanString(input, out ReadOnlySpan<char> newName)); + Assert.True(newName.IsEmpty); } } } diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index d34f65409..2f173b0ce 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -1,4 +1,3 @@ -using System; using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Model.Entities; diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index bc5e6fa63..6e803593e 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -295,12 +295,9 @@ namespace Jellyfin.Naming.Tests.Video FullName = i }).ToList()).ToList(); - Assert.Single(result); + Assert.Equal(7, result.Count); Assert.Empty(result[0].Extras); - Assert.Equal(6, result[0].AlternateVersions.Count); - Assert.False(result[0].AlternateVersions[2].Is3D); - Assert.True(result[0].AlternateVersions[3].Is3D); - Assert.True(result[0].AlternateVersions[4].Is3D); + Assert.Empty(result[0].AlternateVersions); } [Fact] @@ -369,6 +366,44 @@ namespace Jellyfin.Naming.Tests.Video } [Fact] + public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName() + { + var files = new[] + { + @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv", + @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv" + }; + + var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList()).ToList(); + + Assert.Single(result); + Assert.Empty(result[0].Extras); + Assert.Single(result[0].AlternateVersions); + } + + [Fact] + public void Resolve_GivenUnclosedBrackets_DoesNotGroup() + { + var files = new[] + { + @"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 1].mkv", + @"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv" + }; + + var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList()).ToList(); + + Assert.Equal(2, result.Count); + } + + [Fact] public void TestEmptyList() { var result = _videoListResolver.Resolve(new List<FileSystemMetadata>()).ToList(); diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs index ba5eaf1af..9bbbe2970 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -11,7 +11,7 @@ namespace Jellyfin.Naming.Tests.Video { private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions()); - public static IEnumerable<object[]> GetResolveFileTestData() + public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData() { yield return new object[] { @@ -156,7 +156,7 @@ namespace Jellyfin.Naming.Tests.Video } [Theory] - [MemberData(nameof(GetResolveFileTestData))] + [MemberData(nameof(ResolveFile_ValidFileNameTestData))] public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult) { var result = _videoResolver.ResolveFile(expectedResult.Path); diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index fd77397ba..d5268facc 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -8,12 +8,14 @@ <PropertyGroup> <TargetFramework>net5.0</TargetFramework> <IsPackable>false</IsPackable> - <Nullable>enable</Nullable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> @@ -22,18 +24,18 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> + <ItemGroup> - <ProjectReference Include="..\..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> - <ProjectReference Include="..\..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + <ProjectReference Include="../../Emby.Server.Implementations/Emby.Server.Implementations.csproj" /> + <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <DefineConstants>DEBUG</DefineConstants> </PropertyGroup> + </Project> diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs new file mode 100644 index 000000000..1cad625b7 --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -0,0 +1,63 @@ +using System.Net; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Jellyfin.Networking.Tests +{ + public class NetworkManagerTests + { + /// <summary> + /// Checks that the given IP address is in the specified network(s). + /// </summary> + /// <param name="network">Network address(es).</param> + /// <param name="value">The IP to check.</param> + [Theory] + [InlineData("192.168.2.1/24", "192.168.2.123")] + [InlineData("192.168.2.1/24, !192.168.2.122/32", "192.168.2.123")] + [InlineData("fd23:184f:2029:0::/56", "fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d518/128", "fd23:184f:2029:0:3139:7386:67d7:d517")] + public void InNetwork_True_Success(string network, string value) + { + var ip = IPAddress.Parse(value); + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = network.Split(',') + }; + + using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>()); + + Assert.True(networkManager.IsInLocalNetwork(ip)); + } + + /// <summary> + /// Checks that thge given IP address is not in the network provided. + /// </summary> + /// <param name="network">Network address(es).</param> + /// <param name="value">The IP to check.</param> + [Theory] + [InlineData("192.168.10.0/24", "192.168.11.1")] + [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] + [InlineData("192.168.10.0/24", "fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0::/56", "fd24:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d500/120", "fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0::/56", "192.168.10.60")] + public void InNetwork_False_Success(string network, string value) + { + var ip = IPAddress.Parse(value); + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = network.Split(',') + }; + + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>()); + + Assert.False(nm.IsInLocalNetwork(ip)); + } + } +} diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 9f928ded1..671b8598d 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -1,19 +1,19 @@ using System; +using System.Collections.ObjectModel; using System.Net; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; -using Moq; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using Xunit; -using System.Collections.ObjectModel; namespace Jellyfin.Networking.Tests { public class NetworkParseTests { - private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) + internal static IConfigurationManager GetMockConfig(NetworkConfiguration conf) { var configManager = new Mock<IConfigurationManager> { @@ -34,10 +34,12 @@ namespace Jellyfin.Networking.Tests [InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")] // eth16 only [InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] - // All interfaces excluded. - [InlineData("192.168.1.208/24,-16,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[]")] + // All interfaces excluded. (including loopbacks) + [InlineData("192.168.1.208/24,-16,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[127.0.0.1/8,::1/128]")] // vEthernet1 and vEthernet212 should be excluded. - [InlineData("192.168.1.200/24,-20,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.200/24", "[200.200.200.200/24]")] + [InlineData("192.168.1.200/24,-20,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.200/24", "[200.200.200.200/24,127.0.0.1/8,::1/128]")] + // Overlapping interface, + [InlineData("192.168.1.110/24,-20,br0|192.168.1.10/24,-16,br0|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.110/24,192.168.1.10/24]")] public void IgnoreVirtualInterfaces(string interfaces, string lan, string value) { var conf = new NetworkConfiguration() @@ -55,35 +57,9 @@ namespace Jellyfin.Networking.Tests } /// <summary> - /// Check that the value given is in the network provided. - /// </summary> - /// <param name="network">Network address.</param> - /// <param name="value">Value to check.</param> - [Theory] - [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] - public void IsInNetwork(string network, string value) - { - if (network == null) - { - throw new ArgumentNullException(nameof(network)); - } - - var conf = new NetworkConfiguration() - { - EnableIPV6 = true, - EnableIPV4 = true, - LocalNetworkSubnets = network.Split(',') - }; - - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - - Assert.False(nm.IsInLocalNetwork(value)); - } - - /// <summary> /// Checks IP address formats. /// </summary> - /// <param name="address"></param> + /// <param name="address">IP Address.</param> [Theory] [InlineData("127.0.0.1")] [InlineData("127.0.0.1:123")] @@ -107,7 +83,7 @@ namespace Jellyfin.Networking.Tests /// <summary> /// Checks IP address formats. /// </summary> - /// <param name="address"></param> + /// <param name="address">IP Address.</param> [Theory] [InlineData("127.0.0.1")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] @@ -124,7 +100,6 @@ namespace Jellyfin.Networking.Tests Assert.True(IPNetAddress.TryParse(address, out _)); } - /// <summary> /// All should be invalid address strings. /// </summary> @@ -141,7 +116,6 @@ namespace Jellyfin.Networking.Tests Assert.False(IPHost.TryParse(address, out _)); } - /// <summary> /// Test collection parsing. /// </summary> @@ -152,19 +126,22 @@ namespace Jellyfin.Networking.Tests /// <param name="result4">Excluded IP4 addresses from the collection.</param> /// <param name="result5">Network addresses of the collection.</param> [Theory] - [InlineData("127.0.0.1#", + [InlineData( + "127.0.0.1#", "[]", "[]", "[]", "[]", "[]")] - [InlineData("!127.0.0.1", + [InlineData( + "!127.0.0.1", "[]", "[]", "[127.0.0.1/32]", "[127.0.0.1/32]", "[]")] - [InlineData("", + [InlineData( + "", "[]", "[]", "[]", @@ -177,7 +154,8 @@ namespace Jellyfin.Networking.Tests "[10.10.10.10/32]", "[10.10.10.10/32]", "[192.158.0.0/16,127.0.0.1/32,::1/128,fd23:184f:2029:0:3139:7386:67d7:d517/128]")] - [InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8", + [InlineData( + "192.158.1.2/255.255.0.0,192.169.1.2/8", "[192.158.1.2/16,192.169.1.2/8]", "[192.158.1.2/16,192.169.1.2/8]", "[]", @@ -194,34 +172,34 @@ namespace Jellyfin.Networking.Tests { EnableIPV6 = true, EnableIPV4 = true, - }; + }; using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); // Test included. - Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(","), false); + Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(','), false); Assert.Equal(nc.AsString(), result1); // Test excluded. - nc = nm.CreateIPCollection(settings.Split(","), true); + nc = nm.CreateIPCollection(settings.Split(','), true); Assert.Equal(nc.AsString(), result3); conf.EnableIPV6 = false; nm.UpdateSettings(conf); - + // Test IP4 included. - nc = nm.CreateIPCollection(settings.Split(","), false); + nc = nm.CreateIPCollection(settings.Split(','), false); Assert.Equal(nc.AsString(), result2); // Test IP4 excluded. - nc = nm.CreateIPCollection(settings.Split(","), true); + nc = nm.CreateIPCollection(settings.Split(','), true); Assert.Equal(nc.AsString(), result4); conf.EnableIPV6 = true; nm.UpdateSettings(conf); // Test network addresses of collection. - nc = nm.CreateIPCollection(settings.Split(","), false); + nc = nm.CreateIPCollection(settings.Split(','), false); nc = nc.AsNetworks(); Assert.Equal(nc.AsString(), result5); } @@ -252,7 +230,6 @@ namespace Jellyfin.Networking.Tests throw new ArgumentNullException(nameof(result)); } - var conf = new NetworkConfiguration() { EnableIPV6 = true, @@ -261,10 +238,10 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); - Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(","), false); - Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(","), false); + Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(','), false); + Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(','), false); - Assert.Equal(nc1.Union(nc2).AsString(), result); + Assert.Equal(nc1.ThatAreContainedInNetworks(nc2).AsString(), result); } [Theory] @@ -371,14 +348,13 @@ namespace Jellyfin.Networking.Tests using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); // Test included, IP6. - Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(",")); - Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(",")); - Collection<IPObject> ncResult = ncSource.Union(ncDest); - Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(",")); + Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(',')); + Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(',')); + Collection<IPObject> ncResult = ncSource.ThatAreContainedInNetworks(ncDest); + Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(',')); Assert.True(ncResult.Compare(resultCollection)); } - [Theory] [InlineData("10.1.1.1/32", "10.1.1.1")] [InlineData("192.168.1.254/32", "192.168.1.254/255.255.255.255")] @@ -408,6 +384,9 @@ namespace Jellyfin.Networking.Tests [InlineData("jellyfin.org", "eth16", false, "eth16")] // User on external network, no binding - so result is the 1st external. [InlineData("jellyfin.org", "", false, "eth11")] + // Dns failure - should skip the test. + // https://en.wikipedia.org/wiki/.test + [InlineData("invalid.domain.test", "", false, "eth11")] // User assumed to be internal, no binding - so result is the 1st internal. [InlineData("", "", false, "eth16")] public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result) @@ -440,10 +419,13 @@ namespace Jellyfin.Networking.Tests _ = nm.TryParseInterface(result, out Collection<IPObject>? resultObj); - if (resultObj != null) + // Check to see if dns resolution is working. If not, skip test. + _ = IPHost.TryParse(source, out var host); + + if (resultObj != null && host?.HasAddress == true) { result = ((IPNetAddress)resultObj[0]).ToString(true); - var intf = nm.GetBindInterface(source, out int? _); + var intf = nm.GetBindInterface(source, out _); Assert.Equal(intf, result); } @@ -455,7 +437,7 @@ namespace Jellyfin.Networking.Tests // On my system eth16 is internal, eth11 external (Windows defines the indexes). // // This test is to replicate how subnet bound ServerPublisherUri work throughout the system. - + // User on internal network, we're bound internal and external - so result is internal override. [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")] @@ -479,7 +461,6 @@ namespace Jellyfin.Networking.Tests // User is internal, no binding - so result is the 1st internal, which is then overridden. [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")] - public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) { if (lan == null) @@ -515,5 +496,45 @@ namespace Jellyfin.Networking.Tests Assert.Equal(intf, result); } + + [Theory] + [InlineData("185.10.10.10,200.200.200.200", "79.2.3.4", true)] + [InlineData("185.10.10.10", "185.10.10.10", false)] + [InlineData("", "100.100.100.100", false)] + + public void HasRemoteAccess_GivenWhitelist_AllowsOnlyIpsInWhitelist(string addresses, string remoteIp, bool denied) + { + // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. + // If left blank, all remote addresses will be allowed. + var conf = new NetworkConfiguration() + { + EnableIPV4 = true, + RemoteIPFilter = addresses.Split(','), + IsRemoteIPFilterBlacklist = false + }; + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIp)), denied); + } + + [Theory] + [InlineData("185.10.10.10", "79.2.3.4", false)] + [InlineData("185.10.10.10", "185.10.10.10", true)] + [InlineData("", "100.100.100.100", false)] + public void HasRemoteAccess_GivenBlacklist_BlacklistTheIps(string addresses, string remoteIp, bool denied) + { + // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. + // If left blank, all remote addresses will be allowed. + var conf = new NetworkConfiguration() + { + EnableIPV4 = true, + RemoteIPFilter = addresses.Split(','), + IsRemoteIPFilterBlacklist = true + }; + + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIp)), denied); + } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs new file mode 100644 index 000000000..71f8c5181 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using AutoFixture; +using AutoFixture.AutoMoq; +using Emby.Server.Implementations.Data; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Data +{ + public class SqliteItemRepositoryTests + { + public const string VirtualMetaDataPath = "%MetadataPath%"; + public const string MetaDataPath = "/meta/data/path"; + + private readonly IFixture _fixture; + private readonly SqliteItemRepository _sqliteItemRepository; + + public SqliteItemRepositoryTests() + { + var appHost = new Mock<IServerApplicationHost>(); + appHost.Setup(x => x.ExpandVirtualPath(It.IsAny<string>())) + .Returns((string x) => x.Replace(VirtualMetaDataPath, MetaDataPath, StringComparison.Ordinal)); + appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>())) + .Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal)); + + _fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); + _fixture.Inject(appHost); + _sqliteItemRepository = _fixture.Create<SqliteItemRepository>(); + } + + public static IEnumerable<object[]> ItemImageInfoFromValueString_Valid_TestData() + { + yield return new object[] + { + "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN", + new ItemImageInfo + { + Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg", + Type = ImageType.Primary, + DateModified = new DateTime(637452096478512963, DateTimeKind.Utc), + Width = 1920, + Height = 1080, + BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN" + } + }; + + yield return new object[] + { + "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*0*0", + new ItemImageInfo + { + Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg", + Type = ImageType.Primary, + } + }; + + yield return new object[] + { + "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary", + new ItemImageInfo + { + Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg", + Type = ImageType.Primary, + } + }; + + yield return new object[] + { + "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*600", + new ItemImageInfo + { + Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg", + Type = ImageType.Primary, + } + }; + + yield return new object[] + { + "%MetadataPath%/library/68/68578562b96c80a7ebd530848801f645/poster.jpg*637264380567586027*Primary*600*336", + new ItemImageInfo + { + Path = "/meta/data/path/library/68/68578562b96c80a7ebd530848801f645/poster.jpg", + Type = ImageType.Primary, + DateModified = new DateTime(637264380567586027, DateTimeKind.Utc), + Width = 600, + Height = 336 + } + }; + } + + [Theory] + [MemberData(nameof(ItemImageInfoFromValueString_Valid_TestData))] + public void ItemImageInfoFromValueString_Valid_Success(string value, ItemImageInfo expected) + { + var result = _sqliteItemRepository.ItemImageInfoFromValueString(value); + Assert.Equal(expected.Path, result.Path); + Assert.Equal(expected.Type, result.Type); + Assert.Equal(expected.DateModified, result.DateModified); + Assert.Equal(expected.Width, result.Width); + Assert.Equal(expected.Height, result.Height); + Assert.Equal(expected.BlurHash, result.BlurHash); + } + + [Theory] + [InlineData("")] + [InlineData("*")] + [InlineData("https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0")] + public void ItemImageInfoFromValueString_Invalid_Null(string value) + { + Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value)); + } + + public static IEnumerable<object[]> DeserializeImages_Valid_TestData() + { + yield return new object[] + { + "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN", + new ItemImageInfo[] + { + new ItemImageInfo() + { + Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg", + Type = ImageType.Primary, + DateModified = new DateTime(637452096478512963, DateTimeKind.Utc), + Width = 1920, + Height = 1080, + BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN" + } + } + }; + + yield return new object[] + { + "%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/poster.jpg*637261226720645297*Primary*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/logo.png*637261226720805297*Logo*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/landscape.jpg*637261226721285297*Thumb*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/backdrop.jpg*637261226721685297*Backdrop*0*0", + new ItemImageInfo[] + { + new ItemImageInfo() + { + Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/poster.jpg", + Type = ImageType.Primary, + DateModified = new DateTime(637261226720645297, DateTimeKind.Utc), + }, + new ItemImageInfo() + { + Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/logo.png", + Type = ImageType.Logo, + DateModified = new DateTime(637261226720805297, DateTimeKind.Utc), + }, + new ItemImageInfo() + { + Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/landscape.jpg", + Type = ImageType.Thumb, + DateModified = new DateTime(637261226721285297, DateTimeKind.Utc), + }, + new ItemImageInfo() + { + Path = "/meta/data/path/library/2a/2a27372f1e9bc757b1db99721bbeae1e/backdrop.jpg", + Type = ImageType.Backdrop, + DateModified = new DateTime(637261226721685297, DateTimeKind.Utc), + } + } + }; + } + + [Theory] + [MemberData(nameof(DeserializeImages_Valid_TestData))] + public void DeserializeImages_Valid_Success(string value, ItemImageInfo[] expected) + { + var result = _sqliteItemRepository.DeserializeImages(value); + Assert.Equal(expected.Length, result.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i].Path, result[i].Path); + Assert.Equal(expected[i].Type, result[i].Type); + Assert.Equal(expected[i].DateModified, result[i].DateModified); + Assert.Equal(expected[i].Width, result[i].Width); + Assert.Equal(expected[i].Height, result[i].Height); + Assert.Equal(expected[i].BlurHash, result[i].BlurHash); + } + } + + [Theory] + [MemberData(nameof(DeserializeImages_Valid_TestData))] + public void SerializeImages_Valid_Success(string expected, ItemImageInfo[] value) + { + Assert.Equal(expected, _sqliteItemRepository.SerializeImages(value)); + } + + public static IEnumerable<object[]> DeserializeProviderIds_Valid_TestData() + { + yield return new object[] + { + "Imdb=tt0119567", + new Dictionary<string, string>() + { + { "Imdb", "tt0119567" }, + } + }; + + yield return new object[] + { + "Imdb=tt0119567|Tmdb=330|TmdbCollection=328", + new Dictionary<string, string>() + { + { "Imdb", "tt0119567" }, + { "Tmdb", "330" }, + { "TmdbCollection", "328" }, + } + }; + + yield return new object[] + { + "MusicBrainzAlbum=9d363e43-f24f-4b39-bc5a-7ef305c677c7|MusicBrainzReleaseGroup=63eba062-847c-3b73-8b0f-6baf27bba6fa|AudioDbArtist=111352|AudioDbAlbum=2116560|MusicBrainzAlbumArtist=20244d07-534f-4eff-b4d4-930878889970", + new Dictionary<string, string>() + { + { "MusicBrainzAlbum", "9d363e43-f24f-4b39-bc5a-7ef305c677c7" }, + { "MusicBrainzReleaseGroup", "63eba062-847c-3b73-8b0f-6baf27bba6fa" }, + { "AudioDbArtist", "111352" }, + { "AudioDbAlbum", "2116560" }, + { "MusicBrainzAlbumArtist", "20244d07-534f-4eff-b4d4-930878889970" }, + } + }; + } + + [Theory] + [MemberData(nameof(DeserializeProviderIds_Valid_TestData))] + public void DeserializeProviderIds_Valid_Success(string value, Dictionary<string, string> expected) + { + var result = new ProviderIdsExtensionsTestsObject(); + SqliteItemRepository.DeserializeProviderIds(value, result); + Assert.Equal(expected, result.ProviderIds); + } + + [Theory] + [MemberData(nameof(DeserializeProviderIds_Valid_TestData))] + public void SerializeProviderIds_Valid_Success(string expected, Dictionary<string, string> values) + { + Assert.Equal(expected, SqliteItemRepository.SerializeProviderIds(values)); + } + + private class ProviderIdsExtensionsTestsObject : IHasProviderIds + { + public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>(); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs index 671c59b2e..30e6542f9 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.InteropServices; using AutoFixture; using AutoFixture.AutoMoq; using Emby.Server.Implementations.IO; @@ -38,5 +41,36 @@ namespace Jellyfin.Server.Implementations.Tests.IO Assert.Equal(expectedAbsolutePath, generatedPath); } } + + [Theory] + [InlineData("ValidFileName", "ValidFileName")] + [InlineData("AC/DC", "AC DC")] + [InlineData("Invalid\0", "Invalid ")] + [InlineData("AC/DC\0KD/A", "AC DC KD A")] + public void GetValidFilename_ReturnsValidFilename(string filename, string expectedFileName) + { + Assert.Equal(expectedFileName, _sut.GetValidFilename(filename)); + } + + [SkippableFact] + public void GetFileInfo_DanglingSymlink_ExistsFalse() + { + Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + string testFileDir = Path.Combine(Path.GetTempPath(), "jellyfin-test-data"); + string testFileName = Path.Combine(testFileDir, Path.GetRandomFileName() + "-danglingsym.link"); + + Directory.CreateDirectory(testFileDir); + Assert.Equal(0, symlink("thispathdoesntexist", testFileName)); + Assert.True(File.Exists(testFileName)); + + var metadata = _sut.GetFileInfo(testFileName); + Assert.False(metadata.Exists); + } + + [SuppressMessage("Naming Rules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Have to")] + [DllImport("libc", SetLastError = true, CharSet = CharSet.Ansi)] + [DefaultDllImportSearchPaths(DllImportSearchPath.UserDirectories)] + private static extern int symlink(string target, string linkpath); } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 1ad8171be..27713d58a 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -10,6 +10,8 @@ <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace> </PropertyGroup> @@ -20,18 +22,18 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="AutoFixture" Version="4.15.0" /> - <PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="AutoFixture" Version="4.17.0" /> + <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> + <PackageReference Include="Xunit.SkippableFact" Version="1.4.13" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> </ItemGroup> <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> @@ -42,8 +44,4 @@ <ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs new file mode 100644 index 000000000..c393742eb --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -0,0 +1,72 @@ +using System; +using Emby.Server.Implementations.Library.Resolvers.TV; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library +{ + public class EpisodeResolverTest + { + [Fact] + public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode() + { + var season = new Season { Name = "Season 1" }; + var parent = new Folder { Name = "extras" }; + var libraryManagerMock = new Mock<ILibraryManager>(); + libraryManagerMock.Setup(x => x.GetItemById(It.IsAny<Guid>())).Returns(season); + + var episodeResolver = new EpisodeResolver(libraryManagerMock.Object); + var itemResolveArgs = new ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + Mock.Of<IDirectoryService>()) + { + Parent = parent, + CollectionType = CollectionType.TvShows, + FileInfo = new FileSystemMetadata() + { + FullName = "All My Children/Season 01/Extras/All My Children S01E01 - Behind The Scenes.mkv" + } + }; + + Assert.Null(episodeResolver.Resolve(itemResolveArgs)); + } + + [Fact] + public void Resolve_GivenVideoInExtrasSeriesFolder_ResolvesToEpisode() + { + var series = new Series { Name = "Extras" }; + + // Have to create a mock because of moq proxies not being castable to a concrete implementation + // https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48 + var episodeResolver = new EpisodeResolverMock(Mock.Of<ILibraryManager>()); + var itemResolveArgs = new ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + Mock.Of<IDirectoryService>()) + { + Parent = series, + CollectionType = CollectionType.TvShows, + FileInfo = new FileSystemMetadata() + { + FullName = "Extras/Extras S01E01.mkv" + } + }; + Assert.NotNull(episodeResolver.Resolve(itemResolveArgs)); + } + + private class EpisodeResolverMock : EpisodeResolver + { + public EpisodeResolverMock(ILibraryManager libraryManager) : base(libraryManager) + { + } + + protected override TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) => new (); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs index 6d768af89..c5cc056f5 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -24,5 +24,36 @@ namespace Jellyfin.Server.Implementations.Tests.Library { Assert.Throws<ArgumentException>(() => PathExtensions.GetAttributeValue(input, attribute)); } + + [Theory] + [InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")] + [InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff/", "/home/jeff", "/home/jeff/myfile.mkv")] + [InlineData("/home/jeff/music/jeff's band/consistently inconsistent.mp3", "/home/jeff/music/jeff's band", "/home/not jeff", "/home/not jeff/consistently inconsistent.mp3")] + [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")] + [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff/", "/home/jeff/myfile.mkv")] + [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/home/jeff/", "/home/jeff/myfile.mkv")] + [InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/", "/myfile.mkv")] + [InlineData("/o", "/o", "/s", "/s")] // regression test for #5977 + public void TryReplaceSubPath_ValidArgs_Correct(string path, string subPath, string newSubPath, string? expectedResult) + { + Assert.True(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result)); + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData(null, null, null)] + [InlineData(null, "/my/path", "/another/path")] + [InlineData("/my/path", null, "/another/path")] + [InlineData("/my/path", "/another/path", null)] + [InlineData("", "", "")] + [InlineData("/my/path", "", "")] + [InlineData("", "/another/path", "")] + [InlineData("", "", "/new/subpath")] + [InlineData("/home/jeff/music/jeff's band/consistently inconsistent.mp3", "/home/jeff/music/not jeff's band", "/home/not jeff")] + public void TryReplaceSubPath_InvalidInput_ReturnsFalseAndNull(string? path, string? subPath, string? newSubPath) + { + Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result)); + Assert.Null(result); + } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs new file mode 100644 index 000000000..fd499d9cf --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs @@ -0,0 +1,326 @@ +using System; +using System.Text; +using Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.LiveTv +{ + public class HdHomerunManagerTests + { + [Fact] + public void WriteNullTerminatedString_Empty_Success() + { + ReadOnlySpan<byte> expected = stackalloc byte[] + { + 1, 0 + }; + + Span<byte> buffer = stackalloc byte[128]; + int len = HdHomerunManager.WriteNullTerminatedString(buffer, string.Empty); + + Assert.Equal( + Convert.ToHexString(expected), + Convert.ToHexString(buffer.Slice(0, len))); + } + + [Fact] + public void WriteNullTerminatedString_Valid_Success() + { + ReadOnlySpan<byte> expected = stackalloc byte[] + { + 10, (byte)'T', (byte)'h', (byte)'e', (byte)' ', (byte)'q', (byte)'u', (byte)'i', (byte)'c', (byte)'k', 0 + }; + + Span<byte> buffer = stackalloc byte[128]; + int len = HdHomerunManager.WriteNullTerminatedString(buffer, "The quick"); + + Assert.Equal( + Convert.ToHexString(expected), + Convert.ToHexString(buffer.Slice(0, len))); + } + + [Fact] + public void WriteGetMessage_Valid_Success() + { + ReadOnlySpan<byte> expected = stackalloc byte[] + { + 0, 4, + 0, 12, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 0xc0, 0xc9, 0x87, 0x33 + }; + + Span<byte> buffer = stackalloc byte[128]; + int len = HdHomerunManager.WriteGetMessage(buffer, 0, "N"); + + Assert.Equal( + Convert.ToHexString(expected), + Convert.ToHexString(buffer.Slice(0, len))); + } + + [Fact] + public void WriteSetMessage_NoLockKey_Success() + { + ReadOnlySpan<byte> expected = stackalloc byte[] + { + 0, 4, + 0, 20, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0xa9, 0x49, 0xd0, 0x68 + }; + + Span<byte> buffer = stackalloc byte[128]; + int len = HdHomerunManager.WriteSetMessage(buffer, 0, "N", "value", null); + + Assert.Equal( + Convert.ToHexString(expected), + Convert.ToHexString(buffer.Slice(0, len))); + } + + [Fact] + public void WriteSetMessage_LockKey_Success() + { + ReadOnlySpan<byte> expected = stackalloc byte[] + { + 0, 4, + 0, 26, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 21, + 4, 0x00, 0x01, 0x38, 0xd5, + 0x8e, 0xb6, 0x06, 0x82 + }; + + Span<byte> buffer = stackalloc byte[128]; + int len = HdHomerunManager.WriteSetMessage(buffer, 0, "N", "value", 80085); + + Assert.Equal( + Convert.ToHexString(expected), + Convert.ToHexString(buffer.Slice(0, len))); + } + + [Fact] + public void TryGetReturnValueOfGetSet_Valid_Success() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 20, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0x7d, 0xa3, 0xa3, 0xf3 + }; + + Assert.True(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out var value)); + Assert.Equal("value", Encoding.UTF8.GetString(value)); + } + + [Fact] + public void TryGetReturnValueOfGetSet_InvalidCrc_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 20, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0x7d, 0xa3, 0xa3, 0xf4 + }; + + Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _)); + } + + [Fact] + public void TryGetReturnValueOfGetSet_InvalidPacketType_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 4, + 0, 20, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0xa9, 0x49, 0xd0, 0x68 + }; + + Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _)); + } + + [Fact] + public void TryGetReturnValueOfGetSet_InvalidPacket_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 20, + 0x7d, 0xa3, 0xa3 + }; + + Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _)); + } + + [Fact] + public void TryGetReturnValueOfGetSet_TooSmallMessageLength_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 19, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0x25, 0x25, 0x44, 0x9a + }; + + Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _)); + } + + [Fact] + public void TryGetReturnValueOfGetSet_TooLargeMessageLength_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 21, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0xe3, 0x20, 0x79, 0x6c + }; + + Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _)); + } + + [Fact] + public void TryGetReturnValueOfGetSet_TooLargeNameLength_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 20, + 3, + 20, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0xe1, 0x8e, 0x9c, 0x74 + }; + + Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _)); + } + + [Fact] + public void TryGetReturnValueOfGetSet_InvalidGetSetNameTag_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 20, + 4, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0xee, 0x05, 0xe7, 0x12 + }; + + Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _)); + } + + [Fact] + public void TryGetReturnValueOfGetSet_InvalidGetSetValueTag_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 20, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 3, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0x64, 0xaa, 0x66, 0xf9 + }; + + Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _)); + } + + [Fact] + public void TryGetReturnValueOfGetSet_TooLargeValueLength_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 20, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 7, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0xc9, 0xa8, 0xd4, 0x55 + }; + + Assert.False(HdHomerunManager.TryGetReturnValueOfGetSet(packet, out _)); + } + + [Fact] + public void VerifyReturnValueOfGetSet_Valid_True() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 20, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0x7d, 0xa3, 0xa3, 0xf3 + }; + + Assert.True(HdHomerunManager.VerifyReturnValueOfGetSet(packet, "value")); + } + + [Fact] + public void VerifyReturnValueOfGetSet_WrongValue_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 5, + 0, 20, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0x7d, 0xa3, 0xa3, 0xf3 + }; + + Assert.False(HdHomerunManager.VerifyReturnValueOfGetSet(packet, "none")); + } + + [Fact] + public void VerifyReturnValueOfGetSet_InvalidPacket_False() + { + ReadOnlySpan<byte> packet = new byte[] + { + 0, 4, + 0, 20, + 3, + 10, (byte)'/', (byte)'t', (byte)'u', (byte)'n', (byte)'e', (byte)'r', (byte)'0', (byte)'/', (byte)'N', 0, + 4, + 6, (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', 0, + 0x7d, 0xa3, 0xa3, 0xf3 + }; + + Assert.False(HdHomerunManager.VerifyReturnValueOfGetSet(packet, "value")); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json new file mode 100644 index 000000000..b766e668e --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json @@ -0,0 +1,684 @@ +[ + { + "guid": "a4df60c5-6ab4-412a-8f79-2cab93fb2bc5", + "name": "Anime", + "description": "Manage your anime in Jellyfin. This plugin supports several different metadata providers and options for organizing your collection.\n", + "overview": "Manage your anime from Jellyfin", + "owner": "jellyfin", + "category": "Metadata", + "versions": [ + { + "version": "10.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/anime/anime_10.0.0.0.zip", + "checksum": "93e969adeba1050423fc8817ed3c36f8", + "timestamp": "2020-08-17T01:41:13Z" + }, + { + "version": "9.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/anime/anime_9.0.0.0.zip", + "checksum": "9b1cebff835813e15f414f44b40c41c8", + "timestamp": "2020-07-20T01:30:16Z" + } + ] + }, + { + "guid": "70b7b43b-471b-4159-b4be-56750c795499", + "name": "Auto Organize", + "description": "Automatically organize your media", + "overview": "Automatically organize your media", + "owner": "jellyfin", + "category": "General", + "versions": [ + { + "version": "9.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/auto-organize/auto-organize_9.0.0.0.zip", + "checksum": "ff29ac3cbe05d208b6af94cd6d9dea39", + "timestamp": "2020-12-05T22:31:12Z" + }, + { + "version": "8.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/auto-organize/auto-organize_8.0.0.0.zip", + "checksum": "460bbb45e556464a8476b18e41c097f5", + "timestamp": "2020-07-20T01:30:25Z" + } + ] + }, + { + "guid": "9c4e63f1-031b-4f25-988b-4f7d78a8b53e", + "name": "Bookshelf", + "description": "Supports several different metadata providers and options for organizing your collection.\n", + "overview": "Manage your books", + "owner": "jellyfin", + "category": "Metadata", + "versions": [ + { + "version": "5.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/bookshelf/bookshelf_5.0.0.0.zip", + "checksum": "2063fb8ab317b8d77b200fde41eb5e1e", + "timestamp": "2020-12-05T22:03:13Z" + }, + { + "version": "4.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/bookshelf/bookshelf_4.0.0.0.zip", + "checksum": "fc9f76c0815d766491e5b0f30ede55ed", + "timestamp": "2020-07-20T01:30:33Z" + } + ] + }, + { + "guid": "cfa0f7f4-4155-4d71-849b-d6598dc4c5bb", + "name": "Email", + "description": "Send SMTP email notifications", + "overview": "Send SMTP email notifications", + "owner": "jellyfin", + "category": "Notifications", + "versions": [ + { + "version": "9.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/email/email_9.0.0.0.zip", + "checksum": "cfe7afc00f3fbd6d6ab8244d7ff968ce", + "timestamp": "2020-12-05T22:20:32Z" + }, + { + "version": "7.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/email/email_7.0.0.0.zip", + "checksum": "680ca511d8ad84923cb04f024fd8eb19", + "timestamp": "2020-07-20T01:30:40Z" + } + ] + }, + { + "guid": "170a157f-ac6c-437a-abdd-ca9c25cebd39", + "name": "Fanart", + "description": "Scrape poster images for movies, shows, and artists in your library.", + "overview": "Scrape poster images from Fanart", + "owner": "jellyfin", + "category": "Metadata", + "versions": [ + { + "version": "6.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/fanart/fanart_6.0.0.0.zip", + "checksum": "ee4360bfcc8722d5a3a54cfe7eef640f", + "timestamp": "2020-12-05T22:25:43Z" + }, + { + "version": "5.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/fanart/fanart_5.0.0.0.zip", + "checksum": "f842f7d65d23f377761c907d40b89647", + "timestamp": "2020-07-20T01:30:48Z" + } + ] + }, + { + "guid": "e29621a5-fa9e-4330-982e-ef6e54c0cad2", + "name": "Gotify Notification", + "description": "You must have a Gotify server to use this plugin!\n", + "overview": "Sends notifications to your Gotify server", + "owner": "crobibero", + "category": "Notifications", + "versions": [ + { + "version": "7.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/gotify-notification/gotify-notification_7.0.0.0.zip", + "checksum": "7c5ff9e8792c8cdee7e8a2aaeb6cc093", + "timestamp": "2020-07-20T01:30:56Z" + } + ] + }, + { + "guid": "a59b5c4b-05a8-488f-bfa8-7a63fffc7639", + "name": "IPTV", + "description": "Enable IPTV support in Jellyfin", + "overview": "Enable IPTV support in Jellyfin", + "owner": "jellyfin", + "category": "Channel", + "versions": [ + { + "version": "6.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/iptv/iptv_6.0.0.0.zip", + "checksum": "9cf103bf67a4eda7c3a42d9b235f6447", + "timestamp": "2020-07-20T01:31:05Z" + } + ] + }, + { + "guid": "4682DD4C-A675-4F1B-8E7C-79ADF137A8F8", + "name": "ISO Mounter", + "description": "Mount your ISO files for Jellyfin.\n", + "overview": "Mount your ISO files for Jellyfin", + "owner": "jellyfin", + "category": "Metadata", + "versions": [ + { + "version": "1.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/iso-mounter/iso-mounter_1.0.0.0.zip", + "checksum": "847e5bc7ac34c1bf4dc5b28173170fae", + "timestamp": "2020-07-20T01:31:13Z" + } + ] + }, + { + "guid": "771e19d6-5385-4caf-b35c-28a0e865cf63", + "name": "Kodi Sync Queue", + "description": "This plugin will track all media changes while Kodi clients are offline to decrease sync times.", + "overview": "Sync all media changes with Kodi clients", + "owner": "jellyfin", + "category": "General", + "versions": [ + { + "version": "6.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/kodi-sync-queue/kodi-sync-queue_6.0.0.0.zip", + "checksum": "787c856c0d2ad2224cdd8b3094cf0329", + "timestamp": "2020-12-05T22:10:37Z" + }, + { + "version": "5.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/kodi-sync-queue/kodi-sync-queue_5.0.0.0.zip", + "checksum": "08285397aecd93ea64a4f15d38b1bd7b", + "timestamp": "2020-07-20T01:31:22Z" + } + ] + }, + { + "guid": "958aad66-3784-4d2a-b89a-a7b6fab6e25c", + "name": "LDAP Authentication", + "description": "Authenticate your Jellyfin users against an LDAP database, and optionally create users who do not yet exist automatically.\nAllows the administrator to customize most aspects of the LDAP authentication process, including customizable search attributes, username attribute, and a search filter for administrative users (set on user creation). The user, via the \"Manual Login\" process, can enter any valid attribute value, which will be mapped back to the specified username attribute automatically as well.\n", + "overview": "Authenticate users against an LDAP database", + "owner": "jellyfin", + "category": "Authentication", + "versions": [ + { + "version": "10.0.0.0", + "changelog": "Update for 10.7 support\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/ldap-authentication/ldap-authentication_10.0.0.0.zip", + "checksum": "62e7e1cd3ffae0944c14750a3c90df4f", + "timestamp": "2020-12-05T19:48:10Z" + }, + { + "version": "9.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/ldap-authentication/ldap-authentication_9.0.0.0.zip", + "checksum": "7f2f83587a65a43ebf168e4058421463", + "timestamp": "2020-07-22T15:42:57Z" + }, + { + "version": "8.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/ldap-authentication/ldap-authentication_8.0.0.0.zip", + "checksum": "8af8cee62717d63577f8b1e710839415", + "timestamp": "2020-07-20T01:31:30Z" + } + ] + }, + { + "guid": "9574ac10-bf23-49bc-949f-924f23cfa48f", + "name": "NextPVR", + "description": "Provides access to live TV, program guide, and recordings from NextPVR.\n", + "overview": "Live TV plugin for NextPVR", + "owner": "jellyfin", + "category": "LiveTV", + "versions": [ + { + "version": "5.0.0.0", + "changelog": "Updated to use NextPVR API v5, no longer compatable with API v4.\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/nextpvr/nextpvr_5.0.0.0.zip", + "checksum": "d70f694d14bf9462ba2b2ebe110068d3", + "timestamp": "2020-12-05T22:24:03Z" + }, + { + "version": "4.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/nextpvr/nextpvr_4.0.0.0.zip", + "checksum": "b15949d895ac5a8c89496581db350478", + "timestamp": "2020-07-20T01:31:38Z" + } + ] + }, + { + "guid": "4b9ed42f-5185-48b5-9803-6ff2989014c4", + "name": "Open Subtitles", + "description": "Download subtitles from the internet to use with your media files.", + "overview": "Download subtitles for your media", + "owner": "jellyfin", + "category": "Metadata", + "versions": [ + { + "version": "10.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/open-subtitles/open-subtitles_10.0.0.0.zip", + "checksum": "ed99d03ec463bf15fca1256a113f57b4", + "timestamp": "2020-12-05T21:56:19Z" + }, + { + "version": "9.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/open-subtitles/open-subtitles_9.0.0.0.zip", + "checksum": "16789b26497cea0509daf6b18c579340", + "timestamp": "2020-07-20T01:32:00Z" + } + ] + }, + { + "guid": "5c534381-91a3-43cb-907a-35aa02eb9d2c", + "name": "Playback Reporting", + "description": "Collect and show user play statistics", + "overview": "Collect and show user play statistics", + "owner": "jellyfin", + "category": "General", + "versions": [ + { + "version": "9.0.0.0", + "changelog": "Add authentication to plugin endpoints\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/playback-reporting/playback-reporting_9.0.0.0.zip", + "checksum": "ca323b3dcb2cb86cc2e72a7a0f1eee22", + "timestamp": "2020-12-05T22:15:48Z" + }, + { + "version": "8.0.0.0", + "changelog": "Add authentication to plugin endpoints\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/playback-reporting/playback-reporting_8.0.0.0.zip", + "checksum": "58644c505586542ef0b8b65e2f704bd1", + "timestamp": "2020-11-18T03:01:51Z" + }, + { + "version": "7.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/playback-reporting/playback-reporting_7.0.0.0.zip", + "checksum": "6a361ef33bca97f9155856d02ff47380", + "timestamp": "2020-07-20T01:32:09Z" + } + ] + }, + { + "guid": "de228f12-e43e-4bd9-9fc0-2830819c3b92", + "name": "Pushbullet", + "description": "Get notifications via Pushbullet.\n", + "overview": "Pushbullet notification plugin", + "owner": "jellyfin", + "category": "Notifications", + "versions": [ + { + "version": "6.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/pushbullet/pushbullet_6.0.0.0.zip", + "checksum": "248cf3d56644f1d909e75aaddbdfb3a6", + "timestamp": "2020-12-06T02:47:53Z" + }, + { + "version": "5.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/pushbullet/pushbullet_5.0.0.0.zip", + "checksum": "dabbdd86328b2922a69dfa0c9e1c8343", + "timestamp": "2020-07-20T01:32:17Z" + } + ] + }, + { + "guid": "F240D6BE-5743-441B-87F1-A70ECAC42642", + "name": "Pushover", + "description": "Send messages to a wide range of devices through Pushover.", + "overview": "Send notifications via Pushover", + "owner": "crobibero", + "category": "Notifications", + "versions": [ + { + "version": "4.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/pushover/pushover_4.0.0.0.zip", + "checksum": "56a0da16c7e48cc184987737b7e155dd", + "timestamp": "2020-07-20T01:32:25Z" + } + ] + }, + { + "guid": "d4312cd9-5c90-4f38-82e8-51da566790e8", + "name": "Reports", + "description": "Generate reports of your media library", + "overview": "Generate reports of your media library", + "owner": "jellyfin", + "category": "General", + "versions": [ + { + "version": "11.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/reports/reports_11.0.0.0.zip", + "checksum": "d71bc6a4c008e58ee70ad44c83bfd310", + "timestamp": "2020-12-05T22:00:46Z" + }, + { + "version": "10.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/reports/reports_10.0.0.0.zip", + "checksum": "3917e75839337475b42daf2ba0b5bd7b", + "timestamp": "2020-10-19T19:30:41Z" + }, + { + "version": "9.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/reports/reports_9.0.0.0.zip", + "checksum": "5b5ad8d885616a21e8d1e8eecf5ea979", + "timestamp": "2020-10-16T23:52:37Z" + } + ] + }, + { + "guid": "1fc322a1-af2e-49a5-b2eb-a89b4240f700", + "name": "ServerWMC", + "description": "Provides access to Live TV, Program Guide and Recordings from your Windows MediaCenter Server running ServerWMC. Requires ServerWMC to be installed and running on your Windows MediaCenter machine.\n", + "overview": "Jellyfin Live TV plugin for Windows MediaCenter with ServerWMC", + "owner": "jellyfin", + "category": "LiveTV", + "versions": [ + { + "version": "6.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/serverwmc/serverwmc_6.0.0.0.zip", + "checksum": "3120af0cea2c1cb8b7cf578d9b4b862c", + "timestamp": "2020-12-05T22:28:15Z" + }, + { + "version": "5.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/serverwmc/serverwmc_5.0.0.0.zip", + "checksum": "dc44b039aa1b66eaf40a44fbf02d37e2", + "timestamp": "2020-07-20T01:32:42Z" + } + ] + }, + { + "guid": "94fb77c3-55ad-4c50-bf4e-4e5497467b79", + "name": "Slack Notifications", + "description": "Get notifications via Slack.\n", + "overview": "Get notifications via Slack", + "owner": "jellyfin", + "category": "Notifications", + "versions": [ + { + "version": "7.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/slack-notifications/slack-notifications_7.0.0.0.zip", + "checksum": "1d5330a77ce7b2a9ac8e5d58088a012c", + "timestamp": "2020-12-05T22:40:02Z" + }, + { + "version": "6.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/slack-notifications/slack-notifications_6.0.0.0.zip", + "checksum": "ede4cbe064542d1ecccc5823921bee4b", + "timestamp": "2020-07-20T01:32:50Z" + } + ] + }, + { + "guid": "bc4aad2e-d3d0-4725-a5e2-fd07949e5b42", + "name": "TMDb Box Sets", + "description": "Automatically create movie box sets based on TMDb collections", + "overview": "Automatically create movie box sets based on TMDb collections", + "owner": "jellyfin", + "category": "Metadata", + "versions": [ + { + "version": "7.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tmdb-box-sets/tmdb-box-sets_7.0.0.0.zip", + "checksum": "1551792e6af4d36f2cead01153c73cf0", + "timestamp": "2020-12-05T22:07:21Z" + }, + { + "version": "6.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tmdb-box-sets/tmdb-box-sets_6.0.0.0.zip", + "checksum": "b92b68a922c5fcbb8f4d47b8601b01b6", + "timestamp": "2020-07-20T01:32:58Z" + } + ] + }, + { + "guid": "4fe3201e-d6ae-4f2e-8917-e12bda571281", + "name": "Trakt", + "description": "Record your watched media with Trakt.\n", + "overview": "Record your watched media with Trakt", + "owner": "jellyfin", + "category": "General", + "versions": [ + { + "version": "11.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/trakt/trakt_11.0.0.0.zip", + "checksum": "2257ccde1e39114644a27e0966a0bf2d", + "timestamp": "2020-12-05T19:56:12Z" + }, + { + "version": "10.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/trakt/trakt_10.0.0.0.zip", + "checksum": "ab67e6b59ea2e7860a6a3ff7b8452759", + "timestamp": "2020-07-20T01:33:06Z" + } + ] + }, + { + "guid": "3fd018e5-5e78-4e58-b280-a0c068febee0", + "name": "TVHeadend", + "description": "Manage TVHeadend from Jellyfin", + "overview": "Manage TVHeadend from Jellyfin", + "owner": "jellyfin", + "category": "LiveTV", + "versions": [ + { + "version": "7.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tvheadend/tvheadend_7.0.0.0.zip", + "checksum": "1abbfce737b6962f4b1b2255dc63e932", + "timestamp": "2021-01-05T16:20:33Z" + }, + { + "version": "6.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tvheadend/tvheadend_6.0.0.0.zip", + "checksum": "143c34fd70d7173b8912cc03ce4b517d", + "timestamp": "2020-07-20T01:33:15Z" + } + ] + }, + { + "guid": "022a3003-993f-45f1-8565-87d12af2e12a", + "name": "InfuseSync", + "description": "This plugin will track all media changes while any Infuse clients are offline to decrease sync times when logging back in to your server.", + "overview": "Blazing fast indexing for Infuse", + "owner": "Firecore LLC", + "category": "General", + "versions": [ + { + "version": "1.2.4.0", + "changelog": "New Playlist support.\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.4/InfuseSync-jellyfin-1.2.4.zip", + "checksum": "7adde11b8c8404fd2923f59d98fb1a30", + "timestamp": "2020-10-12T08:00:00Z" + }, + { + "version": "1.2.1.3", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.3/InfuseSync-jellyfin-1.2.3.zip", + "checksum": "d8e2c5fe736a302097bb3bac3d04b1c4", + "timestamp": "2020-09-18T12:19:00Z" + }, + { + "version": "1.2.1.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.1/InfuseSync-jellyfin-1.2.1.zip", + "checksum": "1a853e926cc422f5d79d398d9ae18ee8", + "timestamp": "2020-08-21T10:48:00Z" + }, + { + "version": "1.2.0.0", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://github.com/firecore/InfuseSync/releases/download/v1.2.0/InfuseSync-jellyfin-1.2.0.zip", + "checksum": "2d3c7859852695a7f05adc6d3fcbc783", + "timestamp": "2020-07-20T11:51:00Z" + } + ] + }, + { + "guid": "8119f3c6-cfc2-4d9c-a0ba-028f1d93e526", + "name": "Cover Art Archive", + "description": "This plugin provides images from the Cover Art Archive https://musicbrainz.org/doc/Cover_Art_Archive and depends on the MusicBrainz metadata provider to know what images belong where\n", + "overview": "MusicBrainz Cover Art Archive", + "owner": "jellyfin", + "category": "Metadata", + "versions": [ + { + "version": "2.0.0.0", + "changelog": "changelog\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/cover-art-archive/cover-art-archive_2.0.0.0.zip", + "checksum": "bea8fa4a37b3e7ed74e22266e7597a68", + "timestamp": "2020-12-06T02:51:03Z" + }, + { + "version": "1.0.0.3", + "changelog": "changelog\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/cover-art-archive/cover-art-archive_1.0.0.3.zip", + "checksum": "c502a5c54b168810614c1c40709b9598", + "timestamp": "2020-08-06T21:21:22Z" + } + ] + }, + { + "guid": "A4A488D0-17A3-4919-8D82-7F3DE4F6B209", + "name": "TV Maze", + "description": "Get TV metadata from TV Maze\n", + "overview": "Get TV metadata from TV Maze", + "owner": "jellyfin", + "category": "Metadata", + "versions": [ + { + "version": "5.0.0.0", + "changelog": "Get additional image types\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_5.0.0.0.zip", + "checksum": "509a85e40b1d1ac36eef45673deaf606", + "timestamp": "2020-12-06T02:51:56Z" + }, + { + "version": "4.0.0.0", + "changelog": "Get additional image types\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_4.0.0.0.zip", + "checksum": "58ee9ab3f129151bdfff033ad889ad87", + "timestamp": "2020-11-24T14:44:37Z" + }, + { + "version": "3.0.0.0", + "changelog": "Remove unused dependencies \n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_3.0.0.0.zip", + "checksum": "f3b2c70b3e136fb15c917e4420f4fdec", + "timestamp": "2020-11-09T14:32:56Z" + }, + { + "version": "2.0.0.0", + "changelog": "Remove unused dependencies \n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_2.0.0.0.zip", + "checksum": "c7662ae8ae52ce8a4e8d685d55f36e80", + "timestamp": "2020-11-09T02:33:11Z" + }, + { + "version": "1.0.0.0", + "changelog": "Initial release.\n", + "targetAbi": "10.6.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/tv-maze/tv-maze_1.0.0.0.zip", + "checksum": "c90eee48c12f2c07880b4b28e507fd14", + "timestamp": "2020-11-08T19:05:32Z" + } + ] + }, + { + "guid": "a677c0da-fac5-4cde-941a-7134223f14c8", + "name": "TheTVDB", + "description": "Get TV metadata from TheTvdb\n", + "overview": "Get TV metadata from TheTvdb", + "owner": "jellyfin", + "category": "Metadata", + "versions": [ + { + "version": "2.0.0.0", + "changelog": "Remove from Jellyfin core.\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/thetvdb/thetvdb_2.0.0.0.zip", + "checksum": "e46cee334476a1b475e5c553171c4cb6", + "timestamp": "2020-12-16T20:03:28Z" + }, + { + "version": "1.0.0.0", + "changelog": "Remove from Jellyfin core.\n", + "targetAbi": "10.7.0.0", + "sourceUrl": "https://repo.jellyfin.org/releases/plugin/thetvdb/thetvdb_1.0.0.0.zip", + "checksum": "5a3dca5c0db4824d83bfd4e7e2b7bf11", + "timestamp": "2020-12-06T02:56:40Z" + } + ] + } +]
\ No newline at end of file diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs new file mode 100644 index 000000000..4fa64d8a2 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using AutoFixture.AutoMoq; +using Emby.Server.Implementations.Updates; +using MediaBrowser.Model.Updates; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Updates +{ + public class InstallationManagerTests + { + private readonly Fixture _fixture; + private readonly InstallationManager _installationManager; + + public InstallationManagerTests() + { + 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("Test Data/Updates/" + 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); + _installationManager = _fixture.Create<InstallationManager>(); + } + + [Fact] + public async Task GetPackages_Valid_Success() + { + IList<PackageInfo> packages = await _installationManager.GetPackages( + "Jellyfin Stable", + "https://repo.jellyfin.org/releases/plugin/manifest-stable.json", + false); + + Assert.Equal(25, packages.Count); + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs new file mode 100644 index 000000000..ea6838682 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -0,0 +1,59 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Api.Models.StartupDtos; +using Jellyfin.Api.Models.UserDtos; +using MediaBrowser.Common.Json; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests +{ + public static class AuthHelper + { + public const string AuthHeaderName = "X-Emby-Authorization"; + public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\""; + + public static async Task<string> CompleteStartupAsync(HttpClient client) + { + var jsonOptions = JsonDefaults.Options; + var userResponse = await client.GetByteArrayAsync("/Startup/User").ConfigureAwait(false); + var user = JsonSerializer.Deserialize<StartupUserDto>(userResponse, jsonOptions); + + using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode); + + using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes( + new AuthenticateUserByName() + { + Username = user!.Name, + Pw = user.Password, + }, + jsonOptions)); + content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json); + content.Headers.Add("X-Emby-Authorization", DummyAuthHeader); + + using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content).ConfigureAwait(false); + var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>( + await authResponse.Content.ReadAsStreamAsync().ConfigureAwait(false), + jsonOptions).ConfigureAwait(false); + + return auth!.AccessToken; + } + + public static void AddAuthHeader(this HttpHeaders headers, string accessToken) + { + headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}"); + } + + private class AuthenticationResultDto + { + public string AccessToken { get; set; } = string.Empty; + + public string ServerId { get; set; } = string.Empty; + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs new file mode 100644 index 000000000..be89fbc9a --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs @@ -0,0 +1,30 @@ +using System.Net; +using System.Net.Mime; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests.Controllers +{ + public sealed class ActivityLogControllerTests : IClassFixture<JellyfinApplicationFactory> + { + private readonly JellyfinApplicationFactory _factory; + private static string? _accessToken; + + public ActivityLogControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task ActivityLog_GetEntries_Ok() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + var response = await client.GetAsync("System/ActivityLog/Entries").ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Controllers/BrandingControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs index 40933562d..87136dfc8 100644 --- a/tests/Jellyfin.Api.Tests/Controllers/BrandingControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Net.Mime; using System.Text; using System.Text.Json; @@ -5,7 +6,7 @@ using System.Threading.Tasks; using MediaBrowser.Model.Branding; using Xunit; -namespace Jellyfin.Api.Tests +namespace Jellyfin.Server.Integration.Tests { public sealed class BrandingControllerTests : IClassFixture<JellyfinApplicationFactory> { @@ -26,7 +27,7 @@ namespace Jellyfin.Api.Tests var response = await client.GetAsync("/Branding/Configuration"); // Assert - Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); var responseBody = await response.Content.ReadAsStreamAsync(); diff --git a/tests/Jellyfin.Api.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs index 300b2697f..f5411dcb8 100644 --- a/tests/Jellyfin.Api.Tests/Controllers/DashboardControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -8,12 +8,12 @@ using Jellyfin.Api.Models; using MediaBrowser.Common.Json; using Xunit; -namespace Jellyfin.Api.Tests.Controllers +namespace Jellyfin.Server.Integration.Tests.Controllers { public sealed class DashboardControllerTests : IClassFixture<JellyfinApplicationFactory> { private readonly JellyfinApplicationFactory _factory; - private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.GetOptions(); + private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options; public DashboardControllerTests(JellyfinApplicationFactory factory) { @@ -37,9 +37,9 @@ namespace Jellyfin.Api.Tests.Controllers var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin").ConfigureAwait(false); - Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType); - StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Api.Tests.TestPage.html")!); + StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!); Assert.Equal(await response.Content.ReadAsStringAsync(), reader.ReadToEnd()); } @@ -60,7 +60,7 @@ namespace Jellyfin.Api.Tests.Controllers var response = await client.GetAsync("/web/ConfigurationPages").ConfigureAwait(false); - Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); var res = await response.Content.ReadAsStreamAsync(); _ = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions); @@ -74,7 +74,7 @@ namespace Jellyfin.Api.Tests.Controllers var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true").ConfigureAwait(false); - Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs new file mode 100644 index 000000000..169a5a6c5 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs @@ -0,0 +1,119 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Api.Models.StartupDtos; +using MediaBrowser.Common.Json; +using Xunit; +using Xunit.Priority; + +namespace Jellyfin.Server.Integration.Tests.Controllers +{ + [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] + public sealed class StartupControllerTests : IClassFixture<JellyfinApplicationFactory> + { + private readonly JellyfinApplicationFactory _factory; + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + + public StartupControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + [Priority(-2)] + public async Task Configuration_EditConfig_Success() + { + var client = _factory.CreateClient(); + + var config = new StartupConfigurationDto() + { + UICulture = "NewCulture", + MetadataCountryCode = "be", + PreferredMetadataLanguage = "nl" + }; + + using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(config, _jsonOptions)); + postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json); + using var postResponse = await client.PostAsync("/Startup/Configuration", postContent).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode); + + using var getResponse = await client.GetAsync("/Startup/Configuration").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); + Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); + + using var responseStream = await getResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var newConfig = await JsonSerializer.DeserializeAsync<StartupConfigurationDto>(responseStream, _jsonOptions).ConfigureAwait(false); + Assert.Equal(config.UICulture, newConfig!.UICulture); + Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode); + Assert.Equal(config.PreferredMetadataLanguage, newConfig.PreferredMetadataLanguage); + } + + [Fact] + [Priority(-2)] + public async Task User_DefaultUser_NameWithoutPassword() + { + var client = _factory.CreateClient(); + + using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); + + using var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var user = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions).ConfigureAwait(false); + Assert.NotEmpty(user!.Name); + Assert.Null(user.Password); + } + + [Fact] + [Priority(-1)] + public async Task User_EditUser_Success() + { + var client = _factory.CreateClient(); + + var user = new StartupUserDto() + { + Name = "NewName", + Password = "NewPassword" + }; + + using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions)); + postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json); + var postResponse = await client.PostAsync("/Startup/User", postContent).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode); + + var getResponse = await client.GetAsync("/Startup/User").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); + Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); + + var contentStream = await getResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var newUser = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions).ConfigureAwait(false); + Assert.Equal(user.Name, newUser!.Name); + Assert.NotEmpty(newUser.Password); + Assert.NotEqual(user.Password, newUser.Password); + } + + [Fact] + [Priority(0)] + public async Task CompleteWizard_Success() + { + var client = _factory.CreateClient(); + + var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + + [Fact] + [Priority(1)] + public async Task GetFirstUser_CompleteWizard_Unauthorized() + { + var client = _factory.CreateClient(); + + using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs new file mode 100644 index 000000000..6584490de --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -0,0 +1,170 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Api.Models.UserDtos; +using MediaBrowser.Common.Json; +using MediaBrowser.Model.Dto; +using Xunit; +using Xunit.Priority; + +namespace Jellyfin.Server.Integration.Tests.Controllers +{ + [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] + public sealed class UserControllerTests : IClassFixture<JellyfinApplicationFactory> + { + private const string TestUsername = "testUser01"; + + private readonly JellyfinApplicationFactory _factory; + private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options; + private static string? _accessToken; + private static Guid _testUserId = Guid.Empty; + + public UserControllerTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + private Task<HttpResponseMessage> CreateUserByName(HttpClient httpClient, CreateUserByName request) + { + using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(request, _jsonOpions)); + postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json); + return httpClient.PostAsync("Users/New", postContent); + } + + private Task<HttpResponseMessage> UpdateUserPassword(HttpClient httpClient, Guid userId, UpdateUserPassword request) + { + using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(request, _jsonOpions)); + postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json); + return httpClient.PostAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", postContent); + } + + [Fact] + [Priority(-1)] + public async Task GetPublicUsers_Valid_Success() + { + var client = _factory.CreateClient(); + + using var response = await client.GetAsync("Users/Public").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var users = await JsonSerializer.DeserializeAsync<UserDto[]>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + // User are hidden by default + Assert.Empty(users); + } + + [Fact] + [Priority(-1)] + public async Task GetUsers_Valid_Success() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + + using var response = await client.GetAsync("Users").ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var users = await JsonSerializer.DeserializeAsync<UserDto[]>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + Assert.Single(users); + Assert.False(users![0].HasConfiguredPassword); + } + + [Fact] + [Priority(0)] + public async Task New_Valid_Success() + { + var client = _factory.CreateClient(); + + // access token can't be null here as the previous test populated it + client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); + + var createRequest = new CreateUserByName() + { + Name = TestUsername + }; + + using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var user = await JsonSerializer.DeserializeAsync<UserDto>( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + Assert.Equal(TestUsername, user!.Name); + Assert.False(user.HasPassword); + Assert.False(user.HasConfiguredPassword); + + _testUserId = user.Id; + + Console.WriteLine(user.Id.ToString("N", CultureInfo.InvariantCulture)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("‼️")] + [Priority(0)] + public async Task New_Invalid_Fail(string? username) + { + var client = _factory.CreateClient(); + + // access token can't be null here as the previous test populated it + client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); + + var createRequest = new CreateUserByName() + { + Name = username + }; + + using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + [Priority(1)] + public async Task UpdateUserPassword_Valid_Success() + { + var client = _factory.CreateClient(); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); + + var createRequest = new UpdateUserPassword() + { + NewPw = "4randomPa$$word" + }; + + using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + var users = await JsonSerializer.DeserializeAsync<UserDto[]>( + await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + var user = users!.First(x => x.Id == _testUserId); + Assert.True(user.HasPassword); + Assert.True(user.HasConfiguredPassword); + } + + [Fact] + [Priority(2)] + public async Task UpdateUserPassword_Empty_RemoveSetPassword() + { + var client = _factory.CreateClient(); + + client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); + + var createRequest = new UpdateUserPassword() + { + CurrentPw = "4randomPa$$word", + }; + + using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + var users = await JsonSerializer.DeserializeAsync<UserDto[]>( + await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + var user = users!.First(x => x.Id == _testUserId); + Assert.False(user.HasPassword); + Assert.False(user.HasConfiguredPassword); + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj new file mode 100644 index 000000000..938385a2a --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -0,0 +1,47 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="AutoFixture" Version="4.17.0" /> + <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> + <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.5" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> + <PackageReference Include="Xunit.Priority" Version="1.1.6" /> + <PackageReference Include="coverlet.collector" Version="3.0.3" /> + <PackageReference Include="Moq" Version="4.16.0" /> + </ItemGroup> + + <ItemGroup> + <!-- Don't run tests in parallel --> + <None Update="xunit.runner.json"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + + <!-- Code Analyzers --> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" /> + </ItemGroup> + + <ItemGroup> + <EmbeddedResource Include="TestPage.html" /> + </ItemGroup> + +</Project> diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index dbbd5ac28..d9ec81a27 100644 --- a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -1,19 +1,20 @@ using System; using System.Collections.Concurrent; using System.IO; +using System.Threading; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; -using Jellyfin.Server; using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Extensions.Logging; -namespace Jellyfin.Api.Tests +namespace Jellyfin.Server.Integration.Tests { /// <summary> /// Factory for bootstrapping the Jellyfin application in memory for functional end to end tests. @@ -21,12 +22,12 @@ namespace Jellyfin.Api.Tests public class JellyfinApplicationFactory : WebApplicationFactory<Startup> { private static readonly string _testPathRoot = Path.Combine(Path.GetTempPath(), "jellyfin-test-data"); - private static readonly ConcurrentBag<IDisposable> _disposableComponents = new ConcurrentBag<IDisposable>(); + private readonly ConcurrentBag<IDisposable> _disposableComponents = new ConcurrentBag<IDisposable>(); /// <summary> - /// Initializes a new instance of the <see cref="JellyfinApplicationFactory"/> class. + /// Initializes static members of the <see cref="JellyfinApplicationFactory"/> class. /// </summary> - public JellyfinApplicationFactory() + static JellyfinApplicationFactory() { // Perform static initialization that only needs to happen once per test-run Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); @@ -77,6 +78,7 @@ namespace Jellyfin.Api.Tests appPaths, loggerFactory, commandLineOpts, + new ConfigurationBuilder().Build(), new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), serviceCollection); _disposableComponents.Add(appHost); @@ -96,7 +98,7 @@ namespace Jellyfin.Api.Tests var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>(); appHost.ServiceProvider = testServer.Services; appHost.InitializeServices().GetAwaiter().GetResult(); - appHost.RunStartupTasksAsync().GetAwaiter().GetResult(); + appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult(); return testServer; } diff --git a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs index 03ab56d1f..0ade345a1 100644 --- a/tests/Jellyfin.Api.Tests/OpenApiSpecTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs @@ -1,12 +1,10 @@ using System.IO; using System.Reflection; -using System.Text.Json; using System.Threading.Tasks; -using MediaBrowser.Model.Branding; using Xunit; using Xunit.Abstractions; -namespace Jellyfin.Api.Tests +namespace Jellyfin.Server.Integration.Tests { public sealed class OpenApiSpecTests : IClassFixture<JellyfinApplicationFactory> { diff --git a/tests/Jellyfin.Api.Tests/TestAppHost.cs b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs index 772e98d04..0a463cfa3 100644 --- a/tests/Jellyfin.Api.Tests/TestAppHost.cs +++ b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; using System.Reflection; using Emby.Server.Implementations; -using Jellyfin.Server; using MediaBrowser.Controller; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Tests +namespace Jellyfin.Server.Integration.Tests { /// <summary> /// Implementation of the abstract <see cref="ApplicationHost" /> class. @@ -20,18 +20,21 @@ namespace Jellyfin.Api.Tests /// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="startup">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param> public TestAppHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, + IConfiguration startup, IFileSystem fileSystem, IServiceCollection collection) : base( applicationPaths, loggerFactory, options, + startup, fileSystem, collection) { diff --git a/tests/Jellyfin.Api.Tests/TestPage.html b/tests/Jellyfin.Server.Integration.Tests/TestPage.html index 8037af8a6..8037af8a6 100644 --- a/tests/Jellyfin.Api.Tests/TestPage.html +++ b/tests/Jellyfin.Server.Integration.Tests/TestPage.html diff --git a/tests/Jellyfin.Api.Tests/TestPlugin.cs b/tests/Jellyfin.Server.Integration.Tests/TestPlugin.cs index a3b4b6994..1d67ac487 100644 --- a/tests/Jellyfin.Api.Tests/TestPlugin.cs +++ b/tests/Jellyfin.Server.Integration.Tests/TestPlugin.cs @@ -7,7 +7,7 @@ using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; -namespace Jellyfin.Api.Tests +namespace Jellyfin.Server.Integration.Tests { public class TestPlugin : BasePlugin<BasePluginConfiguration>, IHasWebPages { diff --git a/tests/Jellyfin.Server.Integration.Tests/TestPluginWithoutPages.cs b/tests/Jellyfin.Server.Integration.Tests/TestPluginWithoutPages.cs new file mode 100644 index 000000000..ac10c4784 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/TestPluginWithoutPages.cs @@ -0,0 +1,27 @@ +#pragma warning disable CS1591 + +using System; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace Jellyfin.Server.Integration.Tests +{ + public class TestPluginWithoutPages : BasePlugin<BasePluginConfiguration> + { + public TestPluginWithoutPages(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + public static TestPluginWithoutPages? Instance { get; private set; } + + public override Guid Id => new Guid("ae95cbe6-bd3d-4d73-8596-490db334611e"); + + public override string Name => nameof(TestPluginWithoutPages); + + public override string Description => "Server test Plugin without web pages."; + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/WebSocketTests.cs b/tests/Jellyfin.Server.Integration.Tests/WebSocketTests.cs new file mode 100644 index 000000000..ffdc04eba --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/WebSocketTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests +{ + public sealed class WebSocketTests : IClassFixture<JellyfinApplicationFactory> + { + private readonly JellyfinApplicationFactory _factory; + + public WebSocketTests(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task WebSocket_Unauthenticated_ThrowsInvalidOperationException() + { + var server = _factory.Server; + var client = server.CreateWebSocketClient(); + + await Assert.ThrowsAsync<InvalidOperationException>( + () => client.ConnectAsync( + new UriBuilder(server.BaseAddress) + { + Scheme = "ws", + Path = "websocket" + }.Uri, CancellationToken.None)); + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/xunit.runner.json b/tests/Jellyfin.Server.Integration.Tests/xunit.runner.json new file mode 100644 index 000000000..809e880c7 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj new file mode 100644 index 000000000..72e40ebcb --- /dev/null +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -0,0 +1,36 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="AutoFixture" Version="4.17.0" /> + <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> + <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.5" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> + <PackageReference Include="coverlet.collector" Version="3.0.3" /> + <PackageReference Include="Moq" Version="4.16.0" /> + </ItemGroup> + + <!-- Code Analyzers --> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" /> + </ItemGroup> + +</Project> diff --git a/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 3984407ee..146b16cf9 100644 --- a/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -1,4 +1,3 @@ -using System; using System.Globalization; using System.Text; using Jellyfin.Networking.Configuration; @@ -10,7 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; -namespace Jellyfin.Api.Tests +namespace Jellyfin.Server.Tests { public class ParseNetworkTests { diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj index d6aab3f85..4132205c3 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj +++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj @@ -5,6 +5,8 @@ <IsPackable>false</IsPackable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> @@ -14,7 +16,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> @@ -23,7 +25,6 @@ <!-- Code Analyzers --> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> @@ -34,8 +35,4 @@ <ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" /> </ItemGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> - </PropertyGroup> - </Project> diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index 053e0a89e..9ad093a2b 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -37,8 +37,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers .Returns(new XbmcMetadataOptions()); var user = new Mock<IUserManager>(); var userData = new Mock<IUserDataManager>(); - - _parser = new EpisodeNfoParser(new NullLogger<EpisodeNfoParser>(), config.Object, providerManager.Object, user.Object, userData.Object); + var directoryService = new Mock<IDirectoryService>(); + + _parser = new EpisodeNfoParser( + new NullLogger<EpisodeNfoParser>(), + config.Object, + providerManager.Object, + user.Object, + userData.Object, + directoryService.Object); } [Fact] diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index ff4795569..30a48857a 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -9,7 +9,9 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; +using MediaBrowser.Model.System; using MediaBrowser.Providers.Plugins.Tmdb.Movies; using MediaBrowser.XbmcMetadata.Parsers; using Microsoft.Extensions.Logging.Abstractions; @@ -23,6 +25,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers private readonly MovieNfoParser _parser; private readonly IUserDataManager _userDataManager; private readonly User _testUser; + private readonly FileSystemMetadata _localImageFileMetadata; public MovieNfoParserTests() { @@ -52,8 +55,25 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers userData.Setup(x => x.GetUserData(_testUser, It.IsAny<BaseItem>())) .Returns(new UserItemData()); + var directoryService = new Mock<IDirectoryService>(); + _localImageFileMetadata = new FileSystemMetadata() + { + Exists = true, + FullName = MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows ? + "C:\\media\\movies\\Justice League (2017).jpg" + : "/media/movies/Justice League (2017).jpg" + }; + directoryService.Setup(x => x.GetFile(_localImageFileMetadata.FullName)) + .Returns(_localImageFileMetadata); + _userDataManager = userData.Object; - _parser = new MovieNfoParser(new NullLogger<MovieNfoParser>(), configManager.Object, providerManager.Object, user.Object, userData.Object); + _parser = new MovieNfoParser( + new NullLogger<MovieNfoParser>(), + configManager.Object, + providerManager.Object, + user.Object, + userData.Object, + directoryService.Object); } [Fact] @@ -134,6 +154,41 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers // Movie set Assert.Equal("702342", item.ProviderIds[MetadataProvider.TmdbCollection.ToString()]); Assert.Equal("Justice League Collection", item.CollectionName); + + // Images + Assert.Equal(7, result.RemoteImages.Count); + + var posters = result.RemoteImages.Where(x => x.type == ImageType.Primary).ToList(); + Assert.Single(posters); + Assert.Equal("http://image.tmdb.org/t/p/original/9rtrRGeRnL0JKtu9IMBWsmlmmZz.jpg", posters[0].url); + + var logos = result.RemoteImages.Where(x => x.type == ImageType.Logo).ToList(); + Assert.Single(logos); + Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png", logos[0].url); + + var banners = result.RemoteImages.Where(x => x.type == ImageType.Banner).ToList(); + Assert.Single(banners); + Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-586017e95adbd.jpg", banners[0].url); + + var thumbs = result.RemoteImages.Where(x => x.type == ImageType.Thumb).ToList(); + Assert.Single(thumbs); + Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-585fb155c3743.jpg", thumbs[0].url); + + var art = result.RemoteImages.Where(x => x.type == ImageType.Art).ToList(); + Assert.Single(art); + Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/hdmovieclearart/justice-league-5865c23193041.png", art[0].url); + + var discArt = result.RemoteImages.Where(x => x.type == ImageType.Disc).ToList(); + Assert.Single(discArt); + Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a3af26360617.png", discArt[0].url); + + var backdrop = result.RemoteImages.Where(x => x.type == ImageType.Backdrop).ToList(); + Assert.Single(backdrop); + Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg", backdrop[0].url); + + // Local Image - contains only one item depending on operating system + Assert.Single(result.Images); + Assert.Equal(_localImageFileMetadata.Name, result.Images[0].FileInfo.Name); } [Theory] @@ -153,6 +208,21 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers } [Fact] + public void Parse_RadarrUrlFile_Success() + { + var result = new MetadataResult<Video>() + { + Item = new Movie() + }; + + _parser.Fetch(result, "Test Data/Radarr.nfo", CancellationToken.None); + var item = (Movie)result.Item; + + Assert.Equal("583689", item.ProviderIds[MetadataProvider.Tmdb.ToString()]); + Assert.Equal("tt4154796", item.ProviderIds[MetadataProvider.Imdb.ToString()]); + } + + [Fact] public void Fetch_WithNullItem_ThrowsArgumentException() { var result = new MetadataResult<Video>(); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs index 63f6dfce8..2129f3422 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs @@ -1,7 +1,6 @@ #pragma warning disable CA5369 using System; -using System.Linq; using System.Threading; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.Audio; @@ -11,7 +10,6 @@ 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; @@ -38,8 +36,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers .Returns(new XbmcMetadataOptions()); var user = new Mock<IUserManager>(); var userData = new Mock<IUserDataManager>(); + var directoryService = new Mock<IDirectoryService>(); - _parser = new BaseNfoParser<MusicAlbum>(new NullLogger<BaseNfoParser<MusicAlbum>>(), config.Object, providerManager.Object, user.Object, userData.Object); + _parser = new BaseNfoParser<MusicAlbum>( + new NullLogger<BaseNfoParser<MusicAlbum>>(), + config.Object, + providerManager.Object, + user.Object, + userData.Object, + directoryService.Object); } [Fact] diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs index 438d47cac..8ca3dd96e 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Threading; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.Audio; @@ -35,8 +34,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers .Returns(new XbmcMetadataOptions()); var user = new Mock<IUserManager>(); var userData = new Mock<IUserDataManager>(); + var directoryService = new Mock<IDirectoryService>(); - _parser = new BaseNfoParser<MusicArtist>(new NullLogger<BaseNfoParser<MusicArtist>>(), config.Object, providerManager.Object, user.Object, userData.Object); + _parser = new BaseNfoParser<MusicArtist>( + new NullLogger<BaseNfoParser<MusicArtist>>(), + config.Object, + providerManager.Object, + user.Object, + userData.Object, + directoryService.Object); } [Fact] diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicVideoNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicVideoNfoParserTests.cs index 898554936..bf887cab1 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicVideoNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicVideoNfoParserTests.cs @@ -30,8 +30,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers var user = new Mock<IUserManager>(); var userData = new Mock<IUserDataManager>(); + var directoryService = new Mock<IDirectoryService>(); - _parser = new MovieNfoParser(new NullLogger<BaseNfoParser<MusicVideo>>(), config.Object, providerManager.Object, user.Object, userData.Object); + _parser = new MovieNfoParser( + new NullLogger<BaseNfoParser<MusicVideo>>(), + config.Object, + providerManager.Object, + user.Object, + userData.Object, + directoryService.Object); } [Fact] diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs index a677cd724..0e61fa2a1 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs @@ -31,8 +31,15 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers .Returns(new XbmcMetadataOptions()); var user = new Mock<IUserManager>(); var userData = new Mock<IUserDataManager>(); + var directoryService = new Mock<IDirectoryService>(); - _parser = new SeasonNfoParser(new NullLogger<SeasonNfoParser>(), config.Object, providerManager.Object, user.Object, userData.Object); + _parser = new SeasonNfoParser( + new NullLogger<SeasonNfoParser>(), + config.Object, + providerManager.Object, + user.Object, + userData.Object, + directoryService.Object); } [Fact] diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs index 80923957d..bdedae205 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs @@ -29,8 +29,9 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers .Returns(new XbmcMetadataOptions()); var user = new Mock<IUserManager>(); var userData = new Mock<IUserDataManager>(); + var directoryService = new Mock<IDirectoryService>(); - _parser = new SeriesNfoParser(new NullLogger<SeriesNfoParser>(), config.Object, providerManager.Object, user.Object, userData.Object); + _parser = new SeriesNfoParser(new NullLogger<SeriesNfoParser>(), config.Object, providerManager.Object, user.Object, userData.Object, directoryService.Object); } [Fact] diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo index 72e27fe50..4e8c79dca 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo +++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo @@ -59,6 +59,8 @@ <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/exLtrlI7JjKcfQVTccI7XdQRFMz.jpg">http://image.tmdb.org/t/p/original/exLtrlI7JjKcfQVTccI7XdQRFMz.jpg</thumb> <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/paLcue01KpfQftorfjKqqD4qvlL.jpg">http://image.tmdb.org/t/p/original/paLcue01KpfQftorfjKqqD4qvlL.jpg</thumb> <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/yVDIfiKIsCbdFcgLXW34bAsnQvy.jpg">http://image.tmdb.org/t/p/original/yVDIfiKIsCbdFcgLXW34bAsnQvy.jpg</thumb> + <thumb aspect="poster">C:\media\movies\Justice League (2017).jpg</thumb> + <thumb aspect="poster">/media/movies/Justice League (2017).jpg</thumb> <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png</thumb> <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png</thumb> <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-57b476a831d74.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-57b476a831d74.png</thumb> @@ -80,8 +82,8 @@ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a0b913c233be.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a0b913c233be.png</thumb> <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png</thumb> <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-59dc595362ef1.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-59dc595362ef1.png</thumb> + <thumb aspect="fanart">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg</thumb> <fanart> - <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg</thumb> <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg</thumb> <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg</thumb> <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg</thumb> @@ -91,7 +93,8 @@ </fanart> <mpaa>Australia:M</mpaa> <id>tt0974015</id> - <uniqueid type="imdb" default="true">tt0974015</uniqueid> + <uniqueid type="imdb">tt0974015</uniqueid> + <uniqueid type="tmdb">141052</uniqueid> <genre>Action</genre> <genre>Adventure</genre> <genre>Fantasy</genre> diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Radarr.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Radarr.nfo new file mode 100644 index 000000000..43da4881c --- /dev/null +++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Radarr.nfo @@ -0,0 +1,2 @@ +https://www.themoviedb.org/movie/583689 +https://www.imdb.com/title/tt4154796 |
