diff options
173 files changed, 2403 insertions, 2830 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index c28b1bf7f..c91a084e5 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -168,6 +168,7 @@ jobs: - job: CollectArtifacts timeoutInMinutes: 20 displayName: 'Collect Artifacts' + condition: succeededOrFailed() continueOnError: true dependsOn: - BuildPackage diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1f81a332d..5dc38e188 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,18 +20,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: Setup .NET uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 with: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 + uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 + uses: github/codeql-action/autobuild@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 + uses: github/codeql-action/analyze@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 178959afc..ba7883a73 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -51,7 +51,7 @@ jobs: reactions: eyes - name: Checkout the latest code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index d3dfd0a6a..693f98d16 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -14,7 +14,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -25,7 +25,7 @@ jobs: - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: openapi-head retention-days: 14 @@ -39,7 +39,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -59,7 +59,7 @@ jobs: - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: openapi-base retention-days: 14 diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/repo-bump-version.yaml new file mode 100644 index 000000000..0ba68dda3 --- /dev/null +++ b/.github/workflows/repo-bump-version.yaml @@ -0,0 +1,82 @@ +name: '🆙 Auto bump_version' + +on: + release: + types: + - published + workflow_dispatch: + inputs: + TAG_BRANCH: + required: true + description: release-x.y.z + NEXT_VERSION: + required: true + description: x.y.z + +jobs: + auto_bump_version: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'release' && !contains(github.event.release.tag_name, 'rc') }} + env: + TAG_BRANCH: ${{ github.event.release.target_commitish }} + steps: + - name: Wait for deploy checks to finish + uses: jitterbit/await-check-suites@292a541bb7618078395b2ce711a0d89cfb8a568a # v1 + with: + ref: ${{ env.TAG_BRANCH }} + intervalSeconds: 60 + timeoutSeconds: 3600 + + - name: Setup YQ + uses: chrisdickinson/setup-yq@latest + with: + yq-version: v4.9.8 + + - name: Checkout Repository + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + with: + ref: ${{ env.TAG_BRANCH }} + + - name: Setup EnvVars + run: |- + CURRENT_VERSION=$(yq e '.version' build.yaml) + CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*} + CURRENT_PATCH=${CURRENT_VERSION##*.} + echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV + echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV + echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV + echo "NEXT_VERSION=${CURRENT_MAJOR_MINOR}.$(($CURRENT_PATCH + 1))" >> $GITHUB_ENV + + - name: Run bump_version + run: ./bump_version ${{ env.NEXT_VERSION }} + + - name: Commit Changes + run: |- + git config user.name "jellyfin-bot" + git config user.email "team@jellyfin.org" + git checkout ${{ env.TAG_BRANCH }} + git commit -am "Bump version to ${{ env.NEXT_VERSION }}" + git push origin ${{ env.TAG_BRANCH }} + + manual_bump_version: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' }} + env: + TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }} + NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} + steps: + - name: Checkout Repository + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + with: + ref: ${{ env.TAG_BRANCH }} + + - name: Run bump_version + run: ./bump_version ${{ env.NEXT_VERSION }} + + - name: Commit Changes + run: |- + git config user.name "jellyfin-bot" + git config user.email "team@jellyfin.org" + git checkout ${{ env.TAG_BRANCH }} + git commit -am "Bump version to ${{ env.NEXT_VERSION }}" + git push origin ${{ env.TAG_BRANCH }} diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml index c753c1600..4eb0cf099 100644 --- a/.github/workflows/repo-stale.yaml +++ b/.github/workflows/repo-stale.yaml @@ -2,20 +2,21 @@ name: Stale Check on: schedule: - - cron: '30 1 * * *' + - cron: '30 */12 * * *' workflow_dispatch: permissions: issues: write pull-requests: write + actions: write jobs: issues: - name: Check issues + name: Check for stale issues runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} days-before-stale: 120 @@ -26,11 +27,11 @@ jobs: exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed stale-issue-label: stale stale-issue-message: |- - This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments. - - If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label. + This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs. - This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html). + If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact). + close-issue-message: |- + This issue was closed due to inactivity. prs-conflicts: name: Check PRs with merge conflicts diff --git a/Directory.Packages.props b/Directory.Packages.props index 6746bd974..8cf3eae2a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,18 +19,21 @@ <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> + <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> + <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.9" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.11" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.9" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.9" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.9" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.11" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> @@ -39,14 +42,14 @@ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.9" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.9" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.11" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.11" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.1" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> @@ -59,23 +62,20 @@ <PackageVersion Include="prometheus-net" Version="8.0.1" /> <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> - <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" /> + <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" /> <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> - <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" /> + <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> - <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> + <PackageVersion Include="SkiaSharp" Version="2.88.5" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" /> + <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> - <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" /> - <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" /> - <PackageVersion Include="SkiaSharp" Version="2.88.3" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> - <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> - <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> - <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" /> + <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" /> @@ -86,8 +86,8 @@ <PackageVersion Include="TMDbLib" Version="2.0.0" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> - <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0" /> + <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.1" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageVersion Include="xunit" Version="2.4.2" /> + <PackageVersion Include="xunit" Version="2.5.1" /> </ItemGroup> </Project> diff --git a/Dockerfile b/Dockerfile index e51d285e1..9be319311 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # https://github.com/multiarch/qemu-user-static#binfmt_misc-register ARG DOTNET_VERSION=7.0 -FROM node:lts-alpine as web-builder +FROM node:20-alpine as web-builder 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 python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ diff --git a/Dockerfile.arm b/Dockerfile.arm index 46a3e9b99..e8ec6398e 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -5,7 +5,7 @@ ARG DOTNET_VERSION=7.0 -FROM node:lts-alpine as web-builder +FROM node:20-alpine as web-builder 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 python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 4f9d5e1fd..83137ee89 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -5,7 +5,7 @@ ARG DOTNET_VERSION=7.0 -FROM node:lts-alpine as web-builder +FROM node:20-alpine as web-builder 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 python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index f668dc829..5ed982876 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -45,8 +43,8 @@ namespace Emby.Dlna.Didl private readonly DeviceProfile _profile; private readonly IImageProcessor _imageProcessor; private readonly string _serverAddress; - private readonly string _accessToken; - private readonly User _user; + private readonly string? _accessToken; + private readonly User? _user; private readonly IUserDataManager _userDataManager; private readonly ILocalizationManager _localization; private readonly IMediaSourceManager _mediaSourceManager; @@ -56,10 +54,10 @@ namespace Emby.Dlna.Didl public DidlBuilder( DeviceProfile profile, - User user, + User? user, IImageProcessor imageProcessor, string serverAddress, - string accessToken, + string? accessToken, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, @@ -85,7 +83,7 @@ namespace Emby.Dlna.Didl return url + "&dlnaheaders=true"; } - public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo) + public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo) { var settings = new XmlWriterSettings { @@ -140,12 +138,12 @@ namespace Emby.Dlna.Didl public void WriteItemElement( XmlWriter writer, BaseItem item, - User user, - BaseItem context, + User? user, + BaseItem? context, StubType? contextStubType, string deviceId, Filter filter, - StreamInfo streamInfo = null) + StreamInfo? streamInfo = null) { var clientId = GetClientId(item, null); @@ -190,7 +188,7 @@ namespace Emby.Dlna.Didl writer.WriteFullEndElement(); } - private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null) + private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null) { if (streamInfo is null) { @@ -203,7 +201,7 @@ namespace Emby.Dlna.Didl Profile = _profile, DeviceId = deviceId, MaxBitrate = _profile.MaxStreamingBitrate - }); + }) ?? throw new InvalidOperationException("No optimal video stream found"); } var targetWidth = streamInfo.TargetWidth; @@ -315,7 +313,7 @@ namespace Emby.Dlna.Didl var mediaSource = streamInfo.MediaSource; - if (mediaSource.RunTimeTicks.HasValue) + if (mediaSource?.RunTimeTicks.HasValue == true) { writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); } @@ -410,7 +408,7 @@ namespace Emby.Dlna.Didl writer.WriteFullEndElement(); } - private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context) + private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context) { if (itemStubType.HasValue) { @@ -452,7 +450,7 @@ namespace Emby.Dlna.Didl /// <param name="episode">The episode.</param> /// <param name="context">Current context.</param> /// <returns>Formatted name of the episode.</returns> - private string GetEpisodeDisplayName(Episode episode, BaseItem context) + private string GetEpisodeDisplayName(Episode episode, BaseItem? context) { string[] components; @@ -530,7 +528,7 @@ namespace Emby.Dlna.Didl private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s); - private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) + private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null) { writer.WriteStartElement(string.Empty, "res", NsDidl); @@ -544,14 +542,14 @@ namespace Emby.Dlna.Didl MediaSources = sources.ToArray(), Profile = _profile, DeviceId = deviceId - }); + }) ?? throw new InvalidOperationException("No optimal audio stream found"); } var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken)); var mediaSource = streamInfo.MediaSource; - if (mediaSource.RunTimeTicks.HasValue) + if (mediaSource?.RunTimeTicks is not null) { writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); } @@ -634,7 +632,7 @@ namespace Emby.Dlna.Didl // Samsung sometimes uses 1 as root || string.Equals(id, "1", StringComparison.OrdinalIgnoreCase); - public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null) + public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string? requestedId = null) { writer.WriteStartElement(string.Empty, "container", NsDidl); @@ -678,14 +676,14 @@ namespace Emby.Dlna.Didl writer.WriteFullEndElement(); } - private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo) + private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo) { if (!item.SupportsPositionTicksResume || item is Folder) { return; } - XmlAttribute secAttribute = null; + XmlAttribute? secAttribute = null; foreach (var attribute in _profile.XmlRootAttributes) { if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase)) @@ -695,8 +693,8 @@ namespace Emby.Dlna.Didl } } - // Not a samsung device - if (secAttribute is null) + // Not a samsung device or no user data + if (secAttribute is null || user is null) { return; } @@ -717,7 +715,7 @@ namespace Emby.Dlna.Didl /// <summary> /// Adds fields used by both items and folders. /// </summary> - private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) + private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter) { // Don't filter on dc:title because not all devices will include it in the filter // MediaMonkey for example won't display content without a title @@ -795,7 +793,7 @@ namespace Emby.Dlna.Didl if (item.IsDisplayedAsFolder || stubType.HasValue) { - string classType = null; + string? classType = null; if (!_profile.RequiresPlainFolders) { @@ -899,7 +897,7 @@ namespace Emby.Dlna.Didl } } - private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) + private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter) { AddCommonFields(item, itemStubType, context, writer, filter); @@ -975,7 +973,7 @@ namespace Emby.Dlna.Didl private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer) { - ImageDownloadInfo imageInfo = GetImageInfo(item); + ImageDownloadInfo? imageInfo = GetImageInfo(item); if (imageInfo is null) { @@ -1073,7 +1071,7 @@ namespace Emby.Dlna.Didl writer.WriteFullEndElement(); } - private ImageDownloadInfo GetImageInfo(BaseItem item) + private ImageDownloadInfo? GetImageInfo(BaseItem item) { if (item.HasImage(ImageType.Primary)) { @@ -1118,7 +1116,7 @@ namespace Emby.Dlna.Didl return null; } - private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item) + private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item) { if (item is null) { @@ -1148,7 +1146,7 @@ namespace Emby.Dlna.Didl private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type) { var imageInfo = item.GetImageInfo(type, 0); - string tag = null; + string? tag = null; try { @@ -1250,7 +1248,7 @@ namespace Emby.Dlna.Didl { internal Guid ItemId { get; set; } - internal string ImageTag { get; set; } + internal string? ImageTag { get; set; } internal ImageType Type { get; set; } @@ -1260,9 +1258,9 @@ namespace Emby.Dlna.Didl internal bool IsDirectStream { get; set; } - internal string Format { get; set; } + internal required string Format { get; set; } - internal ItemImageInfo ItemImageInfo { get; set; } + internal required ItemImageInfo ItemImageInfo { get; set; } } } } diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 9c476119d..d21cc6913 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -25,7 +23,7 @@ namespace Emby.Dlna.PlayTo private readonly ILogger _logger; private readonly object _timerLock = new object(); - private Timer _timer; + private Timer? _timer; private int _muteVol; private int _volume; private DateTime _lastVolumeRefresh; @@ -40,13 +38,13 @@ namespace Emby.Dlna.PlayTo _logger = logger; } - public event EventHandler<PlaybackStartEventArgs> PlaybackStart; + public event EventHandler<PlaybackStartEventArgs>? PlaybackStart; - public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress; + public event EventHandler<PlaybackProgressEventArgs>? PlaybackProgress; - public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped; + public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped; - public event EventHandler<MediaChangedEventArgs> MediaChanged; + public event EventHandler<MediaChangedEventArgs>? MediaChanged; public DeviceInfo Properties { get; set; } @@ -75,13 +73,13 @@ namespace Emby.Dlna.PlayTo public bool IsStopped => TransportState == TransportState.STOPPED; - public Action OnDeviceUnavailable { get; set; } + public Action? OnDeviceUnavailable { get; set; } - private TransportCommands AvCommands { get; set; } + private TransportCommands? AvCommands { get; set; } - private TransportCommands RendererCommands { get; set; } + private TransportCommands? RendererCommands { get; set; } - public UBaseObject CurrentMediaInfo { get; private set; } + public UBaseObject? CurrentMediaInfo { get; private set; } public void Start() { @@ -131,7 +129,7 @@ namespace Emby.Dlna.PlayTo _volumeRefreshActive = true; var time = immediate ? 100 : 10000; - _timer.Change(time, Timeout.Infinite); + _timer?.Change(time, Timeout.Infinite); } } @@ -149,7 +147,7 @@ namespace Emby.Dlna.PlayTo _volumeRefreshActive = false; - _timer.Change(Timeout.Infinite, Timeout.Infinite); + _timer?.Change(Timeout.Infinite, Timeout.Infinite); } } @@ -199,7 +197,7 @@ namespace Emby.Dlna.PlayTo } } - private DeviceService GetServiceRenderingControl() + private DeviceService? GetServiceRenderingControl() { var services = Properties.Services; @@ -207,7 +205,7 @@ namespace Emby.Dlna.PlayTo services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase)); } - private DeviceService GetAvTransportService() + private DeviceService? GetAvTransportService() { var services = Properties.Services; @@ -240,7 +238,7 @@ namespace Emby.Dlna.PlayTo Properties.BaseUrl, service, command.Name, - rendererCommands.BuildPost(command, service.ServiceType, value), + rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -265,12 +263,7 @@ namespace Emby.Dlna.PlayTo return; } - var service = GetServiceRenderingControl(); - - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } + var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service"); // Set it early and assume it will succeed // Remote control will perform better @@ -281,7 +274,7 @@ namespace Emby.Dlna.PlayTo Properties.BaseUrl, service, command.Name, - rendererCommands.BuildPost(command, service.ServiceType, value), + rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -296,26 +289,20 @@ namespace Emby.Dlna.PlayTo return; } - var service = GetAvTransportService(); - - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, - avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), + avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above cancellationToken: cancellationToken) .ConfigureAwait(false); RestartTimer(true); } - public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken) + public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); @@ -335,14 +322,8 @@ namespace Emby.Dlna.PlayTo { "CurrentURIMetaData", CreateDidlMeta(metaData) } }; - var service = GetAvTransportService(); - - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - - var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); + var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, @@ -372,7 +353,7 @@ namespace Emby.Dlna.PlayTo * SetNextAvTransport is used to specify to the DLNA device what is the next track to play. * Without that information, the next track command on the device does not work. */ - public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default) + public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); @@ -380,7 +361,7 @@ namespace Emby.Dlna.PlayTo _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header); - var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase)); + var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase)); if (command is null) { return; @@ -392,14 +373,8 @@ namespace Emby.Dlna.PlayTo { "NextURIMetaData", CreateDidlMeta(metaData) } }; - var service = GetAvTransportService(); - - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - - var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); + var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken) .ConfigureAwait(false); @@ -423,12 +398,7 @@ namespace Emby.Dlna.PlayTo return Task.CompletedTask; } - var service = GetAvTransportService(); - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, @@ -460,14 +430,13 @@ namespace Emby.Dlna.PlayTo return; } - var service = GetAvTransportService(); - + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, - avCommands.BuildPost(command, service.ServiceType, 1), + avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -484,14 +453,13 @@ namespace Emby.Dlna.PlayTo return; } - var service = GetAvTransportService(); - + var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); await new DlnaHttpClient(_logger, _httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, - avCommands.BuildPost(command, service.ServiceType, 1), + avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -500,7 +468,7 @@ namespace Emby.Dlna.PlayTo RestartTimer(true); } - private async void TimerCallback(object sender) + private async void TimerCallback(object? sender) { if (_disposed) { @@ -623,7 +591,7 @@ namespace Emby.Dlna.PlayTo Properties.BaseUrl, service, command.Name, - rendererCommands.BuildPost(command, service.ServiceType), + rendererCommands!.BuildPost(command, service.ServiceType), // null checked above cancellationToken: cancellationToken).ConfigureAwait(false); if (result is null || result.Document is null) @@ -673,7 +641,7 @@ namespace Emby.Dlna.PlayTo Properties.BaseUrl, service, command.Name, - rendererCommands.BuildPost(command, service.ServiceType), + rendererCommands!.BuildPost(command, service.ServiceType), // null checked above cancellationToken: cancellationToken).ConfigureAwait(false); if (result is null || result.Document is null) @@ -728,7 +696,7 @@ namespace Emby.Dlna.PlayTo return null; } - private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) + private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); if (command is null) @@ -798,7 +766,7 @@ namespace Emby.Dlna.PlayTo return null; } - private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) + private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); if (command is null) @@ -871,7 +839,7 @@ namespace Emby.Dlna.PlayTo return (true, null); } - XElement uPnpResponse = null; + XElement? uPnpResponse = null; try { @@ -895,7 +863,7 @@ namespace Emby.Dlna.PlayTo return (true, uTrack); } - private XElement ParseResponse(string xml) + private XElement? ParseResponse(string xml) { // Handle different variations sent back by devices. try @@ -929,7 +897,7 @@ namespace Emby.Dlna.PlayTo return null; } - private static UBaseObject CreateUBaseObject(XElement container, string trackUri) + private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri) { ArgumentNullException.ThrowIfNull(container); @@ -972,7 +940,7 @@ namespace Emby.Dlna.PlayTo return new string[4]; } - private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken) + private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken) { if (AvCommands is not null) { @@ -1004,7 +972,7 @@ namespace Emby.Dlna.PlayTo return AvCommands; } - private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken) + private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken) { if (RendererCommands is not null) { @@ -1054,7 +1022,7 @@ namespace Emby.Dlna.PlayTo return baseUrl + url; } - public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) + public static async Task<Device?> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) { var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory); @@ -1287,7 +1255,7 @@ namespace Emby.Dlna.PlayTo } _timer = null; - Properties = null; + Properties = null!; _disposed = true; } diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 86db36337..b1ad15cdc 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -42,7 +42,7 @@ namespace Emby.Dlna.PlayTo private readonly IDeviceDiscovery _deviceDiscovery; private readonly string _serverAddress; - private readonly string _accessToken; + private readonly string? _accessToken; private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); private Device _device; @@ -59,7 +59,7 @@ namespace Emby.Dlna.PlayTo IUserManager userManager, IImageProcessor imageProcessor, string serverAddress, - string accessToken, + string? accessToken, IDeviceDiscovery deviceDiscovery, IUserDataManager userDataManager, ILocalizationManager localization, diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index 241dff5ae..ef617422c 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -67,7 +65,7 @@ namespace Emby.Dlna.PlayTo _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; } - private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) + private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e) { if (_disposed) { @@ -76,12 +74,12 @@ namespace Emby.Dlna.PlayTo var info = e.Argument; - if (!info.Headers.TryGetValue("USN", out string usn)) + if (!info.Headers.TryGetValue("USN", out string? usn)) { usn = string.Empty; } - if (!info.Headers.TryGetValue("NT", out string nt)) + if (!info.Headers.TryGetValue("NT", out string? nt)) { nt = string.Empty; } @@ -161,7 +159,7 @@ namespace Emby.Dlna.PlayTo var uri = info.Location; _logger.LogDebug("Attempting to create PlayToController from location {0}", uri); - if (info.Headers.TryGetValue("USN", out string uuid)) + if (info.Headers.TryGetValue("USN", out string? uuid)) { uuid = GetUuid(uuid); } diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index a4deeddb7..a2f38c8c2 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase /// </summary> public abstract class BaseConfigurationManager : IConfigurationManager { - private readonly IFileSystem _fileSystem; - - private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>(); - - /// <summary> - /// The _configuration sync lock. - /// </summary> - private readonly object _configurationSyncLock = new object(); + private readonly ConcurrentDictionary<string, object> _configurations = new(); + private readonly object _configurationSyncLock = new(); private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); @@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase /// <param name="applicationPaths">The application paths.</param> /// <param name="loggerFactory">The logger factory.</param> /// <param name="xmlSerializer">The XML serializer.</param> - /// <param name="fileSystem">The file system.</param> - protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) + protected BaseConfigurationManager( + IApplicationPaths applicationPaths, + ILoggerFactory loggerFactory, + IXmlSerializer xmlSerializer) { CommonApplicationPaths = applicationPaths; XmlSerializer = xmlSerializer; - _fileSystem = fileSystem; Logger = loggerFactory.CreateLogger<BaseConfigurationManager>(); UpdateCachePath(); @@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase { var file = Path.Combine(path, Guid.NewGuid().ToString()); File.WriteAllText(file, string.Empty); - _fileSystem.DeleteFile(file); + File.Delete(file); } private string GetConfigurationFile(string key) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index dd90a8950..8518b1352 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -12,7 +12,6 @@ using System.Linq; using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; -using System.Threading; using System.Threading.Tasks; using Emby.Dlna; using Emby.Dlna.Main; @@ -102,6 +101,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; @@ -112,7 +112,7 @@ namespace Emby.Server.Implementations /// <summary> /// Class CompositionRoot. /// </summary> - public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable + public abstract class ApplicationHost : IServerApplicationHost, IDisposable { /// <summary> /// The disposable parts. @@ -120,14 +120,12 @@ namespace Emby.Server.Implementations private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new(); private readonly DeviceId _deviceId; - private readonly IFileSystem _fileSystemManager; private readonly IConfiguration _startupConfig; private readonly IXmlSerializer _xmlSerializer; private readonly IStartupOptions _startupOptions; private readonly IPluginManager _pluginManager; private List<Type> _creatingInstances; - private ISessionManager _sessionManager; /// <summary> /// Gets or sets all concrete types. @@ -154,10 +152,8 @@ namespace Emby.Server.Implementations LoggerFactory = loggerFactory; _startupOptions = options; _startupConfig = startupConfig; - _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths); Logger = LoggerFactory.CreateLogger<ApplicationHost>(); - _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager)); _deviceId = new DeviceId(ApplicationPaths, LoggerFactory); ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; @@ -165,13 +161,15 @@ namespace Emby.Server.Implementations ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; _xmlSerializer = new MyXmlSerializer(); - ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); + ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer); _pluginManager = new PluginManager( LoggerFactory.CreateLogger<PluginManager>(), this, ConfigurationManager.Configuration, ApplicationPaths.PluginsPath, ApplicationVersion); + + _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue); } /// <summary> @@ -204,6 +202,9 @@ namespace Emby.Server.Implementations /// <inheritdoc /> public bool IsShuttingDown { get; private set; } + /// <inheritdoc /> + public bool ShouldRestart { get; private set; } + /// <summary> /// Gets the logger. /// </summary> @@ -406,11 +407,9 @@ namespace Emby.Server.Implementations /// <summary> /// Runs the startup tasks. /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> /// <returns><see cref="Task" />.</returns> - public async Task RunStartupTasksAsync(CancellationToken cancellationToken) + public async Task RunStartupTasksAsync() { - cancellationToken.ThrowIfCancellationRequested(); Logger.LogInformation("Running startup tasks"); Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); @@ -424,8 +423,6 @@ namespace Emby.Server.Implementations var entryPoints = GetExports<IServerEntryPoint>(); - cancellationToken.ThrowIfCancellationRequested(); - var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -435,8 +432,6 @@ namespace Emby.Server.Implementations Logger.LogInformation("Core startup complete"); CoreStartupHasCompleted = true; - cancellationToken.ThrowIfCancellationRequested(); - stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); @@ -509,7 +504,9 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(_pluginManager); serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); - serviceCollection.AddSingleton(_fileSystemManager); + serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>(); + serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>(); + serviceCollection.AddSingleton<TmdbClientManager>(); serviceCollection.AddSingleton(NetManager); @@ -633,8 +630,6 @@ namespace Emby.Server.Implementations var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); await localizationManager.LoadAll().ConfigureAwait(false); - _sessionManager = Resolve<ISessionManager>(); - SetStaticProperties(); FindParts(); @@ -685,7 +680,7 @@ namespace Emby.Server.Implementations BaseItem.ProviderManager = Resolve<IProviderManager>(); BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); BaseItem.ItemRepository = Resolve<IItemRepository>(); - BaseItem.FileSystem = _fileSystemManager; + BaseItem.FileSystem = Resolve<IFileSystem>(); BaseItem.UserDataManager = Resolve<IUserDataManager>(); BaseItem.ChannelManager = Resolve<IChannelManager>(); Video.LiveTvManager = Resolve<ILiveTvManager>(); @@ -855,38 +850,24 @@ namespace Emby.Server.Implementations } } - /// <summary> - /// Restarts this instance. - /// </summary> + /// <inheritdoc /> public void Restart() { - if (IsShuttingDown) - { - return; - } - - IsShuttingDown = true; - _pluginManager.UnloadAssemblies(); + ShouldRestart = true; + Shutdown(); + } + /// <inheritdoc /> + public void Shutdown() + { Task.Run(async () => { - try - { - await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error sending server restart notification"); - } - - Logger.LogInformation("Calling RestartInternal"); - - RestartInternal(); + await Task.Delay(100).ConfigureAwait(false); + IsShuttingDown = true; + Resolve<IHostApplicationLifetime>().StopApplication(); }); } - protected abstract void RestartInternal(); - /// <summary> /// Gets the composable part assemblies. /// </summary> @@ -1006,7 +987,7 @@ namespace Emby.Server.Implementations if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) { int? requestPort = request.Host.Port; - if (requestPort == null + if (requestPort is null || (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) { @@ -1065,30 +1046,6 @@ namespace Emby.Server.Implementations }.ToString().TrimEnd('/'); } - /// <inheritdoc /> - public async Task Shutdown() - { - if (IsShuttingDown) - { - return; - } - - IsShuttingDown = true; - - try - { - await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error sending server shutdown notification"); - } - - ShutdownInternal(); - } - - protected abstract void ShutdownInternal(); - public IEnumerable<Assembly> GetApiPluginAssemblies() { var assemblies = _allConcreteTypes @@ -1152,52 +1109,5 @@ namespace Emby.Server.Implementations _disposed = true; } - - public async ValueTask DisposeAsync() - { - await DisposeAsyncCore().ConfigureAwait(false); - Dispose(false); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>. - /// </summary> - /// <returns>A ValueTask.</returns> - protected virtual async ValueTask DisposeAsyncCore() - { - var type = GetType(); - - Logger.LogInformation("Disposing {Type}", type.Name); - - foreach (var (part, _) in _disposableParts) - { - var partType = part.GetType(); - if (partType == type) - { - continue; - } - - Logger.LogInformation("Disposing {Type}", partType.Name); - - try - { - part.Dispose(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error disposing {Type}", partType.Name); - } - } - - if (_sessionManager != null) - { - // used for closing websockets - foreach (var session in _sessionManager.Sessions) - { - await session.DisposeAsync().ConfigureAwait(false); - } - } - } } } diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index 6b8b1a620..0ee43ce0a 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; @@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration /// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class. /// </summary> /// <param name="applicationPaths">The application paths.</param> - /// <param name="loggerFactory">The paramref name="loggerFactory" factory.</param> + /// <param name="loggerFactory">The logger factory.</param> /// <param name="xmlSerializer">The XML serializer.</param> - /// <param name="fileSystem">The file system.</param> - public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) - : base(applicationPaths, loggerFactory, xmlSerializer, fileSystem) + public ServerConfigurationManager( + IApplicationPaths applicationPaths, + ILoggerFactory loggerFactory, + IXmlSerializer xmlSerializer) + : base(applicationPaths, loggerFactory, xmlSerializer) { UpdateMetadataPath(); } diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index d05534ee7..bf079d90c 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -5,8 +5,8 @@ using System; using System.Collections.Generic; using Jellyfin.Extensions; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { @@ -46,24 +46,6 @@ namespace Emby.Server.Implementations.Data protected ILogger<BaseSqliteRepository> Logger { get; } /// <summary> - /// Gets the default connection flags. - /// </summary> - /// <value>The default connection flags.</value> - protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex; - - /// <summary> - /// Gets the transaction mode. - /// </summary> - /// <value>The transaction mode.</value>> - protected TransactionMode TransactionMode => TransactionMode.Deferred; - - /// <summary> - /// Gets the transaction mode for read-only operations. - /// </summary> - /// <value>The transaction mode.</value> - protected TransactionMode ReadTransactionMode => TransactionMode.Deferred; - - /// <summary> /// Gets the cache size. /// </summary> /// <value>The cache size or null.</value> @@ -107,23 +89,8 @@ namespace Emby.Server.Implementations.Data /// <see cref="SynchronousMode"/> protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal; - /// <summary> - /// Gets or sets the write lock. - /// </summary> - /// <value>The write lock.</value> - protected ConnectionPool WriteConnections { get; set; } - - /// <summary> - /// Gets or sets the write connection. - /// </summary> - /// <value>The write connection.</value> - protected ConnectionPool ReadConnections { get; set; } - public virtual void Initialize() { - WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection); - ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection); - // Configuration and pragmas can affect VACUUM so it needs to be last. using (var connection = GetConnection()) { @@ -131,57 +98,10 @@ namespace Emby.Server.Implementations.Data } } - protected ManagedConnection GetConnection(bool readOnly = false) - => readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection(); - - protected SQLiteDatabaseConnection CreateWriteConnection() - { - var writeConnection = SQLite3.Open( - DbFilePath, - DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite, - null); - - if (CacheSize.HasValue) - { - writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); - } - - if (!string.IsNullOrWhiteSpace(LockingMode)) - { - writeConnection.Execute("PRAGMA locking_mode=" + LockingMode); - } - - if (!string.IsNullOrWhiteSpace(JournalMode)) - { - writeConnection.Execute("PRAGMA journal_mode=" + JournalMode); - } - - if (JournalSizeLimit.HasValue) - { - writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); - } - - if (Synchronous.HasValue) - { - writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); - } - - if (PageSize.HasValue) - { - writeConnection.Execute("PRAGMA page_size=" + PageSize.Value); - } - - writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore); - - return writeConnection; - } - - protected SQLiteDatabaseConnection CreateReadConnection() + protected SqliteConnection GetConnection() { - var connection = SQLite3.Open( - DbFilePath, - DefaultConnectionFlags | ConnectionFlags.ReadOnly, - null); + var connection = new SqliteConnection($"Filename={DbFilePath}"); + connection.Open(); if (CacheSize.HasValue) { @@ -208,39 +128,38 @@ namespace Emby.Server.Implementations.Data connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); } + if (PageSize.HasValue) + { + connection.Execute("PRAGMA page_size=" + PageSize.Value); + } + connection.Execute("PRAGMA temp_store=" + (int)TempStore); return connection; } - public IStatement PrepareStatement(ManagedConnection connection, string sql) - => connection.PrepareStatement(sql); - - public IStatement PrepareStatement(IDatabaseConnection connection, string sql) - => connection.PrepareStatement(sql); + public SqliteCommand PrepareStatement(SqliteConnection connection, string sql) + { + var command = connection.CreateCommand(); + command.CommandText = sql; + return command; + } - protected bool TableExists(ManagedConnection connection, string name) + protected bool TableExists(SqliteConnection connection, string name) { - return connection.RunInTransaction( - db => + using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master"); + foreach (var row in statement.ExecuteQuery()) + { + if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase)) { - using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master")) - { - foreach (var row in statement.ExecuteQuery()) - { - if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - - return false; - }, - ReadTransactionMode); + return true; + } + } + + return false; } - protected List<string> GetColumnNames(IDatabaseConnection connection, string table) + protected List<string> GetColumnNames(SqliteConnection connection, string table) { var columnNames = new List<string>(); @@ -255,7 +174,7 @@ namespace Emby.Server.Implementations.Data return columnNames; } - protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames) + protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames) { if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase)) { @@ -291,12 +210,6 @@ namespace Emby.Server.Implementations.Data return; } - if (dispose) - { - WriteConnections.Dispose(); - ReadConnections.Dispose(); - } - _disposed = true; } } diff --git a/Emby.Server.Implementations/Data/ConnectionPool.cs b/Emby.Server.Implementations/Data/ConnectionPool.cs deleted file mode 100644 index 5ea7e934f..000000000 --- a/Emby.Server.Implementations/Data/ConnectionPool.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Concurrent; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data; - -/// <summary> -/// A pool of SQLite Database connections. -/// </summary> -public sealed class ConnectionPool : IDisposable -{ - private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new(); - private bool _disposed; - - /// <summary> - /// Initializes a new instance of the <see cref="ConnectionPool" /> class. - /// </summary> - /// <param name="count">The number of database connection to create.</param> - /// <param name="factory">Factory function to create the database connections.</param> - public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory) - { - for (int i = 0; i < count; i++) - { - _connections.Add(factory.Invoke()); - } - } - - /// <summary> - /// Gets a database connection from the pool if one is available, otherwise blocks. - /// </summary> - /// <returns>A database connection.</returns> - public ManagedConnection GetConnection() - { - if (_disposed) - { - ThrowObjectDisposedException(); - } - - return new ManagedConnection(_connections.Take(), this); - - static void ThrowObjectDisposedException() - { - throw new ObjectDisposedException(nameof(ConnectionPool)); - } - } - - /// <summary> - /// Return a database connection to the pool. - /// </summary> - /// <param name="connection">The database connection to return.</param> - public void Return(SQLiteDatabaseConnection connection) - { - if (_disposed) - { - connection.Dispose(); - return; - } - - _connections.Add(connection); - } - - /// <inheritdoc /> - public void Dispose() - { - if (_disposed) - { - return; - } - - foreach (var connection in _connections) - { - connection.Dispose(); - } - - _connections.Dispose(); - - _disposed = true; - } -} diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs deleted file mode 100644 index e84ed8f91..000000000 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ /dev/null @@ -1,81 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Data -{ - public sealed class ManagedConnection : IDisposable - { - private readonly ConnectionPool _pool; - - private SQLiteDatabaseConnection _db; - - private bool _disposed = false; - - public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool) - { - _db = db; - _pool = pool; - } - - public IStatement PrepareStatement(string sql) - { - return _db.PrepareStatement(sql); - } - - public IEnumerable<IStatement> PrepareAll(string sql) - { - return _db.PrepareAll(sql); - } - - public void ExecuteAll(string sql) - { - _db.ExecuteAll(sql); - } - - public void Execute(string sql, params object[] values) - { - _db.Execute(sql, values); - } - - public void RunQueries(string[] sql) - { - _db.RunQueries(sql); - } - - public void RunInTransaction(Action<IDatabaseConnection> action, TransactionMode mode) - { - _db.RunInTransaction(action, mode); - } - - public T RunInTransaction<T>(Func<IDatabaseConnection, T> action, TransactionMode mode) - { - return _db.RunInTransaction(action, mode); - } - - public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql) - { - return _db.Query(sql); - } - - public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values) - { - return _db.Query(sql, values); - } - - public void Dispose() - { - if (_disposed) - { - return; - } - - _pool.Return(_db); - - _db = null!; // Don't dispose it - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 4055b0ba1..01b5fdaee 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -1,11 +1,10 @@ -#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Data; using System.Globalization; -using SQLitePCL.pretty; +using Microsoft.Data.Sqlite; namespace Emby.Server.Implementations.Data { @@ -52,19 +51,29 @@ namespace Emby.Server.Implementations.Data "yy-MM-dd" }; - public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries) + public static IEnumerable<SqliteDataReader> Query(this SqliteConnection sqliteConnection, string commandText) { - ArgumentNullException.ThrowIfNull(queries); + if (sqliteConnection.State != ConnectionState.Open) + { + sqliteConnection.Open(); + } - connection.RunInTransaction(conn => + using var command = sqliteConnection.CreateCommand(); + command.CommandText = commandText; + using (var reader = command.ExecuteReader()) { - conn.ExecuteAll(string.Join(';', queries)); - }); + while (reader.Read()) + { + yield return reader; + } + } } - public static Guid ReadGuidFromBlob(this ResultSetValue result) + public static void Execute(this SqliteConnection sqliteConnection, string commandText) { - return new Guid(result.ToBlob()); + using var command = sqliteConnection.CreateCommand(); + command.CommandText = commandText; + command.ExecuteNonQuery(); } public static string ToDateTimeParamValue(this DateTime dateValue) @@ -83,27 +92,15 @@ namespace Emby.Server.Implementations.Data private static string GetDateTimeKindFormat(DateTimeKind kind) => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal; - public static DateTime ReadDateTime(this ResultSetValue result) - { - var dateText = result.ToString(); - - return DateTime.ParseExact( - dateText, - _datetimeFormats, - DateTimeFormatInfo.InvariantInfo, - DateTimeStyles.AdjustToUniversal); - } - - public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result) + public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - var dateText = item.ToString(); + var dateText = reader.GetString(index); if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult)) { @@ -115,335 +112,145 @@ namespace Emby.Server.Implementations.Data return false; } - public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result) + public static bool TryGetGuid(this SqliteDataReader reader, int index, out Guid result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ReadGuidFromBlob(); + result = reader.GetGuid(index); return true; } - public static bool IsDbNull(this ResultSetValue result) + public static bool TryGetString(this SqliteDataReader reader, int index, out string result) { - return result.SQLiteType == SQLiteType.Null; - } - - public static string GetString(this IReadOnlyList<ResultSetValue> result, int index) - { - return result[index].ToString(); - } + result = string.Empty; - public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result) - { - result = null; - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { return false; } - result = item.ToString(); + result = reader.GetString(index); return true; } - public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index) - { - return result[index].ToBool(); - } - - public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result) + public static bool TryGetBoolean(this SqliteDataReader reader, int index, out bool result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ToBool(); + result = reader.GetBoolean(index); return true; } - public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result) + public static bool TryGetInt32(this SqliteDataReader reader, int index, out int result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ToInt(); + result = reader.GetInt32(index); return true; } - public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index) + public static bool TryGetInt64(this SqliteDataReader reader, int index, out long result) { - return result[index].ToInt64(); - } - - public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result) - { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ToInt64(); + result = reader.GetInt64(index); return true; } - public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result) + public static bool TryGetSingle(this SqliteDataReader reader, int index, out float result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ToFloat(); + result = reader.GetFloat(index); return true; } - public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result) + public static bool TryGetDouble(this SqliteDataReader reader, int index, out double result) { - var item = reader[index]; - if (item.IsDbNull()) + if (reader.IsDBNull(index)) { result = default; return false; } - result = item.ToDouble(); + result = reader.GetDouble(index); return true; } - public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index) + public static void TryBind(this SqliteCommand statement, string name, Guid value) { - return result[index].ReadGuidFromBlob(); + statement.TryBind(name, value, true); } - [Conditional("DEBUG")] - private static void CheckName(string name) + public static void TryBind(this SqliteCommand statement, string name, object? value, bool isBlob = false) { - throw new ArgumentException("Invalid param name: " + name, nameof(name)); - } - - public static void TryBind(this IStatement statement, string name, double value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) + var preparedValue = value ?? DBNull.Value; + if (statement.Parameters.Contains(name)) { - bindParam.Bind(value); + statement.Parameters[name].Value = preparedValue; } else { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, string value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - if (value is null) + // Blobs aren't always detected automatically + if (isBlob) { - bindParam.BindNull(); + statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value }); } else { - bindParam.Bind(value); + statement.Parameters.AddWithValue(name, preparedValue); } } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, bool value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, float value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, int value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, Guid value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - Span<byte> byteValue = stackalloc byte[16]; - value.TryWriteBytes(byteValue); - bindParam.Bind(byteValue); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, DateTime value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value.ToDateTimeParamValue()); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, long value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.Bind(value); - } - else - { - CheckName(name); - } - } - - public static void TryBindNull(this IStatement statement, string name) - { - if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) - { - bindParam.BindNull(); - } - else - { - CheckName(name); - } - } - - public static void TryBind(this IStatement statement, string name, DateTime? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, Guid? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } - } - - public static void TryBind(this IStatement statement, string name, double? value) - { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } } - public static void TryBind(this IStatement statement, string name, int? value) + public static void TryBindNull(this SqliteCommand statement, string name) { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } + statement.TryBind(name, DBNull.Value); } - public static void TryBind(this IStatement statement, string name, float? value) + public static IEnumerable<SqliteDataReader> ExecuteQuery(this SqliteCommand command) { - if (value.HasValue) + using (var reader = command.ExecuteReader()) { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); + while (reader.Read()) + { + yield return reader; + } } } - public static void TryBind(this IStatement statement, string name, bool? value) + public static int SelectScalarInt(this SqliteCommand command) { - if (value.HasValue) - { - TryBind(statement, name, value.Value); - } - else - { - TryBindNull(statement, name); - } + var result = command.ExecuteScalar(); + // Can't be null since the method is used to retrieve Count + return Convert.ToInt32(result!, CultureInfo.InvariantCulture); } - public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement) + public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql) { - while (statement.MoveNext()) - { - yield return statement.Current; - } + var command = sqliteConnection.CreateCommand(); + command.CommandText = sql; + return command; } } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 73ec856fc..77cf4089b 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3,7 +3,6 @@ #pragma warning disable CS1591 using System; -using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -26,7 +25,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; @@ -35,9 +33,9 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { @@ -437,128 +435,126 @@ namespace Emby.Server.Implementations.Data }; using (var connection = GetConnection()) - { - connection.RunQueries(queries); - - connection.RunInTransaction( - db => - { - var existingColumnNames = GetColumnNames(db, "AncestorIds"); - AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(db, "TypedBaseItems"); - - AddColumn(db, "TypedBaseItems", "Path", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); - AddColumn(db, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Name", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "MediaType", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Overview", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Genres", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SortName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Studios", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Audio", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Tags", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); - AddColumn(db, "TypedBaseItems", "CleanName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames); - AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Tagline", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Images", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Artists", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "ShowId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Width", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Height", "INT", existingColumnNames); - AddColumn(db, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); - - existingColumnNames = GetColumnNames(db, "ItemValues"); - AddColumn(db, "ItemValues", "CleanValue", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(db, ChaptersTableName); - AddColumn(db, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); - - existingColumnNames = GetColumnNames(db, "MediaStreams"); - AddColumn(db, "MediaStreams", "IsAvc", "BIT", existingColumnNames); - AddColumn(db, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "Title", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "Comment", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "BitDepth", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "RefFrames", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); - - AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); - AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); - - AddColumn(db, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "DvProfile", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "DvLevel", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); - AddColumn(db, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); - - AddColumn(db, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); - }, - TransactionMode); - - connection.RunQueries(postQueries); + using (var transaction = connection.BeginTransaction()) + { + connection.Execute(string.Join(';', queries)); + + var existingColumnNames = GetColumnNames(connection, "AncestorIds"); + AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); + + existingColumnNames = GetColumnNames(connection, "TypedBaseItems"); + + AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); + + existingColumnNames = GetColumnNames(connection, "ItemValues"); + AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames); + + existingColumnNames = GetColumnNames(connection, ChaptersTableName); + AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); + + existingColumnNames = GetColumnNames(connection, "MediaStreams"); + AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames); + AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); + + AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); + + AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); + + AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); + + connection.Execute(string.Join(';', postQueries)); + + transaction.Commit(); } } @@ -568,21 +564,15 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) - { - saveImagesStatement.TryBind("@Id", item.Id); - saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); + var images = SerializeImages(item.ImageInfos); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id"); + saveImagesStatement.TryBind("@Id", item.Id); + saveImagesStatement.TryBind("@Images", images); - saveImagesStatement.MoveNext(); - } - }, - TransactionMode); - } + saveImagesStatement.ExecuteNonQuery(); + transaction.Commit(); } /// <summary> @@ -618,18 +608,13 @@ namespace Emby.Server.Implementations.Data tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); } - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - SaveItemsInTransaction(db, tuples); - }, - TransactionMode); - } + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + SaveItemsInTransaction(connection, tuples); + transaction.Commit(); } - private void SaveItemsInTransaction(IDatabaseConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples) + private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples) { using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) @@ -639,7 +624,8 @@ namespace Emby.Server.Implementations.Data { if (requiresReset) { - saveItemStatement.Reset(); + saveItemStatement.Parameters.Clear(); + deleteAncestorsStatement.Parameters.Clear(); } var item = tuple.Item; @@ -677,7 +663,7 @@ namespace Emby.Server.Implementations.Data return _appHost.ExpandVirtualPath(path); } - private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, IStatement saveItemStatement) + private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement) { Type type = item.GetType(); @@ -686,7 +672,7 @@ namespace Emby.Server.Implementations.Data if (TypeRequiresDeserialization(type)) { - saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions)); + saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true); } else { @@ -1033,7 +1019,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@OwnerId", ownerId); } - saveItemStatement.MoveNext(); + saveItemStatement.ExecuteNonQuery(); } internal static string SerializeProviderIds(Dictionary<string, string> providerIds) @@ -1287,7 +1273,7 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) { statement.TryBind("@guid", id); @@ -1305,96 +1291,35 @@ namespace Emby.Server.Implementations.Data { if (_config.Configuration.SkipDeserializationForBasicTypes) { - if (type == typeof(Channel)) - { - return false; - } - - if (type == typeof(UserRootFolder)) + if (type == typeof(Channel) + || type == typeof(UserRootFolder)) { return false; } } - if (type == typeof(Season)) - { - return false; - } - - if (type == typeof(MusicArtist)) - { - return false; - } - - if (type == typeof(Person)) - { - return false; - } - - if (type == typeof(MusicGenre)) - { - return false; - } - - if (type == typeof(Genre)) - { - return false; - } - - if (type == typeof(Studio)) - { - return false; - } - - if (type == typeof(PlaylistsFolder)) - { - return false; - } - - if (type == typeof(PhotoAlbum)) - { - return false; - } - - if (type == typeof(Year)) - { - return false; - } - - if (type == typeof(Book)) - { - return false; - } - - if (type == typeof(LiveTvProgram)) - { - return false; - } - - if (type == typeof(AudioBook)) - { - return false; - } - - if (type == typeof(Audio)) - { - return false; - } - - if (type == typeof(MusicAlbum)) - { - return false; - } - - return true; + return type != typeof(Season) + && type != typeof(MusicArtist) + && type != typeof(Person) + && type != typeof(MusicGenre) + && type != typeof(Genre) + && type != typeof(Studio) + && type != typeof(PlaylistsFolder) + && type != typeof(PhotoAlbum) + && type != typeof(Year) + && type != typeof(Book) + && type != typeof(LiveTvProgram) + && type != typeof(AudioBook) + && type != typeof(Audio) + && type != typeof(MusicAlbum); } - private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query) + private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query) { return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query)); } - private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) + private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) { var typeString = reader.GetString(0); @@ -1411,7 +1336,7 @@ namespace Emby.Server.Implementations.Data { try { - item = JsonSerializer.Deserialize(reader[1].ToBlob(), type, _jsonOptions) as BaseItem; + item = JsonSerializer.Deserialize(reader.GetStream(1), type, _jsonOptions) as BaseItem; } catch (JsonException ex) { @@ -1452,17 +1377,9 @@ namespace Emby.Server.Implementations.Data item.EndDate = endDate; } - var channelId = reader[index]; - if (!channelId.IsDbNull()) + if (reader.TryGetGuid(index, out var guid)) { - if (!Utf8Parser.TryParse(channelId.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; + item.ChannelId = guid; } index++; @@ -1978,7 +1895,7 @@ namespace Emby.Server.Implementations.Data CheckDisposed(); var chapters = new List<ChapterInfo>(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) { statement.TryBind("@ItemId", item.Id); @@ -1997,7 +1914,7 @@ namespace Emby.Server.Implementations.Data { CheckDisposed(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) { statement.TryBind("@ItemId", item.Id); @@ -2018,7 +1935,7 @@ namespace Emby.Server.Implementations.Data /// <param name="reader">The reader.</param> /// <param name="item">The item.</param> /// <returns>ChapterInfo.</returns> - private ChapterInfo GetChapter(IReadOnlyList<ResultSetValue> reader, BaseItem item) + private ChapterInfo GetChapter(SqliteDataReader reader, BaseItem item) { var chapter = new ChapterInfo { @@ -2033,18 +1950,7 @@ namespace Emby.Server.Implementations.Data if (reader.TryGetString(2, out var imagePath)) { chapter.ImagePath = imagePath; - - if (!string.IsNullOrEmpty(chapter.ImagePath)) - { - try - { - chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter); - } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to create image cache tag."); - } - } + chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter); } if (reader.TryReadDateTime(3, out var imageDateModified)) @@ -2071,23 +1977,18 @@ namespace Emby.Server.Implementations.Data ArgumentNullException.ThrowIfNull(chapters); - var idBlob = id.ToByteArray(); - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - // First delete chapters - db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + // First delete chapters + using var command = connection.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId"); + command.TryBind("@ItemId", id); + command.ExecuteNonQuery(); - InsertChapters(idBlob, chapters, db); - }, - TransactionMode); - } + InsertChapters(id, chapters, connection); + transaction.Commit(); } - private void InsertChapters(byte[] idBlob, IReadOnlyList<ChapterInfo> chapters, IDatabaseConnection db) + private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db) { var startIndex = 0; var limit = 100; @@ -2105,7 +2006,7 @@ namespace Emby.Server.Implementations.Data insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture)); } - insertText.Length -= 1; // Remove last , + insertText.Length -= 1; // Remove trailing comma using (var statement = PrepareStatement(db, insertText.ToString())) { @@ -2126,8 +2027,7 @@ namespace Emby.Server.Implementations.Data chapterIndex++; } - statement.Reset(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } startIndex += limit; @@ -2463,7 +2363,7 @@ namespace Emby.Server.Implementations.Data } } - private void BindSearchParams(InternalItemsQuery query, IStatement statement) + private void BindSearchParams(InternalItemsQuery query, SqliteCommand statement) { var searchTerm = query.SearchTerm; @@ -2475,7 +2375,7 @@ namespace Emby.Server.Implementations.Data searchTerm = FixUnicodeChars(searchTerm); searchTerm = GetCleanValue(searchTerm); - var commandText = statement.SQL; + var commandText = statement.CommandText; if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase)) { statement.TryBind("@SearchTermStartsWith", searchTerm + "%"); @@ -2492,7 +2392,7 @@ namespace Emby.Server.Implementations.Data } } - private void BindSimilarParams(InternalItemsQuery query, IStatement statement) + private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement) { var item = query.SimilarTo; @@ -2501,7 +2401,7 @@ namespace Emby.Server.Implementations.Data return; } - var commandText = statement.SQL; + var commandText = statement.CommandText; if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase)) { @@ -2584,7 +2484,7 @@ namespace Emby.Server.Implementations.Data var commandText = commandTextBuilder.ToString(); using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText)) { if (EnableJoinUserData(query)) @@ -2598,7 +2498,7 @@ namespace Emby.Server.Implementations.Data // Running this again will bind the params GetWhereClauses(query, statement); - return statement.ExecuteQuery().SelectScalarInt().First(); + return statement.SelectScalarInt(); } } @@ -2652,7 +2552,7 @@ namespace Emby.Server.Implementations.Data var commandText = commandTextBuilder.ToString(); var items = new List<BaseItem>(); using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText)) { if (EnableJoinUserData(query)) @@ -2860,69 +2760,65 @@ namespace Emby.Server.Implementations.Data var list = new List<BaseItem>(); var result = new QueryResult<BaseItem>(); - using (var connection = GetConnection(true)) + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + if (!isReturningZeroItems) { - connection.RunInTransaction( - db => + using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery")) + using (var statement = PrepareStatement(connection, itemQuery)) + { + if (EnableJoinUserData(query)) { - if (!isReturningZeroItems) + statement.TryBind("@UserId", query.User.InternalId); + } + + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + + // Running this again will bind the params + GetWhereClauses(query, statement); + + var hasEpisodeAttributes = HasEpisodeAttributes(query); + var hasServiceName = HasServiceName(query); + var hasProgramAttributes = HasProgramAttributes(query); + var hasStartDate = HasStartDate(query); + var hasTrailerTypes = HasTrailerTypes(query); + var hasArtistFields = HasArtistFields(query); + var hasSeriesFields = HasSeriesFields(query); + + foreach (var row in statement.ExecuteQuery()) + { + var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); + if (item is not null) { - using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery")) - using (var statement = PrepareStatement(db, itemQuery)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasServiceName = HasServiceName(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); - - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); - if (item is not null) - { - list.Add(item); - } - } - } + list.Add(item); } + } + } + } - if (query.EnableTotalRecordCount) - { - using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount")) - using (var statement = PrepareStatement(db, totalRecordCountQuery)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } + if (query.EnableTotalRecordCount) + { + using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount")) + using (var statement = PrepareStatement(connection, totalRecordCountQuery)) + { + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.InternalId); + } - BindSimilarParams(query, statement); - BindSearchParams(query, statement); + BindSimilarParams(query, statement); + BindSearchParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); - } - } - }, - ReadTransactionMode); + result.TotalRecordCount = statement.SelectScalarInt(); + } } + transaction.Commit(); + result.StartIndex = query.StartIndex ?? 0; result.Items = list; return result; @@ -3172,7 +3068,7 @@ namespace Emby.Server.Implementations.Data var commandText = commandTextBuilder.ToString(); var list = new List<Guid>(); using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText)) { if (EnableJoinUserData(query)) @@ -3188,7 +3084,7 @@ namespace Emby.Server.Implementations.Data foreach (var row in statement.ExecuteQuery()) { - list.Add(row[0].ReadGuidFromBlob()); + list.Add(row.GetGuid(0)); } } @@ -3224,7 +3120,7 @@ namespace Emby.Server.Implementations.Data } #nullable enable - private List<string> GetWhereClauses(InternalItemsQuery query, IStatement? statement) + private List<string> GetWhereClauses(InternalItemsQuery query, SqliteCommand? statement) { if (query.IsResumable ?? false) { @@ -3604,7 +3500,6 @@ namespace Emby.Server.Implementations.Data statement?.TryBind(paramName, "%" + trailerTypes[i] + "%"); } - // Remove last " OR " clauseBuilder.Length -= Or.Length; clauseBuilder.Append(')'); @@ -3647,12 +3542,10 @@ namespace Emby.Server.Implementations.Data if (statement is not null) { - query.PersonIds[i].TryWriteBytes(idBytes); - statement.TryBind(paramName, idBytes); + statement.TryBind(paramName, query.PersonIds[i]); } } - // Remove last " OR " clauseBuilder.Length -= Or.Length; clauseBuilder.Append(')'); @@ -3819,215 +3712,219 @@ namespace Emby.Server.Implementations.Data if (query.ArtistIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var artistId in query.ArtistIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.ArtistIds.Length; i++) { - var paramName = "@ArtistIds" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); - statement?.TryBind(paramName, artistId); - index++; + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") and Type<=1)) OR "); + statement?.TryBind("@ArtistIds" + i, query.ArtistIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.AlbumArtistIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var artistId in query.AlbumArtistIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.AlbumArtistIds.Length; i++) { - var paramName = "@ArtistIds" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))"); - statement?.TryBind(paramName, artistId); - index++; + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") and Type=1)) OR "); + statement?.TryBind("@ArtistIds" + i, query.AlbumArtistIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.ContributingArtistIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var artistId in query.ContributingArtistIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.ContributingArtistIds.Length; i++) { - var paramName = "@ArtistIds" + index; - clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))"); - statement?.TryBind(paramName, artistId); - index++; + clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR "); + statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.AlbumIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var albumId in query.AlbumIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.AlbumIds.Length; i++) { - var paramName = "@AlbumIds" + index; - clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")"); - statement?.TryBind(paramName, albumId); - index++; + clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds") + .Append(i) + .Append(") OR "); + statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.ExcludeArtistIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var artistId in query.ExcludeArtistIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.ExcludeArtistIds.Length; i++) { - var paramName = "@ExcludeArtistId" + index; - clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); - statement?.TryBind(paramName, artistId); - index++; + clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId") + .Append(i) + .Append(") and Type<=1)) OR "); + statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.GenreIds.Count > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var genreId in query.GenreIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.GenreIds.Count; i++) { - var paramName = "@GenreId" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))"); - statement?.TryBind(paramName, genreId); - index++; + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId") + .Append(i) + .Append(") and Type=2)) OR "); + statement?.TryBind("@GenreId" + i, query.GenreIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.Genres.Count > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var item in query.Genres) + clauseBuilder.Append('('); + for (var i = 0; i < query.Genres.Count; i++) { - clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)"); - statement?.TryBind("@Genre" + index, GetCleanValue(item)); - index++; + clauseBuilder.Append("@Genre") + .Append(i) + .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR "); + statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i])); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (tags.Count > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var item in tags) + clauseBuilder.Append('('); + for (var i = 0; i < tags.Count; i++) { - clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)"); - statement?.TryBind("@Tag" + index, GetCleanValue(item)); - index++; + clauseBuilder.Append("@Tag") + .Append(i) + .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); + statement?.TryBind("@Tag" + i, GetCleanValue(tags[i])); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (excludeTags.Count > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var item in excludeTags) + clauseBuilder.Append('('); + for (var i = 0; i < excludeTags.Count; i++) { - clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)"); - statement?.TryBind("@ExcludeTag" + index, GetCleanValue(item)); - index++; + clauseBuilder.Append("@ExcludeTag") + .Append(i) + .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); + statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i])); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.StudioIds.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var studioId in query.StudioIds) + clauseBuilder.Append('('); + for (var i = 0; i < query.StudioIds.Length; i++) { - var paramName = "@StudioId" + index; - clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))"); - statement?.TryBind(paramName, studioId); - index++; + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId") + .Append(i) + .Append(") and Type=3)) OR "); + statement?.TryBind("@StudioId" + i, query.StudioIds[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.OfficialRatings.Length > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var item in query.OfficialRatings) + clauseBuilder.Append('('); + for (var i = 0; i < query.OfficialRatings.Length; i++) { - clauses.Add("OfficialRating=@OfficialRating" + index); - statement?.TryBind("@OfficialRating" + index, item); - index++; + clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or); + statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } - var ratingClauseBuilder = new StringBuilder("("); + clauseBuilder.Append('('); if (query.HasParentalRating ?? false) { - ratingClauseBuilder.Append("InheritedParentalRatingValue not null"); + clauseBuilder.Append("InheritedParentalRatingValue not null"); if (query.MinParentalRating.HasValue) { - ratingClauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating"); + clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating"); statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); } if (query.MaxParentalRating.HasValue) { - ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); + clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } } else if (query.BlockUnratedItems.Length > 0) { - var paramName = "@UnratedType"; - var index = 0; - string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++)); - ratingClauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))"); + const string ParamName = "@UnratedType"; + clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in ("); - if (statement is not null) + for (int i = 0; i < query.BlockUnratedItems.Length; i++) { - for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++) - { - statement.TryBind(paramName + ind, query.BlockUnratedItems[ind].ToString()); - } + clauseBuilder.Append(ParamName).Append(i).Append(','); + statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString()); } + // Remove trailing comma + clauseBuilder.Length--; + clauseBuilder.Append("))"); + if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) { - ratingClauseBuilder.Append(" OR ("); + clauseBuilder.Append(" OR ("); } if (query.MinParentalRating.HasValue) { - ratingClauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating"); + clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating"); statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); } @@ -4035,50 +3932,50 @@ namespace Emby.Server.Implementations.Data { if (query.MinParentalRating.HasValue) { - ratingClauseBuilder.Append(" AND "); + clauseBuilder.Append(" AND "); } - ratingClauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating"); + clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) { - ratingClauseBuilder.Append(")"); + clauseBuilder.Append(')'); } if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)) { - ratingClauseBuilder.Append(" OR InheritedParentalRatingValue not null"); + clauseBuilder.Append(" OR InheritedParentalRatingValue not null"); } } else if (query.MinParentalRating.HasValue) { - ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"); + clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"); statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); if (query.MaxParentalRating.HasValue) { - ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); + clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } - ratingClauseBuilder.Append(")"); + clauseBuilder.Append(')'); } else if (query.MaxParentalRating.HasValue) { - ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"); + clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"); statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); } else if (!query.HasParentalRating ?? false) { - ratingClauseBuilder.Append("InheritedParentalRatingValue is null"); + clauseBuilder.Append("InheritedParentalRatingValue is null"); } - var ratingClauseString = ratingClauseBuilder.ToString(); - if (!string.Equals(ratingClauseString, "(", StringComparison.OrdinalIgnoreCase)) + if (clauseBuilder.Length > 1) { - whereClauses.Add(ratingClauseString + ")"); + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; } if (query.HasOfficialRating.HasValue) @@ -4565,7 +4462,6 @@ namespace Emby.Server.Implementations.Data return whereClauses; } -#nullable disable /// <summary> /// Formats a where clause for the specified provider. @@ -4582,6 +4478,7 @@ namespace Emby.Server.Implementations.Data provider); } +#nullable disable private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query) { var list = new List<string>(); @@ -4661,44 +4558,28 @@ namespace Emby.Server.Implementations.Data return true; } - if (query.IncludeItemTypes.Contains(BaseItemKind.Episode) + return query.IncludeItemTypes.Contains(BaseItemKind.Episode) || query.IncludeItemTypes.Contains(BaseItemKind.Video) || query.IncludeItemTypes.Contains(BaseItemKind.Movie) || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) || query.IncludeItemTypes.Contains(BaseItemKind.Series) - || query.IncludeItemTypes.Contains(BaseItemKind.Season)) - { - return true; - } - - return false; + || query.IncludeItemTypes.Contains(BaseItemKind.Season); } public void UpdateInheritedValues() { - string sql = string.Join( - ';', - new string[] - { - "delete from ItemValues where type = 6", - - "insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4", - - @"insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue + const string Statements = """ +delete from ItemValues where type = 6; +insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4; +insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue FROM AncestorIds LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId) -where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 " - }); - - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - connection.ExecuteAll(sql); - }, - TransactionMode); - } +where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4; +"""; + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + connection.Execute(Statements); + transaction.Commit(); } public void DeleteItem(Guid id) @@ -4710,43 +4591,36 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type CheckDisposed(); - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - Span<byte> idBlob = stackalloc byte[16]; - id.TryWriteBytes(idBlob); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + // Delete people + ExecuteWithSingleParam(connection, "delete from People where ItemId=@Id", id); - // Delete people - ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob); + // Delete chapters + ExecuteWithSingleParam(connection, "delete from " + ChaptersTableName + " where ItemId=@Id", id); - // Delete chapters - ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob); + // Delete media streams + ExecuteWithSingleParam(connection, "delete from mediastreams where ItemId=@Id", id); - // Delete media streams - ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob); + // Delete ancestors + ExecuteWithSingleParam(connection, "delete from AncestorIds where ItemId=@Id", id); - // Delete ancestors - ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob); + // Delete item values + ExecuteWithSingleParam(connection, "delete from ItemValues where ItemId=@Id", id); - // Delete item values - ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob); + // Delete the item + ExecuteWithSingleParam(connection, "delete from TypedBaseItems where guid=@Id", id); - // Delete the item - ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob); - }, - TransactionMode); - } + transaction.Commit(); } - private void ExecuteWithSingleParam(IDatabaseConnection db, string query, ReadOnlySpan<byte> value) + private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value) { using (var statement = PrepareStatement(db, query)) { statement.TryBind("@Id", value); - statement.MoveNext(); + statement.ExecuteNonQuery(); } } @@ -4773,7 +4647,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } var list = new List<string>(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText.ToString())) { // Run this again to bind the params @@ -4794,25 +4668,25 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type CheckDisposed(); - var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p"; + StringBuilder commandText = new StringBuilder("select ItemId, Name, Role, PersonType, SortOrder from People p"); var whereClauses = GetPeopleWhereClauses(query, null); if (whereClauses.Count != 0) { - commandText += " where " + string.Join(" AND ", whereClauses); + commandText.Append(" where ").AppendJoin(" AND ", whereClauses); } - commandText += " order by ListOrder"; + commandText.Append(" order by ListOrder"); if (query.Limit > 0) { - commandText += " LIMIT " + query.Limit; + commandText.Append(" LIMIT ").Append(query.Limit); } var list = new List<PersonInfo>(); - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) + using (var connection = GetConnection()) + using (var statement = PrepareStatement(connection, commandText.ToString())) { // Run this again to bind the params GetPeopleWhereClauses(query, statement); @@ -4826,7 +4700,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type return list; } - private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement) + private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, SqliteCommand statement) { var whereClauses = new List<string>(); @@ -4896,7 +4770,7 @@ AND Type = @InternalPersonType)"); return whereClauses; } - private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, IDatabaseConnection db, IStatement deleteAncestorsStatement) + private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement) { if (itemId.Equals(default)) { @@ -4907,13 +4781,9 @@ AND Type = @InternalPersonType)"); CheckDisposed(); - Span<byte> itemIdBlob = stackalloc byte[16]; - itemId.TryWriteBytes(itemIdBlob); - // First delete - deleteAncestorsStatement.Reset(); - deleteAncestorsStatement.TryBind("@ItemId", itemIdBlob); - deleteAncestorsStatement.MoveNext(); + deleteAncestorsStatement.TryBind("@ItemId", itemId); + deleteAncestorsStatement.ExecuteNonQuery(); if (ancestorIds.Count == 0) { @@ -4930,26 +4800,24 @@ AND Type = @InternalPersonType)"); i.ToString(CultureInfo.InvariantCulture)); } - // Remove last , + // Remove trailing comma insertText.Length--; using (var statement = PrepareStatement(db, insertText.ToString())) { - statement.TryBind("@ItemId", itemIdBlob); + statement.TryBind("@ItemId", itemId); for (var i = 0; i < ancestorIds.Count; i++) { var index = i.ToString(CultureInfo.InvariantCulture); var ancestorId = ancestorIds[i]; - ancestorId.TryWriteBytes(itemIdBlob); - statement.TryBind("@AncestorId" + index, itemIdBlob); + statement.TryBind("@AncestorId" + index, ancestorId); statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture)); } - statement.Reset(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } } @@ -5057,7 +4925,7 @@ AND Type = @InternalPersonType)"); var list = new List<string>(); using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, commandText)) { foreach (var row in statement.ExecuteQuery()) @@ -5257,77 +5125,75 @@ AND Type = @InternalPersonType)"); var list = new List<(BaseItem, ItemCounts)>(); var result = new QueryResult<(BaseItem, ItemCounts)>(); using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) + using (var transaction = connection.BeginTransaction(deferred: true)) { - connection.RunInTransaction( - db => + if (!isReturningZeroItems) + { + using (var statement = PrepareStatement(connection, commandText)) { - if (!isReturningZeroItems) + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) { - using (var statement = PrepareStatement(db, commandText)) - { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - if (typeSubQuery is not null) - { - GetWhereClauses(typeSubQuery, null); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); - - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasServiceName = HasServiceName(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); - - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); - if (item is not null) - { - var countStartColumn = columns.Count - 1; - - list.Add((item, GetItemCounts(row, countStartColumn, typesToCount))); - } - } - } + statement.TryBind("@UserId", query.User.InternalId); + } + + if (typeSubQuery is not null) + { + GetWhereClauses(typeSubQuery, null); } - if (query.EnableTotalRecordCount) + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); + + var hasEpisodeAttributes = HasEpisodeAttributes(query); + var hasProgramAttributes = HasProgramAttributes(query); + var hasServiceName = HasServiceName(query); + var hasStartDate = HasStartDate(query); + var hasTrailerTypes = HasTrailerTypes(query); + var hasArtistFields = HasArtistFields(query); + var hasSeriesFields = HasSeriesFields(query); + + foreach (var row in statement.ExecuteQuery()) { - using (var statement = PrepareStatement(db, countText)) + var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); + if (item is not null) { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - if (typeSubQuery is not null) - { - GetWhereClauses(typeSubQuery, null); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); - - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + var countStartColumn = columns.Count - 1; + + list.Add((item, GetItemCounts(row, countStartColumn, typesToCount))); } } - }, - ReadTransactionMode); + } + } + + if (query.EnableTotalRecordCount) + { + using (var statement = PrepareStatement(connection, countText)) + { + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.InternalId); + } + + if (typeSubQuery is not null) + { + GetWhereClauses(typeSubQuery, null); + } + + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); + + result.TotalRecordCount = statement.SelectScalarInt(); + } + } + + transaction.Commit(); } if (result.TotalRecordCount == 0) @@ -5341,7 +5207,7 @@ AND Type = @InternalPersonType)"); return result; } - private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, BaseItemKind[] typesToCount) + private static ItemCounts GetItemCounts(SqliteDataReader reader, int countStartColumn, BaseItemKind[] typesToCount) { var counts = new ItemCounts(); @@ -5420,7 +5286,7 @@ AND Type = @InternalPersonType)"); return list; } - private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, IDatabaseConnection db) + private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db) { if (itemId.Equals(default)) { @@ -5431,15 +5297,15 @@ AND Type = @InternalPersonType)"); CheckDisposed(); - var guidBlob = itemId.ToByteArray(); - // First delete - db.Execute("delete from ItemValues where ItemId=@Id", guidBlob); + using var command = db.PrepareStatement("delete from ItemValues where ItemId=@Id"); + command.TryBind("@Id", itemId); + command.ExecuteNonQuery(); - InsertItemValues(guidBlob, values, db); + InsertItemValues(itemId, values, db); } - private void InsertItemValues(byte[] idBlob, List<(int MagicNumber, string Value)> values, IDatabaseConnection db) + private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db) { const int Limit = 100; var startIndex = 0; @@ -5458,12 +5324,12 @@ AND Type = @InternalPersonType)"); i); } - // Remove last comma + // Remove trailing comma insertText.Length--; using (var statement = PrepareStatement(db, insertText.ToString())) { - statement.TryBind("@ItemId", idBlob); + statement.TryBind("@ItemId", id); for (var i = startIndex; i < endIndex; i++) { @@ -5484,8 +5350,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue)); } - statement.Reset(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } startIndex += Limit; @@ -5504,23 +5369,20 @@ AND Type = @InternalPersonType)"); CheckDisposed(); - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - var itemIdBlob = itemId.ToByteArray(); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + // First delete chapters + using var command = connection.CreateCommand(); + command.CommandText = "delete from People where ItemId=@ItemId"; + command.TryBind("@ItemId", itemId); + command.ExecuteNonQuery(); - // First delete chapters - db.Execute("delete from People where ItemId=@ItemId", itemIdBlob); + InsertPeople(itemId, people, connection); - InsertPeople(itemIdBlob, people, db); - }, - TransactionMode); - } + transaction.Commit(); } - private void InsertPeople(byte[] idBlob, List<PersonInfo> people, IDatabaseConnection db) + private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db) { const int Limit = 100; var startIndex = 0; @@ -5539,12 +5401,12 @@ AND Type = @InternalPersonType)"); i.ToString(CultureInfo.InvariantCulture)); } - // Remove last comma + // Remove trailing comma insertText.Length--; using (var statement = PrepareStatement(db, insertText.ToString())) { - statement.TryBind("@ItemId", idBlob); + statement.TryBind("@ItemId", id); for (var i = startIndex; i < endIndex; i++) { @@ -5561,8 +5423,7 @@ AND Type = @InternalPersonType)"); listIndex++; } - statement.Reset(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } startIndex += Limit; @@ -5570,7 +5431,7 @@ AND Type = @InternalPersonType)"); } } - private PersonInfo GetPerson(IReadOnlyList<ResultSetValue> reader) + private PersonInfo GetPerson(SqliteDataReader reader) { var item = new PersonInfo { @@ -5617,7 +5478,7 @@ AND Type = @InternalPersonType)"); cmdText += " order by StreamIndex ASC"; - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) { var list = new List<MediaStream>(); @@ -5658,23 +5519,19 @@ AND Type = @InternalPersonType)"); cancellationToken.ThrowIfCancellationRequested(); - using (var connection = GetConnection()) - { - connection.RunInTransaction( - db => - { - var itemIdBlob = id.ToByteArray(); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + // Delete existing mediastreams + using var command = connection.PrepareStatement("delete from mediastreams where ItemId=@ItemId"); + command.TryBind("@ItemId", id); + command.ExecuteNonQuery(); - // Delete existing mediastreams - db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob); + InsertMediaStreams(id, streams, connection); - InsertMediaStreams(itemIdBlob, streams, db); - }, - TransactionMode); - } + transaction.Commit(); } - private void InsertMediaStreams(byte[] idBlob, IReadOnlyList<MediaStream> streams, IDatabaseConnection db) + private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db) { const int Limit = 10; var startIndex = 0; @@ -5706,7 +5563,7 @@ AND Type = @InternalPersonType)"); using (var statement = PrepareStatement(db, insertText.ToString())) { - statement.TryBind("@ItemId", idBlob); + statement.TryBind("@ItemId", id); for (var i = startIndex; i < endIndex; i++) { @@ -5742,6 +5599,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@PixelFormat" + index, stream.PixelFormat); statement.TryBind("@BitDepth" + index, stream.BitDepth); + statement.TryBind("@IsAnamorphic" + index, stream.IsAnamorphic); statement.TryBind("@IsExternal" + index, stream.IsExternal); statement.TryBind("@RefFrames" + index, stream.RefFrames); @@ -5770,8 +5628,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired); } - statement.Reset(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } startIndex += Limit; @@ -5784,15 +5641,14 @@ AND Type = @InternalPersonType)"); /// </summary> /// <param name="reader">The reader.</param> /// <returns>MediaStream.</returns> - private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader) + private MediaStream GetMediaStream(SqliteDataReader reader) { var item = new MediaStream { - Index = reader[1].ToInt() + Index = reader.GetInt32(1), + Type = Enum.Parse<MediaStreamType>(reader.GetString(2), true) }; - item.Type = Enum.Parse<MediaStreamType>(reader[2].ToString(), true); - if (reader.TryGetString(3, out var codec)) { item.Codec = codec; @@ -5979,7 +5835,7 @@ AND Type = @InternalPersonType)"); item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId; } - item.IsHearingImpaired = reader.GetBoolean(43); + item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result; if (item.Type == MediaStreamType.Subtitle) { @@ -6009,10 +5865,10 @@ AND Type = @InternalPersonType)"); cmdText += " order by AttachmentIndex ASC"; var list = new List<MediaAttachment>(); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) using (var statement = PrepareStatement(connection, cmdText)) { - statement.TryBind("@ItemId", query.ItemId.ToByteArray()); + statement.TryBind("@ItemId", query.ItemId); if (query.Index.HasValue) { @@ -6044,24 +5900,22 @@ AND Type = @InternalPersonType)"); cancellationToken.ThrowIfCancellationRequested(); using (var connection = GetConnection()) + using (var transaction = connection.BeginTransaction()) + using (var command = connection.PrepareStatement("delete from mediaattachments where ItemId=@ItemId")) { - connection.RunInTransaction( - db => - { - var itemIdBlob = id.ToByteArray(); + command.TryBind("@ItemId", id); + command.ExecuteNonQuery(); - db.Execute("delete from mediaattachments where ItemId=@ItemId", itemIdBlob); + InsertMediaAttachments(id, attachments, connection, cancellationToken); - InsertMediaAttachments(itemIdBlob, attachments, db, cancellationToken); - }, - TransactionMode); + transaction.Commit(); } } private void InsertMediaAttachments( - byte[] idBlob, + Guid id, IReadOnlyList<MediaAttachment> attachments, - IDatabaseConnection db, + SqliteConnection db, CancellationToken cancellationToken) { const int InsertAtOnce = 10; @@ -6073,14 +5927,13 @@ AND Type = @InternalPersonType)"); for (var i = startIndex; i < endIndex; i++) { - var index = i.ToString(CultureInfo.InvariantCulture); insertText.Append("(@ItemId, "); foreach (var column in _mediaAttachmentSaveColumns.Skip(1)) { insertText.Append('@') .Append(column) - .Append(index) + .Append(i) .Append(','); } @@ -6095,7 +5948,7 @@ AND Type = @InternalPersonType)"); using (var statement = PrepareStatement(db, insertText.ToString())) { - statement.TryBind("@ItemId", idBlob); + statement.TryBind("@ItemId", id); for (var i = startIndex; i < endIndex; i++) { @@ -6111,8 +5964,7 @@ AND Type = @InternalPersonType)"); statement.TryBind("@MIMEType" + index, attachment.MimeType); } - statement.Reset(); - statement.MoveNext(); + statement.ExecuteNonQuery(); } insertText.Length = _mediaAttachmentInsertPrefix.Length; @@ -6124,11 +5976,11 @@ AND Type = @InternalPersonType)"); /// </summary> /// <param name="reader">The reader.</param> /// <returns>MediaAttachment.</returns> - private MediaAttachment GetMediaAttachment(IReadOnlyList<ResultSetValue> reader) + private MediaAttachment GetMediaAttachment(SqliteDataReader reader) { var item = new MediaAttachment { - Index = reader[1].ToInt() + Index = reader.GetInt32(1) }; if (reader.TryGetString(2, out var codec)) diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index a1e217ad1..a5edcc58c 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -11,8 +11,8 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { @@ -44,48 +44,48 @@ namespace Emby.Server.Implementations.Data var userDataTableExists = TableExists(connection, "userdata"); var users = userDatasTableExists ? null : _userManager.Users; + using var transaction = connection.BeginTransaction(); + connection.Execute(string.Join( + ';', + "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", + "drop index if exists idx_userdata", + "drop index if exists idx_userdata1", + "drop index if exists idx_userdata2", + "drop index if exists userdataindex1", + "drop index if exists userdataindex", + "drop index if exists userdataindex3", + "drop index if exists userdataindex4", + "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", + "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", + "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", + "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)")); + + if (!userDataTableExists) + { + transaction.Commit(); + return; + } - connection.RunInTransaction( - db => - { - db.ExecuteAll(string.Join(';', new[] - { - "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", - - "drop index if exists idx_userdata", - "drop index if exists idx_userdata1", - "drop index if exists idx_userdata2", - "drop index if exists userdataindex1", - "drop index if exists userdataindex", - "drop index if exists userdataindex3", - "drop index if exists userdataindex4", - "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", - "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", - "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", - "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)" - })); - - if (userDataTableExists) - { - var existingColumnNames = GetColumnNames(db, "userdata"); - - AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames); - AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames); - AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); - - if (!userDatasTableExists) - { - ImportUserIds(db, users); - - db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); - } - } - }, - TransactionMode); + var existingColumnNames = GetColumnNames(connection, "userdata"); + + AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames); + AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames); + AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); + + if (userDatasTableExists) + { + return; + } + + ImportUserIds(connection, users); + + connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); + + transaction.Commit(); } } - private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users) + private void ImportUserIds(SqliteConnection db, IEnumerable<User> users) { var userIdsWithUserData = GetAllUserIdsWithUserData(db); @@ -101,13 +101,12 @@ namespace Emby.Server.Implementations.Data statement.TryBind("@UserId", user.Id); statement.TryBind("@InternalUserId", user.InternalId); - statement.MoveNext(); - statement.Reset(); + statement.ExecuteNonQuery(); } } } - private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db) + private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db) { var list = new List<Guid>(); @@ -117,7 +116,7 @@ namespace Emby.Server.Implementations.Data { try { - list.Add(row[0].ReadGuidFromBlob()); + list.Add(row.GetGuid(0)); } catch (Exception ex) { @@ -169,17 +168,14 @@ namespace Emby.Server.Implementations.Data cancellationToken.ThrowIfCancellationRequested(); using (var connection = GetConnection()) + using (var transaction = connection.BeginTransaction()) { - connection.RunInTransaction( - db => - { - SaveUserData(db, internalUserId, key, userData); - }, - TransactionMode); + SaveUserData(connection, internalUserId, key, userData); + transaction.Commit(); } } - private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData) + private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData) { using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)")) { @@ -227,7 +223,7 @@ namespace Emby.Server.Implementations.Data statement.TryBindNull("@SubtitleStreamIndex"); } - statement.MoveNext(); + statement.ExecuteNonQuery(); } } @@ -239,16 +235,14 @@ namespace Emby.Server.Implementations.Data cancellationToken.ThrowIfCancellationRequested(); using (var connection = GetConnection()) + using (var transaction = connection.BeginTransaction()) { - connection.RunInTransaction( - db => - { - foreach (var userItemData in userDataList) - { - SaveUserData(db, internalUserId, userItemData.Key, userItemData); - } - }, - TransactionMode); + foreach (var userItemData in userDataList) + { + SaveUserData(connection, internalUserId, userItemData.Key, userItemData); + } + + transaction.Commit(); } } @@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.Data ArgumentException.ThrowIfNullOrEmpty(key); - using (var connection = GetConnection(true)) + using (var connection = GetConnection()) { using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId")) { @@ -336,7 +330,7 @@ namespace Emby.Server.Implementations.Data /// </summary> /// <param name="reader">The list of result set values.</param> /// <returns>The user item data.</returns> - private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader) + private UserItemData ReadRow(SqliteDataReader reader) { var userData = new UserItemData(); @@ -348,10 +342,10 @@ namespace Emby.Server.Implementations.Data userData.Rating = rating; } - userData.Played = reader[3].ToBool(); - userData.PlayCount = reader[4].ToInt(); - userData.IsFavorite = reader[5].ToBool(); - userData.PlaybackPositionTicks = reader[6].ToInt64(); + userData.Played = reader.GetBoolean(3); + userData.PlayCount = reader.GetInt32(4); + userData.IsFavorite = reader.GetBoolean(5); + userData.PlaybackPositionTicks = reader.GetInt64(6); if (reader.TryReadDateTime(7, out var lastPlayedDate)) { diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index b8655c760..3aab0a5e9 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -24,6 +24,7 @@ <ItemGroup> <PackageReference Include="DiscUtils.Udf" /> <PackageReference Include="Jellyfin.XmlTv" /> + <PackageReference Include="Microsoft.Data.Sqlite" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> @@ -31,7 +32,6 @@ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="Mono.Nat" /> <PackageReference Include="prometheus-net.DotNetRuntime" /> - <PackageReference Include="SQLitePCL.pretty.netstandard" /> <PackageReference Include="DotNet.Glob" /> </ItemGroup> diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 0ad81b653..15b1836eb 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.IO } } - public void ResetPath(string path, string affectedFile) + public void ResetPath(string path, string? affectedFile) { lock (_timerLock) { @@ -148,13 +148,6 @@ namespace Emby.Server.Implementations.IO { item.ChangedExternally(); } - catch (IOException ex) - { - // For now swallow and log. - // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) - // Should we remove it from it's parent? - _logger.LogError(ex, "Error refreshing {Name}", item.Name); - } catch (Exception ex) { _logger.LogError(ex, "Error refreshing {Name}", item.Name); diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index f67a02be8..dde38906f 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -160,7 +158,7 @@ namespace Emby.Server.Implementations.IO /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> - private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e) + private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e) { if (e.Parent is AggregateFolder) { @@ -173,7 +171,7 @@ namespace Emby.Server.Implementations.IO /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> - private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e) + private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e) { if (e.Parent is AggregateFolder) { @@ -189,19 +187,28 @@ namespace Emby.Server.Implementations.IO /// <param name="path">The path.</param> /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns> /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception> - private static bool ContainsParentFolder(IEnumerable<string> lst, string path) + private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path) { - ArgumentException.ThrowIfNullOrEmpty(path); + if (path.IsEmpty) + { + throw new ArgumentException("Path can't be empty", nameof(path)); + } path = path.TrimEnd(Path.DirectorySeparatorChar); - return lst.Any(str => + foreach (var str in lst) { // this should be a little quicker than examining each actual parent folder... - var compare = str.TrimEnd(Path.DirectorySeparatorChar); + var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar); - return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar); - }); + if (path.Equals(compare, StringComparison.OrdinalIgnoreCase) + || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar)) + { + return true; + } + } + + return false; } /// <summary> @@ -349,21 +356,19 @@ namespace Emby.Server.Implementations.IO { ArgumentException.ThrowIfNullOrEmpty(path); - var monitorPath = !IgnorePatterns.ShouldIgnore(path); + if (IgnorePatterns.ShouldIgnore(path)) + { + return; + } // Ignore certain files, If the parent of an ignored path has a change event, ignore that too - if (_tempIgnoredPaths.Keys.Any(i => + foreach (var i in _tempIgnoredPaths.Keys) { - if (_fileSystem.AreEqual(i, path)) - { - _logger.LogDebug("Ignoring change to {Path}", path); - return true; - } - - if (_fileSystem.ContainsSubPath(i, path)) + if (_fileSystem.AreEqual(i, path) + || _fileSystem.ContainsSubPath(i, path)) { _logger.LogDebug("Ignoring change to {Path}", path); - return true; + return; } // Go up a level @@ -371,20 +376,11 @@ namespace Emby.Server.Implementations.IO if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path)) { _logger.LogDebug("Ignoring change to {Path}", path); - return true; + return; } - - return false; - })) - { - monitorPath = false; } - if (monitorPath) - { - // Avoid implicitly captured closure - CreateRefresher(path); - } + CreateRefresher(path); } private void CreateRefresher(string path) @@ -417,7 +413,8 @@ namespace Emby.Server.Implementations.IO } // They are siblings. Rebase the refresher to the parent folder. - if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal)) + if (parentPath is not null + && Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal)) { refresher.ResetPath(parentPath, path); return; @@ -430,8 +427,13 @@ namespace Emby.Server.Implementations.IO } } - private void OnNewRefresherCompleted(object sender, EventArgs e) + private void OnNewRefresherCompleted(object? sender, EventArgs e) { + if (sender is null) + { + return; + } + var refresher = (FileRefresher)sender; DisposeRefresher(refresher); } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 0ba4a488b..18b00ce0b 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -15,10 +15,6 @@ namespace Emby.Server.Implementations.IO /// </summary> public class ManagedFileSystem : IFileSystem { - private readonly ILogger<ManagedFileSystem> _logger; - - private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); - private readonly string _tempPath; private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows(); private static readonly char[] _invalidPathCharacters = { @@ -29,23 +25,24 @@ namespace Emby.Server.Implementations.IO (char)31, ':', '*', '?', '\\', '/' }; + private readonly ILogger<ManagedFileSystem> _logger; + private readonly List<IShortcutHandler> _shortcutHandlers; + private readonly string _tempPath; + /// <summary> /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. /// </summary> /// <param name="logger">The <see cref="ILogger"/> instance to use.</param> /// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param> + /// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param> public ManagedFileSystem( ILogger<ManagedFileSystem> logger, - IApplicationPaths applicationPaths) + IApplicationPaths applicationPaths, + IEnumerable<IShortcutHandler> shortcutHandlers) { _logger = logger; _tempPath = applicationPaths.TempDirectory; - } - - /// <inheritdoc /> - public virtual void AddShortcutHandler(IShortcutHandler handler) - { - _shortcutHandlers.Add(handler); + _shortcutHandlers = shortcutHandlers.ToList(); } /// <summary> @@ -486,24 +483,10 @@ namespace Emby.Server.Implementations.IO } /// <inheritdoc /> - public virtual string NormalizePath(string path) - { - ArgumentException.ThrowIfNullOrEmpty(path); - - if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase)) - { - return path; - } - - return Path.TrimEndingDirectorySeparator(path); - } - - /// <inheritdoc /> public virtual bool AreEqual(string path1, string path2) { - return string.Equals( - NormalizePath(path1), - NormalizePath(path2), + return Path.TrimEndingDirectorySeparator(path1).Equals( + Path.TrimEndingDirectorySeparator(path2), _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs index c2aab3879..5776c7a7c 100644 --- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs +++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs @@ -8,24 +8,17 @@ namespace Emby.Server.Implementations.IO { public class MbLinkShortcutHandler : IShortcutHandler { - private readonly IFileSystem _fileSystem; - - public MbLinkShortcutHandler(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - public string Extension => ".mblink"; public string? Resolve(string shortcutPath) { ArgumentException.ThrowIfNullOrEmpty(shortcutPath); - if (string.Equals(Path.GetExtension(shortcutPath), ".mblink", StringComparison.OrdinalIgnoreCase)) + if (Path.GetExtension(shortcutPath.AsSpan()).Equals(".mblink", StringComparison.OrdinalIgnoreCase)) { var path = File.ReadAllText(shortcutPath); - return _fileSystem.NormalizePath(path); + return Path.TrimEndingDirectorySeparator(path); } return null; diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 5384c04b3..cf6fc1845 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library // bts sync files "**/*.bts", "**/*.sync", + + // zfs + "**/.zfs/**", + "**/.zfs" }; private static readonly GlobOptions _globOptions = new GlobOptions diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 8bb2d3c02..b0a4a4151 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -63,7 +64,7 @@ namespace Emby.Server.Implementations.Library private const string ShortcutFileExtension = ".mblink"; private readonly ILogger<LibraryManager> _logger; - private readonly IMemoryCache _memoryCache; + private readonly ConcurrentDictionary<Guid, BaseItem> _cache; private readonly ITaskManager _taskManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataRepository; @@ -111,7 +112,6 @@ namespace Emby.Server.Implementations.Library /// <param name="mediaEncoder">The media encoder.</param> /// <param name="itemRepository">The item repository.</param> /// <param name="imageProcessor">The image processor.</param> - /// <param name="memoryCache">The memory cache.</param> /// <param name="namingOptions">The naming options.</param> /// <param name="directoryService">The directory service.</param> public LibraryManager( @@ -128,7 +128,6 @@ namespace Emby.Server.Implementations.Library IMediaEncoder mediaEncoder, IItemRepository itemRepository, IImageProcessor imageProcessor, - IMemoryCache memoryCache, NamingOptions namingOptions, IDirectoryService directoryService) { @@ -145,7 +144,7 @@ namespace Emby.Server.Implementations.Library _mediaEncoder = mediaEncoder; _itemRepository = itemRepository; _imageProcessor = imageProcessor; - _memoryCache = memoryCache; + _cache = new ConcurrentDictionary<Guid, BaseItem>(); _namingOptions = namingOptions; _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService); @@ -300,7 +299,7 @@ namespace Emby.Server.Implementations.Library } } - _memoryCache.Set(item.Id, item); + _cache[item.Id] = item; } public void DeleteItem(BaseItem item, DeleteOptions options) @@ -359,7 +358,7 @@ namespace Emby.Server.Implementations.Library var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren(false) - : Enumerable.Empty<BaseItem>(); + : Array.Empty<BaseItem>(); foreach (var metadataPath in GetMetadataPaths(item, children)) { @@ -441,7 +440,7 @@ namespace Emby.Server.Implementations.Library _itemRepository.DeleteItem(child.Id); } - _memoryCache.Remove(item.Id); + _cache.TryRemove(item.Id, out _); ReportItemRemoved(item, parent); } @@ -609,7 +608,7 @@ namespace Emby.Server.Implementations.Library var originalList = paths.ToList(); var list = originalList.Where(i => i.IsDirectory) - .Select(i => _fileSystem.NormalizePath(i.FullName)) + .Select(i => Path.TrimEndingDirectorySeparator(i.FullName)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); @@ -1233,7 +1232,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("Guid can't be empty", nameof(id)); } - if (_memoryCache.TryGetValue(id, out BaseItem item)) + if (_cache.TryGetValue(id, out BaseItem item)) { return item; } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index 9026160ff..b77c6b204 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -25,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers private readonly NamingOptions _namingOptions; private readonly IDirectoryService _directoryService; - private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + private static readonly string[] _ignoreFiles = new[] { "folder", "thumb", @@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>Trailer.</returns> - protected override Photo Resolve(ItemResolveArgs args) + protected override Photo? Resolve(ItemResolveArgs args) { if (!args.IsDirectory) { @@ -68,10 +66,11 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (IsImageFile(args.Path, _imageProcessor)) { - var filename = Path.GetFileNameWithoutExtension(args.Path); + var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan()); // Make sure the image doesn't belong to a video file - var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)); + var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path) + ?? throw new InvalidOperationException("Path can't be a root directory.")); foreach (var file in files) { @@ -92,32 +91,32 @@ namespace Emby.Server.Implementations.Library.Resolvers return null; } - internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename) + internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename) { return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename); } - internal static bool IsOwnedByResolvedMedia(string file, string imageFilename) + internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename) => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase); internal static bool IsImageFile(string path, IImageProcessor imageProcessor) { ArgumentNullException.ThrowIfNull(path); - var filename = Path.GetFileNameWithoutExtension(path); - - if (_ignoreFiles.Contains(filename)) + var extension = Path.GetExtension(path.AsSpan()).TrimStart('.'); + if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return false; } - if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1)) + var filename = Path.GetFileNameWithoutExtension(path); + + if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase))) { return false; } - string extension = Path.GetExtension(path).TrimStart('.'); - return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase); + return true; } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 7b6c8b80a..1721be9e2 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -23,14 +24,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public abstract class BaseTunerHost { - private readonly IMemoryCache _memoryCache; + private readonly ConcurrentDictionary<string, List<ChannelInfo>> _cache; - protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache) + protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem) { Config = config; Logger = logger; - _memoryCache = memoryCache; FileSystem = fileSystem; + _cache = new ConcurrentDictionary<string, List<ChannelInfo>>(); } protected IServerConfigurationManager Config { get; } @@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var key = tuner.Id; - if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache)) + if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List<ChannelInfo> cache)) { return cache; } @@ -61,7 +62,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!string.IsNullOrEmpty(key) && list.Count > 0) { - _memoryCache.Set(key, list); + _cache[key] = list; } return list; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 1795e85a3..7e588f681 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -50,9 +50,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IHttpClientFactory httpClientFactory, IServerApplicationHost appHost, ISocketFactory socketFactory, - IStreamHelper streamHelper, - IMemoryCache memoryCache) - : base(config, logger, fileSystem, memoryCache) + IStreamHelper streamHelper) + : base(config, logger, fileSystem) { _httpClientFactory = httpClientFactory; _appHost = appHost; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index acf3964c8..613ea117f 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -54,9 +54,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts IHttpClientFactory httpClientFactory, IServerApplicationHost appHost, INetworkManager networkManager, - IStreamHelper streamHelper, - IMemoryCache memoryCache) - : base(config, logger, fileSystem, memoryCache) + IStreamHelper streamHelper) + : base(config, logger, fileSystem) { _httpClientFactory = httpClientFactory; _appHost = appHost; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index df9101f48..341782d9d 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -94,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) { var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine); - if (string.IsNullOrWhiteSpace(channel.Id)) - { - channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - else - { - channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } + channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); channel.Path = trimmedLine; channels.Add(channel); diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 0e27dafe1..93d50e6e3 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -16,7 +16,7 @@ "Folders": "المجلدات", "Genres": "التصنيفات", "HeaderAlbumArtists": "فناني الألبوم", - "HeaderContinueWatching": "أستئناف المشاهدة", + "HeaderContinueWatching": "استئناف المشاهدة", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteEpisodes": "الحلقات المفضلة", diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json index 0967ef424..7c7dd26e9 100644 --- a/Emby.Server.Implementations/Localization/Core/as.json +++ b/Emby.Server.Implementations/Localization/Core/as.json @@ -1 +1,43 @@ -{} +{ + "Albums": "এলবাম", + "Application": "আবেদন", + "AppDeviceValues": "এপ্: {0}, ডিভাইচ: {1}", + "Artists": "শিল্পী", + "Channels": "চেনেলস", + "Default": "ডিফল্ট", + "AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত", + "Books": "পুস্তক", + "Movies": "চলচ্চিত্ৰ", + "CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}", + "Collections": "সংগ্রহ", + "HeaderFavoriteShows": "প্রিয় শোসমূহ", + "Latest": "শেহতীয়া", + "MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে", + "MixedContent": "মিশ্ৰিত সমগ্ৰতা", + "NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.", + "NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল", + "External": "বাহ্যিক", + "Favorites": "পছন্দসই", + "Folders": "ফোল্ডাৰ", + "Forced": "বলপূর্বক", + "Genres": "শ্রেণী", + "HeaderAlbumArtists": "অ্যালবাম শিল্পী", + "HeaderContinueWatching": "দেখা চালিয়ে যান", + "FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}", + "HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ", + "HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ", + "HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ", + "HeaderFavoriteSongs": "প্ৰিয় গীত", + "HeaderLiveTV": "প্ৰতিবেদন টিভি", + "HeaderNextUp": "পৰৱৰ্তী অংশ", + "HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ", + "HearingImpaired": "শ্ৰবণ অক্ষম", + "HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ", + "Inherit": "উত্তপ্ত কৰা", + "MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে", + "NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ", + "NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল", + "NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল", + "NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল", + "NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা" +} diff --git a/Emby.Server.Implementations/Localization/Core/chr.json b/Emby.Server.Implementations/Localization/Core/chr.json new file mode 100644 index 000000000..85d1f4c88 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/chr.json @@ -0,0 +1,52 @@ +{ + "ChapterNameValue": "Didanedi {0}", + "HeaderAlbumArtists": "Didanidanolisgisgi", + "HeaderFavoriteAlbums": "Dvganidi didanidisgisgi", + "HeaderLiveTV": "Anigadi didanidisgosgi", + "HeaderRecordingGroups": "Didanisquodiisgisgi", + "HomeVideos": "Diganadi dinagadisgisgi", + "Inherit": "Anigwe", + "MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}", + "MixedContent": "Ganinidi dininoladisgisgi", + "Movies": "Anidvnisgisgi", + "MusicVideos": "Danodisgisgi didanidisgosgi", + "NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi", + "NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi", + "NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi", + "Albums": "Anigawidaniyv", + "Application": "Didanvyi", + "Artists": "Dinidaniyi", + "AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani", + "Books": "Didanedi", + "CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}", + "Channels": "Diganadasgi", + "Collections": "Diganadisgi", + "Default": "Dinadi", + "DeviceOfflineWithName": "{0} Aniyvolehvi nasgi", + "External": "Amohdi", + "Favorites": "Nvdayelvdisgi", + "Folders": "Didanididisgi", + "Forced": "Ganedi", + "Genres": "Diganadisgi", + "HeaderContinueWatching": "Uwoditsu asdanidisgisgi", + "HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi", + "HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi", + "HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)", + "HeaderFavoriteSongs": "Dvganidi danodisgisgi", + "HeaderNextUp": "Anidvli uwodoli", + "HearingImpaired": "Anitsunidi talunidisgisgi", + "ItemAddedWithName": "{0} Dinigwe anididanidisgi", + "Latest": "Uwodoli", + "MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe", + "MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi", + "Music": "Danodisgisgi", + "NameSeasonUnknown": "Tsunita anidvdisgi", + "NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.", + "NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi", + "NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi", + "NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi", + "NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi", + "NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi", + "NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi", + "NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi" +} diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 08db5a30e..f33ea2fc9 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -22,7 +22,7 @@ "HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteSongs": "Oblíbená hudba", - "HeaderLiveTV": "Televize", + "HeaderLiveTV": "Živý přenos", "HeaderNextUp": "Další díly", "HeaderRecordingGroups": "Skupiny nahrávek", "HomeVideos": "Domácí videa", diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 1b6eecdcf..837172a5b 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -15,13 +15,13 @@ "Favorites": "Favoritter", "Folders": "Mapper", "Genres": "Genrer", - "HeaderAlbumArtists": "Albums kunstnere", + "HeaderAlbumArtists": "Albumkunstnere", "HeaderContinueWatching": "Fortsæt afspilning", - "HeaderFavoriteAlbums": "Favorit albummer", - "HeaderFavoriteArtists": "Favorit kunstnere", - "HeaderFavoriteEpisodes": "Favorit afsnit", - "HeaderFavoriteShows": "Favorit serier", - "HeaderFavoriteSongs": "Favorit sange", + "HeaderFavoriteAlbums": "Favoritalbummer", + "HeaderFavoriteArtists": "Favoritkunstnere", + "HeaderFavoriteEpisodes": "Yndlingsafsnit", + "HeaderFavoriteShows": "Yndlingsserier", + "HeaderFavoriteSongs": "Yndlingssange", "HeaderLiveTV": "Live-TV", "HeaderNextUp": "Næste", "HeaderRecordingGroups": "Optagelsesgrupper", @@ -34,8 +34,8 @@ "Latest": "Seneste", "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret", "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret", - "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret", + "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret", + "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret", "MixedContent": "Blandet indhold", "Movies": "Film", "Music": "Musik", @@ -51,7 +51,7 @@ "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet", "NotificationOptionInstallationFailed": "Installationen mislykkedes", "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet", - "NotificationOptionPluginError": "Plugin fejl", + "NotificationOptionPluginError": "Plugin-fejl", "NotificationOptionPluginInstalled": "Plugin blev installeret", "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret", "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret", @@ -92,26 +92,26 @@ "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek", "ValueSpecialEpisodeName": "Special - {0}", "VersionNumber": "Version {0}", - "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.", + "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.", "TaskDownloadMissingSubtitles": "Hent manglende undertekster", "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.", "TaskUpdatePlugins": "Opdater Plugins", - "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.", - "TaskCleanLogs": "Ryd Log mappe", - "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.", - "TaskRefreshLibrary": "Scan Medie Bibliotek", - "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.", - "TaskCleanCache": "Ryd Cache mappe", - "TasksChannelsCategory": "Internet Kanaler", + "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.", + "TaskCleanLogs": "Ryd Log-mappe", + "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.", + "TaskRefreshLibrary": "Scan Mediebibliotek", + "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.", + "TaskCleanCache": "Ryd Cache-mappe", + "TasksChannelsCategory": "Internetkanaler", "TasksApplicationCategory": "Applikation", "TasksLibraryCategory": "Bibliotek", "TasksMaintenanceCategory": "Vedligeholdelse", - "TaskRefreshChapterImages": "Udtræk kapitel billeder", - "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.", - "TaskRefreshChannelsDescription": "Opdater internet kanal information.", + "TaskRefreshChapterImages": "Udtræk kapitelbilleder", + "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.", + "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.", "TaskRefreshChannels": "Opdater Kanaler", - "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.", - "TaskCleanTranscode": "Tøm Transcode mappen", + "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.", + "TaskCleanTranscode": "Tøm Transcode-mappen", "TaskRefreshPeople": "Opdater Personer", "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.", "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.", @@ -121,8 +121,8 @@ "Default": "Standard", "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.", "TaskOptimizeDatabase": "Optimér database", - "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.", - "TaskKeyframeExtractor": "Nøglebillede udtræk", + "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.", + "TaskKeyframeExtractor": "Udtræk af nøglebillede", "External": "Ekstern", "HearingImpaired": "Hørehæmmet" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index f5636a0af..4c56f789d 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -3,9 +3,9 @@ "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}", "Application": "Aplicación", "Artists": "Artistas", - "AuthenticationSucceededWithUserName": "{0} identificado correctamente", + "AuthenticationSucceededWithUserName": "{0} autenticado correctamente", "Books": "Libros", - "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}", + "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}", "Channels": "Canales", "ChapterNameValue": "Capítulo {0}", "Collections": "Colecciones", diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 4877bcd7a..a2b429dcd 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -105,8 +105,8 @@ "TaskRefreshPeople": "Actualiser les acteurs", "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", "TaskCleanLogs": "Nettoyer le répertoire des journaux", - "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.", - "TaskRefreshLibrary": "Scanner la médiathèque", + "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.", + "TaskRefreshLibrary": "Analyser la médiathèque", "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.", "TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json index 3c8c38ed4..5e2b3756b 100644 --- a/Emby.Server.Implementations/Localization/Core/kn.json +++ b/Emby.Server.Implementations/Localization/Core/kn.json @@ -3,5 +3,125 @@ "TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ", "TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.", "TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್ಟ್ರಾಕ್ಟರ್", - "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್ಗಳಿಂದ ಕೀಫ್ರೇಮ್ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು." + "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್ಗಳಿಂದ ಕೀಫ್ರೇಮ್ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.", + "ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ", + "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}", + "TasksLibraryCategory": "ಸಮೊಹ", + "TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್", + "TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು", + "TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ", + "TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ", + "UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ", + "Albums": "ಸಂಪುಟ", + "Application": "ಅಪ್ಲಿಕೇಶನ್", + "AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}", + "Artists": "ಕಲಾವಿದರು", + "AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ", + "Books": "ಪುಸ್ತಕಗಳು", + "ChapterNameValue": "ಅಧ್ಯಾಯ {0}", + "Collections": "ಸಂಗ್ರಹಣೆಗಳು", + "Default": "ಪೂರ್ವನಿಯೋಜಿತ", + "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ", + "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ", + "External": "ಹೊರಗಿನ", + "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ", + "Favorites": "ಮೆಚ್ಚಿನವುಗಳು", + "Folders": "ಫೋಲ್ಡರ್ಗಳು", + "Forced": "ಬಲವಂತವಾಗಿ", + "Genres": "ಪ್ರಕಾರಗಳು", + "HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ", + "HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು", + "HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು", + "HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು", + "HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು", + "HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ", + "HeaderNextUp": "ಮುಂದೆ", + "HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು", + "MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ", + "Channels": "ಮೂಲಗಳು", + "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು", + "HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು", + "HearingImpaired": "ಮೂಗ", + "ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ", + "MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ", + "MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್ಲೋಡ್ಗೆ ಲಭ್ಯವಿದೆ.", + "NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ", + "NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ", + "NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್ಇನ್ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ", + "NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ", + "NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ", + "PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್ಇನ್ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ", + "ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ", + "ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು", + "ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ", + "UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ", + "UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ", + "UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್ಲೈನ್ನಲ್ಲಿದೆ", + "UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ", + "UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ", + "UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}", + "UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ", + "VersionNumber": "ಆವೃತ್ತಿ {0}", + "TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ", + "TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್", + "TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ", + "TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್ನೇಲ್ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.", + "TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.", + "TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.", + "TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್ಕೋಡ್ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.", + "TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡಿ", + "Shows": "ಧಾರವಾಹಿಗಳು", + "Songs": "ಹಾಡುಗಳು", + "StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", + "UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ", + "UserDownloadingItemWithValues": "{0} ಡೌನ್ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}", + "SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ", + "Sync": "ಹೊಂದಿಕೆ", + "System": "ವ್ಯವಸ್ಥೆ", + "TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು", + "Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ", + "User": "ಬಳಕೆದಾರ", + "HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು", + "Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ", + "ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ", + "LabelIpAddressValue": "IP ವಿಳಾಸ: {0}", + "LabelRunningTimeValue": "ಅವಧಿ: {0}", + "Latest": "ಹೊಸದಾದ", + "MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "MixedContent": "ಮಿಶ್ರ ವಿಷಯ", + "Movies": "ಚಲನಚಿತ್ರಗಳು", + "Music": "ಸಂಗೀತ", + "MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು", + "NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ", + "NameSeasonNumber": "ಸೀಸನ್ {0}", + "NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ", + "NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ", + "NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ", + "NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ", + "NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ", + "NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ", + "NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ", + "NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ", + "NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ", + "NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ", + "NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ", + "NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ", + "Photos": "ಚಿತ್ರಗಳು", + "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು", + "Plugin": "ಪ್ಲಗಿನ್", + "PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ", + "PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "ProviderValue": "ಒದಗಿಸುವವರು: {0}", + "TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ", + "TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ", + "TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.", + "TaskUpdatePlugins": "ಪ್ಲಗಿನ್ಗಳನ್ನು ನವೀಕರಿಸಿ", + "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ", + "TaskRefreshChannels": "ಚಾನಲ್ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ", + "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ." } diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index b2293e4b6..a07222975 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -1,5 +1,5 @@ { - "Albums": "Album-album", + "Albums": "Album", "AppDeviceValues": "Apl: {0}, Peranti: {1}", "Application": "Aplikasi", "Artists": "Artis-artis", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 4eb00d289..ac7b92de6 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -1,9 +1,9 @@ { "Albums": "Albums", "AppDeviceValues": "App: {0}, Apparaat: {1}", - "Application": "Toepassing", + "Application": "Applicatie", "Artists": "Artiesten", - "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd", + "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd", "Books": "Boeken", "CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}", "Channels": "Kanalen", diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index 87800a2fe..26dc5ce82 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -24,5 +24,13 @@ "TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.", "HeaderAlbumArtists": "Buccaneers o' the musical arts", "HeaderFavoriteAlbums": "Beloved booty o' musical adventures", - "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas" + "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas", + "Channels": "Channels", + "Forced": "Pressed", + "External": "Outboard", + "HeaderFavoriteEpisodes": "Treasured Tales", + "HeaderFavoriteShows": "Treasured Tales", + "ChapterNameValue": "Piece {0}", + "HeaderFavoriteSongs": "Treasured Chimes", + "HeaderNextUp": "Incoming" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 421513341..fa6c753b6 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -31,13 +31,13 @@ "ItemRemovedWithName": "{0} - изъято из медиатеки", "LabelIpAddressValue": "IP-адрес: {0}", "LabelRunningTimeValue": "Длительность: {0}", - "Latest": "Новое", + "Latest": "Последние добавленные", "MessageApplicationUpdated": "Jellyfin Server был обновлён", "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена", "MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена", "MixedContent": "Смешанное содержание", - "Movies": "Кино", + "Movies": "Фильмы", "Music": "Музыка", "MusicVideos": "Муз. видео", "NameInstallFailed": "Установка {0} неудачна", @@ -77,7 +77,7 @@ "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}", "Sync": "Синхронизация", "System": "Система", - "TvShows": "ТВ", + "TvShows": "Телесериалы", "User": "Пользователь", "UserCreatedWithName": "Пользователь {0} был создан", "UserDeletedWithName": "Пользователь {0} был удалён", diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index 4c23f71ef..1944e072c 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -11,7 +11,7 @@ "Collections": "Zbirke", "DeviceOfflineWithName": "{0} je prekinil povezavo", "DeviceOnlineWithName": "{0} je povezan", - "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}", + "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", "Favorites": "Priljubljeno", "Folders": "Mape", "Genres": "Zvrsti", diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 9a140f871..3ce928859 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -3,19 +3,19 @@ "AppDeviceValues": "Uygulama: {0}, Aygıt: {1}", "Application": "Uygulama", "Artists": "Sanatçılar", - "AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı", + "AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı", "Books": "Kitaplar", "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi", "Channels": "Kanallar", - "ChapterNameValue": "Bölüm {0}", + "ChapterNameValue": "{0}. Bölüm", "Collections": "Koleksiyonlar", "DeviceOfflineWithName": "{0} bağlantısı kesildi", "DeviceOnlineWithName": "{0} bağlı", - "FailedLoginAttemptWithUserName": "{0} adresinden giriş denemesi başarısız oldu", + "FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu", "Favorites": "Favoriler", "Folders": "Klasörler", "Genres": "Türler", - "HeaderAlbumArtists": "Albüm Sanatçıları", + "HeaderAlbumArtists": "Albüm sanatçıları", "HeaderContinueWatching": "İzlemeye Devam Et", "HeaderFavoriteAlbums": "Favori Albümler", "HeaderFavoriteArtists": "Favori Sanatçılar", @@ -25,7 +25,7 @@ "HeaderLiveTV": "Canlı TV", "HeaderNextUp": "Gelecek Hafta", "HeaderRecordingGroups": "Kayıt Grupları", - "HomeVideos": "Ana sayfa videoları", + "HomeVideos": "Ana Sayfa Videoları", "Inherit": "Devral", "ItemAddedWithName": "{0} kütüphaneye eklendi", "ItemRemovedWithName": "{0} kütüphaneden silindi", @@ -34,14 +34,14 @@ "Latest": "En son", "MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi", "MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi", - "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu ayar kısmı {0} güncellendi", - "MessageServerConfigurationUpdated": "Sunucu ayarları güncellendi", + "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu yapılandırma bölümü {0} güncellendi", + "MessageServerConfigurationUpdated": "Sunucu yapılandırması güncellendi", "MixedContent": "Karışık içerik", "Movies": "Filmler", "Music": "Müzik", - "MusicVideos": "Müzik videoları", + "MusicVideos": "Müzik Videoları", "NameInstallFailed": "{0} kurulumu başarısız", - "NameSeasonNumber": "Sezon {0}", + "NameSeasonNumber": "{0}. Sezon", "NameSeasonUnknown": "Bilinmeyen Sezon", "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.", "NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut", @@ -55,9 +55,9 @@ "NotificationOptionPluginInstalled": "Eklenti yüklendi", "NotificationOptionPluginUninstalled": "Eklenti kaldırıldı", "NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi", - "NotificationOptionServerRestartRequired": "Sunucu yeniden başlatma gerekli", + "NotificationOptionServerRestartRequired": "Sunucunun yeniden başlatılması gerekiyor", "NotificationOptionTaskFailed": "Zamanlanmış görev hatası", - "NotificationOptionUserLockedOut": "Kullanıcı kitlendi", + "NotificationOptionUserLockedOut": "Kullanıcı kilitlendi", "NotificationOptionVideoPlayback": "Video oynatma başladı", "NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu", "Photos": "Fotoğraflar", @@ -74,36 +74,36 @@ "Songs": "Şarkılar", "StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi", + "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} sağlayıcısından indirilemedi", "Sync": "Eşzamanlama", "System": "Sistem", "TvShows": "Diziler", "User": "Kullanıcı", "UserCreatedWithName": "{0} kullanıcısı oluşturuldu", - "UserDeletedWithName": "Kullanıcı {0} silindi", - "UserDownloadingItemWithValues": "{0} indiriliyor {1}", - "UserLockedOutWithName": "Kullanıcı {0} kitlendi", - "UserOfflineFromDevice": "{0}, {1} ile bağlantısı kesildi", - "UserOnlineFromDevice": "{0}, {1} çevrimiçi", - "UserPasswordChangedWithName": "{0} kullanıcısı için şifre değiştirildi", - "UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için güncellendi", + "UserDeletedWithName": "{0} kullanıcısı silindi", + "UserDownloadingItemWithValues": "{0} {1} medyasını indiriyor", + "UserLockedOutWithName": "{0} adlı kullanıcı kilitlendi", + "UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi", + "UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi", + "UserPasswordChangedWithName": "{0} kullanıcısının parolası değiştirildi", + "UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi", "UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor", "UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi", "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi", "ValueSpecialEpisodeName": "Özel - {0}", "VersionNumber": "Sürüm {0}", - "TaskCleanCache": "Geçici dosya klasörünü temizle", - "TasksChannelsCategory": "İnternet kanalları", + "TaskCleanCache": "Geçici Dosya Klasörünü Temizle", + "TasksChannelsCategory": "İnternet Kanalları", "TasksApplicationCategory": "Uygulama", "TasksLibraryCategory": "Kütüphane", "TasksMaintenanceCategory": "Bakım", "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.", - "TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.", + "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.", "TaskDownloadMissingSubtitles": "Eksik altyazıları indir", "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.", "TaskRefreshChannels": "Kanalları Yenile", - "TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.", - "TaskCleanTranscode": "Dönüşüm Dizinini Temizle", + "TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.", + "TaskCleanTranscode": "Kod Dönüştürme Dizinini Temizle", "TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.", "TaskUpdatePlugins": "Eklentileri Güncelle", "TaskRefreshPeople": "Kullanıcıları Yenile", diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json index b5f4b920f..aa056d449 100644 --- a/Emby.Server.Implementations/Localization/Core/zu.json +++ b/Emby.Server.Implementations/Localization/Core/zu.json @@ -25,5 +25,14 @@ "Channels": "Amashaneli", "Books": "Izincwadi", "Artists": "Abadlali", - "Albums": "Ama-albhamu" + "Albums": "Ama-albhamu", + "CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}", + "HeaderFavoriteArtists": "Abasethi Abathandekayo", + "HeaderFavoriteEpisodes": "Izilimi Ezithandekayo", + "HeaderFavoriteShows": "Izisho Ezithandekayo", + "External": "Kwezifungo", + "FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}", + "HeaderContinueWatching": "Buyela Ukubona", + "HeaderFavoriteAlbums": "Izimpahla Ezithandwayo", + "HeaderAlbumArtists": "Abasethi wenkulumo" } diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv index 0bc1d3f7d..619e948d8 100644 --- a/Emby.Server.Implementations/Localization/Ratings/es.csv +++ b/Emby.Server.Implementations/Localization/Ratings/es.csv @@ -3,6 +3,7 @@ A/fig,0 A/i,0 A/fig/i,0 APTA,0 +ERI,0 TP,0 0+,0 6+,6 diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 702f8d45b..0cb450cdf 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -518,6 +518,11 @@ namespace Emby.Server.Implementations.Playlists return relativePath; } + public Folder GetPlaylistsFolder() + { + return GetPlaylistsFolder(Guid.Empty); + } + public Folder GetPlaylistsFolder(Guid userId) { const string TypeName = "PlaylistsFolder"; diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 48584ae0c..d7189ef0c 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Data; using System.Globalization; using System.IO; using System.Linq; @@ -11,7 +10,6 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; using Emby.Server.Implementations.Library; -using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common; @@ -30,7 +28,7 @@ namespace Emby.Server.Implementations.Plugins /// <summary> /// Defines the <see cref="PluginManager" />. /// </summary> - public class PluginManager : IPluginManager + public sealed class PluginManager : IPluginManager, IDisposable { private const string MetafileName = "meta.json"; @@ -191,15 +189,6 @@ namespace Emby.Server.Implementations.Plugins } } - /// <inheritdoc /> - public void UnloadAssemblies() - { - foreach (var assemblyLoadContext in _assemblyLoadContexts) - { - assemblyLoadContext.Unload(); - } - } - /// <summary> /// Creates all the plugin instances. /// </summary> @@ -441,6 +430,15 @@ namespace Emby.Server.Implementations.Plugins return SaveManifest(manifest, path); } + /// <inheritdoc /> + public void Dispose() + { + foreach (var assemblyLoadContext in _assemblyLoadContexts) + { + assemblyLoadContext.Unload(); + } + } + /// <summary> /// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path. /// If no file is found, no reconciliation occurs. @@ -677,7 +675,7 @@ namespace Emby.Server.Implementations.Plugins } catch (JsonException ex) { - _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data!)); + _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data)); } if (manifest is not null) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs new file mode 100644 index 000000000..acd4bf905 --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Deletes path references from collections and playlists that no longer exists. +/// </summary> +public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask +{ + private readonly ILocalizationManager _localization; + private readonly ICollectionManager _collectionManager; + private readonly IPlaylistManager _playlistManager; + private readonly ILogger<CleanupCollectionAndPlaylistPathsTask> _logger; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="CleanupCollectionAndPlaylistPathsTask"/> class. + /// </summary> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param> + /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param> + /// <param name="logger">The logger.</param> + /// <param name="providerManager">The provider manager.</param> + /// <param name="fileSystem">The filesystem.</param> + public CleanupCollectionAndPlaylistPathsTask( + ILocalizationManager localization, + ICollectionManager collectionManager, + IPlaylistManager playlistManager, + ILogger<CleanupCollectionAndPlaylistPathsTask> logger, + IProviderManager providerManager, + IFileSystem fileSystem) + { + _localization = localization; + _collectionManager = collectionManager; + _playlistManager = playlistManager; + _logger = logger; + _providerManager = providerManager; + _fileSystem = fileSystem; + } + + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylists"); + + /// <inheritdoc /> + public string Key => "CleanCollectionsAndPlaylists"; + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylistsDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false); + if (collectionsFolder is null) + { + _logger.LogDebug("There is no collections folder to be found"); + } + else + { + var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray(); + _logger.LogDebug("Found {CollectionLength} boxsets", collections.Length); + + for (var index = 0; index < collections.Length; index++) + { + var collection = collections[index]; + _logger.LogDebug("Checking boxset {CollectionName}", collection.Name); + + CleanupLinkedChildren(collection, cancellationToken); + progress.Report(50D / collections.Length * (index + 1)); + } + } + + var playlistsFolder = _playlistManager.GetPlaylistsFolder(); + if (playlistsFolder is null) + { + _logger.LogDebug("There is no playlists folder to be found"); + return; + } + + var playlists = playlistsFolder.Children.OfType<Playlist>().ToArray(); + _logger.LogDebug("Found {PlaylistLength} playlists", playlists.Length); + + for (var index = 0; index < playlists.Length; index++) + { + var playlist = playlists[index]; + _logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name); + + CleanupLinkedChildren(playlist, cancellationToken); + progress.Report(50D / playlists.Length * (index + 1)); + } + } + + private void CleanupLinkedChildren<T>(T folder, CancellationToken cancellationToken) + where T : Folder + { + List<LinkedChild>? itemsToRemove = null; + foreach (var linkedChild in folder.LinkedChildren) + { + if (!File.Exists(folder.Path)) + { + _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, linkedChild.Path); + (itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild); + } + } + + if (itemsToRemove is not null) + { + _logger.LogDebug("Updating {FolderName}", folder.Name); + folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray(); + folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken); + + _providerManager.QueueRefresh( + folder.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + RefreshPriority.High); + } + } + + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } }; + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs deleted file mode 100644 index f78fc6f97..000000000 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Collections; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.ScheduledTasks.Tasks; - -/// <summary> -/// Deletes Path references from collections that no longer exists. -/// </summary> -public class CleanupCollectionPathsTask : IScheduledTask -{ - private readonly ILocalizationManager _localization; - private readonly ICollectionManager _collectionManager; - private readonly ILogger<CleanupCollectionPathsTask> _logger; - private readonly IProviderManager _providerManager; - private readonly IFileSystem _fileSystem; - - /// <summary> - /// Initializes a new instance of the <see cref="CleanupCollectionPathsTask"/> class. - /// </summary> - /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> - /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param> - /// <param name="logger">The logger.</param> - /// <param name="providerManager">The provider manager.</param> - /// <param name="fileSystem">The filesystem.</param> - public CleanupCollectionPathsTask( - ILocalizationManager localization, - ICollectionManager collectionManager, - ILogger<CleanupCollectionPathsTask> logger, - IProviderManager providerManager, - IFileSystem fileSystem) - { - _localization = localization; - _collectionManager = collectionManager; - _logger = logger; - _providerManager = providerManager; - _fileSystem = fileSystem; - } - - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskCleanCollections"); - - /// <inheritdoc /> - public string Key => "CleanCollections"; - - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription"); - - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - - /// <inheritdoc /> - public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) - { - var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false); - if (collectionsFolder is null) - { - _logger.LogDebug("There is no collection folder to be found"); - return; - } - - var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray(); - _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length); - - var itemsToRemove = new List<LinkedChild>(); - for (var index = 0; index < collections.Length; index++) - { - var collection = collections[index]; - _logger.LogDebug("Check Boxset {CollectionName}", collection.Name); - - foreach (var collectionLinkedChild in collection.LinkedChildren) - { - if (!File.Exists(collectionLinkedChild.Path)) - { - _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}", collection.Name, collectionLinkedChild.Path); - itemsToRemove.Add(collectionLinkedChild); - } - } - - if (itemsToRemove.Count != 0) - { - _logger.LogDebug("Update Boxset {CollectionName}", collection.Name); - collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray(); - await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken) - .ConfigureAwait(false); - - _providerManager.QueueRefresh( - collection.Id, - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ForceSave = true - }, - RefreshPriority.High); - - itemsToRemove.Clear(); - } - - progress.Report(100D / collections.Length * (index + 1)); - } - } - - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() - { - return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } }; - } -} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 03ff96b19..e935f7e5e 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -36,6 +36,7 @@ using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; @@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Session /// <summary> /// Class SessionManager. /// </summary> - public class SessionManager : ISessionManager, IDisposable + public sealed class SessionManager : ISessionManager, IAsyncDisposable { private readonly IUserDataManager _userDataManager; private readonly ILogger<SessionManager> _logger; @@ -57,11 +58,9 @@ namespace Emby.Server.Implementations.Session private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerApplicationHost _appHost; private readonly IDeviceManager _deviceManager; - - /// <summary> - /// The active connections. - /// </summary> - private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase); + private readonly CancellationTokenRegistration _shutdownCallback; + private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections + = new(StringComparer.OrdinalIgnoreCase); private Timer _idleTimer; @@ -79,7 +78,8 @@ namespace Emby.Server.Implementations.Session IImageProcessor imageProcessor, IServerApplicationHost appHost, IDeviceManager deviceManager, - IMediaSourceManager mediaSourceManager) + IMediaSourceManager mediaSourceManager, + IHostApplicationLifetime hostApplicationLifetime) { _logger = logger; _eventManager = eventManager; @@ -92,6 +92,7 @@ namespace Emby.Server.Implementations.Session _appHost = appHost; _deviceManager = deviceManager; _mediaSourceManager = mediaSourceManager; + _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping); _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated; } @@ -151,36 +152,6 @@ namespace Emby.Server.Implementations.Session } } - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and optionally managed resources. - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _idleTimer?.Dispose(); - } - - _idleTimer = null; - - _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated; - - _disposed = true; - } - private void CheckDisposed() { if (_disposed) @@ -980,28 +951,28 @@ namespace Emby.Server.Implementations.Session private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) { - bool playedToCompletion = false; - - if (!playbackFailed) + if (playbackFailed) { - var data = _userDataManager.GetUserData(user, item); - - if (positionTicks.HasValue) - { - playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value); - } - else - { - // If the client isn't able to report this, then we'll just have to make an assumption - data.PlayCount++; - data.Played = item.SupportsPlayedStatus; - data.PlaybackPositionTicks = 0; - playedToCompletion = true; - } + return false; + } - _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None); + var data = _userDataManager.GetUserData(user, item); + bool playedToCompletion; + if (positionTicks.HasValue) + { + playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value); + } + else + { + // If the client isn't able to report this, then we'll just have to make an assumption + data.PlayCount++; + data.Played = item.SupportsPlayedStatus; + data.PlaybackPositionTicks = 0; + playedToCompletion = true; } + _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None); + return playedToCompletion; } @@ -1331,32 +1302,6 @@ namespace Emby.Server.Implementations.Session } /// <summary> - /// Sends the server shutdown notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendServerShutdownNotification(CancellationToken cancellationToken) - { - CheckDisposed(); - - return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken); - } - - /// <summary> - /// Sends the server restart notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendServerRestartNotification(CancellationToken cancellationToken) - { - CheckDisposed(); - - _logger.LogDebug("Beginning SendServerRestartNotification"); - - return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken); - } - - /// <summary> /// Adds the additional user. /// </summary> /// <param name="sessionId">The session identifier.</param> @@ -1509,35 +1454,20 @@ namespace Emby.Server.Implementations.Session new DeviceQuery { DeviceId = deviceId, - UserId = user.Id, - Limit = 1 - }).ConfigureAwait(false)).Items.FirstOrDefault(); - - var allExistingForDevice = (await _deviceManager.GetDevices( - new DeviceQuery - { - DeviceId = deviceId + UserId = user.Id }).ConfigureAwait(false)).Items; - foreach (var auth in allExistingForDevice) + foreach (var auth in existing) { - if (existing is null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal)) + try { - try - { - await Logout(auth).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while logging out."); - } + // Logout any existing sessions for the user on this device + await Logout(auth).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while logging out existing session."); } - } - - if (existing is not null) - { - _logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken); - return existing.AccessToken; } _logger.LogInformation("Creating new access token for user {0}", user.Id); @@ -1848,5 +1778,53 @@ namespace Emby.Server.Implementations.Session return SendMessageToSessions(sessions, name, data, cancellationToken); } + + /// <inheritdoc /> + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + foreach (var session in _activeConnections.Values) + { + await session.DisposeAsync().ConfigureAwait(false); + } + + if (_idleTimer is not null) + { + await _idleTimer.DisposeAsync().ConfigureAwait(false); + _idleTimer = null; + } + + await _shutdownCallback.DisposeAsync().ConfigureAwait(false); + + _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated; + _disposed = true; + } + + private async void OnApplicationStopping() + { + _logger.LogInformation("Sending shutdown notifications"); + try + { + var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.ServerShuttingDown; + + await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending server shutdown notifications"); + } + + // Close open websockets to allow Kestrel to shut down cleanly + foreach (var session in _activeConnections.Values) + { + await session.DisposeAsync().ConfigureAwait(false); + } + + _activeConnections.Clear(); + } } } diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index f0e173f0b..ef890aeb4 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -135,13 +135,13 @@ namespace Emby.Server.Implementations.TV private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions) { - var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false)); + var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, request.EnableResumable, false)); if (request.EnableRewatching) { - allNextUp = allNextUp.Concat( - seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true))) - .OrderByDescending(i => i.LastWatchedDate); + allNextUp = allNextUp + .Concat(seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false, true))) + .OrderByDescending(i => i.LastWatchedDate); } // If viewing all next up for all series, remove first episodes @@ -183,7 +183,7 @@ namespace Emby.Server.Implementations.TV /// Gets the next up. /// </summary> /// <returns>Task{Episode}.</returns> - private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) + private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool includeResumable, bool includePlayed) { var lastQuery = new InternalItemsQuery(user) { @@ -200,8 +200,8 @@ namespace Emby.Server.Implementations.TV } }; - // If rewatching is enabled, sort first by date played and then by season and episode numbers - lastQuery.OrderBy = rewatching + // If including played results, sort first by date played and then by season and episode numbers + lastQuery.OrderBy = includePlayed ? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) } : new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }; @@ -216,7 +216,7 @@ namespace Emby.Server.Implementations.TV IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) }, Limit = 1, - IsPlayed = rewatching, + IsPlayed = includePlayed, IsVirtualItem = false, ParentIndexNumberNotEquals = 0, DtoOptions = dtoOptions @@ -240,7 +240,7 @@ namespace Emby.Server.Implementations.TV SeriesPresentationUniqueKey = seriesKey, ParentIndexNumber = 0, IncludeItemTypes = new[] { BaseItemKind.Episode }, - IsPlayed = rewatching, + IsPlayed = includePlayed, IsVirtualItem = false, DtoOptions = dtoOptions }) @@ -269,7 +269,7 @@ namespace Emby.Server.Implementations.TV nextEpisode = sortedConsideredEpisodes.FirstOrDefault(); } - if (nextEpisode is not null) + if (nextEpisode is not null && !includeResumable) { var userData = _userDataManager.GetUserData(user, nextEpisode); diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index ce684e457..065a4ce5c 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1651,7 +1651,7 @@ public class DynamicHlsController : BaseJellyfinApiController _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer), threads, mapArgs, - GetVideoArguments(state, startNumber, isEventPlaylist), + GetVideoArguments(state, startNumber, isEventPlaylist, segmentContainer), GetAudioArguments(state), maxMuxingQueueSize, state.SegmentLength.ToString(CultureInfo.InvariantCulture), @@ -1703,19 +1703,18 @@ public class DynamicHlsController : BaseJellyfinApiController } var audioCodec = _encodingHelper.GetAudioEncoder(state); + var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); if (!state.IsOutputVideo) { if (EncodingHelper.IsCopyCodec(audioCodec)) { - var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); - return "-acodec copy -strict -2" + bitStreamArgs; } var audioTranscodeParams = string.Empty; - audioTranscodeParams += "-acodec " + audioCodec; + audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs; var audioBitrate = state.OutputAudioBitrate; var audioChannels = state.OutputAudioChannels; @@ -1761,7 +1760,6 @@ public class DynamicHlsController : BaseJellyfinApiController if (EncodingHelper.IsCopyCodec(audioCodec)) { var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); - var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs; if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) @@ -1772,7 +1770,7 @@ public class DynamicHlsController : BaseJellyfinApiController return copyArgs; } - var args = "-codec:a:0 " + audioCodec + strictArgs; + var args = "-codec:a:0 " + audioCodec + bitStreamArgs + strictArgs; var channels = state.OutputAudioChannels; @@ -1816,8 +1814,9 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="state">The <see cref="StreamState"/>.</param> /// <param name="startNumber">The first number in the hls sequence.</param> /// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param> + /// <param name="segmentContainer">The segment container.</param> /// <returns>The command line arguments for video transcoding.</returns> - private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist) + private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist, string segmentContainer) { if (state.VideoStream is null) { @@ -1909,7 +1908,7 @@ public class DynamicHlsController : BaseJellyfinApiController } // TODO why was this not enabled for VOD? - if (isEventPlaylist) + if (isEventPlaylist && string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase)) { args += " -flags -global_header"; } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 267ba4afb..649397d68 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; @@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController private readonly IMediaSourceManager _mediaSourceManager; private readonly IConfigurationManager _configurationManager; private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly ISessionManager _sessionManager; /// <summary> /// Initializes a new instance of the <see cref="LiveTvController"/> class. @@ -61,7 +59,6 @@ public class LiveTvController : BaseJellyfinApiController /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> - /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> public LiveTvController( ILiveTvManager liveTvManager, IUserManager userManager, @@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController IDtoService dtoService, IMediaSourceManager mediaSourceManager, IConfigurationManager configurationManager, - TranscodingJobHelper transcodingJobHelper, - ISessionManager sessionManager) + TranscodingJobHelper transcodingJobHelper) { _liveTvManager = liveTvManager; _userManager = userManager; @@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController _mediaSourceManager = mediaSourceManager; _configurationManager = configurationManager; _transcodingJobHelper = transcodingJobHelper; - _sessionManager = sessionManager; } /// <summary> diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index a29790961..42ac4a9b4 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Mime; -using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using MediaBrowser.Common.Configuration; @@ -107,11 +106,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult RestartApplication() { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - _appHost.Restart(); - }); + _appHost.Restart(); return NoContent(); } @@ -127,11 +122,7 @@ public class SystemController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult ShutdownApplication() { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - await _appHost.Shutdown().ConfigureAwait(false); - }); + _appHost.Shutdown(); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 7d23281f2..bdbbd1e0d 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -68,7 +68,8 @@ public class TvShowsController : BaseJellyfinApiController /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param> /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param> - /// <param name="enableRewatching">Whether to include watched episode in next up results.</param> + /// <param name="enableResumable">Whether to include resumable episodes in next up results.</param> + /// <param name="enableRewatching">Whether to include watched episodes in next up results.</param> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> [HttpGet("NextUp")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -86,6 +87,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool disableFirstEpisode = false, + [FromQuery] bool enableResumable = true, [FromQuery] bool enableRewatching = false) { userId = RequestHelpers.GetUserId(User, userId); @@ -104,6 +106,7 @@ public class TvShowsController : BaseJellyfinApiController EnableTotalRecordCount = enableTotalRecordCount, DisableFirstEpisode = disableFirstEpisode, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, + EnableResumable = enableResumable, EnableRewatching = enableRewatching }, options); diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 63667e7e6..fe602fba3 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -693,7 +693,7 @@ public class DynamicHlsHelper // Currently we only transcode to 8 bits AV1 int bitDepth = 8; if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && state.VideoStream != null + && state.VideoStream is not null && state.VideoStream.BitDepth.HasValue) { bitDepth = state.VideoStream.BitDepth.Value; diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index cee8e0f9b..73ebb396d 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -620,7 +620,7 @@ public class TranscodingJobHelper : IDisposable state.TranscodingJob = transcodingJob; // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback - _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream); + _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream); // Wait for the file to exist before proceeding var ffmpegTargetFile = state.WaitForPath ?? outputPath; diff --git a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs index 8bf626035..acf3645fd 100644 --- a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs +++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs @@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware /// <returns>The async task.</returns> public async Task Invoke(HttpContext httpContext) { - var localPath = httpContext.Request.Path.ToString(); - if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase)) + if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase)) { _logger.LogDebug("Redirecting robots.txt request to web/robots.txt"); httpContext.Response.Redirect("web/robots.txt"); diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs index a34fd01d5..3e3604b2a 100644 --- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs @@ -77,7 +77,7 @@ public class CommaDelimitedArrayModelBinder : IModelBinder var typedValueIndex = 0; for (var i = 0; i < parsedValues.Length; i++) { - if (parsedValues[i] != null) + if (parsedValues[i] is not null) { typedValues.SetValue(parsedValues[i], typedValueIndex); typedValueIndex++; diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs index cb9a82955..ae9f0a8cd 100644 --- a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs @@ -77,7 +77,7 @@ public class PipeDelimitedArrayModelBinder : IModelBinder var typedValueIndex = 0; for (var i = 0; i < parsedValues.Length; i++) { - if (parsedValues[i] != null) + if (parsedValues[i] is not null) { typedValues.SetValue(parsedValues[i], typedValueIndex); typedValueIndex++; diff --git a/Jellyfin.Data/Enums/PersonKind.cs b/Jellyfin.Data/Enums/PersonKind.cs index 10a805666..29308789a 100644 --- a/Jellyfin.Data/Enums/PersonKind.cs +++ b/Jellyfin.Data/Enums/PersonKind.cs @@ -94,4 +94,40 @@ public enum PersonKind /// A person who was the illustrator. /// </summary> Illustrator, + + /// <summary> + /// A person responsible for drawing the art. + /// </summary> + Penciller, + + /// <summary> + /// A person responsible for inking the pencil art. + /// </summary> + Inker, + + /// <summary> + /// A person responsible for applying color to drawings. + /// </summary> + Colorist, + + /// <summary> + /// A person responsible for drawing text and speech bubbles. + /// </summary> + Letterer, + + /// <summary> + /// A person responsible for drawing the cover art. + /// </summary> + CoverArtist, + + /// <summary> + /// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter. + /// An editor may also prepare a resource for production, publication, or distribution. + /// </summary> + Editor, + + /// <summary> + /// A person who renders a text from one language into another. + /// </summary> + Translator } diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 573c36be7..90ebcd390 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -164,7 +164,7 @@ namespace Jellyfin.Networking.Configuration public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>(); /// <summary> - /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>. + /// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="IsRemoteIPFilterBlacklist"/>. /// </summary> public string[] RemoteIPFilter { get; set; } = Array.Empty<string>(); diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs index 72f3d6e8e..cb2d09a67 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Authentication; @@ -39,14 +40,18 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc /> // This is the version that we need to use for local users. Because reasons. - public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser) + public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser) { - if (resolvedUser is null) + [DoesNotReturn] + static void ThrowAuthenticationException() { - throw new AuthenticationException("Specified user does not exist."); + throw new AuthenticationException("Invalid username or password"); } - bool success = false; + if (resolvedUser is null) + { + ThrowAuthenticationException(); + } // As long as jellyfin supports password-less users, we need this little block here to accommodate if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password)) @@ -60,15 +65,13 @@ namespace Jellyfin.Server.Implementations.Users // Handle the case when the stored password is null, but the user tried to login with a password if (resolvedUser.Password is null) { - throw new AuthenticationException("Invalid username or password"); + ThrowAuthenticationException(); } PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); - success = _cryptographyProvider.Verify(readyHash, password); - - if (!success) + if (!_cryptographyProvider.Verify(readyHash, password)) { - throw new AuthenticationException("Invalid username or password"); + ThrowAuthenticationException(); } // Migrate old hashes to the new default diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index ec0c64cd7..94ac4798c 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -20,7 +20,6 @@ using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Users; using Microsoft.EntityFrameworkCore; @@ -35,7 +34,6 @@ namespace Jellyfin.Server.Implementations.Users { private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IEventManager _eventManager; - private readonly ICryptoProvider _cryptoProvider; private readonly INetworkManager _networkManager; private readonly IApplicationHost _appHost; private readonly IImageProcessor _imageProcessor; @@ -53,7 +51,6 @@ namespace Jellyfin.Server.Implementations.Users /// </summary> /// <param name="dbProvider">The database provider.</param> /// <param name="eventManager">The event manager.</param> - /// <param name="cryptoProvider">The cryptography provider.</param> /// <param name="networkManager">The network manager.</param> /// <param name="appHost">The application host.</param> /// <param name="imageProcessor">The image processor.</param> @@ -61,7 +58,6 @@ namespace Jellyfin.Server.Implementations.Users public UserManager( IDbContextFactory<JellyfinDbContext> dbProvider, IEventManager eventManager, - ICryptoProvider cryptoProvider, INetworkManager networkManager, IApplicationHost appHost, IImageProcessor imageProcessor, @@ -69,7 +65,6 @@ namespace Jellyfin.Server.Implementations.Users { _dbProvider = dbProvider; _eventManager = eventManager; - _cryptoProvider = cryptoProvider; _networkManager = networkManager; _appHost = appHost; _imageProcessor = imageProcessor; @@ -384,7 +379,7 @@ namespace Jellyfin.Server.Implementations.Users } var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); - var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint) + var authResult = await AuthenticateLocalUser(username, password, user) .ConfigureAwait(false); var authenticationProvider = authResult.AuthenticationProvider; var success = authResult.Success; @@ -787,8 +782,7 @@ namespace Jellyfin.Server.Implementations.Users private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser( string username, string password, - User? user, - string remoteEndPoint) + User? user) { bool success = false; IAuthenticationProvider? authenticationProvider = null; @@ -833,7 +827,7 @@ namespace Jellyfin.Server.Implementations.Users } catch (AuthenticationException ex) { - _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); + _logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name); return (username, false); } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 0c6315c66..4c116745b 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -103,9 +103,6 @@ namespace Jellyfin.Server } /// <inheritdoc /> - protected override void RestartInternal() => Program.Restart(); - - /// <inheritdoc /> protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal() { // Jellyfin.Server @@ -114,8 +111,5 @@ namespace Jellyfin.Server // Jellyfin.Server.Implementations yield return typeof(JellyfinDbContext).Assembly; } - - /// <inheritdoc /> - protected override void ShutdownInternal() => Program.Shutdown(); } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index e1dfa1d31..89dbbdd2f 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -59,6 +59,7 @@ namespace Jellyfin.Server.Extensions serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>(); + serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>(); return serviceCollection.AddAuthorizationCore(options => { @@ -276,7 +277,7 @@ namespace Jellyfin.Server.Extensions } else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet)) { - if (subnet != null) + if (subnet is not null) { AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs index fda6e5465..66d393dec 100644 --- a/Jellyfin.Server/Helpers/StartupHelpers.cs +++ b/Jellyfin.Server/Helpers/StartupHelpers.cs @@ -15,7 +15,6 @@ using MediaBrowser.Model.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Serilog; -using SQLitePCL; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server.Helpers; @@ -297,7 +296,5 @@ public static class StartupHelpers // Disable the "Expect: 100-Continue" header by default // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c ServicePointManager.Expect100Continue = false; - - Batteries_V2.Init(); } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 146de3ae1..62abb8935 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -48,7 +48,6 @@ <PackageReference Include="Serilog.Sinks.Console" /> <PackageReference Include="Serilog.Sinks.File" /> <PackageReference Include="Serilog.Sinks.Graylog" /> - <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" /> </ItemGroup> <ItemGroup> diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs index bee135efd..0544fe561 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Xml; using System.Xml.Serialization; @@ -59,21 +59,17 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine private OldMusicBrainzConfiguration? ReadOld(string path) { - using (var xmlReader = XmlReader.Create(path)) - { - var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration")); - return serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration; - } + using var xmlReader = XmlReader.Create(path); + var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration")); + return serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration; } private void WriteNew(string path, PluginConfiguration newPluginConfiguration) { var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration")); var xmlWriterSettings = new XmlWriterSettings { Indent = true }; - using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings)) - { - pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration); - } + using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration); } #pragma warning disable diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs index a4379197c..c6d86b8cd 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs @@ -43,10 +43,8 @@ public class MigrateNetworkConfiguration : IMigrationRoutine try { - using (var xmlReader = XmlReader.Create(path)) - { - oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader); - } + using var xmlReader = XmlReader.Create(path); + oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader); } catch (InvalidOperationException ex) { @@ -97,10 +95,8 @@ public class MigrateNetworkConfiguration : IMigrationRoutine var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration)); var xmlWriterSettings = new XmlWriterSettings { Indent = true }; - using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings)) - { - networkConfigSerializer.Serialize(xmlWriter, networkConfiguration); - } + using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings); + networkConfigSerializer.Serialize(xmlWriter, networkConfiguration); } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index e8a0af9f8..2f23cb1f8 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -5,9 +5,9 @@ using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { @@ -61,17 +61,15 @@ namespace Jellyfin.Server.Migrations.Routines }; var dataPath = _paths.DataPath; - using (var connection = SQLite3.Open( - Path.Combine(dataPath, DbFilename), - ConnectionFlags.ReadOnly, - null)) + using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) { - using var userDbConnection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null); + connection.Open(); + + using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}"); + userDbConnection.Open(); _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin."); using var dbContext = _provider.CreateDbContext(); - var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id"); - // Make sure that the database is empty in case of failed migration due to power outages, etc. dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs); dbContext.SaveChanges(); @@ -81,51 +79,52 @@ namespace Jellyfin.Server.Migrations.Routines var newEntries = new List<ActivityLog>(); + var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id"); + foreach (var entry in queryResult) { - if (!logLevelDictionary.TryGetValue(entry[8].ToString(), out var severity)) + if (!logLevelDictionary.TryGetValue(entry.GetString(8), out var severity)) { severity = LogLevel.Trace; } var guid = Guid.Empty; - if (entry[6].SQLiteType != SQLiteType.Null && !Guid.TryParse(entry[6].ToString(), out guid)) + if (!entry.IsDBNull(6) && !entry.TryGetGuid(6, out guid)) { + var id = entry.GetString(6); // This is not a valid Guid, see if it is an internal ID from an old Emby schema - _logger.LogWarning("Invalid Guid in UserId column: {Guid}", entry[6].ToString()); + _logger.LogWarning("Invalid Guid in UserId column: {Guid}", id); using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id"); - statement.TryBind("@Id", entry[6].ToString()); + statement.TryBind("@Id", id); - foreach (var row in statement.Query()) + using var reader = statement.ExecuteReader(); + if (reader.HasRows && reader.Read() && reader.TryGetGuid(0, out guid)) { - if (row.Count > 0 && Guid.TryParse(row[0].ToString(), out guid)) - { - // Successfully parsed a Guid from the user table. - break; - } + // Successfully parsed a Guid from the user table. + break; } } - var newEntry = new ActivityLog(entry[1].ToString(), entry[4].ToString(), guid) + var newEntry = new ActivityLog(entry.GetString(1), entry.GetString(4), guid) { - DateCreated = entry[7].ReadDateTime(), + DateCreated = entry.GetDateTime(7), LogSeverity = severity }; - if (entry[2].SQLiteType != SQLiteType.Null) + if (entry.TryGetString(2, out var result)) { - newEntry.Overview = entry[2].ToString(); + newEntry.Overview = result; } - if (entry[3].SQLiteType != SQLiteType.Null) + if (entry.TryGetString(3, out result)) { - newEntry.ShortOverview = entry[3].ToString(); + newEntry.ShortOverview = result; } - if (entry[5].SQLiteType != SQLiteType.Null) + if (entry.TryGetString(5, out result)) { - newEntry.ItemId = entry[5].ToString(); + newEntry.ItemId = result; } newEntries.Add(newEntry); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index 09daae0ff..c845beef2 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -6,9 +6,9 @@ using Jellyfin.Data.Entities.Security; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { @@ -56,34 +56,32 @@ namespace Jellyfin.Server.Migrations.Routines public void Perform() { var dataPath = _appPaths.DataPath; - using (var connection = SQLite3.Open( - Path.Combine(dataPath, DbFilename), - ConnectionFlags.ReadOnly, - null)) + using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) { + connection.Open(); using var dbContext = _dbProvider.CreateDbContext(); var authenticatedDevices = connection.Query("SELECT * FROM Tokens"); foreach (var row in authenticatedDevices) { - var dateCreatedStr = row[9].ToString(); + var dateCreatedStr = row.GetString(9); _ = DateTime.TryParse(dateCreatedStr, out var dateCreated); - var dateLastActivityStr = row[10].ToString(); + var dateLastActivityStr = row.GetString(10); _ = DateTime.TryParse(dateLastActivityStr, out var dateLastActivity); - if (row[6].IsDbNull()) + if (row.IsDBNull(6)) { - dbContext.ApiKeys.Add(new ApiKey(row[3].ToString()) + dbContext.ApiKeys.Add(new ApiKey(row.GetString(3)) { - AccessToken = row[1].ToString(), + AccessToken = row.GetString(1), DateCreated = dateCreated, DateLastActivity = dateLastActivity }); } else { - var userId = new Guid(row[6].ToString()); + var userId = row.GetGuid(6); var user = _userManager.GetUserById(userId); if (user is null) { @@ -92,14 +90,14 @@ namespace Jellyfin.Server.Migrations.Routines } dbContext.Devices.Add(new Device( - new Guid(row[6].ToString()), - row[3].ToString(), - row[4].ToString(), - row[5].ToString(), - row[2].ToString()) + userId, + row.GetString(3), + row.GetString(4), + row.GetString(5), + row.GetString(2)) { - AccessToken = row[1].ToString(), - IsActive = row[8].ToBool(), + AccessToken = row.GetString(1), + IsActive = row.GetBoolean(8), DateCreated = dateCreated, DateLastActivity = dateLastActivity }); @@ -110,12 +108,12 @@ namespace Jellyfin.Server.Migrations.Routines var deviceIds = new HashSet<string>(); foreach (var row in deviceOptions) { - if (row[2].IsDbNull()) + if (row.IsDBNull(2)) { continue; } - var deviceId = row[2].ToString(); + var deviceId = row.GetString(2); if (deviceIds.Contains(deviceId)) { continue; @@ -125,7 +123,7 @@ namespace Jellyfin.Server.Migrations.Routines dbContext.DeviceOptions.Add(new DeviceOptions(deviceId) { - CustomName = row[1].IsDbNull() ? null : row[1].ToString() + CustomName = row.IsDBNull(1) ? null : row.GetString(1) }); } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 8fe2b087d..249b39ae4 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -4,15 +4,16 @@ using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; +using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { @@ -83,22 +84,23 @@ namespace Jellyfin.Server.Migrations.Routines var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); - using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null)) + using (var connection = new SqliteConnection($"Filename={dbFilePath}")) { + connection.Open(); using var dbContext = _provider.CreateDbContext(); var results = connection.Query("SELECT * FROM userdisplaypreferences"); foreach (var result in results) { - var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToBlob(), _jsonOptions); + var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result.GetStream(3), _jsonOptions); if (dto is null) { continue; } - var itemId = new Guid(result[1].ToBlob()); - var dtoUserId = new Guid(result[1].ToBlob()); - var client = result[2].ToString(); + var itemId = result.GetGuid(1); + var dtoUserId = itemId; + var client = result.GetString(2); var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}"; if (displayPrefs.Contains(displayPreferencesKey)) { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 9dee520a5..e1a43bb48 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -1,13 +1,12 @@ using System; using System.Globalization; using System.IO; - using Emby.Server.Implementations.Data; using MediaBrowser.Controller; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Globalization; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { @@ -20,17 +19,14 @@ namespace Jellyfin.Server.Migrations.Routines private readonly ILogger<MigrateRatingLevels> _logger; private readonly IServerApplicationPaths _applicationPaths; private readonly ILocalizationManager _localizationManager; - private readonly IItemRepository _repository; public MigrateRatingLevels( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, - ILocalizationManager localizationManager, - IItemRepository repository) + ILocalizationManager localizationManager) { _applicationPaths = applicationPaths; _localizationManager = localizationManager; - _repository = repository; _logger = loggerFactory.CreateLogger<MigrateRatingLevels>(); } @@ -70,16 +66,14 @@ namespace Jellyfin.Server.Migrations.Routines // Migrate parental rating strings to new levels _logger.LogInformation("Recalculating parental rating levels based on rating string."); - using (var connection = SQLite3.Open( - dbPath, - ConnectionFlags.ReadWrite, - null)) + using var connection = new SqliteConnection($"Filename={dbPath}"); + connection.Open(); + using (var transaction = connection.BeginTransaction()) { var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems"); foreach (var entry in queryResult) { - var ratingString = entry[0].ToString(); - if (string.IsNullOrEmpty(ratingString)) + if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString)) { connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';"); } @@ -91,12 +85,14 @@ namespace Jellyfin.Server.Migrations.Routines ratingValue = "NULL"; } - var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); + using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); statement.TryBind("@Value", ratingValue); statement.TryBind("@Rating", ratingString); - statement.ExecuteQuery(); + statement.ExecuteNonQuery(); } } + + transaction.Commit(); } } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 0186500a1..4fee88b68 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -11,9 +11,9 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; using JsonSerializer = System.Text.Json.JsonSerializer; namespace Jellyfin.Server.Migrations.Routines @@ -64,8 +64,9 @@ namespace Jellyfin.Server.Migrations.Routines var dataPath = _paths.DataPath; _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin."); - using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null)) + using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) { + connection.Open(); var dbContext = _provider.CreateDbContext(); var queryResult = connection.Query("SELECT * FROM LocalUsersv2"); @@ -75,7 +76,7 @@ namespace Jellyfin.Server.Migrations.Routines foreach (var entry in queryResult) { - UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.Options); + UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry.GetStream(2), JsonDefaults.Options); if (mockup is null) { continue; @@ -108,8 +109,8 @@ namespace Jellyfin.Server.Migrations.Routines var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!) { - Id = entry[1].ReadGuidFromBlob(), - InternalId = entry[0].ToInt64(), + Id = entry.GetGuid(1), + InternalId = entry.GetInt64(0), MaxParentalAgeRating = policy.MaxParentalRating, EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess, RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit, diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs index 6c26e47e1..7b0d9456d 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs @@ -1,10 +1,11 @@ using System; using System.Globalization; using System.IO; - +using System.Linq; +using Emby.Server.Implementations.Data; using MediaBrowser.Controller; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { @@ -37,14 +38,13 @@ namespace Jellyfin.Server.Migrations.Routines { var dataPath = _paths.DataPath; var dbPath = Path.Combine(dataPath, DbFilename); - using (var connection = SQLite3.Open( - dbPath, - ConnectionFlags.ReadWrite, - null)) + using var connection = new SqliteConnection($"Filename={dbPath}"); + connection.Open(); + using (var transaction = connection.BeginTransaction()) { // Query the database for the ids of duplicate extras var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'"); - var bads = string.Join(", ", queryResult.SelectScalarString()); + var bads = string.Join(", ", queryResult.Select(x => x.GetString(0))); // Do nothing if no duplicate extras were detected if (bads.Length == 0) @@ -76,6 +76,7 @@ namespace Jellyfin.Server.Migrations.Routines // Delete all duplicate extras _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads); connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')"); + transaction.Commit(); } } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 6e8b17a73..f9259d0d9 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; -using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; @@ -42,7 +41,6 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); - private static CancellationTokenSource _tokenSource = new(); private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -65,36 +63,9 @@ namespace Jellyfin.Server .MapResult(StartApp, ErrorParsingArguments); } - /// <summary> - /// Shuts down the application. - /// </summary> - internal static void Shutdown() - { - if (!_tokenSource.IsCancellationRequested) - { - _tokenSource.Cancel(); - } - } - - /// <summary> - /// Restarts the application. - /// </summary> - internal static void Restart() - { - _restartOnShutdown = true; - - Shutdown(); - } - private static async Task StartApp(StartupOptions options) { _startTimestamp = Stopwatch.GetTimestamp(); - - // Log all uncaught exceptions to std error - static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) => - Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject); - AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole; - ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager @@ -112,38 +83,10 @@ namespace Jellyfin.Server StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); - // Log uncaught exceptions to the logging instead of std error - AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole; + // Use the logging framework for uncaught exceptions instead of std error AppDomain.CurrentDomain.UnhandledException += (_, e) => _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); - // Intercept Ctrl+C and Ctrl+Break - Console.CancelKeyPress += (_, e) => - { - if (_tokenSource.IsCancellationRequested) - { - return; // Already shutting down - } - - e.Cancel = true; - _logger.LogInformation("Ctrl+C, shutting down"); - Environment.ExitCode = 128 + 2; - Shutdown(); - }; - - // Register a SIGTERM handler - AppDomain.CurrentDomain.ProcessExit += (_, _) => - { - if (_tokenSource.IsCancellationRequested) - { - return; // Already shutting down - } - - _logger.LogInformation("Received a SIGTERM signal, shutting down"); - Environment.ExitCode = 128 + 15; - Shutdown(); - }; - _logger.LogInformation( "Jellyfin version: {Version}", Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3)); @@ -173,12 +116,10 @@ namespace Jellyfin.Server do { - _restartOnShutdown = false; await StartServer(appPaths, options, startupConfig).ConfigureAwait(false); if (_restartOnShutdown) { - _tokenSource = new CancellationTokenSource(); _startTimestamp = Stopwatch.GetTimestamp(); } } while (_restartOnShutdown); @@ -186,7 +127,7 @@ namespace Jellyfin.Server private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) { - var appHost = new CoreAppHost( + using var appHost = new CoreAppHost( appPaths, _loggerFactory, options, @@ -196,6 +137,7 @@ namespace Jellyfin.Server try { host = Host.CreateDefaultBuilder() + .UseConsoleLifetime() .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger)) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) @@ -210,7 +152,7 @@ namespace Jellyfin.Server try { - await host.StartAsync(_tokenSource.Token).ConfigureAwait(false); + await host.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { @@ -219,22 +161,18 @@ namespace Jellyfin.Server StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger); } } - catch (Exception ex) when (ex is not TaskCanceledException) + catch (Exception) { _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(_tokenSource.Token).ConfigureAwait(false); + await appHost.RunStartupTasksAsync().ConfigureAwait(false); _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); - // Block main thread until shutdown - await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - // Don't throw on cancellation + await host.WaitForShutdownAsync().ConfigureAwait(false); + _restartOnShutdown = appHost.ShouldRestart; } catch (Exception ex) { @@ -257,7 +195,6 @@ namespace Jellyfin.Server } } - await appHost.DisposeAsync().ConfigureAwait(false); host?.Dispose(); } } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 96ee701b3..5985d3dd8 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common @@ -48,6 +47,11 @@ namespace MediaBrowser.Common bool IsShuttingDown { get; } /// <summary> + /// Gets a value indicating whether the application should restart. + /// </summary> + bool ShouldRestart { get; } + + /// <summary> /// Gets the application version. /// </summary> /// <value>The application version.</value> @@ -126,8 +130,7 @@ namespace MediaBrowser.Common /// <summary> /// Shuts down. /// </summary> - /// <returns>A task.</returns> - Task Shutdown(); + void Shutdown(); /// <summary> /// Initializes this instance. diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs index 1d73de3c9..0ff9719e9 100644 --- a/MediaBrowser.Common/Plugins/IPluginManager.cs +++ b/MediaBrowser.Common/Plugins/IPluginManager.cs @@ -30,11 +30,6 @@ namespace MediaBrowser.Common.Plugins IEnumerable<Assembly> LoadAssemblies(); /// <summary> - /// Unloads all of the assemblies. - /// </summary> - void UnloadAssemblies(); - - /// <summary> /// Registers the plugin's services with the DI. /// Note: DI is not yet instantiated yet. /// </summary> diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index a56d3c822..81b532fda 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System.Threading.Tasks; @@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Authentication public interface IRequiresResolvedUser { - Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser); + Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser); } public interface IHasNewUserPolicy @@ -33,8 +31,8 @@ namespace MediaBrowser.Controller.Authentication public class ProviderAuthenticationResult { - public string Username { get; set; } + public required string Username { get; set; } - public string DisplayName { get; set; } + public string? DisplayName { get; set; } } } diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index e5ce0aa21..cdc3d52b9 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -66,7 +66,7 @@ namespace MediaBrowser.Controller.Drawing /// <returns>Guid.</returns> string GetImageCacheTag(BaseItem item, ItemImageInfo image); - string GetImageCacheTag(BaseItem item, ChapterInfo chapter); + string? GetImageCacheTag(BaseItem item, ChapterInfo chapter); string? GetImageCacheTag(User user); diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs index 62b70ce53..10326363a 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using MediaBrowser.Controller.Entities; @@ -9,12 +7,12 @@ namespace MediaBrowser.Controller.Drawing { public static class ImageProcessorExtensions { - public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType) + public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType) { return processor.GetImageCacheTag(item, imageType, 0); } - public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex) + public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex) { var imageInfo = item.GetImageInfo(imageType, imageIndex); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 501811003..9f3e8eec9 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1864,7 +1864,7 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception> public bool HasImage(ImageType type, int imageIndex) { - return GetImageInfo(type, imageIndex) != null; + return GetImageInfo(type, imageIndex) is not null; } public void SetImage(ItemImageInfo image, int index) diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 095b261c0..f51162f9d 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -3,6 +3,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities public class CollectionFolder : Folder, ICollectionFolder { private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>(); + private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>(); private bool _requiresRefresh; /// <summary> @@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities } public static LibraryOptions GetLibraryOptions(string path) - { - lock (_libraryOptions) - { - if (!_libraryOptions.TryGetValue(path, out var options)) - { - options = LoadLibraryOptions(path); - _libraryOptions[path] = options; - } - - return options; - } - } + => _libraryOptions.GetOrAdd(path, LoadLibraryOptions); public static void SaveLibraryOptions(string path, LibraryOptions options) { - lock (_libraryOptions) - { - _libraryOptions[path] = options; + _libraryOptions[path] = options; - var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); - foreach (var mediaPath in clone.PathInfos) + var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); + foreach (var mediaPath in clone.PathInfos) + { + if (!string.IsNullOrEmpty(mediaPath.Path)) { - if (!string.IsNullOrEmpty(mediaPath.Path)) - { - mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); - } + mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path); } - - XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); } + + XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path)); } public static void OnCollectionFolderChange() - { - lock (_libraryOptions) - { - _libraryOptions.Clear(); - } - } + => _libraryOptions.Clear(); public override bool IsSaveLocalMetadataEnabled() { diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index 0171af27c..1d45d4da0 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -14,7 +12,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the path. /// </summary> /// <value>The path.</value> - public string Path { get; set; } + public required string Path { get; set; } /// <summary> /// Gets or sets the type. @@ -36,9 +34,9 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the blurhash. /// </summary> /// <value>The blurhash.</value> - public string BlurHash { get; set; } + public string? BlurHash { get; set; } [JsonIgnore] - public bool IsLocalFile => Path is null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); + public bool IsLocalFile => !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 597b4cecb..bf31508c1 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.Controller.Entities.TV } [JsonIgnore] - public bool IsInSeasonFolder => FindParent<Season>() != null; + public bool IsInSeasonFolder => FindParent<Season>() is not null; [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 5b7abea10..9f685b7e2 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -333,7 +333,7 @@ namespace MediaBrowser.Controller.Entities protected override bool IsActiveRecording() { - return LiveTvManager.GetActiveRecordingInfo(Path) != null; + return LiveTvManager.GetActiveRecordingInfo(Path) is not null; } public override bool CanDelete() diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index c70102167..dcd0110fb 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Library /// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns> public bool ContainsFileSystemEntryByName(string name) { - return GetFileSystemEntryByName(name) != null; + return GetFileSystemEntryByName(name) is not null; } public string GetCollectionType() diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 449ea6489..bdbc30de4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -37,6 +37,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly IMediaEncoder _mediaEncoder; private readonly ISubtitleEncoder _subtitleEncoder; private readonly IConfiguration _config; + private readonly IConfigurationManager _configurationManager; // i915 hang was fixed by linux 6.2 (3f882f2) private readonly Version _minKerneli915Hang = new Version(5, 18); @@ -47,6 +48,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3); private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1); + private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0); private static readonly string[] _videoProfilesH264 = new[] { @@ -112,12 +114,14 @@ namespace MediaBrowser.Controller.MediaEncoding IApplicationPaths appPaths, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, - IConfiguration config) + IConfiguration config, + IConfigurationManager configurationManager) { _appPaths = appPaths; _mediaEncoder = mediaEncoder; _subtitleEncoder = subtitleEncoder; _config = config; + _configurationManager = configurationManager; } [GeneratedRegex(@"\s+")] @@ -1058,7 +1062,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) { - var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat"); + var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat"); _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath); arg.Append(" -f concat -safe 0 -i ") .Append(tmpConcatPath); @@ -1213,6 +1217,12 @@ namespace MediaBrowser.Controller.MediaEncoding int bitrate = state.OutputVideoBitrate.Value; + // Bit rate under 1000k is not allowed in h264_qsv + if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + bitrate = Math.Max(bitrate, 1000); + } + // Currently use the same buffer size for all encoders int bufsize = bitrate * 2; @@ -1907,7 +1917,9 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(profile)) { - if (!string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + // Currently there's no profile option in av1_nvenc encoder + if (!(string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))) { param += " -profile:v:0 " + profile; } @@ -1995,6 +2007,14 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0"; } + /* Access unit too large: 8192 < 20880 error */ + if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) && + _mediaEncoder.EncoderVersion >= _minFFmpegVaapiH26xEncA53CcSei) + { + param += " -sei -a53_cc"; + } + return param; } @@ -2692,7 +2712,7 @@ namespace MediaBrowser.Controller.MediaEncoding string args = string.Empty; // http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1 - if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal)) + if (state.VideoStream is not null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal)) { int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream); @@ -3811,12 +3831,6 @@ namespace MediaBrowser.Controller.MediaEncoding // map from d3d11va to qsv. mainFilters.Add("hwmap=derive_device=qsv"); } - else - { - // Insert a qsv scaler to sync the decoder surface, - // msdk will passthrough this internally. - mainFilters.Add("hwmap=derive_device=qsv,scale_qsv"); - } } // hw deint @@ -5241,10 +5255,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (isD3d11Supported && isCodecAvailable) { - // set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock. - // on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11. return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) - + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty); + + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty); } } else diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index 3b34af4e9..3d288b9f8 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -20,12 +20,12 @@ namespace MediaBrowser.Controller.MediaEncoding _logger = logger; } - public async Task StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) + public async Task StartStreamingLog(EncodingJobInfo state, StreamReader reader, Stream target) { try { using (target) - using (var reader = new StreamReader(source)) + using (reader) { while (!reader.EndOfStream && reader.BaseStream.CanRead) { diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index d1a51c2cf..bb68a3b6d 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -44,6 +44,12 @@ namespace MediaBrowser.Controller.Playlists /// <summary> /// Gets the playlists folder. /// </summary> + /// <returns>Folder.</returns> + Folder GetPlaylistsFolder(); + + /// <summary> + /// Gets the playlists folder for a user. + /// </summary> /// <param name="userId">The user identifier.</param> /// <returns>Folder.</returns> Folder GetPlaylistsFolder(Guid userId); diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index b95d00aa3..282aa721e 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>BaseItem.</returns> - BaseItem ResolvePath(ItemResolveArgs args); + BaseItem? ResolvePath(ItemResolveArgs args); } public interface IMultiItemResolver diff --git a/MediaBrowser.Controller/Resolvers/ItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs index a6da8384e..5c9dd6f07 100644 --- a/MediaBrowser.Controller/Resolvers/ItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs @@ -1,5 +1,3 @@ -#nullable disable - using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>`0.</returns> - protected internal virtual T Resolve(ItemResolveArgs args) + protected internal virtual T? Resolve(ItemResolveArgs args) { return null; } @@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers /// </summary> /// <param name="args">The args.</param> /// <returns>BaseItem.</returns> - public BaseItem ResolvePath(ItemResolveArgs args) + public BaseItem? ResolvePath(ItemResolveArgs args) { var item = Resolve(args); diff --git a/MediaBrowser.Controller/Security/IAuthenticationManager.cs b/MediaBrowser.Controller/Security/IAuthenticationManager.cs index e3d18c8c0..070ab7a85 100644 --- a/MediaBrowser.Controller/Security/IAuthenticationManager.cs +++ b/MediaBrowser.Controller/Security/IAuthenticationManager.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; namespace MediaBrowser.Controller.Security diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 0c4719a0e..53df7133b 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -233,20 +233,6 @@ namespace MediaBrowser.Controller.Session Task SendRestartRequiredNotification(CancellationToken cancellationToken); /// <summary> - /// Sends the server shutdown notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendServerShutdownNotification(CancellationToken cancellationToken); - - /// <summary> - /// Sends the server restart notification. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task SendServerRestartNotification(CancellationToken cancellationToken); - - /// <summary> /// Adds the additional user. /// </summary> /// <param name="sessionId">The session identifier.</param> diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 38118ed0e..db119ce5c 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -553,7 +553,8 @@ namespace MediaBrowser.MediaEncoding.Encoder private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey) { - using (var process = new Process() + var redirectStandardIn = !string.IsNullOrEmpty(testKey); + using (var process = new Process { StartInfo = new ProcessStartInfo(path, arguments) { @@ -561,7 +562,7 @@ namespace MediaBrowser.MediaEncoding.Encoder UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false, - RedirectStandardInput = !string.IsNullOrEmpty(testKey), + RedirectStandardInput = redirectStandardIn, RedirectStandardOutput = true, RedirectStandardError = true } @@ -571,12 +572,14 @@ namespace MediaBrowser.MediaEncoding.Encoder process.Start(); - if (!string.IsNullOrEmpty(testKey)) + if (redirectStandardIn) { - process.StandardInput.Write(testKey); + using var writer = process.StandardInput; + writer.Write(testKey); } - return readStdErr ? process.StandardError.ReadToEnd() : process.StandardOutput.ReadToEnd(); + using var reader = readStdErr ? process.StandardError : process.StandardOutput; + return reader.ReadToEnd(); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 9d6cdf728..346e97ae1 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -511,7 +511,8 @@ namespace MediaBrowser.MediaEncoding.Encoder using (var processWrapper = new ProcessWrapper(process, this)) { StartProcess(processWrapper); - await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); + using var reader = process.StandardOutput; + await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); memoryStream.Seek(0, SeekOrigin.Begin); InternalMediaInfoResult result; try diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index aeb08cea3..441a3abd4 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -78,6 +78,7 @@ namespace MediaBrowser.MediaEncoding.Probing "She/Her/Hers", "5/8erl in Ehr'n", "Smith/Kotzen", + "We;Na", }; /// <summary> @@ -762,9 +763,11 @@ namespace MediaBrowser.MediaEncoding.Probing && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase); if (isAudio - || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) - || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) - || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)) + && (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase) + || string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase))) { stream.Type = MediaStreamType.EmbeddedImage; } diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index b7c23669d..07bb002ea 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -314,7 +314,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) { @@ -438,14 +438,14 @@ namespace MediaBrowser.Model.Dlna /// <param name="isAvc">True if Avc.</param> /// <returns>The <see cref="ResponseProfile"/>.</returns> public ResponseProfile? GetVideoMediaProfile( - string container, + string? container, string? audioCodec, string? videoCodec, int? width, int? height, int? bitDepth, int? videoBitrate, - string videoProfile, + string? videoProfile, VideoRangeType videoRangeType, double? videoLevel, float? videoFramerate, @@ -456,7 +456,7 @@ namespace MediaBrowser.Model.Dlna int? refFrames, int? numVideoStreams, int? numAudioStreams, - string videoCodecTag, + string? videoCodecTag, bool? isAvc) { foreach (var i in ResponseProfiles) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index f6b882c3e..889e2494a 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -135,7 +135,7 @@ namespace MediaBrowser.Model.Dlna } } - if (transcodingProfile != null) + if (transcodingProfile is not null) { if (!item.SupportsTranscoding) { @@ -179,15 +179,9 @@ namespace MediaBrowser.Model.Dlna { ValidateMediaOptions(options, true); - var mediaSources = new List<MediaSourceInfo>(); - foreach (var mediaSourceInfo in options.MediaSources) - { - if (string.IsNullOrEmpty(options.MediaSourceId) - || string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) - { - mediaSources.Add(mediaSourceInfo); - } - } + var mediaSources = string.IsNullOrEmpty(options.MediaSourceId) + ? options.MediaSources + : options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)); var streams = new List<StreamInfo>(); foreach (var mediaSourceInfo in mediaSources) @@ -216,7 +210,7 @@ namespace MediaBrowser.Model.Dlna return streams.OrderBy(i => { // Nothing beats direct playing a file - if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource.Protocol == MediaProtocol.File) + if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource?.Protocol == MediaProtocol.File) { return 0; } @@ -235,7 +229,7 @@ namespace MediaBrowser.Model.Dlna } }).ThenBy(i => { - switch (i.MediaSource.Protocol) + switch (i.MediaSource?.Protocol) { case MediaProtocol.File: return 0; @@ -246,7 +240,7 @@ namespace MediaBrowser.Model.Dlna { if (maxBitrate > 0) { - if (i.MediaSource.Bitrate.HasValue) + if (i.MediaSource?.Bitrate is not null) { return Math.Abs(i.MediaSource.Bitrate.Value - maxBitrate); } @@ -585,10 +579,10 @@ namespace MediaBrowser.Model.Dlna MediaSource = item, RunTimeTicks = item.RunTimeTicks, Context = options.Context, - DeviceProfile = options.Profile + DeviceProfile = options.Profile, + SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles) }; - playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles); var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null; var audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex); @@ -659,7 +653,8 @@ namespace MediaBrowser.Model.Dlna if (audioStreamIndex.HasValue) { playlistItem.AudioStreamIndex = audioStreamIndex; - playlistItem.AudioCodecs = new[] { item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec }; + var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec; + playlistItem.AudioCodecs = audioCodec is null ? Array.Empty<string>() : new[] { audioCodec }; } } else if (directPlay == PlayMethod.DirectStream) @@ -759,7 +754,7 @@ namespace MediaBrowser.Model.Dlna { // prefer direct copy profile float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0; - TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; + TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); @@ -842,7 +837,7 @@ namespace MediaBrowser.Model.Dlna if (videoStream is not null && videoStream.Level != 0) { - playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString()); + playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString() ?? string.Empty); } // Prefer matching audio codecs, could do better here @@ -871,7 +866,7 @@ namespace MediaBrowser.Model.Dlna // Copy matching audio codec options playlistItem.AudioSampleRate = audioStream.SampleRate; - playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString()); + playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString() ?? string.Empty); if (!string.IsNullOrEmpty(audioStream.Profile)) { @@ -880,7 +875,7 @@ namespace MediaBrowser.Model.Dlna if (audioStream.Level != 0) { - playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString()); + playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString() ?? string.Empty); } } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 00543616d..fc146df30 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1,9 +1,9 @@ -#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -34,9 +34,9 @@ namespace MediaBrowser.Model.Dlna public DlnaProfileType MediaType { get; set; } - public string Container { get; set; } + public string? Container { get; set; } - public string SubProtocol { get; set; } + public string? SubProtocol { get; set; } public long StartPositionTicks { get; set; } @@ -80,11 +80,11 @@ namespace MediaBrowser.Model.Dlna public float? MaxFramerate { get; set; } - public DeviceProfile DeviceProfile { get; set; } + public required DeviceProfile DeviceProfile { get; set; } - public string DeviceProfileId { get; set; } + public string? DeviceProfileId { get; set; } - public string DeviceId { get; set; } + public string? DeviceId { get; set; } public long? RunTimeTicks { get; set; } @@ -92,21 +92,21 @@ namespace MediaBrowser.Model.Dlna public bool EstimateContentLength { get; set; } - public MediaSourceInfo MediaSource { get; set; } + public MediaSourceInfo? MediaSource { get; set; } public string[] SubtitleCodecs { get; set; } public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } - public string SubtitleFormat { get; set; } + public string? SubtitleFormat { get; set; } - public string PlaySessionId { get; set; } + public string? PlaySessionId { get; set; } public TranscodeReason TranscodeReasons { get; set; } public Dictionary<string, string> StreamOptions { get; private set; } - public string MediaSourceId => MediaSource?.Id; + public string? MediaSourceId => MediaSource?.Id; public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; @@ -114,12 +114,12 @@ namespace MediaBrowser.Model.Dlna /// <summary> /// Gets the audio stream that will be used. /// </summary> - public MediaStream TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); + public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); /// <summary> /// Gets the video stream that will be used. /// </summary> - public MediaStream TargetVideoStream => MediaSource?.VideoStream; + public MediaStream? TargetVideoStream => MediaSource?.VideoStream; /// <summary> /// Gets the audio sample rate that will be in the output stream. @@ -259,7 +259,7 @@ namespace MediaBrowser.Model.Dlna /// <summary> /// Gets the audio sample rate that will be in the output stream. /// </summary> - public string TargetVideoProfile + public string? TargetVideoProfile { get { @@ -307,7 +307,7 @@ namespace MediaBrowser.Model.Dlna /// Gets the target video codec tag. /// </summary> /// <value>The target video codec tag.</value> - public string TargetVideoCodecTag + public string? TargetVideoCodecTag { get { @@ -364,7 +364,7 @@ namespace MediaBrowser.Model.Dlna { var stream = TargetAudioStream; - string inputCodec = stream?.Codec; + string? inputCodec = stream?.Codec; if (IsDirectStream) { @@ -389,7 +389,7 @@ namespace MediaBrowser.Model.Dlna { var stream = TargetVideoStream; - string inputCodec = stream?.Codec; + string? inputCodec = stream?.Codec; if (IsDirectStream) { @@ -417,7 +417,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return MediaSource.Size; + return MediaSource?.Size; } if (RunTimeTicks.HasValue) @@ -580,7 +580,7 @@ namespace MediaBrowser.Model.Dlna } } - public void SetOption(string qualifier, string name, string value) + public void SetOption(string? qualifier, string name, string value) { if (string.IsNullOrEmpty(qualifier)) { @@ -597,7 +597,7 @@ namespace MediaBrowser.Model.Dlna StreamOptions[name] = value; } - public string GetOption(string qualifier, string name) + public string? GetOption(string? qualifier, string name) { var value = GetOption(qualifier + "-" + name); @@ -609,7 +609,7 @@ namespace MediaBrowser.Model.Dlna return value; } - public string GetOption(string name) + public string? GetOption(string name) { if (StreamOptions.TryGetValue(name, out var value)) { @@ -619,7 +619,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public string ToUrl(string baseUrl, string accessToken) + public string ToUrl(string baseUrl, string? accessToken) { ArgumentException.ThrowIfNullOrEmpty(baseUrl); @@ -686,7 +686,7 @@ namespace MediaBrowser.Model.Dlna return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } - private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string accessToken) + private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string? accessToken) { var list = new List<NameValuePair>(); @@ -730,7 +730,7 @@ namespace MediaBrowser.Model.Dlna list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); - string liveStreamId = item.MediaSource?.LiveStreamId; + string? liveStreamId = item.MediaSource?.LiveStreamId; list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); @@ -772,7 +772,7 @@ namespace MediaBrowser.Model.Dlna list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty)); + list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); string subtitleCodecs = item.SubtitleCodecs.Length == 0 ? string.Empty : @@ -816,13 +816,18 @@ namespace MediaBrowser.Model.Dlna return list; } - public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken) + public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken) { return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); } - public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken) + public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken) { + if (MediaSource is null) + { + return Enumerable.Empty<SubtitleStreamInfo>(); + } + var list = new List<SubtitleStreamInfo>(); // HLS will preserve timestamps so we can just grab the full subtitle stream @@ -856,27 +861,36 @@ namespace MediaBrowser.Model.Dlna return list; } - private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks) + private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks) { if (enableAllProfiles) { foreach (var profile in DeviceProfile.SubtitleProfiles) { var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); - - list.Add(info); + if (info is not null) + { + list.Add(info); + } } } else { var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); - - list.Add(info); + if (info is not null) + { + list.Add(info); + } } } - private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) + private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) { + if (MediaSource is null) + { + return null; + } + var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); var info = new SubtitleStreamInfo { @@ -920,7 +934,7 @@ namespace MediaBrowser.Model.Dlna return info; } - public int? GetTargetVideoBitDepth(string codec) + public int? GetTargetVideoBitDepth(string? codec) { var value = GetOption(codec, "videobitdepth"); @@ -932,7 +946,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public int? GetTargetAudioBitDepth(string codec) + public int? GetTargetAudioBitDepth(string? codec) { var value = GetOption(codec, "audiobitdepth"); @@ -944,7 +958,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public double? GetTargetVideoLevel(string codec) + public double? GetTargetVideoLevel(string? codec) { var value = GetOption(codec, "level"); @@ -956,7 +970,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public int? GetTargetRefFrames(string codec) + public int? GetTargetRefFrames(string? codec) { var value = GetOption(codec, "maxrefframes"); @@ -968,7 +982,7 @@ namespace MediaBrowser.Model.Dlna return null; } - public int? GetTargetAudioChannels(string codec) + public int? GetTargetAudioChannels(string? codec) { var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; @@ -988,7 +1002,7 @@ namespace MediaBrowser.Model.Dlna private int? GetMediaStreamCount(MediaStreamType type, int limit) { - var count = MediaSource.GetStreamCount(type); + var count = MediaSource?.GetStreamCount(type); if (count.HasValue) { diff --git a/MediaBrowser.Model/Entities/ChapterInfo.cs b/MediaBrowser.Model/Entities/ChapterInfo.cs index 45554c3dc..d6b905651 100644 --- a/MediaBrowser.Model/Entities/ChapterInfo.cs +++ b/MediaBrowser.Model/Entities/ChapterInfo.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CS1591 using System; @@ -20,16 +19,16 @@ namespace MediaBrowser.Model.Entities /// Gets or sets the name. /// </summary> /// <value>The name.</value> - public string Name { get; set; } + public string? Name { get; set; } /// <summary> /// Gets or sets the image path. /// </summary> /// <value>The image path.</value> - public string ImagePath { get; set; } + public string? ImagePath { get; set; } public DateTime ImageDateModified { get; set; } - public string ImageTag { get; set; } + public string? ImageTag { get; set; } } } diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index 786b20e9e..ec381d423 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -10,8 +10,6 @@ namespace MediaBrowser.Model.IO /// </summary> public interface IFileSystem { - void AddShortcutHandler(IShortcutHandler handler); - /// <summary> /// Determines whether the specified filename is shortcut. /// </summary> @@ -117,13 +115,6 @@ namespace MediaBrowser.Model.IO bool ContainsSubPath(string parentPath, string path); /// <summary> - /// Normalizes the path. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>System.String.</returns> - string NormalizePath(string path); - - /// <summary> /// Gets the file name without extension. /// </summary> /// <param name="info">The information.</param> diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index 0fb996df9..35353e6fa 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Querying EnableTotalRecordCount = true; DisableFirstEpisode = false; NextUpDateCutoff = DateTime.MinValue; + EnableResumable = false; EnableRewatching = false; } @@ -84,6 +85,11 @@ namespace MediaBrowser.Model.Querying public DateTime NextUpDateCutoff { get; set; } /// <summary> + /// Gets or sets a value indicating whether to include resumable episodes as next up. + /// </summary> + public bool EnableResumable { get; set; } + + /// <summary> /// Gets or sets a value indicating whether getting rewatching next up list. /// </summary> public bool EnableRewatching { get; set; } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 5cb28402e..f3211ba45 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -765,10 +765,12 @@ namespace MediaBrowser.Providers.Manager { try { - var results = await GetSearchResults(provider, searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false); + var results = await provider.GetSearchResults(searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false); foreach (var result in results) { + result.SearchProviderName = provider.Name; + var existingMatch = resultList.FirstOrDefault(i => i.ProviderIds.Any(p => string.Equals(result.GetProviderId(p.Key), p.Value, StringComparison.OrdinalIgnoreCase))); if (existingMatch is null) @@ -800,22 +802,6 @@ namespace MediaBrowser.Providers.Manager return resultList; } - private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>( - IRemoteSearchProvider<TLookupType> provider, - TLookupType searchInfo, - CancellationToken cancellationToken) - where TLookupType : ItemLookupInfo - { - var results = await provider.GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - - foreach (var item in results) - { - item.SearchProviderName = provider.Name; - } - - return results; - } - private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item) { return _externalIds.Where(i => diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 9bcb1c39b..44f998742 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -130,7 +130,8 @@ namespace MediaBrowser.Providers.MediaInfo throw; } - output = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + using var reader = process.StandardError; + output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); MatchCollection split = LUFSRegex().Matches(output); diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index f58f5f7a3..c24f4e2fc 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -177,9 +177,11 @@ namespace MediaBrowser.Providers.MediaInfo var format = imageStream.Codec switch { + "bmp" => ImageFormat.Bmp, + "gif" => ImageFormat.Gif, "mjpeg" => ImageFormat.Jpg, "png" => ImageFormat.Png, - "gif" => ImageFormat.Gif, + "webp" => ImageFormat.Webp, _ => ImageFormat.Jpg }; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 213639371..35ea04d21 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -1,11 +1,8 @@ -#nullable disable - #pragma warning disable CA1068, CS1591 using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -83,9 +80,9 @@ namespace MediaBrowser.Providers.MediaInfo CancellationToken cancellationToken) where T : Video { - BlurayDiscInfo blurayDiscInfo = null; + BlurayDiscInfo? blurayDiscInfo = null; - Model.MediaInfo.MediaInfo mediaInfoResult = null; + Model.MediaInfo.MediaInfo? mediaInfoResult = null; if (!item.IsShortcut || options.EnableRemoteContentProbe) { @@ -131,7 +128,7 @@ namespace MediaBrowser.Providers.MediaInfo var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path); // Return if no playable .m2ts files are found - if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) + if (blurayDiscInfo is null || blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) { _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe."); return ItemUpdateType.MetadataImport; @@ -192,16 +189,14 @@ namespace MediaBrowser.Providers.MediaInfo protected async Task Fetch( Video video, CancellationToken cancellationToken, - Model.MediaInfo.MediaInfo mediaInfo, - BlurayDiscInfo blurayInfo, + Model.MediaInfo.MediaInfo? mediaInfo, + BlurayDiscInfo? blurayInfo, MetadataRefreshOptions options) { - List<MediaStream> mediaStreams; + List<MediaStream> mediaStreams = new List<MediaStream>(); IReadOnlyList<MediaAttachment> mediaAttachments; ChapterInfo[] chapters; - mediaStreams = new List<MediaStream>(); - // Add external streams before adding the streams from the file to preserve stream IDs on remote videos await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false); @@ -221,18 +216,6 @@ namespace MediaBrowser.Providers.MediaInfo video.TotalBitrate = mediaInfo.Bitrate; video.RunTimeTicks = mediaInfo.RunTimeTicks; video.Size = mediaInfo.Size; - - if (video.VideoType == VideoType.VideoFile) - { - var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.'); - - video.Container = extension; - } - else - { - video.Container = null; - } - video.Container = mediaInfo.Container; chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>(); @@ -243,8 +226,7 @@ namespace MediaBrowser.Providers.MediaInfo } else { - var currentMediaStreams = video.GetMediaStreams(); - foreach (var mediaStream in currentMediaStreams) + foreach (var mediaStream in video.GetMediaStreams()) { if (!mediaStream.IsExternal) { @@ -295,8 +277,8 @@ namespace MediaBrowser.Providers.MediaInfo _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken); } - if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || - options.MetadataRefreshMode == MetadataRefreshMode.Default) + if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh + || options.MetadataRefreshMode == MetadataRefreshMode.Default) { if (_config.Configuration.DummyChapterDuration > 0 && chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) { @@ -321,11 +303,11 @@ namespace MediaBrowser.Providers.MediaInfo { for (int i = 0; i < chapters.Length; i++) { - string name = chapters[i].Name; + string? name = chapters[i].Name; // Check if the name is empty and/or if the name is a time // Some ripping programs do that. - if (string.IsNullOrWhiteSpace(name) || - TimeSpan.TryParse(name, out _)) + if (string.IsNullOrWhiteSpace(name) + || TimeSpan.TryParse(name, out _)) { chapters[i].Name = string.Format( CultureInfo.InvariantCulture, @@ -384,23 +366,18 @@ namespace MediaBrowser.Providers.MediaInfo // Use the ffprobe values if these are empty if (videoStream is not null) { - videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate; - videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width; - videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height; + videoStream.BitRate = videoStream.BitRate.GetValueOrDefault() == 0 ? currentBitRate : videoStream.BitRate; + videoStream.Width = videoStream.Width.GetValueOrDefault() == 0 ? currentWidth : videoStream.Width; + videoStream.Height = videoStream.Height.GetValueOrDefault() == 0 ? currentHeight : videoStream.Height; } } - private bool IsEmpty(int? num) - { - return !num.HasValue || num.Value == 0; - } - /// <summary> /// Gets information about the longest playlist on a bdrom. /// </summary> /// <param name="path">The path.</param> /// <returns>VideoStream.</returns> - private BlurayDiscInfo GetBDInfo(string path) + private BlurayDiscInfo? GetBDInfo(string path) { ArgumentException.ThrowIfNullOrEmpty(path); @@ -527,32 +504,29 @@ namespace MediaBrowser.Providers.MediaInfo private void FetchPeople(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions options) { - var replaceData = options.ReplaceAllMetadata; + if (video.IsLocked + || video.LockedFields.Contains(MetadataField.Cast) + || data.People.Length == 0) + { + return; + } - if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Cast)) + if (options.ReplaceAllMetadata || _libraryManager.GetPeople(video).Count == 0) { - if (replaceData || _libraryManager.GetPeople(video).Count == 0) - { - var people = new List<PersonInfo>(); + var people = new List<PersonInfo>(); - foreach (var person in data.People) + foreach (var person in data.People) + { + PeopleHelper.AddPerson(people, new PersonInfo { - PeopleHelper.AddPerson(people, new PersonInfo - { - Name = person.Name, - Type = person.Type, - Role = person.Role - }); - } - - _libraryManager.UpdatePeople(video, people); + Name = person.Name, + Type = person.Type, + Role = person.Role + }); } - } - } - private SubtitleOptions GetOptions() - { - return _config.GetConfiguration<SubtitleOptions>("subtitles"); + _libraryManager.UpdatePeople(video, people); + } } /// <summary> @@ -575,7 +549,7 @@ namespace MediaBrowser.Providers.MediaInfo var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh; - var subtitleOptions = GetOptions(); + var subtitleOptions = _config.GetConfiguration<SubtitleOptions>("subtitles"); var libraryOptions = _libraryManager.GetLibraryOptions(video); @@ -659,9 +633,9 @@ namespace MediaBrowser.Providers.MediaInfo /// </summary> /// <param name="video">The video.</param> /// <returns>An array of dummy chapters.</returns> - private ChapterInfo[] CreateDummyChapters(Video video) + internal ChapterInfo[] CreateDummyChapters(Video video) { - var runtime = video.RunTimeTicks ?? 0; + var runtime = video.RunTimeTicks.GetValueOrDefault(); // Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted. if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks) @@ -671,30 +645,30 @@ namespace MediaBrowser.Providers.MediaInfo CultureInfo.InvariantCulture, "{0} has an invalid runtime of {1} minutes", video.Name, - TimeSpan.FromTicks(runtime).Minutes)); + TimeSpan.FromTicks(runtime).TotalMinutes)); } long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks; - if (runtime > dummyChapterDuration) + if (runtime <= dummyChapterDuration) { - int chapterCount = (int)(runtime / dummyChapterDuration); - var chapters = new ChapterInfo[chapterCount]; + return Array.Empty<ChapterInfo>(); + } - long currentChapterTicks = 0; - for (int i = 0; i < chapterCount; i++) - { - chapters[i] = new ChapterInfo - { - StartPositionTicks = currentChapterTicks - }; + int chapterCount = (int)(runtime / dummyChapterDuration); + var chapters = new ChapterInfo[chapterCount]; - currentChapterTicks += dummyChapterDuration; - } + long currentChapterTicks = 0; + for (int i = 0; i < chapterCount; i++) + { + chapters[i] = new ChapterInfo + { + StartPositionTicks = currentChapterTicks + }; - return chapters; + currentChapterTicks += dummyChapterDuration; } - return Array.Empty<ChapterInfo>(); + return chapters; } } } diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 9016e5de0..e01c0f483 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -213,11 +214,10 @@ namespace MediaBrowser.Providers.TV { // Null season numbers will have a 'dummy' season created because seasons are always required. var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); - string? seasonName = null; - if (seasonNumber.HasValue && seasonNames.TryGetValue(seasonNumber.Value, out var tmp)) + if (!seasonNumber.HasValue || !seasonNames.TryGetValue(seasonNumber.Value, out var seasonName)) { - seasonName = tmp; + seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber); } if (existingSeason is null) @@ -225,9 +225,9 @@ namespace MediaBrowser.Providers.TV var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); series.AddChild(season); } - else + else if (!string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal)) { - existingSeason.Name = GetValidSeasonNameForSeries(series, seasonName, seasonNumber); + existingSeason.Name = seasonName; await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } @@ -247,7 +247,6 @@ namespace MediaBrowser.Providers.TV int? seasonNumber, CancellationToken cancellationToken) { - seasonName = GetValidSeasonNameForSeries(series, seasonName, seasonNumber); Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); var season = new Season diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin index 912996755..aec1d4d10 100644 --- a/debian/conf/jellyfin +++ b/debian/conf/jellyfin @@ -47,4 +47,4 @@ JELLYFIN_ADDITIONAL_OPTS="" # Application username JELLYFIN_USER="jellyfin" # Full application command -JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS" +JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS --datadir $JELLYFIN_DATA_DIR --configdir $JELLYFIN_CONFIG_DIR --logdir $JELLYFIN_LOG_DIR --cachedir $JELLYFIN_CACHE_DIR" diff --git a/debian/jellyfin.init b/debian/jellyfin.init index 7f5642bac..784536d87 100644 --- a/debian/jellyfin.init +++ b/debian/jellyfin.init @@ -1,3 +1,4 @@ +#!/bin/sh ### BEGIN INIT INFO # Provides: Jellyfin Media Server # Required-Start: $local_fs $network diff --git a/debian/rules b/debian/rules index f55b1807e..069d48aad 100755 --- a/debian/rules +++ b/debian/rules @@ -25,6 +25,10 @@ ifeq ($(HOST_ARCH),arm64) # Building ARM DOTNETRUNTIME := debian-arm64 endif +ifeq ($(HOST_ARCH),aarch64) + # Building ARM + DOTNETRUNTIME := debian-arm64 +endif export DH_VERBOSE=1 export DOTNET_CLI_TELEMETRY_OPTOUT=1 diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index da986a07e..28cf585e9 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-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 09f93d41b..0e8166f46 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-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 9910773da..04c748d09 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-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 aa69b27f4..5bc197679 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-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 bb8597b41..fab869a6b 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0be7a87e-3a3f-4500-8301-49ccd6f24887/e9e36f35dbaf6625fec3e18f5c2b613f/dotnet-sdk-7.0.306-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-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-selinux-launcher.sh b/fedora/jellyfin-selinux-launcher.sh new file mode 100644 index 000000000..e07a351d9 --- /dev/null +++ b/fedora/jellyfin-selinux-launcher.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec /usr/lib64/jellyfin/jellyfin "${@}" diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service index 1b3f8032c..01accdc0c 100644 --- a/fedora/jellyfin.service +++ b/fedora/jellyfin.service @@ -8,7 +8,7 @@ EnvironmentFile = /etc/sysconfig/jellyfin User = jellyfin Group = jellyfin WorkingDirectory = /var/lib/jellyfin -ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS +ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS Restart = on-failure TimeoutSec = 15 SuccessExitStatus=0 143 diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index a759b29b1..e78368906 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -14,6 +14,7 @@ License: GPLv2 URL: https://jellyfin.org # Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz` Source0: jellyfin-server-%{version}.tar.gz +Source10: jellyfin-selinux-launcher.sh Source11: jellyfin.service Source12: jellyfin.env Source13: jellyfin.override.conf @@ -73,7 +74,8 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti # Jellyfin files %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir} %{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin -ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin +%{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin +sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin # Jellyfin config %{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json @@ -106,6 +108,7 @@ ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin %attr(755,root,root) %{_libdir}/jellyfin/createdump %attr(755,root,root) %{_libdir}/jellyfin/jellyfin %{_libdir}/jellyfin/* +%attr(755,root,root) %{_bindir}/jellyfin # Jellyfin config %config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json diff --git a/global.json b/global.json new file mode 100644 index 000000000..24335d7a0 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestMinor" + } +} diff --git a/jellyfin.ruleset b/jellyfin.ruleset index c846e2cd4..870cf253f 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -52,6 +52,8 @@ <Rule Id="SA1204" Action="None" /> <!-- disable warning SA1309: Fields must not begin with an underscore --> <Rule Id="SA1309" Action="None" /> + <!-- disable warning SA1311: Static readonly fields should begin with upper-case letter --> + <Rule Id="SA1311" Action="None" /> <!-- disable warning SA1413: Use trailing comma in multi-line initializers --> <Rule Id="SA1413" Action="None" /> <!-- disable warning SA1512: Single-line comments must not be followed by blank line --> @@ -89,6 +91,8 @@ <Rule Id="CA1727" Action="Error" /> <!-- error on CA1813: Avoid unsealed attributes --> <Rule Id="CA1813" Action="Error" /> + <!-- error on CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string --> + <Rule Id="CA1834" Action="Error" /> <!-- error on CA1843: Do not use 'WaitAll' with a single task --> <Rule Id="CA1843" Action="Error" /> <!-- error on CA1845: Use span-based 'string.Concat' --> @@ -169,4 +173,18 @@ <!-- error on RS0030: Do not used banned APIs --> <Rule Id="RS0030" Action="Error" /> </Rules> + + <Rules AnalyzerId="IDisposableAnalyzers" RuleNamespace="IDisposableAnalyzers.Correctness"> + <!-- disable warning IDISP001: Dispose created --> + <Rule Id="IDISP001" Action="Info" /> + <!-- TODO: Enable when false positives are fixed --> + <!-- disable warning IDISP003: Dispose previous before re-assigning --> + <Rule Id="IDISP003" Action="Info" /> + <!-- disable warning IDISP004: Don't ignore created IDisposable --> + <Rule Id="IDISP004" Action="Info" /> + <!-- disable warning IDISP007: Don't dispose injected --> + <Rule Id="IDISP007" Action="Info" /> + <!-- disable warning IDISP008: Don't assign member with injected and created disposables --> + <Rule Id="IDISP008" Action="Info" /> + </Rules> </RuleSet> diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 034691322..c465c4ad0 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -31,8 +31,12 @@ <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> </ItemGroup> - <!-- Code analysers--> + <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2d980db18..126c0503e 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -122,8 +122,8 @@ public class SkiaEncoder : IImageEncoder var svg = new SKSvg(); try { - svg.Load(path); - return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height)); + using var picture = svg.Load(path); + return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Height)); } catch (FormatException skiaColorException) { @@ -432,7 +432,8 @@ public class SkiaEncoder : IImageEncoder // scale image (the FromImage creates a copy) var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace); - using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo)); + using var resizedImage = ResizeImage(bitmap, imageInfo); + using var resizedBitmap = SKBitmap.FromImage(resizedImage); // If all we're doing is resizing then we can stop now if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) @@ -489,10 +490,8 @@ public class SkiaEncoder : IImageEncoder Directory.CreateDirectory(directory); using (var outputStream = new SKFileWStream(outputPath)) { - using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels())) - { - pixmap.Encode(outputStream, skiaOutputFormat, quality); - } + using var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()); + pixmap.Encode(outputStream, skiaOutputFormat, quality); } return outputPath; diff --git a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs index 00d224da9..bd1b2b0da 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs @@ -19,7 +19,6 @@ public static class SkiaHelper public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex) { var imagesTested = new Dictionary<int, int>(); - SKBitmap? bitmap = null; while (imagesTested.Count < paths.Count) { @@ -28,7 +27,7 @@ public static class SkiaHelper currentIndex = 0; } - bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _); + SKBitmap? bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _); imagesTested[currentIndex] = 0; @@ -36,11 +35,12 @@ public static class SkiaHelper if (bitmap is not null) { - break; + newIndex = currentIndex; + return bitmap; } } newIndex = currentIndex; - return bitmap; + return null; } } diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index a8f80f7e2..6dff7aa9b 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -189,12 +189,12 @@ public partial class StripCollageBuilder // Scale image. The FromBitmap creates a copy var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace); - using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo)); + using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo); // draw this image into the strip at the next position var xPos = x * cellWidth; var yPos = y * cellHeight; - canvas.DrawBitmap(resizedBitmap, xPos, yPos); + canvas.DrawImage(resizeImage, xPos, yPos); } } diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 4e5d3b4d5..4f16e294b 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -13,7 +13,6 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -112,10 +111,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) { var file = await ProcessImage(options).ConfigureAwait(false); - using (var fileStream = AsyncFile.OpenRead(file.Path)) - { - await fileStream.CopyToAsync(toStream).ConfigureAwait(false); - } + using var fileStream = AsyncFile.OpenRead(file.Path); + await fileStream.CopyToAsync(toStream).ConfigureAwait(false); } /// <inheritdoc /> @@ -437,8 +434,13 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); /// <inheritdoc /> - public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) + public string? GetImageCacheTag(BaseItem item, ChapterInfo chapter) { + if (chapter.ImagePath is null) + { + return null; + } + return GetImageCacheTag(item, new ItemImageInfo { Path = chapter.ImagePath, diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index e0963ac34..2a5e24a44 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -21,8 +21,12 @@ <Compile Include="..\..\SharedVersion.cs" /> </ItemGroup> - <!-- Code analysers--> + <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 4f80aa941..36ae55ed2 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -34,6 +34,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs index 321cfa502..17096c017 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Extensions.Json.Converters var typedValueIndex = 0; for (var i = 0; i < stringEntries.Length; i++) { - if (parsedValues[i] != null) + if (parsedValues[i] is not null) { typedValues.SetValue(parsedValues[i], typedValueIndex); typedValueIndex++; diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs index 9751d9d42..182996852 100644 --- a/src/Jellyfin.Extensions/StreamExtensions.cs +++ b/src/Jellyfin.Extensions/StreamExtensions.cs @@ -26,10 +26,8 @@ namespace Jellyfin.Extensions /// <returns>All lines in the stream.</returns> public static string[] ReadAllLines(this Stream stream, Encoding encoding) { - using (StreamReader reader = new StreamReader(stream, encoding)) - { - return ReadAllLines(reader).ToArray(); - } + using StreamReader reader = new StreamReader(stream, encoding); + return ReadAllLines(reader).ToArray(); } /// <summary> @@ -40,7 +38,7 @@ namespace Jellyfin.Extensions public static IEnumerable<string> ReadAllLines(this TextReader reader) { string? line; - while ((line = reader.ReadLine()) != null) + while ((line = reader.ReadLine()) is not null) { yield return line; } @@ -54,7 +52,7 @@ namespace Jellyfin.Extensions public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader) { string? line; - while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) + while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) is not null) { yield return line; } diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index 3f4f55ee4..b792e7ec6 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -7,6 +7,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs index febe9516a..479e6ffdc 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs @@ -68,51 +68,54 @@ public static class FfProbeKeyframeExtractor double streamDuration = 0; double formatDuration = 0; - while (!reader.EndOfStream) + using (reader) { - var line = reader.ReadLine().AsSpan(); - if (line.IsEmpty) + while (!reader.EndOfStream) { - continue; - } + var line = reader.ReadLine().AsSpan(); + if (line.IsEmpty) + { + continue; + } - var firstComma = line.IndexOf(','); - var lineType = line[..firstComma]; - var rest = line[(firstComma + 1)..]; - if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase)) - { - // Split time and flags from the packet line. Example line: packet,7169.079000,K_ - var secondComma = rest.IndexOf(','); - var ptsTime = rest[..secondComma]; - var flags = rest[(secondComma + 1)..]; - if (flags.StartsWith("K_")) + var firstComma = line.IndexOf(','); + var lineType = line[..firstComma]; + var rest = line[(firstComma + 1)..]; + if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase)) { - if (double.TryParse(ptsTime, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var keyframe)) + // Split time and flags from the packet line. Example line: packet,7169.079000,K_ + var secondComma = rest.IndexOf(','); + var ptsTime = rest[..secondComma]; + var flags = rest[(secondComma + 1)..]; + if (flags.StartsWith("K_")) { - // Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double. - keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond)); + if (double.TryParse(ptsTime, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var keyframe)) + { + // Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double. + keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond)); + } } } - } - else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase)) - { - if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult)) + else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase)) { - streamDuration = streamDurationResult; + if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult)) + { + streamDuration = streamDurationResult; + } } - } - else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase)) - { - if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult)) + else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase)) { - formatDuration = formatDurationResult; + if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult)) + { + formatDuration = formatDurationResult; + } } } - } - // Prefer the stream duration as it should be more accurate - var duration = streamDuration > 0 ? streamDuration : formatDuration; + // Prefer the stream duration as it should be more accurate + var duration = streamDuration > 0 ? streamDuration : formatDuration; - return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes); + return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes); + } } } diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index 71572bcf6..09b1f8faa 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -11,6 +11,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="IDisposableAnalyzers"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs index fe0d7fc90..1f908d7e0 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs @@ -12,17 +12,16 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests [Fact] public void Parse_Valid_Success() { - using (var stream = File.OpenRead("Test Data/example.ass")) - { - var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "ass"); - Assert.Single(parsed.TrackEvents); - var trackEvent = parsed.TrackEvents[0]; + using var stream = File.OpenRead("Test Data/example.ass"); - Assert.Equal("1", trackEvent.Id); - Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks); - Assert.Equal("{\\pos(400,570)}Like an Angel with pity on nobody" + Environment.NewLine + "The second line in subtitle", trackEvent.Text); - } + var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "ass"); + Assert.Single(parsed.TrackEvents); + var trackEvent = parsed.TrackEvents[0]; + + Assert.Equal("1", trackEvent.Id); + Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks); + Assert.Equal("{\\pos(400,570)}Like an Angel with pity on nobody" + Environment.NewLine + "The second line in subtitle", trackEvent.Text); } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs index 2aebee556..b7152961c 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs @@ -12,45 +12,43 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests [Fact] public void Parse_Valid_Success() { - using (var stream = File.OpenRead("Test Data/example.srt")) - { - var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt"); - Assert.Equal(2, parsed.TrackEvents.Count); - - var trackEvent1 = parsed.TrackEvents[0]; - Assert.Equal("1", trackEvent1.Id); - Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks); - Assert.Equal("Senator, we're making" + Environment.NewLine + "our final approach into Coruscant.", trackEvent1.Text); - - var trackEvent2 = parsed.TrackEvents[1]; - Assert.Equal("2", trackEvent2.Id); - Assert.Equal(TimeSpan.Parse("00:02:20.476", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:02:22.501", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks); - Assert.Equal("Very good, Lieutenant.", trackEvent2.Text); - } + using var stream = File.OpenRead("Test Data/example.srt"); + + var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt"); + Assert.Equal(2, parsed.TrackEvents.Count); + + var trackEvent1 = parsed.TrackEvents[0]; + Assert.Equal("1", trackEvent1.Id); + Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks); + Assert.Equal("Senator, we're making" + Environment.NewLine + "our final approach into Coruscant.", trackEvent1.Text); + + var trackEvent2 = parsed.TrackEvents[1]; + Assert.Equal("2", trackEvent2.Id); + Assert.Equal(TimeSpan.Parse("00:02:20.476", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:02:22.501", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks); + Assert.Equal("Very good, Lieutenant.", trackEvent2.Text); } [Fact] public void Parse_EmptyNewlineBetweenText_Success() { - using (var stream = File.OpenRead("Test Data/example2.srt")) - { - var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt"); - Assert.Equal(2, parsed.TrackEvents.Count); - - var trackEvent1 = parsed.TrackEvents[0]; - Assert.Equal("311", trackEvent1.Id); - Assert.Equal(TimeSpan.Parse("00:16:46.465", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:16:49.009", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks); - Assert.Equal("Una vez que la gente se entere" + Environment.NewLine + Environment.NewLine + "de que ustedes están aquí,", trackEvent1.Text); - - var trackEvent2 = parsed.TrackEvents[1]; - Assert.Equal("312", trackEvent2.Id); - Assert.Equal(TimeSpan.Parse("00:16:49.092", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:16:51.470", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks); - Assert.Equal("este lugar se convertirá" + Environment.NewLine + Environment.NewLine + "en un maldito zoológico.", trackEvent2.Text); - } + using var stream = File.OpenRead("Test Data/example2.srt"); + + var parsed = new SubtitleEditParser(new NullLogger<SubtitleEditParser>()).Parse(stream, "srt"); + Assert.Equal(2, parsed.TrackEvents.Count); + + var trackEvent1 = parsed.TrackEvents[0]; + Assert.Equal("311", trackEvent1.Id); + Assert.Equal(TimeSpan.Parse("00:16:46.465", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:16:49.009", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks); + Assert.Equal("Una vez que la gente se entere" + Environment.NewLine + Environment.NewLine + "de que ustedes están aquí,", trackEvent1.Text); + + var trackEvent2 = parsed.TrackEvents[1]; + Assert.Equal("312", trackEvent2.Id); + Assert.Equal(TimeSpan.Parse("00:16:49.092", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:16:51.470", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks); + Assert.Equal("este lugar se convertirá" + Environment.NewLine + Environment.NewLine + "en un maldito zoológico.", trackEvent2.Text); } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs index 6abf2d26c..5b7aa7eaa 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs @@ -18,22 +18,21 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests [MemberData(nameof(Parse_MultipleDialogues_TestData))] public void Parse_MultipleDialogues_Success(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents) { - using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa))) - { - SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, "ssa"); + using Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)); - Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count); + SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, "ssa"); - for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i) - { - SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i]; - SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i]; + Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count); + + for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i) + { + SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i]; + SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i]; - Assert.Equal(expected.Id, actual.Id); - Assert.Equal(expected.Text, actual.Text); - Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks); - Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks); - } + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.Text, actual.Text); + Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks); + Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks); } } @@ -73,17 +72,16 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests [Fact] public void Parse_Valid_Success() { - using (var stream = File.OpenRead("Test Data/example.ssa")) - { - var parsed = _parser.Parse(stream, "ssa"); - Assert.Single(parsed.TrackEvents); - var trackEvent = parsed.TrackEvents[0]; + using var stream = File.OpenRead("Test Data/example.ssa"); - Assert.Equal("1", trackEvent.Id); - Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks); - Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks); - Assert.Equal("{\\pos(400,570)}Like an angel with pity on nobody", trackEvent.Text); - } + var parsed = _parser.Parse(stream, "ssa"); + Assert.Single(parsed.TrackEvents); + var trackEvent = parsed.TrackEvents[0]; + + Assert.Equal("1", trackEvent.Id); + Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks); + Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks); + Assert.Equal("{\\pos(400,570)}Like an angel with pity on nobody", trackEvent.Text); } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs index 9ace80bbd..ce1f005f4 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs @@ -97,7 +97,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests { var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); var subtitleEncoder = fixture.Create<SubtitleEncoder>(); - var result = await subtitleEncoder.GetReadableFile(mediaSource, subtitleStream, CancellationToken.None).ConfigureAwait(false); + var result = await subtitleEncoder.GetReadableFile(mediaSource, subtitleStream, CancellationToken.None); Assert.Equal(subtitleInfo.Path, result.Path); Assert.Equal(subtitleInfo.Protocol, result.Protocol); Assert.Equal(subtitleInfo.Format, result.Format); diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index c30dad6f9..210ce4a47 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -351,11 +351,11 @@ namespace Jellyfin.Model.Tests // Assert.Contains(uri.Extension, containers); // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec); Assert.Single(streamInfo.TargetVideoCodec); // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, streamInfo.TargetAudioCodec); + Assert.Contains(targetAudioStream?.Codec, streamInfo.TargetAudioCodec); Assert.Single(streamInfo.TargetAudioCodec); // Assert.Single(val.AudioCodecs); @@ -410,13 +410,13 @@ namespace Jellyfin.Model.Tests else { // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Contains(targetVideoStream?.Codec, streamInfo.TargetVideoCodec); Assert.Single(streamInfo.TargetVideoCodec); if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { // Check expected audio codecs (1) - if (!targetAudioStream.IsExternal) + if (targetAudioStream?.IsExternal == false) { // Check expected audio codecs (1) if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) @@ -432,7 +432,7 @@ namespace Jellyfin.Model.Tests else if (transcodeMode.Equals("Remux", StringComparison.Ordinal)) { // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); + Assert.Contains(targetAudioStream?.Codec, streamInfo.AudioCodecs); Assert.Single(streamInfo.AudioCodecs); } @@ -440,10 +440,10 @@ namespace Jellyfin.Model.Tests var videoStream = targetVideoStream; Assert.False(streamInfo.EstimateContentLength); Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo); - Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>()); - Assert.Equal(videoStream.Level, streamInfo.TargetVideoLevel); - Assert.Equal(videoStream.BitDepth, streamInfo.TargetVideoBitDepth); - Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); + Assert.Contains(videoStream?.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>()); + Assert.Equal(videoStream?.Level, streamInfo.TargetVideoLevel); + Assert.Equal(videoStream?.BitDepth, streamInfo.TargetVideoBitDepth); + Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream?.BitRate.GetValueOrDefault() ?? 0, int.MaxValue); // Audio codec not supported if ((why & TranscodeReason.AudioCodecNotSupported) != 0) @@ -452,7 +452,7 @@ namespace Jellyfin.Model.Tests if (options.AudioStreamIndex >= 0) { // TODO:fixme - if (!targetAudioStream.IsExternal) + if (targetAudioStream?.IsExternal == false) { Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } @@ -488,16 +488,16 @@ namespace Jellyfin.Model.Tests private static async ValueTask<T> TestData<T>(string name) { var path = Path.Join("Test Data", typeof(T).Name + "-" + name + ".json"); - using (var stream = File.OpenRead(path)) - { - var value = await JsonSerializer.DeserializeAsync<T>(stream, JsonDefaults.Options); - if (value is not null) - { - return value; - } - throw new SerializationException("Invalid test data: " + name); + using var stream = File.OpenRead(path); + + var value = await JsonSerializer.DeserializeAsync<T>(stream, JsonDefaults.Options); + if (value is not null) + { + return value; } + + throw new SerializationException("Invalid test data: " + name); } private StreamBuilder GetStreamBuilder() diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj index c12f0cd68..1263043a5 100644 --- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj +++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj @@ -7,6 +7,9 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="AutoFixture" /> + <PackageReference Include="AutoFixture.AutoMoq" /> + <PackageReference Include="AutoFixture.Xunit2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> <PackageReference Include="xunit" /> diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs index 400e30bd6..1e0851993 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs @@ -82,7 +82,7 @@ namespace Jellyfin.Providers.Tests.Manager AddParts(providerManager, metadataServices: servicesList.Select(s => s.Object).ToArray()); var refreshOptions = new MetadataRefreshOptions(Mock.Of<IDirectoryService>(MockBehavior.Strict)); - var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None).ConfigureAwait(false); + var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None); Assert.Equal(ItemUpdateType.MetadataDownload, actual); for (var i = 0; i < servicesList.Length; i++) @@ -105,7 +105,7 @@ namespace Jellyfin.Providers.Tests.Manager AddParts(providerManager, metadataServices: servicesList.Select(s => s.Object).ToArray()); var refreshOptions = new MetadataRefreshOptions(Mock.Of<IDirectoryService>(MockBehavior.Strict)); - var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None).ConfigureAwait(false); + var actual = await providerManager.RefreshSingleItem(item, refreshOptions, CancellationToken.None); var expectedResult = serviceFound ? ItemUpdateType.MetadataDownload : ItemUpdateType.None; Assert.Equal(expectedResult, actual); diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs index 6b2d9021c..2bc686a33 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs @@ -98,9 +98,11 @@ namespace Jellyfin.Providers.Tests.MediaInfo [InlineData(null, null, 1, ImageType.Primary, ImageFormat.Jpg)] // no label, finds primary [InlineData("backdrop", null, 2, ImageType.Backdrop, ImageFormat.Jpg)] // uses label to find index 2, not just pulling first stream [InlineData("cover", null, 2, ImageType.Primary, ImageFormat.Jpg)] // uses label to find index 2, not just pulling first stream + [InlineData(null, "bmp", 1, ImageType.Primary, ImageFormat.Bmp)] + [InlineData(null, "gif", 1, ImageType.Primary, ImageFormat.Gif)] [InlineData(null, "mjpeg", 1, ImageType.Primary, ImageFormat.Jpg)] [InlineData(null, "png", 1, ImageType.Primary, ImageFormat.Png)] - [InlineData(null, "gif", 1, ImageType.Primary, ImageFormat.Gif)] + [InlineData(null, "webp", 1, ImageType.Primary, ImageFormat.Webp)] public async void GetImage_Embedded_ReturnsCorrectSelection(string label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat) { var streams = new List<MediaStream>(); diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs new file mode 100644 index 000000000..76922af8d --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs @@ -0,0 +1,61 @@ +using System; +using AutoFixture; +using AutoFixture.AutoMoq; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Providers.MediaInfo; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.MediaInfo; + +public class FFProbeVideoInfoTests +{ + private readonly FFProbeVideoInfo _fFProbeVideoInfo; + + public FFProbeVideoInfoTests() + { + var serverConfiguration = new ServerConfiguration() + { + DummyChapterDuration = (int)TimeSpan.FromMinutes(5).TotalSeconds + }; + var serverConfig = new Mock<IServerConfigurationManager>(); + serverConfig.Setup(c => c.Configuration) + .Returns(serverConfiguration); + + IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); + fixture.Inject(serverConfig); + _fFProbeVideoInfo = fixture.Create<FFProbeVideoInfo>(); + } + + [Theory] + [InlineData(-1L)] + [InlineData(long.MinValue)] + [InlineData(long.MaxValue)] + public void CreateDummyChapters_InvalidRuntime_ThrowsArgumentException(long? runtime) + { + Assert.Throws<ArgumentException>( + () => _fFProbeVideoInfo.CreateDummyChapters(new Video() + { + RunTimeTicks = runtime + })); + } + + [Theory] + [InlineData(null, 0)] + [InlineData(0L, 0)] + [InlineData(1L, 0)] + [InlineData(TimeSpan.TicksPerMinute * 5, 0)] + [InlineData((TimeSpan.TicksPerMinute * 5) + 1, 1)] + [InlineData(TimeSpan.TicksPerMinute * 50, 10)] + public void CreateDummyChapters_ValidRuntime_CorrectChaptersCount(long? runtime, int chaptersCount) + { + var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video() + { + RunTimeTicks = runtime + }); + + Assert.Equal(chaptersCount, chapters.Length); + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 09eb22328..07061cfc7 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -31,6 +31,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R./epic.flac", false)] [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] + [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs index c859d11c6..13ac3ddb0 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs @@ -52,7 +52,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv Url = "192.168.1.182" }; - var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false); + var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None); Assert.Equal("HDHomeRun PRIME", modelInfo.FriendlyName); Assert.Equal("HDHR3-CC", modelInfo.ModelNumber); Assert.Equal("hdhomerun3_cablecard", modelInfo.FirmwareName); @@ -72,7 +72,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv Url = "10.10.10.100" }; - var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false); + var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None); Assert.Equal("HDHomeRun DUAL", modelInfo.FriendlyName); Assert.Equal("HDHR3-US", modelInfo.ModelNumber); Assert.Equal("hdhomerun3_atsc", modelInfo.FirmwareName); @@ -103,7 +103,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv Url = "192.168.1.182" }; - var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false); + var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None); Assert.Equal(6, channels.Count); Assert.Equal("4.1", channels[0].GuideNumber); Assert.Equal("WCMH-DT", channels[0].GuideName); @@ -133,7 +133,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv ImportFavoritesOnly = true }; - var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false); + var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None); Assert.Single(channels); Assert.Equal("4.1", channels[0].GuideNumber); Assert.Equal("WCMH-DT", channels[0].GuideName); @@ -145,7 +145,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv [Fact] public async Task TryGetTunerHostInfo_Valid_Success() { - var host = await _hdHomerunHost.TryGetTunerHostInfo("192.168.1.182", CancellationToken.None).ConfigureAwait(false); + var host = await _hdHomerunHost.TryGetTunerHostInfo("192.168.1.182", CancellationToken.None); Assert.Equal(_hdHomerunHost.Type, host.Type); Assert.Equal("192.168.1.182", host.Url); Assert.Equal("HDHomeRun PRIME", host.FriendlyName); diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs index e1d2bb2d5..d4f28f327 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs @@ -96,7 +96,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect var days = JsonSerializer.Deserialize<IReadOnlyList<DayDto>>(bytes, _jsonOptions); Assert.NotNull(days); - Assert.Equal(1, days!.Count); + Assert.Single(days); var dayDto = days[0]; Assert.Equal("20454", dayDto.StationId); @@ -110,7 +110,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect Assert.Equal(2, dayDto.Programs[0].AudioProperties.Count); Assert.Equal("stereo", dayDto.Programs[0].AudioProperties[0]); Assert.Equal("cc", dayDto.Programs[0].AudioProperties[1]); - Assert.Equal(1, dayDto.Programs[0].VideoProperties.Count); + Assert.Single(dayDto.Programs[0].VideoProperties); Assert.Equal("hdtv", dayDto.Programs[0].VideoProperties[0]); } @@ -126,13 +126,13 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect Assert.NotNull(programDtos); Assert.Equal(2, programDtos!.Count); Assert.Equal("EP000000060003", programDtos[0].ProgramId); - Assert.Equal(1, programDtos[0].Titles.Count); + Assert.Single(programDtos[0].Titles); Assert.Equal("'Allo 'Allo!", programDtos[0].Titles[0].Title120); Assert.Equal("Series", programDtos[0].EventDetails?.SubType); Assert.Equal("en", programDtos[0].Descriptions?.Description1000[0].DescriptionLanguage); Assert.Equal("A disguised British Intelligence officer is sent to help the airmen.", programDtos[0].Descriptions?.Description1000[0].Description); Assert.Equal(new DateTime(1985, 11, 04), programDtos[0].OriginalAirDate); - Assert.Equal(1, programDtos[0].Genres.Count); + Assert.Single(programDtos[0].Genres); Assert.Equal("Sitcom", programDtos[0].Genres[0]); Assert.Equal("The Poloceman Cometh", programDtos[0].EpisodeTitle150); Assert.Equal(2, programDtos[0].Metadata[0].Gracenote?.Season); @@ -161,7 +161,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect var showImagesDtos = JsonSerializer.Deserialize<IReadOnlyList<ShowImagesDto>>(bytes, _jsonOptions); Assert.NotNull(showImagesDtos); - Assert.Equal(1, showImagesDtos!.Count); + Assert.Single(showImagesDtos!); Assert.Equal("SH00712240", showImagesDtos[0].ProgramId); Assert.Equal(4, showImagesDtos[0].Data.Count); Assert.Equal("135", showImagesDtos[0].Data[0].Width); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs index 7abd2e685..5caf7d124 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs @@ -90,7 +90,7 @@ namespace Jellyfin.Server.Implementations.Tests.Updates Checksum = "InvalidChecksum" }; - await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false); + await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)); } [Fact] @@ -103,7 +103,7 @@ namespace Jellyfin.Server.Implementations.Tests.Updates Checksum = "11b5b2f1a9ebc4f66d6ef19018543361" }; - var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false); + var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)); Assert.Null(ex); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index 3737fee0a..3dc62afaf 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -21,10 +21,10 @@ namespace Jellyfin.Server.Integration.Tests public static async Task<string> CompleteStartupAsync(HttpClient client) { var jsonOptions = JsonDefaults.Options; - var userResponse = await client.GetByteArrayAsync("/Startup/User").ConfigureAwait(false); + var userResponse = await client.GetByteArrayAsync("/Startup/User"); var user = JsonSerializer.Deserialize<StartupUserDto>(userResponse, jsonOptions); - using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false); + using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())); Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode); using var content = JsonContent.Create( @@ -36,20 +36,20 @@ namespace Jellyfin.Server.Integration.Tests options: jsonOptions); content.Headers.Add("X-Emby-Authorization", DummyAuthHeader); - using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content).ConfigureAwait(false); + using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content); var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>( - await authResponse.Content.ReadAsStreamAsync().ConfigureAwait(false), - jsonOptions).ConfigureAwait(false); + await authResponse.Content.ReadAsStreamAsync(), + jsonOptions); return auth!.AccessToken; } public static async Task<UserDto> GetUserDtoAsync(HttpClient client) { - using var response = await client.GetAsync("Users/Me").ConfigureAwait(false); + using var response = await client.GetAsync("Users/Me"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var userDto = await JsonSerializer.DeserializeAsync<UserDto>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), JsonDefaults.Options).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), JsonDefaults.Options); Assert.NotNull(userDto); return userDto; } @@ -58,15 +58,15 @@ namespace Jellyfin.Server.Integration.Tests { if (userId.Equals(default)) { - var userDto = await GetUserDtoAsync(client).ConfigureAwait(false); + var userDto = await GetUserDtoAsync(client); userId = userDto.Id; } - var response = await client.GetAsync($"Users/{userId}/Items/Root").ConfigureAwait(false); + var response = await client.GetAsync($"Users/{userId}/Items/Root"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - JsonDefaults.Options).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + JsonDefaults.Options); Assert.NotNull(rootDto); return rootDto; } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs index be89fbc9a..96ca96558 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs @@ -19,9 +19,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task ActivityLog_GetEntries_Ok() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("System/ActivityLog/Entries").ConfigureAwait(false); + var response = await client.GetAsync("System/ActivityLog/Entries"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs index 52df1cd60..bd6e1b690 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -26,7 +26,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists").ConfigureAwait(false); + var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -36,12 +36,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin").ConfigureAwait(false); + var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin"); 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.Server.Integration.Tests.TestPage.html")!); - Assert.Equal(await response.Content.ReadAsStringAsync().ConfigureAwait(false), await reader.ReadToEndAsync().ConfigureAwait(false)); + Assert.Equal(await response.Content.ReadAsStringAsync(), await reader.ReadToEndAsync()); } [Fact] @@ -49,7 +49,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage").ConfigureAwait(false); + var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -58,9 +58,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetConfigurationPages_NoParams_AllConfigurationPages() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("/web/ConfigurationPages").ConfigureAwait(false); + var response = await client.GetAsync("/web/ConfigurationPages"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -73,9 +73,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetConfigurationPages_True_MainMenuConfigurationPages() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true").ConfigureAwait(false); + var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs index a65f65bb2..65e70caa0 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs @@ -32,9 +32,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetProfile_DoesNotExist_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false); + using var response = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -43,9 +43,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task DeleteProfile_DoesNotExist_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false); + using var response = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -54,14 +54,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task UpdateProfile_DoesNotExist_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); var deviceProfile = new DeviceProfile() { Name = "ThisProfileDoesNotExist" }; - using var response = await client.PostAsJsonAsync("/Dlna/Profiles/" + NonExistentProfile, deviceProfile, _jsonOptions).ConfigureAwait(false); + using var response = await client.PostAsJsonAsync("/Dlna/Profiles/" + NonExistentProfile, deviceProfile, _jsonOptions); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -70,14 +70,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task CreateProfile_Valid_NoContent() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); var deviceProfile = new DeviceProfile() { Name = "ThisProfileIsNew" }; - using var response = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions).ConfigureAwait(false); + using var response = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } @@ -86,16 +86,16 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetProfileInfos_Valid_ContainsThisProfileIsNew() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false); + using var response = await client.GetAsync("/Dlna/ProfileInfos"); 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 profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal)); Assert.NotNull(newProfile); @@ -107,7 +107,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task UpdateProfile_Valid_NoContent() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); var updatedProfile = new DeviceProfile() { @@ -115,18 +115,18 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Id = _newDeviceProfileId }; - using var postResponse = await client.PostAsJsonAsync("/Dlna/Profiles/" + _newDeviceProfileId, updatedProfile, _jsonOptions).ConfigureAwait(false); + using var postResponse = await client.PostAsJsonAsync("/Dlna/Profiles/" + _newDeviceProfileId, updatedProfile, _jsonOptions); Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode); // Verify that the profile got updated - using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false); + using var response = await client.GetAsync("/Dlna/ProfileInfos"); 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 profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal))); var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal)); @@ -139,20 +139,20 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task DeleteProfile_Valid_NoContent() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var deleteResponse = await client.DeleteAsync("/Dlna/Profiles/" + _newDeviceProfileId).ConfigureAwait(false); + using var deleteResponse = await client.DeleteAsync("/Dlna/Profiles/" + _newDeviceProfileId); Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode); // Verify that the profile got deleted - using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false); + using var response = await client.GetAsync("/Dlna/ProfileInfos"); 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 profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal))); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs index 078002994..a12e7ca0d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs @@ -25,9 +25,9 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact public async Task GetItems_NoApiKeyOrUserId_Success() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Items").ConfigureAwait(false); + var response = await client.GetAsync("Items"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } @@ -37,9 +37,9 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact public async Task GetUserItems_NonExistentUserId_NotFound(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -50,15 +50,15 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact public async Task GetItems_UserId_Ok(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var items = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.NotNull(items); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs index 8998683a7..06abae14c 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs @@ -32,9 +32,9 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa public async Task Get_NonExistentItemId_NotFound(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -45,7 +45,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa { var client = _factory.CreateClient(); - var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } @@ -55,9 +55,9 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa public async Task Delete_NonExistentItemId_NotFound(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false); + var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs index 34d26680a..abc8b6009 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs @@ -20,9 +20,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task BitrateTest_Default_Ok() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Playback/BitrateTest").ConfigureAwait(false); + var response = await client.GetAsync("Playback/BitrateTest"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType); @@ -34,9 +34,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task BitrateTest_WithValidParam_Ok(int size) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); + var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType); @@ -51,9 +51,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task BitrateTest_InvalidValue_BadRequest(int size) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); + var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs index 24251013c..6699c6834 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs @@ -26,10 +26,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task RenameVirtualFolder_WhiteSpaceName_ReturnsBadRequest() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); using var postContent = new ByteArrayContent(Array.Empty<byte>()); - var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent).ConfigureAwait(false); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -38,10 +38,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task RenameVirtualFolder_WhiteSpaceNewName_ReturnsBadRequest() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); using var postContent = new ByteArrayContent(Array.Empty<byte>()); - var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent).ConfigureAwait(false); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -50,10 +50,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task RenameVirtualFolder_NameDoesntExist_ReturnsNotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); using var postContent = new ByteArrayContent(Array.Empty<byte>()); - var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent).ConfigureAwait(false); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -62,7 +62,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task AddMediaPath_PathDoesntExist_ReturnsNotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); var data = new MediaPathDto() { @@ -70,7 +70,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Path = "/this/path/doesnt/exist" }; - var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions).ConfigureAwait(false); + var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -79,7 +79,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task UpdateMediaPath_WhiteSpaceName_ReturnsBadRequest() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); var data = new UpdateMediaPathRequestDto() { @@ -87,7 +87,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers PathInfo = new MediaPathInfo("test") }; - var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions).ConfigureAwait(false); + var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -96,9 +96,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task RemoveMediaPath_WhiteSpaceName_ReturnsBadRequest() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+").ConfigureAwait(false); + var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+"); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -107,9 +107,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task RemoveMediaPath_PathDoesntExist_ReturnsNotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist").ConfigureAwait(false); + var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs index 17f3dc99f..f9982cf12 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs @@ -18,9 +18,9 @@ public sealed class MusicGenreControllerTests : IClassFixture<JellyfinApplicatio public async Task MusicGenres_FakeMusicGenre_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("MusicGenres/Fake-MusicGenre").ConfigureAwait(false); + var response = await client.GetAsync("MusicGenres/Fake-MusicGenre"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs index 868ecd53f..9554d3ebc 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs @@ -19,9 +19,9 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory public async Task DeleteMarkUnplayedItem_NonExistentUserId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false); + using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -29,9 +29,9 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory public async Task PostMarkPlayedItem_NonExistentUserId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false); + using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -39,11 +39,11 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory public async Task DeleteMarkUnplayedItem_NonExistentItemId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); - using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false); + using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -51,11 +51,11 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory public async Task PostMarkPlayedItem_NonExistentItemId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); - using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false); + using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs index cb0a829e8..b9def13f8 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs @@ -19,9 +19,9 @@ public class SessionControllerTests : IClassFixture<JellyfinApplicationFactory> public async Task GetSessions_NonExistentUserId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}").ConfigureAwait(false); + using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs index 0dd22644a..2d3879bdb 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs @@ -36,15 +36,15 @@ namespace Jellyfin.Server.Integration.Tests.Controllers PreferredMetadataLanguage = "nl" }; - using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions).ConfigureAwait(false); + using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions); Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode); - using var getResponse = await client.GetAsync("/Startup/Configuration").ConfigureAwait(false); + using var getResponse = await client.GetAsync("/Startup/Configuration"); 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); + using var responseStream = await getResponse.Content.ReadAsStreamAsync(); + var newConfig = await JsonSerializer.DeserializeAsync<StartupConfigurationDto>(responseStream, _jsonOptions); Assert.Equal(config.UICulture, newConfig!.UICulture); Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode); Assert.Equal(config.PreferredMetadataLanguage, newConfig.PreferredMetadataLanguage); @@ -56,12 +56,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false); + using var response = await client.GetAsync("/Startup/User"); 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); + using var contentStream = await response.Content.ReadAsStreamAsync(); + var user = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions); Assert.NotNull(user); Assert.NotNull(user.Name); Assert.NotEmpty(user.Name); @@ -80,15 +80,15 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Password = "NewPassword" }; - var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions).ConfigureAwait(false); + var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions); Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode); - var getResponse = await client.GetAsync("/Startup/User").ConfigureAwait(false); + var getResponse = await client.GetAsync("/Startup/User"); 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); + var contentStream = await getResponse.Content.ReadAsStreamAsync(); + var newUser = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions); Assert.NotNull(newUser); Assert.Equal(user.Name, newUser.Name); Assert.NotNull(newUser.Password); @@ -102,7 +102,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false); + var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } @@ -112,7 +112,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - using var response = await client.GetAsync("/Startup/User").ConfigureAwait(false); + using var response = await client.GetAsync("/Startup/User"); 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 index 2a3c53dbe..79d03d539 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -41,10 +41,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - using var response = await client.GetAsync("Users/Public").ConfigureAwait(false); + using var response = await client.GetAsync("Users/Public"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), _jsonOpions); // User are hidden by default Assert.NotNull(users); Assert.Empty(users); @@ -55,12 +55,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task GetUsers_Valid_Success() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.GetAsync("Users").ConfigureAwait(false); + using var response = await client.GetAsync("Users"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), _jsonOpions); Assert.NotNull(users); Assert.Single(users); Assert.False(users![0].HasConfiguredPassword); @@ -71,9 +71,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers public async Task Me_Valid_Success() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - _ = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + _ = await AuthHelper.GetUserDtoAsync(client); } [Fact] @@ -90,10 +90,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Name = TestUsername }; - using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false); + using var response = await CreateUserByName(client, createRequest); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var user = await JsonSerializer.DeserializeAsync<UserDto>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), _jsonOpions); Assert.Equal(TestUsername, user!.Name); Assert.False(user.HasPassword); Assert.False(user.HasConfiguredPassword); @@ -121,7 +121,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Name = username! }; - using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false); + using var response = await CreateUserByName(client, createRequest); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -134,7 +134,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers // access token can't be null here as the previous test populated it client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); - using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}").ConfigureAwait(false); + using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -150,11 +150,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers NewPw = "4randomPa$$word" }; - using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false); + using var response = await UpdateUserPassword(client, _testUserId, createRequest); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + await client.GetStreamAsync("Users"), _jsonOpions); var user = users!.First(x => x.Id.Equals(_testUserId)); Assert.True(user.HasPassword); Assert.True(user.HasConfiguredPassword); @@ -173,11 +173,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers CurrentPw = "4randomPa$$word", }; - using var response = await UpdateUserPassword(client, _testUserId, createRequest).ConfigureAwait(false); + using var response = await UpdateUserPassword(client, _testUserId, createRequest); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await client.GetStreamAsync("Users").ConfigureAwait(false), _jsonOpions).ConfigureAwait(false); + await client.GetStreamAsync("Users"), _jsonOpions); var user = users!.First(x => x.Id.Equals(_testUserId)); Assert.False(user.HasPassword); Assert.False(user.HasConfiguredPassword); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs index 69f2ccf33..826a0a69d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs @@ -25,9 +25,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetRootFolder_NonExistenUserId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root").ConfigureAwait(false); + var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -35,9 +35,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetRootFolder_UserId_Valid() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - _ = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false); + _ = await AuthHelper.GetRootFolderDtoAsync(client); } [Theory] @@ -49,11 +49,11 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetItem_NonExistenUserId_NotFound(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -66,11 +66,11 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetItem_NonExistentItemId_NotFound(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -78,16 +78,16 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetItem_UserIdAndItemId_Valid() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); - var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id); - var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}").ConfigureAwait(false); + var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.NotNull(rootDto); } @@ -95,16 +95,16 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task GetIntros_UserIdAndItemId_Valid() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); - var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id); - var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros").ConfigureAwait(false); + var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var rootDto = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.NotNull(rootDto); } @@ -114,16 +114,16 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati public async Task LocalTrailersAndSpecialFeatures_UserIdAndItemId_Valid(string format) { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false); - var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false); + var userDto = await AuthHelper.GetUserDtoAsync(client); + var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)).ConfigureAwait(false); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto[]>( - await response.Content.ReadAsStreamAsync().ConfigureAwait(false), - _jsonOptions).ConfigureAwait(false); + await response.Content.ReadAsStreamAsync(), + _jsonOptions); Assert.NotNull(rootDto); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs index 0f9a2e90a..47bec5d79 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs @@ -19,9 +19,9 @@ public sealed class VideosControllerTests : IClassFixture<JellyfinApplicationFac public async Task DeleteAlternateSources_NonExistentItemId_NotFound() { var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false)); + client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}").ConfigureAwait(false); + var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs index 2361e4aa4..d2249cdc3 100644 --- a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs +++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs @@ -27,9 +27,9 @@ namespace Jellyfin.Server.Integration.Tests { var client = _factory.CreateClient(); - var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl).ConfigureAwait(false); + var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + string reply = await response.Content.ReadAsStringAsync(); Assert.Equal(unencodedUrl, reply); } @@ -40,9 +40,9 @@ namespace Jellyfin.Server.Integration.Tests { var client = _factory.CreateClient(); - var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl).ConfigureAwait(false); + var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + string reply = await response.Content.ReadAsStringAsync(); Assert.Equal(unencodedUrl, reply); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 55bc43455..1c87d11f1 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Globalization; using System.IO; -using System.Threading; using Emby.Server.Implementations; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; @@ -105,7 +104,7 @@ namespace Jellyfin.Server.Integration.Tests var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>(); appHost.ServiceProvider = testServer.Services; appHost.InitializeServices().GetAwaiter().GetResult(); - appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult(); + appHost.RunStartupTasksAsync().GetAwaiter().GetResult(); return testServer; } diff --git a/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs b/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs index 8c49a2e2b..c8ad9d2a1 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Server.Integration.Tests.Middleware AllowAutoRedirect = false }); - var response = await client.GetAsync("robots.txt").ConfigureAwait(false); + var response = await client.GetAsync("robots.txt"); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("web/robots.txt", response.Headers.Location?.ToString()); diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs index 797fc8f64..93e065685 100644 --- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs +++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Server.Tests Assert.Single(test.Query); var (k, v) = test.Query.First(); Assert.Equal(key, k); - Assert.Empty(v); + Assert.True(StringValues.IsNullOrEmpty(v)); } } } diff --git a/tests/jellyfin-tests.ruleset b/tests/jellyfin-tests.ruleset index e2abaf5bb..9d133da56 100644 --- a/tests/jellyfin-tests.ruleset +++ b/tests/jellyfin-tests.ruleset @@ -19,4 +19,10 @@ <!-- CA2234: Pass system uri objects instead of strings --> <Rule Id="CA2234" Action="Info" /> </Rules> + + <!-- xUnit --> + <Rules AnalyzerId="xUnit" RuleNamespace="xUnit"> + <!-- Test methods must have a supported return type. --> + <Rule Id="xUnit1028" Action="None" /> + </Rules> </RuleSet> |
