diff options
| author | BaronGreenback <jimcartlidge@yahoo.co.uk> | 2021-06-19 15:04:30 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-06-19 15:04:30 +0100 |
| commit | 6648b7d7dabeaa84835fc7a8a7a2a468a00cad5c (patch) | |
| tree | 4b3eeee4f10f5465eaee0110aa18452dab2f9f6d | |
| parent | 97c2c523a89dabead25b5b0d028acbd92d136660 (diff) | |
| parent | 0c3dcdf77b0d124517bffa608bfddf7d8f7682db (diff) | |
Merge branch 'master' into comparisons
486 files changed, 5496 insertions, 4067 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d67e1c98b..12f1f5ed5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -33,7 +33,13 @@ assignees: '' **Expected behavior** <!-- A clear and concise description of what you expected to happen. --> -**Logs** +**Server Logs** +<!-- Please paste any log errors. --> + +**FFmpeg Logs** +<!-- Please paste any log errors. --> + +**Browser Console Logs** <!-- Please paste any log errors. --> **Screenshots** diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml index 2529d8099..8da2349c8 100644 --- a/.github/workflows/automation.yml +++ b/.github/workflows/automation.yml @@ -1,26 +1,36 @@ name: Automation on: - pull_request: + push: + branches: + - master + pull_request_target: + issue_comment: jobs: - main: + label: + name: Labeling runs-on: ubuntu-latest steps: - - name: Does PR has the stable backport label? - uses: Dreamcodeio/does-pr-has-label@v1.2 - id: checkLabel + - name: Apply label + uses: eps1lon/actions-label-merge-conflict@v2.0.1 + if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} with: - label: stable backport + dirtyLabel: 'merge conflict' + repoToken: ${{ secrets.JF_BOT_TOKEN }} + project: + name: Project board + runs-on: ubuntu-latest + steps: - name: Remove from 'Current Release' project uses: alex-page/github-project-automation-plus@v0.7.1 - if: (github.event.pull_request || github.event.issue.pull_request) && !steps.checkLabel.outputs.hasLabel + if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport') continue-on-error: true with: project: Current Release action: delete - repo-token: ${{ secrets.GH_TOKEN }} + repo-token: ${{ secrets.JF_BOT_TOKEN }} - name: Add to 'Release Next' project uses: alex-page/github-project-automation-plus@v0.7.1 @@ -29,16 +39,16 @@ jobs: with: project: Release Next column: In progress - repo-token: ${{ secrets.GH_TOKEN }} + repo-token: ${{ secrets.JF_BOT_TOKEN }} - name: Add to 'Current Release' project uses: alex-page/github-project-automation-plus@v0.7.1 - if: (github.event.pull_request || github.event.issue.pull_request) && steps.checkLabel.outputs.hasLabel + if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport') continue-on-error: true with: project: Current Release column: In progress - repo-token: ${{ secrets.GH_TOKEN }} + repo-token: ${{ secrets.JF_BOT_TOKEN }} - name: Check number of comments from the team member if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' @@ -52,7 +62,7 @@ jobs: with: project: Issue Triage for Main Repo column: Needs triage - repo-token: ${{ secrets.GH_TOKEN }} + repo-token: ${{ secrets.JF_BOT_TOKEN }} - name: Add issue to triage project uses: alex-page/github-project-automation-plus@v0.7.1 @@ -61,4 +71,4 @@ jobs: with: project: Issue Triage for Main Repo column: Pending response - repo-token: ${{ secrets.GH_TOKEN }} + repo-token: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml new file mode 100644 index 000000000..e0b91ecee --- /dev/null +++ b/.github/workflows/commands.yml @@ -0,0 +1,119 @@ +name: Commands +on: + issue_comment: + types: + - created + - edited + pull_request_target: + types: + - labeled + - synchronize + +jobs: + rebase: + name: Rebase + if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER' + runs-on: ubuntu-latest + steps: + - name: Notify as seen + uses: peter-evans/create-or-update-comment@v1.4.5 + with: + token: ${{ secrets.JF_BOT_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: '+1' + + - name: Checkout the latest code + uses: actions/checkout@v2 + with: + token: ${{ secrets.JF_BOT_TOKEN }} + fetch-depth: 0 + + - name: Automatic Rebase + uses: cirrus-actions/rebase@1.4 + env: + GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} + + check-backport: + name: Check Backport + if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }} + runs-on: ubuntu-latest + steps: + - name: Notify as seen + uses: peter-evans/create-or-update-comment@v1.4.5 + if: ${{ github.event.comment != null }} + with: + token: ${{ secrets.JF_BOT_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: eyes + + - name: Checkout the latest code + uses: actions/checkout@v2 + with: + token: ${{ secrets.JF_BOT_TOKEN }} + fetch-depth: 0 + + - name: Notify as running + id: comment_running + uses: peter-evans/create-or-update-comment@v1.4.5 + if: ${{ github.event.comment != null }} + with: + token: ${{ secrets.JF_BOT_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: | + Running backport tests... + + - name: Perform test backport + id: run_tests + run: | + set +o errexit + git config --global user.name "Jellyfin Bot" + git config --global user.email "team@jellyfin.org" + CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}" + git checkout master + git merge --no-ff ${CURRENT_BRANCH} + MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' ) + git fetch --all + CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' ) + stable_branch="Current stable release branch: ${CURRENT_STABLE}" + echo ${stable_branch} + echo ::set-output name=branch::${stable_branch} + git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE} + git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt + retcode=$? + cat output.txt | grep -v 'hint:' + output="$( grep -v 'hint:' output.txt )" + output="${output//'%'/'%25'}" + output="${output//$'\n'/'%0A'}" + output="${output//$'\r'/'%0D'}" + echo ::set-output name=output::$output + exit ${retcode} + + - name: Notify with result success + uses: peter-evans/create-or-update-comment@v1.4.5 + if: ${{ github.event.comment != null && success() }} + with: + token: ${{ secrets.JF_BOT_TOKEN }} + comment-id: ${{ steps.comment_running.outputs.comment-id }} + body: | + ${{ steps.run_tests.outputs.branch }} + Output from `git cherry-pick`: + + --- + + ${{ steps.run_tests.outputs.output }} + reactions: hooray + + - name: Notify with result failure + uses: peter-evans/create-or-update-comment@v1.4.5 + if: ${{ github.event.comment != null && failure() }} + with: + token: ${{ secrets.JF_BOT_TOKEN }} + comment-id: ${{ steps.comment_running.outputs.comment-id }} + body: | + ${{ steps.run_tests.outputs.branch }} + Output from `git cherry-pick`: + + --- + + ${{ steps.run_tests.outputs.output }} + reactions: confused diff --git a/.github/workflows/label-commenter-config.yml b/.github/workflows/label-commenter-config.yml deleted file mode 100644 index 78b75be43..000000000 --- a/.github/workflows/label-commenter-config.yml +++ /dev/null @@ -1,43 +0,0 @@ -comment: - header: Hello @{{ issue.user.login }} - footer: "\ - ---\n\n - > This is an automated comment created by the [peaceiris/actions-label-commenter]. \ - Responding to the bot or mentioning it won't have any effect.\n\n - [peaceiris/actions-label-commenter]: https://github.com/peaceiris/actions-label-commenter - " - -labels: - - name: stable backport - labeled: - pr: - body: | - This pull request has been tagged as a stable backport. It will be cherry-picked into the next stable point release. - - Please observe the following: - - * Any dependent PRs that this PR requires **must** be tagged for stable backporting as well. - - * Any issue(s) this PR fixes or closes **should** target the current stable release or a previous stable release to which a fix has not yet entered the current stable release. - - * This PR **must** be test cherry-picked against the current release branch (`release-X.Y.z` where X and Y are numbers). It must apply cleanly, or a diff of the expected change must be provided. - - To do this, run the following commands from your local copy of the Jellyfin repository: - - 1. `git checkout master` - - 1. `git merge --no-ff <myPullRequestBranch>` - - 1. `git log` -> `commit xxxxxxxxx`, grab hash - - 1. `git checkout release-X.Y.z` replacing X and Y with the *current* stable version (e.g. `release-10.7.z`) - - 1. `git cherry-pick -sx -m1 <hash>` - - Ensure the `cherry-pick` applies cleanly. If it does not, fix any merge conflicts *preserving as much of the original code as possible*, and make note of the resulting diff. - - Test your changes with a build to ensure they are successful. If not, adjust the diff accordingly. - - **Do not** push your merges to either branch. Use `git reset --hard HEAD~1` to revert both branches to their original state. - - Reply to this PR with a comment beginning "Cherry-pick test completed." and including the merge-conflict-fixing diff(s) if applicable. diff --git a/.github/workflows/label-commenter.yml b/.github/workflows/label-commenter.yml deleted file mode 100644 index be9216cc1..000000000 --- a/.github/workflows/label-commenter.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Label Commenter - -on: - issues: - types: - - labeled - - unlabeled - pull_request_target: - types: - - labeled - - unlabeled - -jobs: - comment: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - with: - ref: master - - - name: Label Commenter - uses: peaceiris/actions-label-commenter@v1 diff --git a/.github/workflows/merge-conflicts.yml b/.github/workflows/merge-conflicts.yml deleted file mode 100644 index ce808617a..000000000 --- a/.github/workflows/merge-conflicts.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: 'Merge Conflicts' - -on: - push: - branches: - - master - pull_request_target: - types: - - synchronize -jobs: - triage: - runs-on: ubuntu-latest - steps: - - uses: eps1lon/actions-label-merge-conflict@v2.0.1 - with: - dirtyLabel: 'merge conflict' - repoToken: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml deleted file mode 100644 index 3172ec0d9..000000000 --- a/.github/workflows/rebase.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Automatic Rebase -on: - issue_comment: - -jobs: - rebase: - name: Rebase - if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER' - runs-on: ubuntu-latest - steps: - - name: Notify as seen - uses: peter-evans/create-or-update-comment@v1.4.5 - with: - token: ${{ secrets.GH_TOKEN }} - comment-id: ${{ github.event.comment.id }} - reactions: '+1' - - - name: Checkout the latest code - uses: actions/checkout@v2 - with: - token: ${{ secrets.GH_TOKEN }} - fetch-depth: 0 - - - name: Automatic Rebase - uses: cirrus-actions/rebase@1.4 - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.gitignore b/.gitignore index 7cd3d0068..252210e57 100644 --- a/.gitignore +++ b/.gitignore @@ -268,6 +268,7 @@ doc/ # Deployment artifacts dist *.exe +*.dll # BenchmarkDotNet artifacts BenchmarkDotNet.Artifacts diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7a763a46c..b44961bf8 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -70,6 +70,7 @@ - [marius-luca-87](https://github.com/marius-luca-87) - [mark-monteiro](https://github.com/mark-monteiro) - [Matt07211](https://github.com/Matt07211) + - [Maxr1998](https://github.com/Maxr1998) - [mcarlton00](https://github.com/mcarlton00) - [mitchfizz05](https://github.com/mitchfizz05) - [MrTimscampi](https://github.com/MrTimscampi) @@ -110,7 +111,7 @@ - [sorinyo2004](https://github.com/sorinyo2004) - [sparky8251](https://github.com/sparky8251) - [spookbits](https://github.com/spookbits) - - [ssenart] (https://github.com/ssenart) + - [ssenart](https://github.com/ssenart) - [stanionascu](https://github.com/stanionascu) - [stevehayles](https://github.com/stevehayles) - [SuperSandro2000](https://github.com/SuperSandro2000) @@ -146,6 +147,7 @@ - [nielsvanvelzen](https://github.com/nielsvanvelzen) - [skyfrk](https://github.com/skyfrk) - [ianjazz246](https://github.com/ianjazz246) + - [peterspenler](https://github.com/peterspenler) # Emby Contributors diff --git a/Dockerfile b/Dockerfile index ebe5eb00c..4e2d06b82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ ARG DOTNET_VERSION=5.0 -FROM node:alpine as web-builder +FROM node:lts-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 python \ +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 - \ && cd jellyfin-web-* \ - && npm ci --no-audit \ + && npm ci --no-audit --unsafe-perm \ && mv dist /dist FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder diff --git a/Dockerfile.arm b/Dockerfile.arm index d63dbee75..25a0de7db 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -5,12 +5,12 @@ ARG DOTNET_VERSION=5.0 -FROM node:alpine as web-builder +FROM node:lts-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 python \ +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 - \ && cd jellyfin-web-* \ - && npm ci --no-audit \ + && npm ci --no-audit --unsafe-perm \ && mv dist /dist diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index e95999f2a..c9f19c5a3 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -5,12 +5,12 @@ ARG DOTNET_VERSION=5.0 -FROM node:alpine as web-builder +FROM node:lts-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 python \ +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 - \ && cd jellyfin-web-* \ - && npm ci --no-audit \ + && npm ci --no-audit --unsafe-perm \ && mv dist /dist diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 5fa1fd589..6c580d15b 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -370,6 +370,42 @@ namespace Emby.Dlna.PlayTo RestartTimer(true); } + /* + * 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) + { + var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); + + url = url.Replace("&", "&", StringComparison.Ordinal); + + _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)); + if (command == null) + { + return; + } + + var dictionary = new Dictionary<string, string> + { + { "NextURI", url }, + { "NextURIMetaData", CreateDidlMeta(metaData) } + }; + + var service = GetAvTransportService(); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); + await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken) + .ConfigureAwait(false); + } + private static string CreateDidlMeta(string value) { if (string.IsNullOrEmpty(value)) diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 1e6a5fadb..0e49fd2c0 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -104,6 +104,22 @@ namespace Emby.Dlna.PlayTo _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft; } + /* + * Send a message to the DLNA device to notify what is the next track in the playlist. + */ + private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken) + { + if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1) + { + // The current playing item is indeed in the play list and we are not yet at the end of the playlist. + var nextItemIndex = currentPlayListItemIndex + 1; + var nextItem = _playlist[nextItemIndex]; + + // Send the SetNextAvTransport message. + await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false); + } + } + private void OnDeviceUnavailable() { try @@ -158,6 +174,15 @@ namespace Emby.Dlna.PlayTo var newItemProgress = GetProgressInfo(streamInfo); await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false); + + // Send a message to the DLNA device to notify what is the next track in the playlist. + var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId); + if (currentItemIndex >= 0) + { + _currentPlaylistIndex = currentItemIndex; + } + + await SendNextTrackMessage(currentItemIndex, CancellationToken.None); } catch (Exception ex) { @@ -427,6 +452,11 @@ namespace Emby.Dlna.PlayTo var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex); await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); + + // Send a message to the DLNA device to notify what is the next track in the play list. + var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); + await SendNextTrackMessage(newItemIndex, CancellationToken.None); + return; } @@ -625,6 +655,9 @@ namespace Emby.Dlna.PlayTo await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false); + // Send a message to the DLNA device to notify what is the next track in the play list. + await SendNextTrackMessage(index, cancellationToken); + var streamInfo = currentitem.StreamInfo; if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo)) { @@ -738,6 +771,10 @@ namespace Emby.Dlna.PlayTo await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); + // Send a message to the DLNA device to notify what is the next track in the play list. + var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); + await SendNextTrackMessage(newItemIndex, CancellationToken.None); + if (EnableClientSideSeek(newItem.StreamInfo)) { await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); @@ -763,6 +800,10 @@ namespace Emby.Dlna.PlayTo await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false); + // Send a message to the DLNA device to notify what is the next track in the play list. + var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl); + await SendNextTrackMessage(newItemIndex, CancellationToken.None); + if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0) { await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs index 8b47dd12e..af4aa0059 100644 --- a/Emby.Naming/Audio/AudioFileParser.cs +++ b/Emby.Naming/Audio/AudioFileParser.cs @@ -1,7 +1,7 @@ using System; using System.IO; -using System.Linq; using Emby.Naming.Common; +using MediaBrowser.Common.Extensions; namespace Emby.Naming.Audio { @@ -18,8 +18,8 @@ namespace Emby.Naming.Audio /// <returns>True if file at path is audio file.</returns> public static bool IsAudioFile(string path, NamingOptions options) { - var extension = Path.GetExtension(path); - return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + var extension = Path.GetExtension(path.AsSpan()); + return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 63116f368..3224ff412 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -23,11 +23,12 @@ </PropertyGroup> <ItemGroup> - <Compile Include="..\SharedVersion.cs" /> + <Compile Include="../SharedVersion.cs" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> + <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" /> + <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" /> </ItemGroup> <PropertyGroup> diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs index c63aec64e..5e952e47b 100644 --- a/Emby.Naming/TV/EpisodeResolver.cs +++ b/Emby.Naming/TV/EpisodeResolver.cs @@ -16,7 +16,7 @@ namespace Emby.Naming.TV /// <summary> /// Initializes a new instance of the <see cref="EpisodeResolver"/> class. /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param> + /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param> public EpisodeResolver(NamingOptions options) { _options = options; @@ -62,8 +62,7 @@ namespace Emby.Naming.TV container = extension.TrimStart('.'); } - var flags = new FlagParser(_options).GetFlags(path); - var format3DResult = new Format3DParser(_options).Parse(flags); + var format3DResult = Format3DParser.Parse(path, _options); var parsingResult = new EpisodePathParser(_options) .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo); diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs index 1d3b36a1a..1fade985b 100644 --- a/Emby.Naming/Video/ExtraResolver.cs +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -30,69 +30,72 @@ namespace Emby.Naming.Video /// <returns>Returns <see cref="ExtraResult"/> object.</returns> public ExtraResult GetExtraInfo(string path) { - return _options.VideoExtraRules - .Select(i => GetExtraInfo(path, i)) - .FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult(); - } - - private ExtraResult GetExtraInfo(string path, ExtraRule rule) - { var result = new ExtraResult(); - if (rule.MediaType == MediaType.Audio) + for (var i = 0; i < _options.VideoExtraRules.Length; i++) { - if (!AudioFileParser.IsAudioFile(path, _options)) + var rule = _options.VideoExtraRules[i]; + if (rule.MediaType == MediaType.Audio) { - return result; + if (!AudioFileParser.IsAudioFile(path, _options)) + { + continue; + } } - } - else if (rule.MediaType == MediaType.Video) - { - if (!new VideoResolver(_options).IsVideoFile(path)) + else if (rule.MediaType == MediaType.Video) { - return result; + if (!VideoResolver.IsVideoFile(path, _options)) + { + continue; + } } - } - - if (rule.RuleType == ExtraRuleType.Filename) - { - var filename = Path.GetFileNameWithoutExtension(path); - if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase)) + var pathSpan = path.AsSpan(); + if (rule.RuleType == ExtraRuleType.Filename) { - result.ExtraType = rule.ExtraType; - result.Rule = rule; - } - } - else if (rule.RuleType == ExtraRuleType.Suffix) - { - var filename = Path.GetFileNameWithoutExtension(path); + var filename = Path.GetFileNameWithoutExtension(pathSpan); - if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0) + if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } + } + else if (rule.RuleType == ExtraRuleType.Suffix) { - result.ExtraType = rule.ExtraType; - result.Rule = rule; + var filename = Path.GetFileNameWithoutExtension(pathSpan); + + if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } } - } - else if (rule.RuleType == ExtraRuleType.Regex) - { - var filename = Path.GetFileName(path); + else if (rule.RuleType == ExtraRuleType.Regex) + { + var filename = Path.GetFileName(path); - var regex = new Regex(rule.Token, RegexOptions.IgnoreCase); + var regex = new Regex(rule.Token, RegexOptions.IgnoreCase); - if (regex.IsMatch(filename)) + if (regex.IsMatch(filename)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } + } + else if (rule.RuleType == ExtraRuleType.DirectoryName) { - result.ExtraType = rule.ExtraType; - result.Rule = rule; + var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan)); + if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } } - } - else if (rule.RuleType == ExtraRuleType.DirectoryName) - { - var directoryName = Path.GetFileName(Path.GetDirectoryName(path)); - if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase)) + + if (result.ExtraType != null) { - result.ExtraType = rule.ExtraType; - result.Rule = rule; + return result; } } diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs deleted file mode 100644 index 439de1813..000000000 --- a/Emby.Naming/Video/FlagParser.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.IO; -using Emby.Naming.Common; - -namespace Emby.Naming.Video -{ - /// <summary> - /// Parses list of flags from filename based on delimiters. - /// </summary> - public class FlagParser - { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="FlagParser"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param> - public FlagParser(NamingOptions options) - { - _options = options; - } - - /// <summary> - /// Calls GetFlags function with _options.VideoFlagDelimiters parameter. - /// </summary> - /// <param name="path">Path to file.</param> - /// <returns>List of found flags.</returns> - public string[] GetFlags(string path) - { - return GetFlags(path, _options.VideoFlagDelimiters); - } - - /// <summary> - /// Parses flags from filename based on delimiters. - /// </summary> - /// <param name="path">Path to file.</param> - /// <param name="delimiters">Delimiters used to extract flags.</param> - /// <returns>List of found flags.</returns> - public string[] GetFlags(string path, char[] delimiters) - { - if (string.IsNullOrEmpty(path)) - { - return Array.Empty<string>(); - } - - // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _. - - var file = Path.GetFileName(path); - - return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); - } - } -} diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs index 4fd5d78ba..089089989 100644 --- a/Emby.Naming/Video/Format3DParser.cs +++ b/Emby.Naming/Video/Format3DParser.cs @@ -1,45 +1,37 @@ using System; -using System.Linq; using Emby.Naming.Common; namespace Emby.Naming.Video { /// <summary> - /// Parste 3D format related flags. + /// Parse 3D format related flags. /// </summary> - public class Format3DParser + public static class Format3DParser { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="Format3DParser"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param> - public Format3DParser(NamingOptions options) - { - _options = options; - } + // Static default result to save on allocation costs. + private static readonly Format3DResult _defaultResult = new (false, null); /// <summary> /// Parse 3D format related flags. /// </summary> /// <param name="path">Path to file.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>Returns <see cref="Format3DResult"/> object.</returns> - public Format3DResult Parse(string path) + public static Format3DResult Parse(ReadOnlySpan<char> path, NamingOptions namingOptions) { - int oldLen = _options.VideoFlagDelimiters.Length; - var delimiters = new char[oldLen + 1]; - _options.VideoFlagDelimiters.CopyTo(delimiters, 0); + int oldLen = namingOptions.VideoFlagDelimiters.Length; + Span<char> delimiters = stackalloc char[oldLen + 1]; + namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters); delimiters[oldLen] = ' '; - return Parse(new FlagParser(_options).GetFlags(path, delimiters)); + return Parse(path, delimiters, namingOptions); } - internal Format3DResult Parse(string[] videoFlags) + private static Format3DResult Parse(ReadOnlySpan<char> path, ReadOnlySpan<char> delimiters, NamingOptions namingOptions) { - foreach (var rule in _options.Format3DRules) + foreach (var rule in namingOptions.Format3DRules) { - var result = Parse(videoFlags, rule); + var result = Parse(path, rule, delimiters); if (result.Is3D) { @@ -47,51 +39,43 @@ namespace Emby.Naming.Video } } - return new Format3DResult(); + return _defaultResult; } - private static Format3DResult Parse(string[] videoFlags, Format3DRule rule) + private static Format3DResult Parse(ReadOnlySpan<char> path, Format3DRule rule, ReadOnlySpan<char> delimiters) { - var result = new Format3DResult(); + bool is3D = false; + string? format3D = null; - if (string.IsNullOrEmpty(rule.PrecedingToken)) + // If there's no preceding token we just consider it found + var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken); + while (path.Length > 0) { - result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase)); - result.Is3D = !string.IsNullOrEmpty(result.Format3D); - - if (result.Is3D) + var index = path.IndexOfAny(delimiters); + if (index == -1) { - result.Tokens.Add(rule.Token); + index = path.Length - 1; } - } - else - { - var foundPrefix = false; - string? format = null; - foreach (var flag in videoFlags) - { - if (foundPrefix) - { - result.Tokens.Add(rule.PrecedingToken); + var currentSlice = path[..index]; + path = path[(index + 1)..]; - if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase)) - { - format = flag; - result.Tokens.Add(rule.Token); - } + if (!foundPrefix) + { + foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase); + continue; + } - break; - } + is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase); - foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase); + if (is3D) + { + format3D = rule.Token; + break; } - - result.Is3D = foundPrefix && !string.IsNullOrEmpty(format); - result.Format3D = format; } - return result; + return is3D ? new Format3DResult(true, format3D) : _defaultResult; } } } diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs index ac935f203..aac959c13 100644 --- a/Emby.Naming/Video/Format3DResult.cs +++ b/Emby.Naming/Video/Format3DResult.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Emby.Naming.Video { /// <summary> @@ -10,27 +8,24 @@ namespace Emby.Naming.Video /// <summary> /// Initializes a new instance of the <see cref="Format3DResult"/> class. /// </summary> - public Format3DResult() + /// <param name="is3D">A value indicating whether the parsed string contains 3D tokens.</param> + /// <param name="format3D">The 3D format. Value might be null if [is3D] is <c>false</c>.</param> + public Format3DResult(bool is3D, string? format3D) { - Tokens = new List<string>(); + Is3D = is3D; + Format3D = format3D; } /// <summary> - /// Gets or sets a value indicating whether [is3 d]. + /// Gets a value indicating whether [is3 d]. /// </summary> /// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value> - public bool Is3D { get; set; } + public bool Is3D { get; } /// <summary> - /// Gets or sets the format3 d. + /// Gets the format3 d. /// </summary> /// <value>The format3 d.</value> - public string? Format3D { get; set; } - - /// <summary> - /// Gets or sets the tokens. - /// </summary> - /// <value>The tokens.</value> - public List<string> Tokens { get; set; } + public string? Format3D { get; } } } diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs index 550c42961..36f65a562 100644 --- a/Emby.Naming/Video/StackResolver.cs +++ b/Emby.Naming/Video/StackResolver.cs @@ -85,10 +85,8 @@ namespace Emby.Naming.Video /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns> public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files) { - var resolver = new VideoResolver(_options); - var list = files - .Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName)) + .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options)) .OrderBy(i => i.FullName) .ToList(); diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs index 1457db737..481773ff6 100644 --- a/Emby.Naming/Video/VideoFileInfo.cs +++ b/Emby.Naming/Video/VideoFileInfo.cs @@ -1,3 +1,4 @@ +using System; using MediaBrowser.Model.Entities; namespace Emby.Naming.Video @@ -106,9 +107,9 @@ namespace Emby.Naming.Video /// Gets the file name without extension. /// </summary> /// <value>The file name without extension.</value> - public string FileNameWithoutExtension => !IsDirectory - ? System.IO.Path.GetFileNameWithoutExtension(Path) - : System.IO.Path.GetFileName(Path); + public ReadOnlySpan<char> FileNameWithoutExtension => !IsDirectory + ? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan()) + : System.IO.Path.GetFileName(Path.AsSpan()); /// <inheritdoc /> public override string ToString() diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 7b6a1705b..7da2dcd7a 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -12,31 +12,19 @@ namespace Emby.Naming.Video /// <summary> /// Resolves alternative versions and extras from list of video files. /// </summary> - public class VideoListResolver + public static class VideoListResolver { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="VideoListResolver"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param> - public VideoListResolver(NamingOptions options) - { - _options = options; - } - /// <summary> /// Resolves alternative versions and extras from list of video files. /// </summary> /// <param name="files">List of related video files.</param> + /// <param name="namingOptions">The naming options.</param> /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param> /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns> - public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true) + public static IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true) { - var videoResolver = new VideoResolver(_options); - var videoInfos = files - .Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory)) + .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions)) .OfType<VideoFileInfo>() .ToList(); @@ -46,7 +34,7 @@ namespace Emby.Naming.Video .Where(i => i.ExtraType == null) .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory }); - var stackResult = new StackResolver(_options) + var stackResult = new StackResolver(namingOptions) .Resolve(nonExtras).ToList(); var remainingFiles = videoInfos @@ -59,23 +47,17 @@ namespace Emby.Naming.Video { var info = new VideoInfo(stack.Name) { - Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack)) + Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions)) .OfType<VideoFileInfo>() .ToList() }; info.Year = info.Files[0].Year; - var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) }; - - var extras = GetExtras(remainingFiles, extraBaseNames); + var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters); if (extras.Count > 0) { - remainingFiles = remainingFiles - .Except(extras) - .ToList(); - info.Extras = extras; } @@ -88,15 +70,12 @@ namespace Emby.Naming.Video foreach (var media in standaloneMedia) { - var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } }; + var info = new VideoInfo(media.Name) { Files = new[] { media } }; info.Year = info.Files[0].Year; - var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension }); - - remainingFiles = remainingFiles - .Except(extras.Concat(new[] { media })) - .ToList(); + remainingFiles.Remove(media); + var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters); info.Extras = extras; @@ -105,8 +84,7 @@ namespace Emby.Naming.Video if (supportMultiVersion) { - list = GetVideosGroupedByVersion(list) - .ToList(); + list = GetVideosGroupedByVersion(list, namingOptions); } // If there's only one resolved video, use the folder name as well to find extras @@ -114,19 +92,14 @@ namespace Emby.Naming.Video { var info = list[0]; var videoPath = list[0].Files[0].Path; - var parentPath = Path.GetDirectoryName(videoPath); + var parentPath = Path.GetDirectoryName(videoPath.AsSpan()); - if (!string.IsNullOrEmpty(parentPath)) + if (!parentPath.IsEmpty) { var folderName = Path.GetFileName(parentPath); - if (!string.IsNullOrEmpty(folderName)) + if (!folderName.IsEmpty) { - var extras = GetExtras(remainingFiles, new List<string> { folderName }); - - remainingFiles = remainingFiles - .Except(extras) - .ToList(); - + var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters); extras.AddRange(info.Extras); info.Extras = extras; } @@ -164,96 +137,168 @@ namespace Emby.Naming.Video // Whatever files are left, just add them list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name) { - Files = new List<VideoFileInfo> { i }, + Files = new[] { i }, Year = i.Year })); return list; } - private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos) + private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions) { if (videos.Count == 0) { return videos; } - var list = new List<VideoInfo>(); - - var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path)); + var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan())); - if (!string.IsNullOrEmpty(folderName) - && folderName.Length > 1 - && videos.All(i => i.Files.Count == 1 - && IsEligibleForMultiVersion(folderName, i.Files[0].Path)) - && HaveSameYear(videos)) + if (folderName.Length <= 1 || !HaveSameYear(videos)) { - var ordered = videos.OrderBy(i => i.Name).ToList(); - - list.Add(ordered[0]); + return videos; + } - var alternateVersionsLen = ordered.Count - 1; - var alternateVersions = new VideoFileInfo[alternateVersionsLen]; - for (int i = 0; i < alternateVersionsLen; i++) + // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if] + for (var i = 0; i < videos.Count; i++) + { + var video = videos[i]; + if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions)) { - alternateVersions[i] = ordered[i + 1].Files[0]; + return videos; } + } + + // The list is created and overwritten in the caller, so we are allowed to do in-place sorting + videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); - list[0].AlternateVersions = alternateVersions; - list[0].Name = folderName; - var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList(); - extras.AddRange(list[0].Extras); - list[0].Extras = extras; + var list = new List<VideoInfo> + { + videos[0] + }; - return list; + var alternateVersionsLen = videos.Count - 1; + var alternateVersions = new VideoFileInfo[alternateVersionsLen]; + var extras = new List<VideoFileInfo>(list[0].Extras); + for (int i = 0; i < alternateVersionsLen; i++) + { + var video = videos[i + 1]; + alternateVersions[i] = video.Files[0]; + extras.AddRange(video.Extras); } - return videos; - } + list[0].AlternateVersions = alternateVersions; + list[0].Name = folderName.ToString(); + list[0].Extras = extras; - private bool HaveSameYear(List<VideoInfo> videos) - { - return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2; + return list; } - private bool IsEligibleForMultiVersion(string folderName, string testFilePath) + private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos) { - string testFilename = Path.GetFileNameWithoutExtension(testFilePath); - if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) + if (videos.Count == 1) { - // Remove the folder name before cleaning as we don't care about cleaning that part - if (folderName.Length <= testFilename.Length) - { - testFilename = testFilename.Substring(folderName.Length).Trim(); - } + return true; + } - if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName)) + var firstYear = videos[0].Year ?? -1; + for (var i = 1; i < videos.Count; i++) + { + if ((videos[i].Year ?? -1) != firstYear) { - testFilename = cleanName.Trim().ToString(); + return false; } + } - // The CleanStringParser should have removed common keywords etc. - return string.IsNullOrEmpty(testFilename) - || testFilename[0] == '-' - || Regex.IsMatch(testFilename, @"^\[([^]]*)\]"); + return true; + } + + private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions) + { + var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan()); + if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) + { + return false; } - return false; + // Remove the folder name before cleaning as we don't care about cleaning that part + if (folderName.Length <= testFilename.Length) + { + testFilename = testFilename[folderName.Length..].Trim(); + } + + // There are no span overloads for regex unfortunately + var tmpTestFilename = testFilename.ToString(); + if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName)) + { + tmpTestFilename = cleanName.Trim().ToString(); + } + + // The CleanStringParser should have removed common keywords etc. + return string.IsNullOrEmpty(tmpTestFilename) + || testFilename[0] == '-' + || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); + } + + private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters) + { + return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd(); } - private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames) + private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName) { - foreach (var name in baseNames.ToList()) + if (baseName.IsEmpty) { - var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd(); - baseNames.Add(trimmedName); + return false; } - return remainingFiles - .Where(i => i.ExtraType != null) - .Where(i => baseNames.Any(b => - i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase))) - .ToList(); + return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase) + || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase)); + } + + /// <summary> + /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles]. + /// </summary> + /// <param name="remainingFiles">The list of remaining filenames.</param> + /// <param name="baseName">The base name to use for the comparison.</param> + /// <param name="videoFlagDelimiters">The video flag delimiters.</param> + /// <returns>A list of video extras for [baseName].</returns> + private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters) + { + return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters); + } + + /// <summary> + /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles]. + /// </summary> + /// <param name="remainingFiles">The list of remaining filenames.</param> + /// <param name="firstBaseName">The first base name to use for the comparison.</param> + /// <param name="secondBaseName">The second base name to use for the comparison.</param> + /// <param name="videoFlagDelimiters">The video flag delimiters.</param> + /// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns> + private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters) + { + var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters); + var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters); + + var result = new List<VideoFileInfo>(); + for (var pos = remainingFiles.Count - 1; pos >= 0; pos--) + { + var file = remainingFiles[pos]; + if (file.ExtraType == null) + { + continue; + } + + var filename = file.FileNameWithoutExtension; + if (StartsWith(filename, firstBaseName, trimmedFirstBaseName) + || StartsWith(filename, secondBaseName, trimmedSecondBaseName)) + { + result.Add(file); + remainingFiles.RemoveAt(pos); + } + } + + return result; } } } diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index 79a6da8f7..c4ac5fdc6 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -1,46 +1,36 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using Emby.Naming.Common; +using MediaBrowser.Common.Extensions; namespace Emby.Naming.Video { /// <summary> /// Resolves <see cref="VideoFileInfo"/> from file path. /// </summary> - public class VideoResolver + public static class VideoResolver { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="VideoResolver"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes - /// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param> - public VideoResolver(NamingOptions options) - { - _options = options; - } - /// <summary> /// Resolves the directory. /// </summary> /// <param name="path">The path.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>VideoFileInfo.</returns> - public VideoFileInfo? ResolveDirectory(string? path) + public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions) { - return Resolve(path, true); + return Resolve(path, true, namingOptions); } /// <summary> /// Resolves the file. /// </summary> /// <param name="path">The path.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>VideoFileInfo.</returns> - public VideoFileInfo? ResolveFile(string? path) + public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions) { - return Resolve(path, false); + return Resolve(path, false, namingOptions); } /// <summary> @@ -48,10 +38,11 @@ namespace Emby.Naming.Video /// </summary> /// <param name="path">The path.</param> /// <param name="isDirectory">if set to <c>true</c> [is folder].</param> + /// <param name="namingOptions">The naming options.</param> /// <param name="parseName">Whether or not the name should be parsed for info.</param> /// <returns>VideoFileInfo.</returns> /// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception> - public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true) + public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true) { if (string.IsNullOrEmpty(path)) { @@ -59,18 +50,18 @@ namespace Emby.Naming.Video } bool isStub = false; - string? container = null; + ReadOnlySpan<char> container = ReadOnlySpan<char>.Empty; string? stubType = null; if (!isDirectory) { - var extension = Path.GetExtension(path); + var extension = Path.GetExtension(path.AsSpan()); // Check supported extensions - if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (!namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { // It's not supported. Check stub extensions - if (!StubResolver.TryResolveFile(path, _options, out stubType)) + if (!StubResolver.TryResolveFile(path, namingOptions, out stubType)) { return null; } @@ -81,25 +72,22 @@ namespace Emby.Naming.Video container = extension.TrimStart('.'); } - var flags = new FlagParser(_options).GetFlags(path); - var format3DResult = new Format3DParser(_options).Parse(flags); + var format3DResult = Format3DParser.Parse(path, namingOptions); - var extraResult = new ExtraResolver(_options).GetExtraInfo(path); + var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path); - var name = isDirectory - ? Path.GetFileName(path) - : Path.GetFileNameWithoutExtension(path); + var name = Path.GetFileNameWithoutExtension(path); int? year = null; if (parseName) { - var cleanDateTimeResult = CleanDateTime(name); + var cleanDateTimeResult = CleanDateTime(name, namingOptions); name = cleanDateTimeResult.Name; year = cleanDateTimeResult.Year; if (extraResult.ExtraType == null - && TryCleanString(name, out ReadOnlySpan<char> newName)) + && TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName)) { name = newName.ToString(); } @@ -107,7 +95,7 @@ namespace Emby.Naming.Video return new VideoFileInfo( path: path, - container: container, + container: container.IsEmpty ? null : container.ToString(), isStub: isStub, name: name, year: year, @@ -123,43 +111,47 @@ namespace Emby.Naming.Video /// Determines if path is video file based on extension. /// </summary> /// <param name="path">Path to file.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>True if is video file.</returns> - public bool IsVideoFile(string path) + public static bool IsVideoFile(string path, NamingOptions namingOptions) { - var extension = Path.GetExtension(path); - return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + var extension = Path.GetExtension(path.AsSpan()); + return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase); } /// <summary> /// Determines if path is video file stub based on extension. /// </summary> /// <param name="path">Path to file.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>True if is video file stub.</returns> - public bool IsStubFile(string path) + public static bool IsStubFile(string path, NamingOptions namingOptions) { - var extension = Path.GetExtension(path); - return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + var extension = Path.GetExtension(path.AsSpan()); + return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase); } /// <summary> /// Tries to clean name of clutter. /// </summary> /// <param name="name">Raw name.</param> + /// <param name="namingOptions">The naming options.</param> /// <param name="newName">Clean name.</param> /// <returns>True if cleaning of name was successful.</returns> - public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName) + public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName) { - return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName); + return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName); } /// <summary> /// Tries to get name and year from raw name. /// </summary> /// <param name="name">Raw name.</param> + /// <param name="namingOptions">The naming options.</param> /// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns> - public CleanDateTimeResult CleanDateTime(string name) + public static CleanDateTimeResult CleanDateTime(string name, NamingOptions namingOptions) { - return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes); + return CleanDateTimeParser.Clean(name, namingOptions.CleanDateTimeRegexes); } } } diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 660bbb2de..6edfad575 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase CachePath = cacheDirectoryPath; WebPath = webDirectoryPath; - DataPath = Path.Combine(ProgramDataPath, "data"); + _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; } /// <summary> @@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase /// Gets the folder path to the data directory. /// </summary> /// <value>The data directory.</value> - public string DataPath - { - get => _dataPath; - private set => _dataPath = Directory.CreateDirectory(value).FullName; - } + public string DataPath => _dataPath; /// <inheritdoc /> public string VirtualDataPath => "%AppDataPath%"; diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 4f72c8ce1..d38535634 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>(); + /// <summary> + /// The _configuration sync lock. + /// </summary> + private readonly object _configurationSyncLock = new object(); + private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); @@ -32,11 +39,6 @@ namespace Emby.Server.Implementations.AppBase private bool _configurationLoaded; /// <summary> - /// The _configuration sync lock. - /// </summary> - private readonly object _configurationSyncLock = new object(); - - /// <summary> /// The _configuration. /// </summary> private BaseApplicationConfiguration _configuration; @@ -297,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase /// <inheritdoc /> public object GetConfiguration(string key) { - return _configurations.GetOrAdd(key, k => - { - var file = GetConfigurationFile(key); + return _configurations.GetOrAdd( + key, + (k, configurationManager) => + { + var file = configurationManager.GetConfigurationFile(k); - var configurationInfo = _configurationStores - .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase)); + var configurationInfo = Array.Find( + configurationManager._configurationStores, + i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase)); - if (configurationInfo == null) - { - throw new ResourceNotFoundException("Configuration with key " + key + " not found."); - } + if (configurationInfo == null) + { + throw new ResourceNotFoundException("Configuration with key " + k + " not found."); + } - var configurationType = configurationInfo.ConfigurationType; + var configurationType = configurationInfo.ConfigurationType; - lock (_configurationSyncLock) - { - return LoadConfiguration(file, configurationType); - } - }); + lock (configurationManager._configurationSyncLock) + { + return configurationManager.LoadConfiguration(file, configurationType); + } + }, + this); } private object LoadConfiguration(string path, Type configurationType) diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs index 29bac6634..0308a68e4 100644 --- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs +++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.IO; using System.Linq; @@ -35,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase } catch (Exception) { - configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type)); + // Note: CreateInstance returns null for Nullable<T>, e.g. CreateInstance(typeof(int?)) returns null. + configuration = Activator.CreateInstance(type)!; } using var stream = new MemoryStream(buffer?.Length ?? 0); diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 75d8fc113..82995deb3 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 7324b0ee9..448f12403 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Globalization; diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs index c69a07e83..ca8409402 100644 --- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections return null; }) .Where(i => i != null) - .GroupBy(x => x.Id) + .GroupBy(x => x!.Id) // We removed the null values .Select(x => x.First()) - .ToList(); + .ToList()!; // Again... the list doesn't contain any null values } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 1b85a9d4b..82d80fc83 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.IO; @@ -164,7 +166,7 @@ namespace Emby.Server.Implementations.Collections parentFolder.AddChild(collection, CancellationToken.None); - if (options.ItemIdList.Length > 0) + if (options.ItemIdList.Count > 0) { await AddToCollectionAsync( collection.Id, @@ -248,11 +250,7 @@ namespace Emby.Server.Implementations.Collections if (fireEvent) { - ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs - { - Collection = collection, - ItemsChanged = itemList - }); + ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList)); } } } @@ -304,11 +302,7 @@ namespace Emby.Server.Implementations.Collections }, RefreshPriority.High); - ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs - { - Collection = collection, - ItemsChanged = itemList - }); + ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList)); } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index 7a8ed8c29..ff5602f24 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Globalization; using System.IO; diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 12a9e44e7..4a9b28085 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.Security.Cryptography; diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 8c756a7f4..6f23a0888 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) { - if (row[1].SQLiteType != SQLiteType.Null) + if (row.TryGetString(1, out var columnName)) { - var name = row[1].ToString(); - - columnNames.Add(name); + columnNames.Add(columnName); } } diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs index 5c094ddd2..afc8966f9 100644 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ b/Emby.Server.Implementations/Data/ManagedConnection.cs @@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Data { public class ManagedConnection : IDisposable { - private SQLiteDatabaseConnection _db; + private SQLiteDatabaseConnection? _db; private readonly SemaphoreSlim _writeLock; private bool _disposed = false; @@ -54,12 +54,12 @@ namespace Emby.Server.Implementations.Data return _db.RunInTransaction(action, mode); } - public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql) + public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql) { return _db.Query(sql); } - public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql, params object[] values) + public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values) { return _db.Query(sql, values); } diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index a04d63088..3289e7609 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Data }); } - public static Guid ReadGuidFromBlob(this IResultSetValue result) + public static Guid ReadGuidFromBlob(this ResultSetValue result) { return new Guid(result.ToBlob()); } @@ -85,7 +86,7 @@ namespace Emby.Server.Implementations.Data private static string GetDateTimeKindFormat(DateTimeKind kind) => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal; - public static DateTime ReadDateTime(this IResultSetValue result) + public static DateTime ReadDateTime(this ResultSetValue result) { var dateText = result.ToString(); @@ -96,49 +97,139 @@ namespace Emby.Server.Implementations.Data DateTimeStyles.None).ToUniversalTime(); } - public static DateTime? TryReadDateTime(this IResultSetValue result) + public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result) { - var dateText = result.ToString(); + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + var dateText = item.ToString(); if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult)) { - return dateTimeResult.ToUniversalTime(); + result = dateTimeResult.ToUniversalTime(); + return true; + } + + result = default; + return false; + } + + public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; } - return null; + result = item.ReadGuidFromBlob(); + return true; } - public static bool IsDBNull(this IReadOnlyList<IResultSetValue> result, int index) + public static bool IsDbNull(this ResultSetValue result) { - return result[index].SQLiteType == SQLiteType.Null; + return result.SQLiteType == SQLiteType.Null; } - public static string GetString(this IReadOnlyList<IResultSetValue> result, int index) + public static string GetString(this IReadOnlyList<ResultSetValue> result, int index) { return result[index].ToString(); } - public static bool GetBoolean(this IReadOnlyList<IResultSetValue> result, int index) + public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result) + { + result = null; + var item = reader[index]; + if (item.IsDbNull()) + { + return false; + } + + result = item.ToString(); + return true; + } + + public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index) { return result[index].ToBool(); } - public static int GetInt32(this IReadOnlyList<IResultSetValue> result, int index) + public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToBool(); + return true; + } + + public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result) { - return result[index].ToInt(); + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToInt(); + return true; } - public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index) + public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index) { return result[index].ToInt64(); } - public static float GetFloat(this IReadOnlyList<IResultSetValue> result, int index) + public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToInt64(); + return true; + } + + public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result) + { + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToFloat(); + return true; + } + + public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result) { - return result[index].ToFloat(); + var item = reader[index]; + if (item.IsDbNull()) + { + result = default; + return false; + } + + result = item.ToDouble(); + return true; } - public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index) + public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index) { return result[index].ReadGuidFromBlob(); } @@ -350,7 +441,7 @@ namespace Emby.Server.Implementations.Data } } - public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement) + public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement) { while (statement.MoveNext()) { diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 72c9d82ad..9b147b5d7 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -41,6 +43,7 @@ namespace Emby.Server.Implementations.Data /// </summary> public class SqliteItemRepository : BaseSqliteRepository, IItemRepository { + private const string FromText = " from TypedBaseItems A"; private const string ChaptersTableName = "Chapters2"; private readonly IServerConfigurationManager _config; @@ -1043,18 +1046,34 @@ namespace Emby.Server.Implementations.Data return Array.Empty<ItemImageInfo>(); } - var list = new List<ItemImageInfo>(); - foreach (var part in value.SpanSplit('|')) + // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed + var valueSpan = value.AsSpan(); + var count = valueSpan.Count('|') + 1; + + var position = 0; + var result = new ItemImageInfo[count]; + foreach (var part in valueSpan.Split('|')) { var image = ItemImageInfoFromValueString(part); if (image != null) { - list.Add(image); + result[position++] = image; } } - return list.ToArray(); + if (position == count) + { + return result; + } + + if (position == 0) + { + return Array.Empty<ItemImageInfo>(); + } + + // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array. + return result[..position]; } private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) @@ -1282,12 +1301,12 @@ namespace Emby.Server.Implementations.Data return true; } - private BaseItem GetItem(IReadOnlyList<IResultSetValue> reader, InternalItemsQuery query) + private BaseItem GetItem(IReadOnlyList<ResultSetValue> 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<IResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) + private BaseItem GetItem(IReadOnlyList<ResultSetValue> reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields) { var typeString = reader.GetString(0); @@ -1332,27 +1351,23 @@ namespace Emby.Server.Implementations.Data if (queryHasStartDate) { - if (!reader.IsDBNull(index)) + if (item is IHasStartDate hasStartDate && reader.TryReadDateTime(index, out var startDate)) { - if (item is IHasStartDate hasStartDate) - { - hasStartDate.StartDate = reader[index].ReadDateTime(); - } + hasStartDate.StartDate = startDate; } index++; } - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var endDate)) { - item.EndDate = reader[index].TryReadDateTime(); + item.EndDate = endDate; } - index++; - - if (!reader.IsDBNull(index)) + var channelId = reader[index]; + if (!channelId.IsDbNull()) { - if (!Utf8Parser.TryParse(reader[index].ToBlob(), out Guid value, out _, standardFormat: 'N')) + 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); @@ -1368,33 +1383,25 @@ namespace Emby.Server.Implementations.Data { if (item is IHasProgramAttributes hasProgramAttributes) { - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isMovie)) { - hasProgramAttributes.IsMovie = reader.GetBoolean(index); + hasProgramAttributes.IsMovie = isMovie; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isSeries)) { - hasProgramAttributes.IsSeries = reader.GetBoolean(index); + hasProgramAttributes.IsSeries = isSeries; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var episodeTitle)) { - hasProgramAttributes.EpisodeTitle = reader.GetString(index); + hasProgramAttributes.EpisodeTitle = episodeTitle; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isRepeat)) { - hasProgramAttributes.IsRepeat = reader.GetBoolean(index); + hasProgramAttributes.IsRepeat = isRepeat; } - - index++; } else { @@ -1402,242 +1409,190 @@ namespace Emby.Server.Implementations.Data } } - if (!reader.IsDBNull(index)) + if (reader.TryGetSingle(index++, out var communityRating)) { - item.CommunityRating = reader.GetFloat(index); + item.CommunityRating = communityRating; } - index++; - if (HasField(query, ItemFields.CustomRating)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var customRating)) { - item.CustomRating = reader.GetString(index); + item.CustomRating = customRating; } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var indexNumber)) { - item.IndexNumber = reader.GetInt32(index); + item.IndexNumber = indexNumber; } - index++; - if (HasField(query, ItemFields.Settings)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isLocked)) { - item.IsLocked = reader.GetBoolean(index); + item.IsLocked = isLocked; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var preferredMetadataLanguage)) { - item.PreferredMetadataLanguage = reader.GetString(index); + item.PreferredMetadataLanguage = preferredMetadataLanguage; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var preferredMetadataCountryCode)) { - item.PreferredMetadataCountryCode = reader.GetString(index); + item.PreferredMetadataCountryCode = preferredMetadataCountryCode; } - - index++; } if (HasField(query, ItemFields.Width)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var width)) { - item.Width = reader.GetInt32(index); + item.Width = width; } - - index++; } if (HasField(query, ItemFields.Height)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var height)) { - item.Height = reader.GetInt32(index); + item.Height = height; } - - index++; } if (HasField(query, ItemFields.DateLastRefreshed)) { - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var dateLastRefreshed)) { - item.DateLastRefreshed = reader[index].ReadDateTime(); + item.DateLastRefreshed = dateLastRefreshed; } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var name)) { - item.Name = reader.GetString(index); + item.Name = name; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var restorePath)) { - item.Path = RestorePath(reader.GetString(index)); + item.Path = RestorePath(restorePath); } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var premiereDate)) { - item.PremiereDate = reader[index].TryReadDateTime(); + item.PremiereDate = premiereDate; } - index++; - if (HasField(query, ItemFields.Overview)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var overview)) { - item.Overview = reader.GetString(index); + item.Overview = overview; } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var parentIndexNumber)) { - item.ParentIndexNumber = reader.GetInt32(index); + item.ParentIndexNumber = parentIndexNumber; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var productionYear)) { - item.ProductionYear = reader.GetInt32(index); + item.ProductionYear = productionYear; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var officialRating)) { - item.OfficialRating = reader.GetString(index); + item.OfficialRating = officialRating; } - index++; - if (HasField(query, ItemFields.SortName)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var forcedSortName)) { - item.ForcedSortName = reader.GetString(index); + item.ForcedSortName = forcedSortName; } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetInt64(index++, out var runTimeTicks)) { - item.RunTimeTicks = reader.GetInt64(index); + item.RunTimeTicks = runTimeTicks; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetInt64(index++, out var size)) { - item.Size = reader.GetInt64(index); + item.Size = size; } - index++; - if (HasField(query, ItemFields.DateCreated)) { - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var dateCreated)) { - item.DateCreated = reader[index].ReadDateTime(); + item.DateCreated = dateCreated; } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var dateModified)) { - item.DateModified = reader[index].ReadDateTime(); + item.DateModified = dateModified; } - index++; - - item.Id = reader.GetGuid(index); - index++; + item.Id = reader.GetGuid(index++); if (HasField(query, ItemFields.Genres)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var genres)) { - item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); + item.Genres = genres.Split('|', StringSplitOptions.RemoveEmptyEntries); } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetGuid(index++, out var parentId)) { - item.ParentId = reader.GetGuid(index); + item.ParentId = parentId; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var audioString)) { - if (Enum.TryParse(reader.GetString(index), true, out ProgramAudio audio)) + // TODO Span overload coming in the future https://github.com/dotnet/runtime/issues/1916 + if (Enum.TryParse(audioString, true, out ProgramAudio audio)) { item.Audio = audio; } } - index++; - // TODO: Even if not needed by apps, the server needs it internally // But get this excluded from contexts where it is not needed if (hasServiceName) { if (item is LiveTvChannel liveTvChannel) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var serviceName)) { - liveTvChannel.ServiceName = reader.GetString(index); + liveTvChannel.ServiceName = serviceName; } } index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isInMixedFolder)) { - item.IsInMixedFolder = reader.GetBoolean(index); + item.IsInMixedFolder = isInMixedFolder; } - index++; - if (HasField(query, ItemFields.DateLastSaved)) { - if (!reader.IsDBNull(index)) + if (reader.TryReadDateTime(index++, out var dateLastSaved)) { - item.DateLastSaved = reader[index].ReadDateTime(); + item.DateLastSaved = dateLastSaved; } - - index++; } if (HasField(query, ItemFields.Settings)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var lockedFields)) { IEnumerable<MetadataField> GetLockedFields(string s) { @@ -1650,37 +1605,31 @@ namespace Emby.Server.Implementations.Data } } - item.LockedFields = GetLockedFields(reader.GetString(index)).ToArray(); + item.LockedFields = GetLockedFields(lockedFields).ToArray(); } - - index++; } if (HasField(query, ItemFields.Studios)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var studios)) { - item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); + item.Studios = studios.Split('|', StringSplitOptions.RemoveEmptyEntries); } - - index++; } if (HasField(query, ItemFields.Tags)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var tags)) { - item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); + item.Tags = tags.Split('|', StringSplitOptions.RemoveEmptyEntries); } - - index++; } if (hasTrailerTypes) { if (item is Trailer trailer) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var trailerTypes)) { IEnumerable<TrailerType> GetTrailerTypes(string s) { @@ -1693,7 +1642,7 @@ namespace Emby.Server.Implementations.Data } } - trailer.TrailerTypes = GetTrailerTypes(reader.GetString(index)).ToArray(); + trailer.TrailerTypes = GetTrailerTypes(trailerTypes).ToArray(); } } @@ -1702,19 +1651,17 @@ namespace Emby.Server.Implementations.Data if (HasField(query, ItemFields.OriginalTitle)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var originalTitle)) { - item.OriginalTitle = reader.GetString(index); + item.OriginalTitle = originalTitle; } - - index++; } if (item is Video video) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var primaryVersionId)) { - video.PrimaryVersionId = reader.GetString(index); + video.PrimaryVersionId = primaryVersionId; } } @@ -1722,40 +1669,34 @@ namespace Emby.Server.Implementations.Data if (HasField(query, ItemFields.DateLastMediaAdded)) { - if (item is Folder folder && !reader.IsDBNull(index)) + if (item is Folder folder && reader.TryReadDateTime(index, out var dateLastMediaAdded)) { - folder.DateLastMediaAdded = reader[index].TryReadDateTime(); + folder.DateLastMediaAdded = dateLastMediaAdded; } index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var album)) { - item.Album = reader.GetString(index); + item.Album = album; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetSingle(index++, out var criticRating)) { - item.CriticRating = reader.GetFloat(index); + item.CriticRating = criticRating; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetBoolean(index++, out var isVirtualItem)) { - item.IsVirtualItem = reader.GetBoolean(index); + item.IsVirtualItem = isVirtualItem; } - index++; - if (item is IHasSeries hasSeriesName) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var seriesName)) { - hasSeriesName.SeriesName = reader.GetString(index); + hasSeriesName.SeriesName = seriesName; } } @@ -1765,15 +1706,15 @@ namespace Emby.Server.Implementations.Data { if (item is Episode episode) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var seasonName)) { - episode.SeasonName = reader.GetString(index); + episode.SeasonName = seasonName; } index++; - if (!reader.IsDBNull(index)) + if (reader.TryGetGuid(index, out var seasonId)) { - episode.SeasonId = reader.GetGuid(index); + episode.SeasonId = seasonId; } } else @@ -1789,9 +1730,9 @@ namespace Emby.Server.Implementations.Data { if (hasSeries != null) { - if (!reader.IsDBNull(index)) + if (reader.TryGetGuid(index, out var seriesId)) { - hasSeries.SeriesId = reader.GetGuid(index); + hasSeries.SeriesId = seriesId; } } @@ -1800,56 +1741,48 @@ namespace Emby.Server.Implementations.Data if (HasField(query, ItemFields.PresentationUniqueKey)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var presentationUniqueKey)) { - item.PresentationUniqueKey = reader.GetString(index); + item.PresentationUniqueKey = presentationUniqueKey; } - - index++; } if (HasField(query, ItemFields.InheritedParentalRatingValue)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var parentalRating)) { - item.InheritedParentalRatingValue = reader.GetInt32(index); + item.InheritedParentalRatingValue = parentalRating; } - - index++; } if (HasField(query, ItemFields.ExternalSeriesId)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var externalSeriesId)) { - item.ExternalSeriesId = reader.GetString(index); + item.ExternalSeriesId = externalSeriesId; } - - index++; } if (HasField(query, ItemFields.Taglines)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var tagLine)) { - item.Tagline = reader.GetString(index); + item.Tagline = tagLine; } - - index++; } - if (item.ProviderIds.Count == 0 && !reader.IsDBNull(index)) + if (item.ProviderIds.Count == 0 && reader.TryGetString(index, out var providerIds)) { - DeserializeProviderIds(reader.GetString(index), item); + DeserializeProviderIds(providerIds, item); } index++; if (query.DtoOptions.EnableImages) { - if (item.ImageInfos.Length == 0 && !reader.IsDBNull(index)) + if (item.ImageInfos.Length == 0 && reader.TryGetString(index, out var imageInfos)) { - item.ImageInfos = DeserializeImages(reader.GetString(index)); + item.ImageInfos = DeserializeImages(imageInfos); } index++; @@ -1857,72 +1790,62 @@ namespace Emby.Server.Implementations.Data if (HasField(query, ItemFields.ProductionLocations)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var productionLocations)) { - item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray(); + item.ProductionLocations = productionLocations.Split('|', StringSplitOptions.RemoveEmptyEntries); } - - index++; } if (HasField(query, ItemFields.ExtraIds)) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var extraIds)) { - item.ExtraIds = SplitToGuids(reader.GetString(index)); + item.ExtraIds = SplitToGuids(extraIds); } - - index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetInt32(index++, out var totalBitrate)) { - item.TotalBitrate = reader.GetInt32(index); + item.TotalBitrate = totalBitrate; } - index++; - - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var extraTypeString)) { - if (Enum.TryParse(reader.GetString(index), true, out ExtraType extraType)) + if (Enum.TryParse(extraTypeString, true, out ExtraType extraType)) { item.ExtraType = extraType; } } - index++; - if (hasArtistFields) { - if (item is IHasArtist hasArtists && !reader.IsDBNull(index)) + if (item is IHasArtist hasArtists && reader.TryGetString(index, out var artists)) { - hasArtists.Artists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); + hasArtists.Artists = artists.Split('|', StringSplitOptions.RemoveEmptyEntries); } index++; - if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index)) + if (item is IHasAlbumArtist hasAlbumArtists && reader.TryGetString(index, out var albumArtists)) { - hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); + hasAlbumArtists.AlbumArtists = albumArtists.Split('|', StringSplitOptions.RemoveEmptyEntries); } index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index++, out var externalId)) { - item.ExternalId = reader.GetString(index); + item.ExternalId = externalId; } - index++; - if (HasField(query, ItemFields.SeriesPresentationUniqueKey)) { if (hasSeries != null) { - if (!reader.IsDBNull(index)) + if (reader.TryGetString(index, out var seriesPresentationUniqueKey)) { - hasSeries.SeriesPresentationUniqueKey = reader.GetString(index); + hasSeries.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; } } @@ -1931,21 +1854,19 @@ namespace Emby.Server.Implementations.Data if (enableProgramAttributes) { - if (item is LiveTvProgram program && !reader.IsDBNull(index)) + if (item is LiveTvProgram program && reader.TryGetString(index, out var showId)) { - program.ShowId = reader.GetString(index); + program.ShowId = showId; } index++; } - if (!reader.IsDBNull(index)) + if (reader.TryGetGuid(index, out var ownerId)) { - item.OwnerId = reader.GetGuid(index); + item.OwnerId = ownerId; } - index++; - return item; } @@ -2025,21 +1946,21 @@ namespace Emby.Server.Implementations.Data /// <param name="reader">The reader.</param> /// <param name="item">The item.</param> /// <returns>ChapterInfo.</returns> - private ChapterInfo GetChapter(IReadOnlyList<IResultSetValue> reader, BaseItem item) + private ChapterInfo GetChapter(IReadOnlyList<ResultSetValue> reader, BaseItem item) { var chapter = new ChapterInfo { StartPositionTicks = reader.GetInt64(0) }; - if (!reader.IsDBNull(1)) + if (reader.TryGetString(1, out var chapterName)) { - chapter.Name = reader.GetString(1); + chapter.Name = chapterName; } - if (!reader.IsDBNull(2)) + if (reader.TryGetString(2, out var imagePath)) { - chapter.ImagePath = reader.GetString(2); + chapter.ImagePath = imagePath; if (!string.IsNullOrEmpty(chapter.ImagePath)) { @@ -2054,9 +1975,9 @@ namespace Emby.Server.Implementations.Data } } - if (!reader.IsDBNull(3)) + if (reader.TryReadDateTime(3, out var imageDateModified)) { - chapter.ImageDateModified = reader[3].ReadDateTime(); + chapter.ImageDateModified = imageDateModified; } return chapter; @@ -2346,10 +2267,8 @@ namespace Emby.Server.Implementations.Data return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x)); } - private List<string> GetFinalColumnsToSelect(InternalItemsQuery query, IEnumerable<string> startColumns) + private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns) { - var list = startColumns.ToList(); - foreach (var field in _allFields) { if (!HasField(query, field)) @@ -2357,28 +2276,28 @@ namespace Emby.Server.Implementations.Data switch (field) { case ItemFields.Settings: - list.Remove("IsLocked"); - list.Remove("PreferredMetadataCountryCode"); - list.Remove("PreferredMetadataLanguage"); - list.Remove("LockedFields"); + columns.Remove("IsLocked"); + columns.Remove("PreferredMetadataCountryCode"); + columns.Remove("PreferredMetadataLanguage"); + columns.Remove("LockedFields"); break; case ItemFields.ServiceName: - list.Remove("ExternalServiceId"); + columns.Remove("ExternalServiceId"); break; case ItemFields.SortName: - list.Remove("ForcedSortName"); + columns.Remove("ForcedSortName"); break; case ItemFields.Taglines: - list.Remove("Tagline"); + columns.Remove("Tagline"); break; case ItemFields.Tags: - list.Remove("Tags"); + columns.Remove("Tags"); break; case ItemFields.IsHD: // do nothing break; default: - list.Remove(field.ToString()); + columns.Remove(field.ToString()); break; } } @@ -2386,60 +2305,60 @@ namespace Emby.Server.Implementations.Data if (!HasProgramAttributes(query)) { - list.Remove("IsMovie"); - list.Remove("IsSeries"); - list.Remove("EpisodeTitle"); - list.Remove("IsRepeat"); - list.Remove("ShowId"); + columns.Remove("IsMovie"); + columns.Remove("IsSeries"); + columns.Remove("EpisodeTitle"); + columns.Remove("IsRepeat"); + columns.Remove("ShowId"); } if (!HasEpisodeAttributes(query)) { - list.Remove("SeasonName"); - list.Remove("SeasonId"); + columns.Remove("SeasonName"); + columns.Remove("SeasonId"); } if (!HasStartDate(query)) { - list.Remove("StartDate"); + columns.Remove("StartDate"); } if (!HasTrailerTypes(query)) { - list.Remove("TrailerTypes"); + columns.Remove("TrailerTypes"); } if (!HasArtistFields(query)) { - list.Remove("AlbumArtists"); - list.Remove("Artists"); + columns.Remove("AlbumArtists"); + columns.Remove("Artists"); } if (!HasSeriesFields(query)) { - list.Remove("SeriesId"); + columns.Remove("SeriesId"); } if (!HasEpisodeAttributes(query)) { - list.Remove("SeasonName"); - list.Remove("SeasonId"); + columns.Remove("SeasonName"); + columns.Remove("SeasonId"); } if (!query.DtoOptions.EnableImages) { - list.Remove("Images"); + columns.Remove("Images"); } if (EnableJoinUserData(query)) { - list.Add("UserDatas.UserId"); - list.Add("UserDatas.lastPlayedDate"); - list.Add("UserDatas.playbackPositionTicks"); - list.Add("UserDatas.playcount"); - list.Add("UserDatas.isFavorite"); - list.Add("UserDatas.played"); - list.Add("UserDatas.rating"); + columns.Add("UserDatas.UserId"); + columns.Add("UserDatas.lastPlayedDate"); + columns.Add("UserDatas.playbackPositionTicks"); + columns.Add("UserDatas.playcount"); + columns.Add("UserDatas.isFavorite"); + columns.Add("UserDatas.played"); + columns.Add("UserDatas.rating"); } if (query.SimilarTo != null) @@ -2487,7 +2406,7 @@ namespace Emby.Server.Implementations.Data builder.Append(") as SimilarityScore"); - list.Add(builder.ToString()); + columns.Add(builder.ToString()); var oldLen = query.ExcludeItemIds.Length; var newLen = oldLen + item.ExtraIds.Length + 1; @@ -2514,10 +2433,8 @@ namespace Emby.Server.Implementations.Data builder.Append(") as SearchScore"); - list.Add(builder.ToString()); + columns.Add(builder.ToString()); } - - return list; } private void BindSearchParams(InternalItemsQuery query, IStatement statement) @@ -2583,31 +2500,25 @@ namespace Emby.Server.Implementations.Data private string GetGroupBy(InternalItemsQuery query) { - var groups = new List<string>(); - - if (EnableGroupByPresentationUniqueKey(query)) + var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query); + if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey) { - groups.Add("PresentationUniqueKey"); + return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey"; } - if (query.GroupBySeriesPresentationUniqueKey) + if (enableGroupByPresentationUniqueKey) { - groups.Add("SeriesPresentationUniqueKey"); + return " Group by PresentationUniqueKey"; } - if (groups.Count > 0) + if (query.GroupBySeriesPresentationUniqueKey) { - return " Group by " + string.Join(',', groups); + return " Group by SeriesPresentationUniqueKey"; } return string.Empty; } - private string GetFromText(string alias = "A") - { - return " from TypedBaseItems " + alias; - } - public int GetCount(InternalItemsQuery query) { if (query == null) @@ -2625,17 +2536,21 @@ namespace Emby.Server.Implementations.Data query.Limit = query.Limit.Value + 4; } - var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" })) - + GetFromText() - + GetJoinUserDataText(query); + var columns = new List<string> { "count(distinct PresentationUniqueKey)" }; + SetFinalColumnsToSelect(query, columns); + var commandTextBuilder = new StringBuilder("select ", 256) + .AppendJoin(',', columns) + .Append(FromText) + .Append(GetJoinUserDataText(query)); var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) { - commandText += " where " + string.Join(" AND ", whereClauses); + commandTextBuilder.Append(" where ") + .AppendJoin(" AND ", whereClauses); } + var commandText = commandTextBuilder.ToString(); int count; using (var connection = GetConnection(true)) { @@ -2677,20 +2592,23 @@ namespace Emby.Server.Implementations.Data query.Limit = query.Limit.Value + 4; } - var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns)) - + GetFromText() - + GetJoinUserDataText(query); + var columns = _retriveItemColumns.ToList(); + SetFinalColumnsToSelect(query, columns); + var commandTextBuilder = new StringBuilder("select ", 1024) + .AppendJoin(',', columns) + .Append(FromText) + .Append(GetJoinUserDataText(query)); var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) { - commandText += " where " + string.Join(" AND ", whereClauses); + commandTextBuilder.Append(" where ") + .AppendJoin(" AND ", whereClauses); } - commandText += GetGroupBy(query) - + GetOrderByText(query); + commandTextBuilder.Append(GetGroupBy(query)) + .Append(GetOrderByText(query)); if (query.Limit.HasValue || query.StartIndex.HasValue) { @@ -2698,15 +2616,18 @@ namespace Emby.Server.Implementations.Data if (query.Limit.HasValue || offset > 0) { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" LIMIT ") + .Append(query.Limit ?? int.MaxValue); } if (offset > 0) { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" OFFSET ") + .Append(offset); } } + var commandText = commandTextBuilder.ToString(); var items = new List<BaseItem>(); using (var connection = GetConnection(true)) { @@ -2862,20 +2783,27 @@ namespace Emby.Server.Implementations.Data query.Limit = query.Limit.Value + 4; } - var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns)) - + GetFromText() - + GetJoinUserDataText(query); + var columns = _retriveItemColumns.ToList(); + SetFinalColumnsToSelect(query, columns); + var commandTextBuilder = new StringBuilder("select ", 512) + .AppendJoin(',', columns) + .Append(FromText) + .Append(GetJoinUserDataText(query)); var whereClauses = GetWhereClauses(query, null); var whereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", whereClauses); + string.Join(" AND ", whereClauses); - commandText += whereText - + GetGroupBy(query) - + GetOrderByText(query); + if (!string.IsNullOrEmpty(whereText)) + { + commandTextBuilder.Append(" where ") + .Append(whereText); + } + + commandTextBuilder.Append(GetGroupBy(query)) + .Append(GetOrderByText(query)); if (query.Limit.HasValue || query.StartIndex.HasValue) { @@ -2883,43 +2811,58 @@ namespace Emby.Server.Implementations.Data if (query.Limit.HasValue || offset > 0) { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" LIMIT ") + .Append(query.Limit ?? int.MaxValue); } if (offset > 0) { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" OFFSET ") + .Append(offset); } } var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - var statementTexts = new List<string>(); + var itemQuery = string.Empty; + var totalRecordCountQuery = string.Empty; if (!isReturningZeroItems) { - statementTexts.Add(commandText); + itemQuery = commandTextBuilder.ToString(); } if (query.EnableTotalRecordCount) { - commandText = string.Empty; + commandTextBuilder.Clear(); + + commandTextBuilder.Append(" select "); + List<string> columnsToSelect; if (EnableGroupByPresentationUniqueKey(query)) { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" }; } else if (query.GroupBySeriesPresentationUniqueKey) { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" }; } else { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (guid)" }; } - commandText += GetJoinUserDataText(query) - + whereText; - statementTexts.Add(commandText); + SetFinalColumnsToSelect(query, columnsToSelect); + + commandTextBuilder.AppendJoin(',', columnsToSelect) + .Append(FromText) + .Append(GetJoinUserDataText(query)); + if (!string.IsNullOrEmpty(whereText)) + { + commandTextBuilder.Append(" where ") + .Append(whereText); + } + + totalRecordCountQuery = commandTextBuilder.ToString(); } var list = new List<BaseItem>(); @@ -2929,11 +2872,12 @@ namespace Emby.Server.Implementations.Data connection.RunInTransaction( db => { - var statements = PrepareAll(db, statementTexts); + var itemQueryStatement = PrepareStatement(db, itemQuery); + var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery); if (!isReturningZeroItems) { - using (var statement = statements[0]) + using (var statement = itemQueryStatement) { if (EnableJoinUserData(query)) { @@ -2963,11 +2907,14 @@ namespace Emby.Server.Implementations.Data } } } + + LogQueryTime("GetItems.ItemQuery", itemQuery, now); } + now = DateTime.UtcNow; if (query.EnableTotalRecordCount) { - using (var statement = statements[statements.Length - 1]) + using (var statement = totalRecordCountQueryStatement) { if (EnableJoinUserData(query)) { @@ -2982,11 +2929,12 @@ namespace Emby.Server.Implementations.Data result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } + + LogQueryTime("GetItems.TotalRecordCount", totalRecordCountQuery, now); } }, ReadTransactionMode); } - LogQueryTime("GetItems", commandText, now); result.Items = list; return result; } @@ -3119,19 +3067,22 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; - var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" })) - + GetFromText() - + GetJoinUserDataText(query); + var columns = new List<string> { "guid" }; + SetFinalColumnsToSelect(query, columns); + var commandTextBuilder = new StringBuilder("select ", 256) + .AppendJoin(',', columns) + .Append(FromText) + .Append(GetJoinUserDataText(query)); var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) { - commandText += " where " + string.Join(" AND ", whereClauses); + commandTextBuilder.Append(" where ") + .AppendJoin(" AND ", whereClauses); } - commandText += GetGroupBy(query) - + GetOrderByText(query); + commandTextBuilder.Append(GetGroupBy(query)) + .Append(GetOrderByText(query)); if (query.Limit.HasValue || query.StartIndex.HasValue) { @@ -3139,15 +3090,18 @@ namespace Emby.Server.Implementations.Data if (query.Limit.HasValue || offset > 0) { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" LIMIT ") + .Append(query.Limit ?? int.MaxValue); } if (offset > 0) { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + commandTextBuilder.Append(" OFFSET ") + .Append(offset); } } + var commandText = commandTextBuilder.ToString(); var list = new List<Guid>(); using (var connection = GetConnection(true)) { @@ -3186,7 +3140,9 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; - var commandText = "select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText(); + var columns = new List<string> { "guid", "path" }; + SetFinalColumnsToSelect(query, columns); + var commandText = "select " + string.Join(',', columns) + FromText; var whereClauses = GetWhereClauses(query, null); if (whereClauses.Count != 0) @@ -3228,12 +3184,8 @@ namespace Emby.Server.Implementations.Data foreach (var row in statement.ExecuteQuery()) { var id = row.GetGuid(0); - string path = null; - if (!row.IsDBNull(1)) - { - path = row.GetString(1); - } + row.TryGetString(1, out var path); list.Add(new Tuple<Guid, string>(id, path)); } @@ -3266,9 +3218,11 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; + var columns = new List<string> { "guid" }; + SetFinalColumnsToSelect(query, columns); var commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" })) - + GetFromText() + + string.Join(',', columns) + + FromText + GetJoinUserDataText(query); var whereClauses = GetWhereClauses(query, null); @@ -3308,19 +3262,23 @@ namespace Emby.Server.Implementations.Data { commandText = string.Empty; + List<string> columnsToSelect; if (EnableGroupByPresentationUniqueKey(query)) { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" }; } else if (query.GroupBySeriesPresentationUniqueKey) { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" }; } else { - commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText(); + columnsToSelect = new List<string> { "count (guid)" }; } + SetFinalColumnsToSelect(query, columnsToSelect); + commandText += " select " + string.Join(',', columnsToSelect) + FromText; + commandText += GetJoinUserDataText(query) + whereText; statementTexts.Add(commandText); @@ -4427,7 +4385,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(string.Join(" AND ", excludeIds)); } - if (query.ExcludeProviderIds.Count > 0) + if (query.ExcludeProviderIds != null && query.ExcludeProviderIds.Count > 0) { var excludeIds = new List<string>(); @@ -4457,7 +4415,7 @@ namespace Emby.Server.Implementations.Data } } - if (query.HasAnyProviderId.Count > 0) + if (query.HasAnyProviderId != null && query.HasAnyProviderId.Count > 0) { var hasProviderIds = new List<string>(); @@ -4515,56 +4473,50 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb")); } - var includedItemByNameTypes = GetItemByNameTypesInQuery(query); - var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; - var queryTopParentIds = query.TopParentIds; - if (queryTopParentIds.Length == 1) + if (queryTopParentIds.Length > 0) { - if (enableItemsByName && includedItemByNameTypes.Count == 1) + var includedItemByNameTypes = GetItemByNameTypesInQuery(query); + var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; + + if (queryTopParentIds.Length == 1) { - whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)"); - if (statement != null) + if (enableItemsByName && includedItemByNameTypes.Count == 1) { - statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); + whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)"); + statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); + } + else if (enableItemsByName && includedItemByNameTypes.Count > 1) + { + var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); + whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))"); + } + else + { + whereClauses.Add("(TopParentId=@TopParentId)"); } - } - else if (enableItemsByName && includedItemByNameTypes.Count > 1) - { - var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); - whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))"); - } - else - { - whereClauses.Add("(TopParentId=@TopParentId)"); - } - if (statement != null) - { - statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture)); + statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture)); } - } - else if (queryTopParentIds.Length > 1) - { - var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); - - if (enableItemsByName && includedItemByNameTypes.Count == 1) + else if (queryTopParentIds.Length > 1) { - whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))"); - if (statement != null) + var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); + + if (enableItemsByName && includedItemByNameTypes.Count == 1) { - statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); + whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))"); + statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); + } + else if (enableItemsByName && includedItemByNameTypes.Count > 1) + { + var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); + whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))"); + } + else + { + whereClauses.Add("TopParentId in (" + val + ")"); } - } - else if (enableItemsByName && includedItemByNameTypes.Count > 1) - { - var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); - whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))"); - } - else - { - whereClauses.Add("TopParentId in (" + val + ")"); } } @@ -4846,17 +4798,12 @@ namespace Emby.Server.Implementations.Data return true; } - var types = new[] - { - nameof(Episode), - nameof(Video), - nameof(Movie), - nameof(MusicVideo), - nameof(Series), - nameof(Season) - }; - - if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase))) + if (query.IncludeItemTypes.Contains(nameof(Episode), StringComparer.OrdinalIgnoreCase) + || query.IncludeItemTypes.Contains(nameof(Video), StringComparer.OrdinalIgnoreCase) + || query.IncludeItemTypes.Contains(nameof(Movie), StringComparer.OrdinalIgnoreCase) + || query.IncludeItemTypes.Contains(nameof(MusicVideo), StringComparer.OrdinalIgnoreCase) + || query.IncludeItemTypes.Contains(nameof(Series), StringComparer.OrdinalIgnoreCase) + || query.IncludeItemTypes.Contains(nameof(Season), StringComparer.OrdinalIgnoreCase)) { return true; } @@ -5300,37 +5247,45 @@ AND Type = @InternalPersonType)"); var now = DateTime.UtcNow; - var typeClause = itemValueTypes.Length == 1 ? - ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : - ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")"); - - var commandText = "Select Value From ItemValues where " + typeClause; + var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128); + if (itemValueTypes.Length == 1) + { + stringBuilder.Append('=') + .Append(itemValueTypes[0]); + } + else + { + stringBuilder.Append(" in (") + .AppendJoin(',', itemValueTypes) + .Append(')'); + } if (withItemTypes.Count > 0) { - var typeString = string.Join(',', withItemTypes.Select(i => "'" + i + "'")); - commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))"; + stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (") + .AppendJoinInSingleQuotes(',', withItemTypes) + .Append("))"); } if (excludeItemTypes.Count > 0) { - var typeString = string.Join(',', excludeItemTypes.Select(i => "'" + i + "'")); - commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))"; + stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (") + .AppendJoinInSingleQuotes(',', excludeItemTypes) + .Append("))"); } - commandText += " Group By CleanValue"; + stringBuilder.Append(" Group By CleanValue"); + var commandText = stringBuilder.ToString(); var list = new List<string>(); using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText)) { - using (var statement = PrepareStatement(connection, commandText)) + foreach (var row in statement.ExecuteQuery()) { - foreach (var row in statement.ExecuteQuery()) + if (row.TryGetString(0, out var result)) { - if (!row.IsDBNull(0)) - { - list.Add(row.GetString(0)); - } + list.Add(result); } } } @@ -5356,18 +5311,19 @@ AND Type = @InternalPersonType)"); var now = DateTime.UtcNow; var typeClause = itemValueTypes.Length == 1 ? - ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : - ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")"); + ("Type=" + itemValueTypes[0]) : + ("Type in (" + string.Join(',', itemValueTypes) + ")"); InternalItemsQuery typeSubQuery = null; - Dictionary<string, string> itemCountColumns = null; + string itemCountColumns = null; + var stringBuilder = new StringBuilder(1024); var typesToCount = query.IncludeItemTypes; if (typesToCount.Length > 0) { - var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B"); + stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B"); typeSubQuery = new InternalItemsQuery(query.User) { @@ -5383,20 +5339,22 @@ AND Type = @InternalPersonType)"); }; var whereClauses = GetWhereClauses(typeSubQuery, null); - whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")"); + stringBuilder.Append(" where ") + .AppendJoin(" AND ", whereClauses) + .Append(" AND ") + .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ") + .Append(typeClause) + .Append(")) as itemTypes"); - itemCountColumnQuery += " where " + string.Join(" AND ", whereClauses); - - itemCountColumns = new Dictionary<string, string>() - { - { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" } - }; + itemCountColumns = stringBuilder.ToString(); + stringBuilder.Clear(); } List<string> columns = _retriveItemColumns.ToList(); - if (itemCountColumns != null) + // Unfortunately we need to add it to columns to ensure the order of the columns in the select + if (!string.IsNullOrEmpty(itemCountColumns)) { - columns.AddRange(itemCountColumns.Values); + columns.Add(itemCountColumns); } // do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo @@ -5417,20 +5375,20 @@ AND Type = @InternalPersonType)"); IsSeries = query.IsSeries }; - columns = GetFinalColumnsToSelect(query, columns); - - var commandText = "select " - + string.Join(',', columns) - + GetFromText() - + GetJoinUserDataText(query); + SetFinalColumnsToSelect(query, columns); var innerWhereClauses = GetWhereClauses(innerQuery, null); - var innerWhereText = innerWhereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", innerWhereClauses); + stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ") + .Append(typeClause) + .Append(" AND ItemId in (select guid from TypedBaseItems"); + if (innerWhereClauses.Count > 0) + { + stringBuilder.Append(" where ") + .AppendJoin(" AND ", innerWhereClauses); + } - var whereText = " where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + stringBuilder.Append("))"); var outerQuery = new InternalItemsQuery(query.User) { @@ -5455,21 +5413,31 @@ AND Type = @InternalPersonType)"); }; var outerWhereClauses = GetWhereClauses(outerQuery, null); - if (outerWhereClauses.Count != 0) { - whereText += " AND " + string.Join(" AND ", outerWhereClauses); + stringBuilder.Append(" AND ") + .AppendJoin(" AND ", outerWhereClauses); } - commandText += whereText + " group by PresentationUniqueKey"; + var whereText = stringBuilder.ToString(); + stringBuilder.Clear(); - if (query.SimilarTo != null || !string.IsNullOrEmpty(query.SearchTerm)) + stringBuilder.Append("select ") + .AppendJoin(',', columns) + .Append(FromText) + .Append(GetJoinUserDataText(query)) + .Append(whereText) + .Append(" group by PresentationUniqueKey"); + + if (query.OrderBy.Count != 0 + || query.SimilarTo != null + || !string.IsNullOrEmpty(query.SearchTerm)) { - commandText += GetOrderByText(query); + stringBuilder.Append(GetOrderByText(query)); } else { - commandText += " order by SortName"; + stringBuilder.Append(" order by SortName"); } if (query.Limit.HasValue || query.StartIndex.HasValue) @@ -5478,32 +5446,39 @@ AND Type = @InternalPersonType)"); if (query.Limit.HasValue || offset > 0) { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + stringBuilder.Append(" LIMIT ") + .Append(query.Limit ?? int.MaxValue); } if (offset > 0) { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + stringBuilder.Append(" OFFSET ") + .Append(offset); } } var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - var statementTexts = new List<string>(); + string commandText = string.Empty; + if (!isReturningZeroItems) { - statementTexts.Add(commandText); + commandText = stringBuilder.ToString(); } + string countText = string.Empty; if (query.EnableTotalRecordCount) { - var countText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) - + GetFromText() - + GetJoinUserDataText(query) - + whereText; + stringBuilder.Clear(); + var columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" }; + SetFinalColumnsToSelect(query, columnsToSelect); + stringBuilder.Append("select ") + .AppendJoin(',', columnsToSelect) + .Append(FromText) + .Append(GetJoinUserDataText(query)) + .Append(whereText); - statementTexts.Add(countText); + countText = stringBuilder.ToString(); } var list = new List<(BaseItem, ItemCounts)>(); @@ -5513,11 +5488,9 @@ AND Type = @InternalPersonType)"); connection.RunInTransaction( db => { - var statements = PrepareAll(db, statementTexts); - if (!isReturningZeroItems) { - using (var statement = statements[0]) + using (var statement = PrepareStatement(db, commandText)) { statement.TryBind("@SelectType", returnType); if (EnableJoinUserData(query)) @@ -5558,13 +5531,7 @@ AND Type = @InternalPersonType)"); if (query.EnableTotalRecordCount) { - commandText = "select " - + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) - + GetFromText() - + GetJoinUserDataText(query) - + whereText; - - using (var statement = statements[statements.Length - 1]) + using (var statement = PrepareStatement(db, countText)) { statement.TryBind("@SelectType", returnType); if (EnableJoinUserData(query)) @@ -5601,7 +5568,7 @@ AND Type = @InternalPersonType)"); return result; } - private ItemCounts GetItemCounts(IReadOnlyList<IResultSetValue> reader, int countStartColumn, string[] typesToCount) + private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, string[] typesToCount) { var counts = new ItemCounts(); @@ -5610,51 +5577,43 @@ AND Type = @InternalPersonType)"); return counts; } - var typeString = reader.IsDBNull(countStartColumn) ? null : reader.GetString(countStartColumn); - - if (string.IsNullOrWhiteSpace(typeString)) + if (!reader.TryGetString(countStartColumn, out var typeString)) { return counts; } - var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries) - .ToLookup(x => x); - - foreach (var type in allTypes) + foreach (var typeName in typeString.AsSpan().Split('|')) { - var value = type.Count(); - var typeName = type.Key; - - if (string.Equals(typeName, typeof(Series).FullName, StringComparison.OrdinalIgnoreCase)) + if (typeName.Equals(typeof(Series).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.SeriesCount = value; + counts.SeriesCount++; } - else if (string.Equals(typeName, typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.EpisodeCount = value; + counts.EpisodeCount++; } - else if (string.Equals(typeName, typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.MovieCount = value; + counts.MovieCount++; } - else if (string.Equals(typeName, typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.AlbumCount = value; + counts.AlbumCount++; } - else if (string.Equals(typeName, typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.ArtistCount = value; + counts.ArtistCount++; } - else if (string.Equals(typeName, typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.SongCount = value; + counts.SongCount++; } - else if (string.Equals(typeName, typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase)) + else if (typeName.Equals(typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase)) { - counts.TrailerCount = value; + counts.TrailerCount++; } - counts.ItemCount += value; + counts.ItemCount++; } return counts; @@ -5840,7 +5799,7 @@ AND Type = @InternalPersonType)"); } } - private PersonInfo GetPerson(IReadOnlyList<IResultSetValue> reader) + private PersonInfo GetPerson(IReadOnlyList<ResultSetValue> reader) { var item = new PersonInfo { @@ -5848,19 +5807,19 @@ AND Type = @InternalPersonType)"); Name = reader.GetString(1) }; - if (!reader.IsDBNull(2)) + if (reader.TryGetString(2, out var role)) { - item.Role = reader.GetString(2); + item.Role = role; } - if (!reader.IsDBNull(3)) + if (reader.TryGetString(3, out var type)) { - item.Type = reader.GetString(3); + item.Type = type; } - if (!reader.IsDBNull(4)) + if (reader.TryGetInt32(4, out var sortOrder)) { - item.SortOrder = reader.GetInt32(4); + item.SortOrder = sortOrder; } return item; @@ -6047,7 +6006,7 @@ AND Type = @InternalPersonType)"); /// </summary> /// <param name="reader">The reader.</param> /// <returns>ChapterInfo.</returns> - private MediaStream GetMediaStream(IReadOnlyList<IResultSetValue> reader) + private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader) { var item = new MediaStream { @@ -6056,150 +6015,150 @@ AND Type = @InternalPersonType)"); item.Type = Enum.Parse<MediaStreamType>(reader[2].ToString(), true); - if (reader[3].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(3, out var codec)) { - item.Codec = reader[3].ToString(); + item.Codec = codec; } - if (reader[4].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(4, out var language)) { - item.Language = reader[4].ToString(); + item.Language = language; } - if (reader[5].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(5, out var channelLayout)) { - item.ChannelLayout = reader[5].ToString(); + item.ChannelLayout = channelLayout; } - if (reader[6].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(6, out var profile)) { - item.Profile = reader[6].ToString(); + item.Profile = profile; } - if (reader[7].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(7, out var aspectRatio)) { - item.AspectRatio = reader[7].ToString(); + item.AspectRatio = aspectRatio; } - if (reader[8].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(8, out var path)) { - item.Path = RestorePath(reader[8].ToString()); + item.Path = RestorePath(path); } item.IsInterlaced = reader.GetBoolean(9); - if (reader[10].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(10, out var bitrate)) { - item.BitRate = reader.GetInt32(10); + item.BitRate = bitrate; } - if (reader[11].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(11, out var channels)) { - item.Channels = reader.GetInt32(11); + item.Channels = channels; } - if (reader[12].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(12, out var sampleRate)) { - item.SampleRate = reader.GetInt32(12); + item.SampleRate = sampleRate; } item.IsDefault = reader.GetBoolean(13); item.IsForced = reader.GetBoolean(14); item.IsExternal = reader.GetBoolean(15); - if (reader[16].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(16, out var width)) { - item.Width = reader.GetInt32(16); + item.Width = width; } - if (reader[17].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(17, out var height)) { - item.Height = reader.GetInt32(17); + item.Height = height; } - if (reader[18].SQLiteType != SQLiteType.Null) + if (reader.TryGetSingle(18, out var averageFrameRate)) { - item.AverageFrameRate = reader.GetFloat(18); + item.AverageFrameRate = averageFrameRate; } - if (reader[19].SQLiteType != SQLiteType.Null) + if (reader.TryGetSingle(19, out var realFrameRate)) { - item.RealFrameRate = reader.GetFloat(19); + item.RealFrameRate = realFrameRate; } - if (reader[20].SQLiteType != SQLiteType.Null) + if (reader.TryGetSingle(20, out var level)) { - item.Level = reader.GetFloat(20); + item.Level = level; } - if (reader[21].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(21, out var pixelFormat)) { - item.PixelFormat = reader[21].ToString(); + item.PixelFormat = pixelFormat; } - if (reader[22].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(22, out var bitDepth)) { - item.BitDepth = reader.GetInt32(22); + item.BitDepth = bitDepth; } - if (reader[23].SQLiteType != SQLiteType.Null) + if (reader.TryGetBoolean(23, out var isAnamorphic)) { - item.IsAnamorphic = reader.GetBoolean(23); + item.IsAnamorphic = isAnamorphic; } - if (reader[24].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(24, out var refFrames)) { - item.RefFrames = reader.GetInt32(24); + item.RefFrames = refFrames; } - if (reader[25].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(25, out var codecTag)) { - item.CodecTag = reader.GetString(25); + item.CodecTag = codecTag; } - if (reader[26].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(26, out var comment)) { - item.Comment = reader.GetString(26); + item.Comment = comment; } - if (reader[27].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(27, out var nalLengthSize)) { - item.NalLengthSize = reader.GetString(27); + item.NalLengthSize = nalLengthSize; } - if (reader[28].SQLiteType != SQLiteType.Null) + if (reader.TryGetBoolean(28, out var isAVC)) { - item.IsAVC = reader[28].ToBool(); + item.IsAVC = isAVC; } - if (reader[29].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(29, out var title)) { - item.Title = reader[29].ToString(); + item.Title = title; } - if (reader[30].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(30, out var timeBase)) { - item.TimeBase = reader[30].ToString(); + item.TimeBase = timeBase; } - if (reader[31].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(31, out var codecTimeBase)) { - item.CodecTimeBase = reader[31].ToString(); + item.CodecTimeBase = codecTimeBase; } - if (reader[32].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(32, out var colorPrimaries)) { - item.ColorPrimaries = reader[32].ToString(); + item.ColorPrimaries = colorPrimaries; } - if (reader[33].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(33, out var colorSpace)) { - item.ColorSpace = reader[33].ToString(); + item.ColorSpace = colorSpace; } - if (reader[34].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(34, out var colorTransfer)) { - item.ColorTransfer = reader[34].ToString(); + item.ColorTransfer = colorTransfer; } if (item.Type == MediaStreamType.Subtitle) @@ -6348,36 +6307,36 @@ AND Type = @InternalPersonType)"); /// </summary> /// <param name="reader">The reader.</param> /// <returns>MediaAttachment.</returns> - private MediaAttachment GetMediaAttachment(IReadOnlyList<IResultSetValue> reader) + private MediaAttachment GetMediaAttachment(IReadOnlyList<ResultSetValue> reader) { var item = new MediaAttachment { Index = reader[1].ToInt() }; - if (reader[2].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(2, out var codec)) { - item.Codec = reader[2].ToString(); + item.Codec = codec; } - if (reader[2].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(3, out var codecTag)) { - item.CodecTag = reader[3].ToString(); + item.CodecTag = codecTag; } - if (reader[4].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(4, out var comment)) { - item.Comment = reader[4].ToString(); + item.Comment = comment; } - if (reader[6].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(5, out var fileName)) { - item.FileName = reader[5].ToString(); + item.FileName = fileName; } - if (reader[6].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(6, out var mimeType)) { - item.MimeType = reader[6].ToString(); + item.MimeType = mimeType; } return item; diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 6574db607..ef9af1dcd 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -348,16 +350,16 @@ namespace Emby.Server.Implementations.Data /// Read a row from the specified reader into the provided userData object. /// </summary> /// <param name="reader"></param> - private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader) + private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader) { var userData = new UserItemData(); userData.Key = reader[0].ToString(); // userData.UserId = reader[1].ReadGuidFromBlob(); - if (reader[2].SQLiteType != SQLiteType.Null) + if (reader.TryGetDouble(2, out var rating)) { - userData.Rating = reader[2].ToDouble(); + userData.Rating = rating; } userData.Played = reader[3].ToBool(); @@ -365,19 +367,19 @@ namespace Emby.Server.Implementations.Data userData.IsFavorite = reader[5].ToBool(); userData.PlaybackPositionTicks = reader[6].ToInt64(); - if (reader[7].SQLiteType != SQLiteType.Null) + if (reader.TryReadDateTime(7, out var lastPlayedDate)) { - userData.LastPlayedDate = reader[7].TryReadDateTime(); + userData.LastPlayedDate = lastPlayedDate; } - if (reader[8].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(8, out var audioStreamIndex)) { - userData.AudioStreamIndex = reader[8].ToInt(); + userData.AudioStreamIndex = audioStreamIndex; } - if (reader[9].SQLiteType != SQLiteType.Null) + if (reader.TryGetInt32(9, out var subtitleStreamIndex)) { - userData.SubtitleStreamIndex = reader[9].ToInt(); + userData.SubtitleStreamIndex = subtitleStreamIndex; } return userData; diff --git a/Emby.Server.Implementations/Data/TypeMapper.cs b/Emby.Server.Implementations/Data/TypeMapper.cs index 7044b1d19..064664e1f 100644 --- a/Emby.Server.Implementations/Data/TypeMapper.cs +++ b/Emby.Server.Implementations/Data/TypeMapper.cs @@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Data /// This holds all the types in the running assemblies /// so that we can de-serialize properly when we don't have strong types. /// </summary> - private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>(); + private readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>(); /// <summary> /// Gets the type. @@ -21,26 +21,16 @@ namespace Emby.Server.Implementations.Data /// <param name="typeName">Name of the type.</param> /// <returns>Type.</returns> /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception> - public Type GetType(string typeName) + public Type? GetType(string typeName) { if (string.IsNullOrEmpty(typeName)) { throw new ArgumentNullException(nameof(typeName)); } - return _typeMap.GetOrAdd(typeName, LookupType); - } - - /// <summary> - /// Lookups the type. - /// </summary> - /// <param name="typeName">Name of the type.</param> - /// <returns>Type.</returns> - private Type LookupType(string typeName) - { - return AppDomain.CurrentDomain.GetAssemblies() - .Select(a => a.GetType(typeName)) - .FirstOrDefault(t => t != null); + return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies() + .Select(a => a.GetType(k)) + .FirstOrDefault(t => t != null)); } } } diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs index fa6ac95fd..3d15b3e76 100644 --- a/Emby.Server.Implementations/Devices/DeviceId.cs +++ b/Emby.Server.Implementations/Devices/DeviceId.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index da5047d24..2637addce 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 4ae35039a..7411239a1 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index b8a544b8c..9c90de1ed 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -9,6 +9,7 @@ <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" /> <ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" /> <ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" /> + <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> @@ -27,11 +28,11 @@ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" /> <PackageReference Include="Mono.Nat" Version="3.0.1" /> - <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.0.0" /> - <PackageReference Include="sharpcompress" Version="0.28.2" /> - <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.2.0" /> + <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" /> + <PackageReference Include="sharpcompress" Version="0.28.3" /> + <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageReference Include="DotNet.Glob" Version="3.1.2" /> </ItemGroup> @@ -44,6 +45,7 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> <NoWarn>AD0001</NoWarn> <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode> diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 14201ead2..0a4efd73c 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -106,8 +108,6 @@ namespace Emby.Server.Implementations.EntryPoints NatUtility.StartDiscovery(); _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); - - _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; } private void Stop() @@ -118,13 +118,6 @@ namespace Emby.Server.Implementations.EntryPoints NatUtility.DeviceFound -= OnNatUtilityDeviceFound; _timer?.Dispose(); - - _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; - } - - private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) - { - NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp); } private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index ae1b51b4c..5bb4100ba 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index 824bb85f4..e0ca02d98 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 3624e079f..2e72b18f5 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Net.Sockets; using System.Threading; @@ -56,8 +54,8 @@ namespace Emby.Server.Implementations.EntryPoints try { - _udpServer = new UdpServer(_logger, _appHost, _config); - _udpServer.Start(PortNumber, _cancellationTokenSource.Token); + _udpServer = new UdpServer(_logger, _appHost, _config, PortNumber); + _udpServer.Start(_cancellationTokenSource.Token); } catch (SocketException ex) { diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 1989e9ed2..d3bcd5e13 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.EntryPoints private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>(); private readonly object _syncLock = new object(); - private Timer _updateTimer; + private Timer? _updateTimer; public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager) { @@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e) + private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e) { if (e.SaveReason == UserDataSaveReason.PlaybackProgress) { @@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints _updateTimer.Change(UpdateDuration, Timeout.Infinite); } - if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys)) + if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem>? keys)) { keys = new List<BaseItem>(); _changedItems[e.UserId] = keys; @@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.EntryPoints } } - private void UpdateTimerCallback(object state) + private void UpdateTimerCallback(object? state) { lock (_syncLock) { diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index 024404ceb..c87f7dbbd 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.HttpServer.Security { if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached)) { - return (AuthorizationInfo)cached; + return (AuthorizationInfo)cached!; // Cache should never contain null } return GetAuthorization(requestContext); @@ -55,15 +55,15 @@ namespace Emby.Server.Implementations.HttpServer.Security } private AuthorizationInfo GetAuthorizationInfoFromDictionary( - in Dictionary<string, string> auth, + in Dictionary<string, string>? auth, in IHeaderDictionary headers, in IQueryCollection queryString) { - string deviceId = null; - string device = null; - string client = null; - string version = null; - string token = null; + string? deviceId = null; + string? device = null; + string? client = null; + string? version = null; + string? token = null; if (auth != null) { @@ -206,7 +206,7 @@ namespace Emby.Server.Implementations.HttpServer.Security /// </summary> /// <param name="httpReq">The HTTP req.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq) + private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq) { var auth = httpReq.Request.Headers["X-Emby-Authorization"]; @@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security auth = httpReq.Request.Headers[HeaderNames.Authorization]; } - return GetAuthorization(auth); + return GetAuthorization(auth.Count > 0 ? auth[0] : null); } /// <summary> @@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer.Security /// </summary> /// <param name="httpReq">The HTTP req.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq) + private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq) { var auth = httpReq.Headers["X-Emby-Authorization"]; @@ -232,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security auth = httpReq.Headers[HeaderNames.Authorization]; } - return GetAuthorization(auth); + return GetAuthorization(auth.Count > 0 ? auth[0] : null); } /// <summary> @@ -240,43 +240,43 @@ namespace Emby.Server.Implementations.HttpServer.Security /// </summary> /// <param name="authorizationHeader">The authorization header.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private Dictionary<string, string> GetAuthorization(string authorizationHeader) + private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader) { if (authorizationHeader == null) { return null; } - var parts = authorizationHeader.Split(' ', 2); + var firstSpace = authorizationHeader.IndexOf(' '); - // There should be at least to parts - if (parts.Length != 2) + // There should be at least two parts + if (firstSpace == -1) { return null; } - var acceptedNames = new[] { "MediaBrowser", "Emby" }; + var name = authorizationHeader[..firstSpace]; - // It has to be a digest request - if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase)) + if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase) + && !name.Equals("Emby", StringComparison.OrdinalIgnoreCase)) { return null; } - // Remove uptil the first space - authorizationHeader = parts[1]; - parts = authorizationHeader.Split(','); + authorizationHeader = authorizationHeader[(firstSpace + 1)..]; var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - foreach (var item in parts) + foreach (var item in authorizationHeader.Split(',')) { - var param = item.Trim().Split('=', 2); + var trimmedItem = item.Trim(); + var firstEqualsSign = trimmedItem.IndexOf('='); - if (param.Length == 2) + if (firstEqualsSign > 0) { - var value = NormalizeValue(param[1].Trim('"')); - result[param[0]] = value; + var key = trimmedItem[..firstEqualsSign].ToString(); + var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString()); + result[key] = value; } } diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index dd77b45d8..c375f36ce 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -36,14 +36,14 @@ namespace Emby.Server.Implementations.HttpServer.Security return GetSession((HttpContext)requestContext); } - public User GetUser(HttpContext requestContext) + public User? GetUser(HttpContext requestContext) { var session = GetSession(requestContext); return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId); } - public User GetUser(object requestContext) + public User? GetUser(object requestContext) { return GetUser(((HttpRequest)requestContext).HttpContext); } diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index c661e6757..362f51e50 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Buffers; using System.IO.Pipelines; diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs index 1bee1ac31..861c0a95e 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 7435e9d0b..47a83d77c 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 3353fae9d..aa80bccd7 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 27096ed33..64d802457 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; @@ -61,7 +62,7 @@ namespace Emby.Server.Implementations.IO /// <param name="filename">The filename.</param> /// <returns>System.String.</returns> /// <exception cref="ArgumentNullException">filename</exception> - public virtual string ResolveShortcut(string filename) + public virtual string? ResolveShortcut(string filename) { if (string.IsNullOrEmpty(filename)) { @@ -243,8 +244,8 @@ namespace Emby.Server.Implementations.IO { result.Length = fileInfo.Length; - // Issue #2354 get the size of files behind symbolic links - if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) + // Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes! + if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) { try { @@ -601,7 +602,7 @@ namespace Emby.Server.Implementations.IO return GetFiles(path, null, false, recursive); } - public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false) + public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var enumerationOptions = GetEnumerationOptions(recursive); @@ -618,13 +619,13 @@ namespace Emby.Server.Implementations.IO { files = files.Where(i => { - var ext = i.Extension; - if (ext == null) + var ext = i.Extension.AsSpan(); + if (ext.IsEmpty) { return false; } - return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase); + return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase); }); } @@ -636,8 +637,7 @@ namespace Emby.Server.Implementations.IO var directoryInfo = new DirectoryInfo(path); var enumerationOptions = GetEnumerationOptions(recursive); - return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions)) - .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions))); + return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", enumerationOptions)); } private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos) @@ -655,7 +655,7 @@ namespace Emby.Server.Implementations.IO return GetFilePaths(path, null, false, recursive); } - public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false) + public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var enumerationOptions = GetEnumerationOptions(recursive); @@ -672,13 +672,13 @@ namespace Emby.Server.Implementations.IO { files = files.Where(i => { - var ext = Path.GetExtension(i); - if (ext == null) + var ext = Path.GetExtension(i.AsSpan()); + if (ext.IsEmpty) { return false; } - return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase); + return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase); }); } diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs index e6696b8c4..76c58d5dc 100644 --- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs +++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.IO public string Extension => ".mblink"; - public string Resolve(string shortcutPath) + public string? Resolve(string shortcutPath) { if (string.IsNullOrEmpty(shortcutPath)) { diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs index c16ebd61b..e4f5f4cf0 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.IO { public class StreamHelper : IStreamHelper { - public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken) + public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken) { byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); try diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index f719dc5f8..a430b9e72 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#nullable enable namespace Emby.Server.Implementations { diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 5f7e51858..833fb0b7a 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -191,7 +193,7 @@ namespace Emby.Server.Implementations.Images InputPaths = GetStripCollageImagePaths(primaryItem, items).ToArray() }; - if (options.InputPaths.Length == 0) + if (options.InputPaths.Count == 0) { return null; } diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 161b4c452..ff5f26ce0 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 50c531482..900b3fd9c 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs index 0224ab32a..859017f86 100644 --- a/Emby.Server.Implementations/Images/FolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index 381788231..6da431c68 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs index a4c106e87..b8f0f0d65 100644 --- a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 3380e29d4..c7d113963 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library if (parent != null) { // Don't resolve these into audio files - if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal) + if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal) && _libraryManager.IsAudioFile(filename)) { return true; diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs index 236453e80..6c65b5899 100644 --- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs +++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index e30a67593..5384c04b3 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Linq; using DotNet.Globbing; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 4d207471a..028673529 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -694,25 +696,32 @@ namespace Emby.Server.Implementations.Library } private IEnumerable<BaseItem> ResolveFileList( - IEnumerable<FileSystemMetadata> fileList, + IReadOnlyList<FileSystemMetadata> fileList, IDirectoryService directoryService, Folder parent, string collectionType, IItemResolver[] resolvers, LibraryOptions libraryOptions) { - return fileList.Select(f => + // Given that fileList is a list we can save enumerator allocations by indexing + for (var i = 0; i < fileList.Count; i++) { + var file = fileList[i]; + BaseItem result = null; try { - return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions); + result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions); } catch (Exception ex) { - _logger.LogError(ex, "Error resolving path {path}", f.FullName); - return null; + _logger.LogError(ex, "Error resolving path {Path}", file.FullName); + } + + if (result != null) + { + yield return result; } - }).Where(i => i != null); + } } /// <summary> @@ -1063,17 +1072,17 @@ namespace Emby.Server.Implementations.Library // Start by just validating the children of the root, but go no further await RootFolder.ValidateChildren( new SimpleProgress<double>(), - cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), - recursive: false).ConfigureAwait(false); + recursive: false, + cancellationToken).ConfigureAwait(false); await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); await GetUserRootFolder().ValidateChildren( new SimpleProgress<double>(), - cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), - recursive: false).ConfigureAwait(false); + recursive: false, + cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes foreach (var folder in GetUserRootFolder().Children.OfType<Folder>()) @@ -1093,7 +1102,7 @@ namespace Emby.Server.Implementations.Library innerProgress.RegisterAction(pct => progress.Report(pct * 0.96)); // Validate the entire media library - await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false); + await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false); progress.Report(96); @@ -2074,7 +2083,7 @@ namespace Emby.Server.Implementations.Library return new List<Folder>(); } - return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>().ToList()); + return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>()); } public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren) @@ -2099,10 +2108,10 @@ namespace Emby.Server.Implementations.Library return GetCollectionFoldersInternal(item, allUserRootChildren); } - private static List<Folder> GetCollectionFoldersInternal(BaseItem item, List<Folder> allUserRootChildren) + private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren) { return allUserRootChildren - .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase)) + .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path.AsSpan(), StringComparison.OrdinalIgnoreCase)) .ToList(); } @@ -2110,9 +2119,9 @@ namespace Emby.Server.Implementations.Library { if (!(item is CollectionFolder collectionFolder)) { + // List.Find is more performant than FirstOrDefault due to enumerator allocation collectionFolder = GetCollectionFolders(item) - .OfType<CollectionFolder>() - .FirstOrDefault(); + .Find(folder => folder is CollectionFolder) as CollectionFolder; } return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); @@ -2498,8 +2507,7 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public bool IsVideoFile(string path) { - var resolver = new VideoResolver(GetNamingOptions()); - return resolver.IsVideoFile(path); + return VideoResolver.IsVideoFile(path, GetNamingOptions()); } /// <inheritdoc /> @@ -2677,6 +2685,7 @@ namespace Emby.Server.Implementations.Library return changed; } + /// <inheritdoc /> public NamingOptions GetNamingOptions() { if (_namingOptions == null) @@ -2690,13 +2699,12 @@ namespace Emby.Server.Implementations.Library public ItemLookupInfo ParseName(string name) { - var resolver = new VideoResolver(GetNamingOptions()); - - var result = resolver.CleanDateTime(name); + var namingOptions = GetNamingOptions(); + var result = VideoResolver.CleanDateTime(name, namingOptions); return new ItemLookupInfo { - Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name, + Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name, Year = result.Year }; } @@ -2710,9 +2718,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .ToList(); - var videoListResolver = new VideoListResolver(namingOptions); - - var videos = videoListResolver.Resolve(fileSystemChildren); + var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase)); @@ -2756,9 +2762,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .ToList(); - var videoListResolver = new VideoListResolver(namingOptions); - - var videos = videoListResolver.Resolve(fileSystemChildren); + var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index c2951dd15..4ef7923db 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 85d6d3043..b812b6b61 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -350,7 +352,7 @@ namespace Emby.Server.Implementations.Library private string[] NormalizeLanguage(string language) { - if (language == null) + if (string.IsNullOrEmpty(language)) { return Array.Empty<string>(); } @@ -379,8 +381,7 @@ namespace Emby.Server.Implementations.Library } } - var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference) - ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference); + var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference); var defaultAudioIndex = source.DefaultAudioStreamIndex; var audioLangage = defaultAudioIndex == null @@ -409,9 +410,7 @@ namespace Emby.Server.Implementations.Library } } - var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference) - ? Array.Empty<string>() - : NormalizeLanguage(user.AudioLanguagePreference); + var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference); source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); } diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 28fa06239..b833122ea 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index f8bae4fd1..06300adeb 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 0de4edb7e..86b8039fa 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Diagnostics.CodeAnalysis; using MediaBrowser.Common.Providers; diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index 1d9b44874..ac75e5d3a 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.IO; using System.Linq; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 4ad84579d..e893d6335 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index bf32381eb..8e1eccb10 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 60f82806f..3d2ae95d2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Linq; using System.Threading.Tasks; diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 6e688693b..cdb492022 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -45,11 +47,9 @@ namespace Emby.Server.Implementations.Library.Resolvers protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) where TVideoType : Video, new() { - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); + var namingOptions = LibraryManager.GetNamingOptions(); // If the path is a file check for a matching extensions - var parser = new VideoResolver(namingOptions); - if (args.IsDirectory) { TVideoType video = null; @@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (IsDvdDirectory(child.FullName, filename, args.DirectoryService)) { - videoInfo = parser.ResolveDirectory(args.Path); + videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); if (videoInfo == null) { @@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService)) { - videoInfo = parser.ResolveDirectory(args.Path); + videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); if (videoInfo == null) { @@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers } else if (IsDvdFile(filename)) { - videoInfo = parser.ResolveDirectory(args.Path); + videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions); if (videoInfo == null) { @@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Library.Resolvers } else { - var videoInfo = parser.Resolve(args.Path, false, false); + var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false); if (videoInfo == null) { @@ -165,13 +165,13 @@ namespace Emby.Server.Implementations.Library.Resolvers protected void SetVideoType(Video video, VideoFileInfo videoInfo) { - var extension = Path.GetExtension(video.Path); - video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) || - string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ? - VideoType.Iso : - VideoType.VideoFile; + var extension = Path.GetExtension(video.Path.AsSpan()); + video.VideoType = extension.Equals(".iso", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".img", StringComparison.OrdinalIgnoreCase) + ? VideoType.Iso + : VideoType.VideoFile; - video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); + video.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase); video.IsPlaceHolder = videoInfo.IsStub; if (videoInfo.IsStub) @@ -193,11 +193,11 @@ namespace Emby.Server.Implementations.Library.Resolvers { if (video.VideoType == VideoType.Iso) { - if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1) + if (video.Path.Contains("dvd", StringComparison.OrdinalIgnoreCase)) { video.IsoType = IsoType.Dvd; } - else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1) + else if (video.Path.Contains("bluray", StringComparison.OrdinalIgnoreCase)) { video.IsoType = IsoType.BluRay; } @@ -250,10 +250,7 @@ namespace Emby.Server.Implementations.Library.Resolvers protected void Set3DFormat(Video video) { - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - - var resolver = new Format3DParser(namingOptions); - var result = resolver.Parse(video.Path); + var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions()); Set3DFormat(video, result.Is3D, result.Format3D); } diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 0525c7e30..68076730b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs index 7dbce7a6e..7aaee017d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs index 92fb2a753..fa45ccf84 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs index 295e9e120..69d71d0d9 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.IO; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 16bf4dc4a..97f96f746 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -1,9 +1,12 @@ +#nullable disable + using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Emby.Naming.Video; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -255,10 +258,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); + var namingOptions = LibraryManager.GetNamingOptions(); - var resolver = new VideoListResolver(namingOptions); - var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList(); + var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList(); var result = new MultiItemResolverResult { @@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return returnVideo; } - private bool IsInvalid(Folder parent, string collectionType) + private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType) { if (parent != null) { @@ -545,12 +547,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } } - if (string.IsNullOrEmpty(collectionType)) + if (collectionType.IsEmpty) { return false; } - return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase); + return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs index 204c8a62e..534bc80dd 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index 3cb6542cf..57bf40e9e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index 5f051321f..ecd44be47 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs index 99f304190..7b4e14334 100644 --- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 6f29bc649..d6ae91056 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Linq; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 768e2e4f5..7d707df18 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Globalization; using Emby.Naming.TV; using MediaBrowser.Controller.Entities.TV; diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 8fc3e3e75..a1562abd3 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs index 62268fce9..9599faea4 100644 --- a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index bcdf854ca..26e615fa0 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 827e3c64b..8aa605a90 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -220,7 +222,7 @@ namespace Emby.Server.Implementations.Library var hasRuntime = runtimeTicks > 0; // If a position has been reported, and if we know the duration - if (positionTicks > 0 && hasRuntime && !(item is AudioBook)) + if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book) { var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100; @@ -239,7 +241,7 @@ namespace Emby.Server.Implementations.Library { // Enforce MinResumeDuration var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds; - if (durationSeconds < _config.Configuration.MinResumeDurationSeconds && !(item is Book)) + if (durationSeconds < _config.Configuration.MinResumeDurationSeconds) { positionTicks = 0; data.Played = playedToCompletion = true; diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index ac041bcf6..e2da672a3 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 7a6b1d8b6..3fcadf5b1 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 28a2095e1..797063120 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 9372b0f6c..26e4ef1ed 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs index 8c27ca76e..0ec52a959 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs @@ -6,7 +6,6 @@ using MediaBrowser.Controller.LiveTv; namespace Emby.Server.Implementations.LiveTv.EmbyTV { - internal class EpgChannelData { @@ -39,13 +38,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - public ChannelInfo GetChannelById(string id) + public ChannelInfo? GetChannelById(string id) => _channelsById.GetValueOrDefault(id); - public ChannelInfo GetChannelByNumber(string number) + public ChannelInfo? GetChannelByNumber(string number) => _channelsByNumber.GetValueOrDefault(number); - public ChannelInfo GetChannelByName(string name) + public ChannelInfo? GetChannelByName(string name) => _channelsByName.GetValueOrDefault(name); public static string NormalizeName(string value) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index 1cac9cb96..bdab8c3e4 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs index 142c59542..32245f899 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs @@ -6,7 +6,7 @@ using MediaBrowser.Controller.LiveTv; namespace Emby.Server.Implementations.LiveTv.EmbyTV { - internal class RecordingHelper + internal static class RecordingHelper { public static DateTime GetStartTime(TimerInfo timer) { @@ -70,17 +70,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private static string GetDateString(DateTime date) { - date = date.ToLocalTime(); - - return string.Format( - CultureInfo.InvariantCulture, - "{0}_{1}_{2}_{3}_{4}_{5}", - date.Year.ToString("0000", CultureInfo.InvariantCulture), - date.Month.ToString("00", CultureInfo.InvariantCulture), - date.Day.ToString("00", CultureInfo.InvariantCulture), - date.Hour.ToString("00", CultureInfo.InvariantCulture), - date.Minute.ToString("00", CultureInfo.InvariantCulture), - date.Second.ToString("00", CultureInfo.InvariantCulture)); + return date.ToLocalTime().ToString("yyyy_MM_dd_HH_mm_ss", CultureInfo.InvariantCulture); } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 1efa90e25..6c52a9a73 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 9af65cabb..00d02873c 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 6824aa442..ebad4eddf 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs index ba916af38..098f193fb 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs @@ -1,21 +1,23 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.LiveTv; namespace Emby.Server.Implementations.LiveTv { + /// <summary> + /// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />. + /// </summary> public class LiveTvConfigurationFactory : IConfigurationFactory { + /// <inheritdoc /> public IEnumerable<ConfigurationStore> GetConfigurations() { return new ConfigurationStore[] { new ConfigurationStore { - ConfigurationType = typeof(LiveTvOptions), - Key = "livetv" + ConfigurationType = typeof(LiveTvOptions), + Key = "livetv" } }; } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 6af49dd45..21e1409ac 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 1145d8aa1..d964769b5 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -2264,7 +2266,7 @@ namespace Emby.Server.Implementations.LiveTv if (dataSourceChanged) { - _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>(); + _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); } return info; @@ -2307,7 +2309,7 @@ namespace Emby.Server.Implementations.LiveTv _config.SaveConfiguration("livetv", config); - _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>(); + _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); return info; } @@ -2319,7 +2321,7 @@ namespace Emby.Server.Implementations.LiveTv config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray(); _config.SaveConfiguration("livetv", config); - _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>(); + _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); } public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId) @@ -2353,7 +2355,7 @@ namespace Emby.Server.Implementations.LiveTv var tunerChannelMappings = tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList(); - _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>(); + _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase)); } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 3a738fd5d..ecd28097d 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs index 582b64923..15df0dcf1 100644 --- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs @@ -1,7 +1,6 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.LiveTv; @@ -10,34 +9,55 @@ using MediaBrowser.Model.Tasks; namespace Emby.Server.Implementations.LiveTv { - public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask + /// <summary> + /// The "Refresh Guide" scheduled task. + /// </summary> + public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledTask { private readonly ILiveTvManager _liveTvManager; private readonly IConfigurationManager _config; - public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config) + /// <summary> + /// Initializes a new instance of the <see cref="RefreshGuideScheduledTask"/> class. + /// </summary> + /// <param name="liveTvManager">The live tv manager.</param> + /// <param name="config">The configuration manager.</param> + public RefreshGuideScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config) { _liveTvManager = liveTvManager; _config = config; } + /// <inheritdoc /> public string Name => "Refresh Guide"; + /// <inheritdoc /> public string Description => "Downloads channel information from live tv services."; + /// <inheritdoc /> public string Category => "Live TV"; - public Task Execute(System.Threading.CancellationToken cancellationToken, IProgress<double> progress) + /// <inheritdoc /> + public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> + public bool IsLogged => true; + + /// <inheritdoc /> + public string Key => "RefreshGuide"; + + /// <inheritdoc /> + public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { var manager = (LiveTvManager)_liveTvManager; return manager.RefreshChannels(progress, cancellationToken); } - /// <summary> - /// Creates the triggers that define when the task will run. - /// </summary> - /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + /// <inheritdoc /> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { return new[] @@ -51,13 +71,5 @@ namespace Emby.Server.Implementations.LiveTv { return _config.GetConfiguration<LiveTvOptions>("livetv"); } - - public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0; - - public bool IsEnabled => true; - - public bool IsLogged => true; - - public string Key => "RefreshGuide"; } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index fbcd4ef37..5941613cf 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -38,6 +40,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public virtual bool IsSupported => true; protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken); + public abstract string Type { get; } public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs index 740cbb66e..0f0453189 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { internal class Channels diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs index 09d77f838..42068cd34 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 324109bcf..54de841fe 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -74,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken) .ConfigureAwait(false) ?? new List<Channels>(); @@ -581,7 +583,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Logger, Config, _appHost, - _networkManager, _streamHelper); } @@ -622,7 +623,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Logger, Config, _appHost, - _networkManager, _streamHelper); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index a7fda1d72..3016eeda2 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 61262eb14..df3460212 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -10,7 +12,6 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; @@ -28,7 +29,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private readonly IServerApplicationHost _appHost; private readonly IHdHomerunChannelCommands _channelCommands; private readonly int _numTuners; - private readonly INetworkManager _networkManager; public HdHomerunUdpStream( MediaSourceInfo mediaSource, @@ -40,12 +40,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun ILogger logger, IConfigurationManager configurationManager, IServerApplicationHost appHost, - INetworkManager networkManager, IStreamHelper streamHelper) : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper) { _appHost = appHost; - _networkManager = networkManager; OriginalStreamId = originalStreamId; _channelCommands = channelCommands; _numTuners = numTuners; @@ -126,7 +124,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun using (udpClient) using (hdHomerunManager) { - if (!(ex is OperationCanceledException)) + if (ex is not OperationCanceledException) { Logger.LogError(ex, "Error opening live stream:"); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index f8baf55da..96a678c1d 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 4b170b2e4..8fa6f5ad6 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -27,6 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { + private static readonly string[] _disallowedSharedStreamExtensions = + { + ".mkv", + ".mp4", + ".m3u8", + ".mpd" + }; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; private readonly INetworkManager _networkManager; @@ -65,7 +75,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var channelIdPrefix = GetFullChannelIdPrefix(info); - return await new M3uParser(Logger, _httpClientFactory, _appHost) + return await new M3uParser(Logger, _httpClientFactory) .Parse(info, channelIdPrefix, cancellationToken) .ConfigureAwait(false); } @@ -86,14 +96,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return Task.FromResult(list); } - private static readonly string[] _disallowedSharedStreamExtensions = - { - ".mkv", - ".mp4", - ".m3u8", - ".mpd" - }; - protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { var tunerCount = info.TunerCount; @@ -128,7 +130,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public async Task Validate(TunerHostInfo info) { - using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false)) + using (var stream = await new M3uParser(Logger, _httpClientFactory).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false)) { } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 84d416149..40a162890 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -19,15 +21,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class M3uParser { + private const string ExtInfPrefix = "#EXTINF:"; + private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; - private readonly IServerApplicationHost _appHost; - public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost) + public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory) { _logger = logger; _httpClientFactory = httpClientFactory; - _appHost = appHost; } public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken) @@ -59,8 +61,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return File.OpenRead(info.Url); } - private const string ExtInfPrefix = "#EXTINF:"; - private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId) { var channels = new List<ChannelInfo>(); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index eeb2426f4..f572151b8 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -89,8 +91,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var taskCompletionSource = new TaskCompletionSource<bool>(); - var now = DateTime.UtcNow; - _ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token); // OpenedMediaSource.Protocol = MediaProtocol.File; @@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!taskCompletionSource.Task.Result) { Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath); - throw new EndOfStreamException(String.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name)); + throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name)); } } diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index b029b7042..4f21c66bc 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -115,5 +115,7 @@ "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde", "Undefined": "Ongedefineerd", "Forced": "Geforseer", - "Default": "Oorspronklik" + "Default": "Oorspronklik", + "TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.", + "TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon" } diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 4b898e6fe..3d6e159b1 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -113,5 +113,10 @@ "TaskRefreshPeopleDescription": "تحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.", "TaskRefreshPeople": "إعادة تحميل الأشخاص", "TaskCleanLogsDescription": "حذف السجلات الأقدم من {0} يوم.", - "TaskCleanLogs": "حذف دليل السجل" + "TaskCleanLogs": "حذف دليل السجل", + "TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الموضوع.", + "TaskCleanActivityLog": "حذف سجل الأنشطة", + "Default": "الإعدادات الافتراضية", + "Undefined": "غير معرف", + "Forced": "ملحقة" } diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 9db3b50d9..bc25531d3 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -39,7 +39,7 @@ "MixedContent": "Смесено съдържание", "Movies": "Филми", "Music": "Музика", - "MusicVideos": "Музикални клипове", + "MusicVideos": "Музикални видеа", "NameInstallFailed": "{0} не можа да се инсталира", "NameSeasonNumber": "Сезон {0}", "NameSeasonUnknown": "Неразпознат сезон", @@ -62,7 +62,7 @@ "NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно", "Photos": "Снимки", "Playlists": "Списъци", - "Plugin": "Приставка", + "Plugin": "Добавка", "PluginInstalledWithName": "{0} е инсталиранa", "PluginUninstalledWithName": "{0} е деинсталиранa", "PluginUpdatedWithName": "{0} е обновенa", @@ -116,5 +116,7 @@ "TasksMaintenanceCategory": "Поддръжка", "Undefined": "Неопределено", "Forced": "Принудително", - "Default": "По подразбиране" + "Default": "По подразбиране", + "TaskCleanActivityLogDescription": "Изтрива записите в дневника с активност по стари от конфигурираната възраст.", + "TaskCleanActivityLog": "Изчисти дневника с активност" } diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index a23037af8..c3fbe2408 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -1,7 +1,7 @@ { "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে", "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে", - "Collections": "কলেক্শন", + "Collections": "সংগ্রহ", "ChapterNameValue": "অধ্যায় {0}", "Channels": "চ্যানেল", "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে", @@ -115,7 +115,7 @@ "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।", "Undefined": "অসঙ্গায়িত", "Forced": "জোরকরে", - "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন", + "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.", "TaskCleanActivityLog": "কাজের ফাইল খালি করুন", "Default": "প্রাথমিক" } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 775267183..ff14c1929 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -39,7 +39,7 @@ "MixedContent": "Smíšený obsah", "Movies": "Filmy", "Music": "Hudba", - "MusicVideos": "Hudební klipy", + "MusicVideos": "Hudební videa", "NameInstallFailed": "Instalace {0} selhala", "NameSeasonNumber": "Sezóna {0}", "NameSeasonUnknown": "Neznámá sezóna", diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index 051d6d009..3453507d9 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -39,7 +39,7 @@ "MixedContent": "Blandet indhold", "Movies": "Film", "Music": "Musik", - "MusicVideos": "Musikvideoer", + "MusicVideos": "Musik videoer", "NameInstallFailed": "{0} installationen mislykkedes", "NameSeasonNumber": "Sæson {0}", "NameSeasonUnknown": "Ukendt Sæson", diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index f8f595faa..65964f6d9 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -117,5 +117,7 @@ "TaskRefreshChannels": "Refresh Channels", "TaskRefreshChannelsDescription": "Refreshes internet channel information.", "TaskDownloadMissingSubtitles": "Download missing subtitles", - "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration." + "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.", + "TaskOptimizeDatabase": "Optimize database", + "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance." } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index 16fde325f..91939843f 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -118,5 +118,7 @@ "TaskCleanActivityLog": "Limpiar registro de actividad", "Undefined": "Indefinido", "Forced": "Forzado", - "Default": "Predeterminado" + "Default": "Predeterminado", + "TaskOptimizeDatabase": "Optimizar la base de datos", + "TaskOptimizeDatabaseDescription": "Compacta y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento." } diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index e9e4f61b8..8ab657e5b 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -34,7 +34,7 @@ "Latest": "جدیدترینها", "MessageApplicationUpdated": "سرور Jellyfin بروزرسانی شد", "MessageApplicationUpdatedTo": "سرور Jellyfin به نسخه {0} بروزرسانی شد", - "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد", + "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد", "MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد", "MixedContent": "محتوای مخلوط", "Movies": "فیلمها", diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index fd6148e78..633968d26 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -1,5 +1,5 @@ { - "HeaderLiveTV": "Live TV", + "HeaderLiveTV": "Suora TV", "NewVersionIsAvailable": "Uusi versio Jellyfin-palvelimesta on ladattavissa.", "NameSeasonUnknown": "Tuntematon kausi", "NameSeasonNumber": "Kausi {0}", diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 1e195378f..ce1493be8 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -15,7 +15,7 @@ "Favorites": "Favoris", "Folders": "Dossiers", "Genres": "Genres", - "HeaderAlbumArtists": "Artistes", + "HeaderAlbumArtists": "Artistes de l'album", "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteArtists": "Artistes préférés", @@ -39,7 +39,7 @@ "MixedContent": "Contenu mixte", "Movies": "Films", "Music": "Musique", - "MusicVideos": "Vidéos musicales", + "MusicVideos": "Clips musicaux", "NameInstallFailed": "{0} échec de l'installation", "NameSeasonNumber": "Saison {0}", "NameSeasonUnknown": "Saison Inconnue", @@ -99,7 +99,7 @@ "TaskRefreshChannels": "Rafraîchir les chaines", "TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.", "TaskCleanTranscode": "Nettoyer les dossier des transcodages", - "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour être mises à jour automatiquement.", + "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurées pour être mises à jour automatiquement.", "TaskUpdatePlugins": "Mettre à jour les extensions", "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.", "TaskRefreshPeople": "Rafraîchir les acteurs", @@ -107,7 +107,7 @@ "TaskCleanLogs": "Nettoyer le répertoire des journaux", "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.", "TaskRefreshLibrary": "Scanner toutes les Bibliothèques", - "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.", + "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.", "TaskCleanCache": "Vider le répertoire cache", diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index 11139d32a..0398e1c9e 100644 --- a/Emby.Server.Implementations/Localization/Core/gl.json +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -79,5 +79,14 @@ "PluginUninstalledWithName": "{0} foi desinstalado", "PluginInstalledWithName": "{0} foi instalado", "Playlists": "Listas de reproducción", - "Photos": "Fotos" + "Photos": "Fotos", + "UserLockedOutWithName": "O usuario {0} foi bloqueado", + "UserDownloadingItemWithValues": "{0} está a ser transferido {1}", + "UserDeletedWithName": "O usuario {0} foi borrado", + "UserCreatedWithName": "O usuario {0} foi creado", + "Plugin": "Plugin", + "NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo parada", + "NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada", + "NotificationOptionUserLockedOut": "Usuario bloqueado", + "NotificationOptionTaskFailed": "Falla na tarefa axendada" } diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index ef3697b15..82dc601bc 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -51,5 +51,14 @@ "Latest": "सबसे नया", "LabelIpAddressValue": "आई पी एड्रेस: {0}", "ItemRemovedWithName": "{0} लाइब्रेरी में से निकाल दिया है", - "HomeVideos": "होम वीडियोस" + "HomeVideos": "होम वीडियोस", + "NotificationOptionVideoPlayback": "वीडियो प्लेबैक शुरू हुआ", + "NotificationOptionUserLockedOut": "उपयोगकर्ता लॉक हो गया", + "NotificationOptionTaskFailed": "निर्धारित कार्य विफलता", + "NotificationOptionServerRestartRequired": "सर्वर पुनरारंभ आवश्यक है", + "NotificationOptionPluginUpdateInstalled": "प्लगइन अद्यतन स्थापित", + "NotificationOptionNewLibraryContent": "नई सामग्री जोड़ी गई", + "LabelRunningTimeValue": "चलने का समय: {0}", + "ItemAddedWithName": "{0} को लाइब्रेरी में जोड़ा गया", + "Inherit": "इनहेरिट" } diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index ef8070503..85848fed6 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -39,7 +39,7 @@ "MixedContent": "Vegyes tartalom", "Movies": "Filmek", "Music": "Zene", - "MusicVideos": "Zenei videók", + "MusicVideos": "Zenei videóklippek", "NameInstallFailed": "{0} sikertelen telepítés", "NameSeasonNumber": "{0}. évad", "NameSeasonUnknown": "Ismeretlen évad", @@ -74,7 +74,7 @@ "Songs": "Dalok", "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}", + "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}", "Sync": "Szinkronizál", "System": "Rendszer", "TvShows": "TV műsorok", @@ -82,12 +82,12 @@ "UserCreatedWithName": "{0} felhasználó létrehozva", "UserDeletedWithName": "{0} felhasználó törölve", "UserDownloadingItemWithValues": "{0} letölti {1}", - "UserLockedOutWithName": "{0} felhasználó zárolva van", - "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}", + "UserLockedOutWithName": "{0} felhasználó zárolva van", + "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}", "UserOnlineFromDevice": "{0} online innen: {1}", "UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}", "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}", - "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}", + "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}", "UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}", "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz", "ValueSpecialEpisodeName": "Special - {0}", diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index 0f769eaad..b262a8b42 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -25,7 +25,7 @@ "Channels": "Stöðvar", "CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}", "Books": "Bækur", - "AuthenticationSucceededWithUserName": "{0} náði að auðkennast", + "AuthenticationSucceededWithUserName": "{0} auðkenning tókst", "Artists": "Listamaður", "Application": "Forrit", "AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}", @@ -106,5 +106,6 @@ "TasksChannelsCategory": "Netrásir", "TasksApplicationCategory": "Forrit", "TasksLibraryCategory": "Miðlasafn", - "TasksMaintenanceCategory": "Viðhald" + "TasksMaintenanceCategory": "Viðhald", + "Default": "Sjálfgefið" } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 110f8043d..bd06f0a25 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -62,7 +62,7 @@ "NotificationOptionVideoPlaybackStopped": "La riproduzione video è stata interrotta", "Photos": "Foto", "Playlists": "Playlist", - "Plugin": "Plug-in", + "Plugin": "Plugin", "PluginInstalledWithName": "{0} è stato Installato", "PluginUninstalledWithName": "{0} è stato disinstallato", "PluginUpdatedWithName": "{0} è stato aggiornato", @@ -87,7 +87,7 @@ "UserOnlineFromDevice": "{0} è online su {1}", "UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}", "UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}", - "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}", + "UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di \"{1}\" su {2}", "UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1} su {2}", "ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale", "ValueSpecialEpisodeName": "Speciale - {0}", diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index 829a29ad4..4eee36989 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -5,23 +5,23 @@ "Artists": "Oryndauşylar", "AuthenticationSucceededWithUserName": "{0} tüpnūsqalyq rastaluy sättı aiaqtaldy", "Books": "Kıtaptar", - "CameraImageUploadedFrom": "{0} kamerasynan jaŋa suret jüktep salyndy", + "CameraImageUploadedFrom": "{0} kamerasynan jaña suret jüktep salyndy", "Channels": "Arnalar", "ChapterNameValue": "{0}-sahna", "Collections": "Jiyntyqtar", "DeviceOfflineWithName": "{0} ajyratylğan", "DeviceOnlineWithName": "{0} qosylğan", "FailedLoginAttemptWithUserName": "{0} tarapynan kıru äreketı sätsız aiaqtaldy", - "Favorites": "Taŋdaulylar", + "Favorites": "Tañdaulylar", "Folders": "Qaltalar", "Genres": "Janrlar", "HeaderAlbumArtists": "Älbom oryndauşylary", "HeaderContinueWatching": "Qaraudy jalğastyru", - "HeaderFavoriteAlbums": "Taŋdauly älbomdar", - "HeaderFavoriteArtists": "Taŋdauly oryndauşylar", - "HeaderFavoriteEpisodes": "Taŋdauly telebölımder", - "HeaderFavoriteShows": "Taŋdauly körsetımder", - "HeaderFavoriteSongs": "Taŋdauly äuender", + "HeaderFavoriteAlbums": "Tañdauly älbomdar", + "HeaderFavoriteArtists": "Tañdauly oryndauşylar", + "HeaderFavoriteEpisodes": "Tañdauly telebölımder", + "HeaderFavoriteShows": "Tañdauly körsetımder", + "HeaderFavoriteSongs": "Tañdauly äuender", "HeaderLiveTV": "Efir", "HeaderNextUp": "Kezektı", "HeaderRecordingGroups": "Jazba toptary", @@ -31,11 +31,11 @@ "ItemRemovedWithName": "{0} tasyğyşhanadan alastaldy", "LabelIpAddressValue": "IP-mekenjaiy: {0}", "LabelRunningTimeValue": "Oinatu uaqyty: {0}", - "Latest": "Eŋ keiıngı", - "MessageApplicationUpdated": "Jellyfin Serverı jaŋartyldy", - "MessageApplicationUpdatedTo": "Jellyfin Serverı {0} nūsqasyna jaŋartyldy", - "MessageNamedServerConfigurationUpdatedWithValue": "Server teŋşelımderınıŋ {0} bölımı jaŋartyldy", - "MessageServerConfigurationUpdated": "Server teŋşelımderı jaŋartyldy", + "Latest": "Eñ keiıngı", + "MessageApplicationUpdated": "Jellyfin Serverı jañartyldy", + "MessageApplicationUpdatedTo": "Jellyfin Serverı {0} nūsqasyna jañartyldy", + "MessageNamedServerConfigurationUpdatedWithValue": "Server teñşelımderınıñ {0} bölımı jañartyldy", + "MessageServerConfigurationUpdated": "Server teñşelımderı jañartyldy", "MixedContent": "Aralas mazmūn", "Movies": "Filmder", "Music": "Muzyka", @@ -43,18 +43,18 @@ "NameInstallFailed": "{0} ornatyluy sätsız", "NameSeasonNumber": "{0}-mausym", "NameSeasonUnknown": "Belgısız mausym", - "NewVersionIsAvailable": "Jaŋa Jellyfin Server nūsqasy jüktep aluğa qoljetımdı.", - "NotificationOptionApplicationUpdateAvailable": "Qoldanba jaŋartuy qoljetımdı", - "NotificationOptionApplicationUpdateInstalled": "Qoldanba jaŋartuy ornatyldy", + "NewVersionIsAvailable": "Jaña Jellyfin Server nūsqasy jüktep aluğa qoljetımdı.", + "NotificationOptionApplicationUpdateAvailable": "Qoldanba jañartuy qoljetımdı", + "NotificationOptionApplicationUpdateInstalled": "Qoldanba jañartuy ornatyldy", "NotificationOptionAudioPlayback": "Dybys oinatuy bastaldy", "NotificationOptionAudioPlaybackStopped": "Dybys oinatuy toqtatyldy", "NotificationOptionCameraImageUploaded": "Kameradan fotosuret jüktep salynğan", "NotificationOptionInstallationFailed": "Ornatu sätsızdıgı", - "NotificationOptionNewLibraryContent": "Jaŋa mazmūn üstelıngen", + "NotificationOptionNewLibraryContent": "Jaña mazmūn üstelıngen", "NotificationOptionPluginError": "Plagin sätsızdıgı", "NotificationOptionPluginInstalled": "Plagin ornatyldy", "NotificationOptionPluginUninstalled": "Plagin ornatuy boldyrylmady", - "NotificationOptionPluginUpdateInstalled": "Plagin jaŋartuy ornatyldy", + "NotificationOptionPluginUpdateInstalled": "Plagin jañartuy ornatyldy", "NotificationOptionServerRestartRequired": "Serverdı qaita ıske qosu qajet", "NotificationOptionTaskFailed": "Josparlağan tapsyrma sätsızdıgı", "NotificationOptionUserLockedOut": "Paidalanuşy qūrsauly", @@ -65,14 +65,14 @@ "Plugin": "Plagin", "PluginInstalledWithName": "{0} ornatyldy", "PluginUninstalledWithName": "{0} joiyldy", - "PluginUpdatedWithName": "{0} jaŋartyldy", + "PluginUpdatedWithName": "{0} jañartyldy", "ProviderValue": "Jetkızuşı: {0}", "ScheduledTaskFailedWithName": "{0} sätsız", "ScheduledTaskStartedWithName": "{0} ıske qosyldy", "ServerNameNeedsToBeRestarted": "{0} qaita ıske qosu qajet", "Shows": "Körsetımder", "Songs": "Äuender", - "StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalaŋyz.", + "StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalañyz.", "SubtitleDownloadFailureForItem": "Субтитрлер {0} үшін жүктеліп алынуы сәтсіз", "SubtitleDownloadFailureFromForItem": "{1} üşın subtitrlerdı {0} közınen jüktep alu sätsız", "Sync": "Ündestıru", @@ -86,7 +86,7 @@ "UserOfflineFromDevice": "{0} — {1} tarapynan ajyratyldy", "UserOnlineFromDevice": "{0} — {1} tarapynan qosyldy", "UserPasswordChangedWithName": "Paidalanuşy {0} üşın paröl özgertıldı", - "UserPolicyUpdatedWithName": "Paidalanuşy {0} üşın saiasattary jaŋartyldy", + "UserPolicyUpdatedWithName": "Paidalanuşy {0} üşın saiasattary jañartyldy", "UserStartedPlayingItemWithValues": "{0} — {2} tarapynan {1} oinatuda", "UserStoppedPlayingItemWithValues": "{0} — {2} tarapynan {1} oinatuyn toqtatty", "ValueHasBeenAddedToLibrary": "{0} tasyğyşhanağa üstelındı", @@ -94,10 +94,10 @@ "VersionNumber": "Nūsqasy {0}", "Default": "Ädepkı", "TaskDownloadMissingSubtitles": "Joq subtitrlerdı jüktep alu", - "TaskRefreshChannels": "Arnalardy jaŋğyrtu", + "TaskRefreshChannels": "Arnalardy jañğyrtu", "TaskCleanTranscode": "Qaita kodtau katalogyn tazalau", - "TaskUpdatePlugins": "Plaginderdı jaŋartu", - "TaskRefreshPeople": "Adamdardy jaŋğyrtu", + "TaskUpdatePlugins": "Plaginderdı jañartu", + "TaskRefreshPeople": "Adamdardy jañğyrtu", "TaskCleanLogs": "Jūrnal katalogyn tazalau", "TaskRefreshLibrary": "Tasyğyşhanany skanerleu", "TaskRefreshChapterImages": "Sahna suretterın şyğaryp alu", @@ -109,14 +109,14 @@ "TasksMaintenanceCategory": "Qyzmet körsetu", "Undefined": "Anyqtalmağan", "Forced": "Mäjbürlı", - "TaskDownloadMissingSubtitlesDescription": "Metaderekter teŋşelımderı negızınde joq subtitrlerdı İnternetten ızdeidı.", - "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jaŋğyrtady.", + "TaskDownloadMissingSubtitlesDescription": "Metaderekter teñşelımderı negızınde joq subtitrlerdı İnternetten ızdeidı.", + "TaskRefreshChannelsDescription": "Internet-arnalar mälımetterın jañğyrtady.", "TaskCleanTranscodeDescription": "Bіr künnen asqan qaita kodtau faildaryn joiady.", - "TaskUpdatePluginsDescription": "Avtomatty türde jaŋartuğa teŋşelgen plaginder üşın jaŋartulardy jüktep alady jäne ornatady.", - "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jaŋartady.", + "TaskUpdatePluginsDescription": "Avtomatty türde jañartuğa teñşelgen plaginder üşın jañartulardy jüktep alady jäne ornatady.", + "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jañartady.", "TaskCleanLogsDescription": "{0} künnen asqan jūrnal faildaryn joiady.", - "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaŋa faildardy skanerleidі jäne metaderekterdı jaŋğyrtady.", + "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaña faildardy skanerleidі jäne metaderekterdı jañğyrtady.", "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.", "TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.", - "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teŋşelgen jasynan asqan jazbalary joiady." + "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teñşelgen jasynan asqan jazbalary joiady." } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 9920ef4d5..f3a131d40 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -114,8 +114,9 @@ "TasksApplicationCategory": "Programa", "TasksLibraryCategory": "Mediateka", "TasksMaintenanceCategory": "Priežiūra", - "TaskCleanActivityLog": "Švarus veiklos žurnalas", + "TaskCleanActivityLog": "Išvalyti veiklos žurnalą", "Undefined": "Neapibrėžtas", "Forced": "Priverstas", - "Default": "Numatytas" + "Default": "Numatytas", + "TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius." } diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index 5e3d095ff..5b4c8ae10 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -39,29 +39,29 @@ "MixedContent": "Kandungan campuran", "Movies": "Filem", "Music": "Muzik", - "MusicVideos": "Video muzik", + "MusicVideos": "Muzik video", "NameInstallFailed": "{0} pemasangan gagal", "NameSeasonNumber": "Musim {0}", "NameSeasonUnknown": "Musim Tidak Diketahui", "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.", "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", + "NotificationOptionApplicationUpdateInstalled": "Kemas kini aplikasi telah dipasang", + "NotificationOptionAudioPlayback": "Ulangmain audio bermula", + "NotificationOptionAudioPlaybackStopped": "Ulangmain audio dihentikan", + "NotificationOptionCameraImageUploaded": "Imej kamera telah dimuatnaik", "NotificationOptionInstallationFailed": "Pemasangan gagal", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionPluginInstalled": "Plugin installed", + "NotificationOptionNewLibraryContent": "Kandungan baru telah ditambah", + "NotificationOptionPluginError": "Kegagalan plugin", + "NotificationOptionPluginInstalled": "Plugin telah dipasang", "NotificationOptionPluginUninstalled": "Plugin uninstalled", "NotificationOptionPluginUpdateInstalled": "Plugin update installed", "NotificationOptionServerRestartRequired": "Server restart required", "NotificationOptionTaskFailed": "Scheduled task failure", "NotificationOptionUserLockedOut": "User locked out", "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", - "Photos": "Photos", - "Playlists": "Playlists", + "NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan", + "Photos": "Gambar-gambar", + "Playlists": "Senarai main", "Plugin": "Plugin", "PluginInstalledWithName": "{0} was installed", "PluginUninstalledWithName": "{0} was uninstalled", @@ -71,10 +71,10 @@ "ScheduledTaskStartedWithName": "{0} bermula", "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", "Shows": "Series", - "Songs": "Songs", - "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", + "Songs": "Lagu-lagu", + "StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}", "Sync": "Sync", "System": "Sistem", "TvShows": "TV Shows", @@ -82,14 +82,24 @@ "UserCreatedWithName": "User {0} has been created", "UserDeletedWithName": "User {0} has been deleted", "UserDownloadingItemWithValues": "{0} is downloading {1}", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} has disconnected from {1}", - "UserOnlineFromDevice": "{0} is online from {1}", - "UserPasswordChangedWithName": "Password has been changed for user {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", + "UserLockedOutWithName": "Pengguna {0} telah dikunci", + "UserOfflineFromDevice": "{0} telah terputus dari {1}", + "UserOnlineFromDevice": "{0} berada dalam talian dari {1}", + "UserPasswordChangedWithName": "Kata laluan telah ditukar bagi pengguna {0}", + "UserPolicyUpdatedWithName": "Dasar pengguna telah dikemas kini untuk {0}", "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueSpecialEpisodeName": "Khas - {0}", - "VersionNumber": "Versi {0}" + "VersionNumber": "Versi {0}", + "TaskCleanActivityLog": "Log Aktiviti Bersih", + "TasksChannelsCategory": "Saluran Internet", + "TasksApplicationCategory": "Aplikasi", + "TasksLibraryCategory": "Perpustakaan", + "TasksMaintenanceCategory": "Penyelenggaraan", + "Undefined": "Tidak ditentukan", + "Forced": "Paksa", + "Default": "Asal", + "TaskCleanCache": "Bersihkan Direktori Cache", + "TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi." } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index d5bca9f6c..fbe1f7c4d 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -30,20 +30,20 @@ "ItemAddedWithName": "{0} ble lagt til i biblioteket", "ItemRemovedWithName": "{0} ble fjernet fra biblioteket", "LabelIpAddressValue": "IP-adresse: {0}", - "LabelRunningTimeValue": "Kjøretid {0}", + "LabelRunningTimeValue": "Spilletid {0}", "Latest": "Siste", - "MessageApplicationUpdated": "Jellyfin Server har blitt oppdatert", - "MessageApplicationUpdatedTo": "Jellyfin Server ble oppdatert til {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurasjon seksjon {0} har blitt oppdatert", - "MessageServerConfigurationUpdated": "Serverkonfigurasjon er oppdatert", + "MessageApplicationUpdated": "Jellyfin-tjeneren har blitt oppdatert", + "MessageApplicationUpdatedTo": "Jellyfin-tjeneren ble oppdatert til {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Tjenerkonfigurasjonsseksjon {0} har blitt oppdatert", + "MessageServerConfigurationUpdated": "Tjenerkonfigurasjon er oppdatert", "MixedContent": "Blandet innhold", "Movies": "Filmer", "Music": "Musikk", "MusicVideos": "Musikkvideoer", - "NameInstallFailed": "{0}-installasjonen mislyktes", + "NameInstallFailed": "Installasjonen av {0} mislyktes", "NameSeasonNumber": "Sesong {0}", - "NameSeasonUnknown": "Sesong ukjent", - "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.", + "NameSeasonUnknown": "Ukjent sesong", + "NewVersionIsAvailable": "En ny versjon av Jellyfin-tjeneren er tilgjengelig for nedlasting.", "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig", "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert", "NotificationOptionAudioPlayback": "Lydavspilling startet", @@ -51,18 +51,18 @@ "NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp", "NotificationOptionInstallationFailed": "Installasjonen feilet", "NotificationOptionNewLibraryContent": "Nytt innhold lagt til", - "NotificationOptionPluginError": "Pluginfeil", - "NotificationOptionPluginInstalled": "Plugin installert", - "NotificationOptionPluginUninstalled": "Plugin avinstallert", - "NotificationOptionPluginUpdateInstalled": "Pluginoppdatering installert", - "NotificationOptionServerRestartRequired": "Serveromstart er nødvendig", + "NotificationOptionPluginError": "Programvareutvidelsesfeil", + "NotificationOptionPluginInstalled": "Programvareutvidelse installert", + "NotificationOptionPluginUninstalled": "Programvareutvidelse avinstallert", + "NotificationOptionPluginUpdateInstalled": "Programvareutvidelsesoppdatering installert", + "NotificationOptionServerRestartRequired": "Tjeneromstart er nødvendig", "NotificationOptionTaskFailed": "Feil under utføring av planlagt oppgave", "NotificationOptionUserLockedOut": "Bruker er utestengt", "NotificationOptionVideoPlayback": "Videoavspilling startet", "NotificationOptionVideoPlaybackStopped": "Videoavspilling stoppet", "Photos": "Bilder", "Playlists": "Spillelister", - "Plugin": "Plugin", + "Plugin": "Programvareutvidelse", "PluginInstalledWithName": "{0} ble installert", "PluginUninstalledWithName": "{0} ble avinstallert", "PluginUpdatedWithName": "{0} ble oppdatert", @@ -72,7 +72,7 @@ "ServerNameNeedsToBeRestarted": "{0} må startes på nytt", "Shows": "Program", "Songs": "Sanger", - "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.", + "StartupEmbyServerIsLoading": "Jellyfin-tjener laster. Prøv igjen snart.", "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}", "SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}", "Sync": "Synkroniser", @@ -86,37 +86,37 @@ "UserOfflineFromDevice": "{0} har koblet fra {1}", "UserOnlineFromDevice": "{0} er tilkoblet fra {1}", "UserPasswordChangedWithName": "Passordet for {0} er oppdatert", - "UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}", + "UserPolicyUpdatedWithName": "Brukerretningslinjene har blitt oppdatert for {0}", "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}", - "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}", + "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}", "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt", "ValueSpecialEpisodeName": "Spesialepisode - {0}", "VersionNumber": "Versjon {0}", - "TasksChannelsCategory": "Internett kanaler", + "TasksChannelsCategory": "Internettkanaler", "TasksApplicationCategory": "Applikasjon", "TasksLibraryCategory": "Bibliotek", "TasksMaintenanceCategory": "Vedlikehold", - "TaskCleanCache": "Tøm buffer katalog", + "TaskCleanCache": "Tøm hurtigbuffer", "TaskRefreshLibrary": "Skann mediebibliotek", "TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.", - "TaskRefreshChapterImages": "Trekk ut Kapittelbilder", + "TaskRefreshChapterImages": "Trekk ut kapittelbilder", "TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet.", - "TaskDownloadMissingSubtitlesDescription": "Søker etter manglende underteksting på nett basert på metadatakonfigurasjon.", - "TaskDownloadMissingSubtitles": "Last ned manglende underteksting", - "TaskRefreshChannelsDescription": "Frisker opp internettkanalinformasjon.", - "TaskRefreshChannels": "Oppfrisk kanaler", + "TaskDownloadMissingSubtitlesDescription": "Søker etter manglende undertekster på nett basert på metadatakonfigurasjon.", + "TaskDownloadMissingSubtitles": "Last ned manglende undertekster", + "TaskRefreshChannelsDescription": "Oppdaterer internettkanalinformasjon.", + "TaskRefreshChannels": "Oppdater kanaler", "TaskCleanTranscodeDescription": "Sletter omkodede filer som er mer enn én dag gamle.", "TaskCleanTranscode": "Tøm transkodingmappe", - "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringer for utvidelser som er stilt inn til å oppdatere automatisk.", - "TaskUpdatePlugins": "Oppdater utvidelser", + "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringer for programvareutvidelser som er stilt inn til å oppdatere automatisk.", + "TaskUpdatePlugins": "Oppdater programvareutvidelse", "TaskRefreshPeopleDescription": "Oppdaterer metadata for skuespillere og regissører i mediebiblioteket ditt.", - "TaskRefreshPeople": "Oppfrisk personer", + "TaskRefreshPeople": "Oppdater personer", "TaskCleanLogsDescription": "Sletter loggfiler som er eldre enn {0} dager gamle.", "TaskCleanLogs": "Tøm loggmappe", "TaskRefreshLibraryDescription": "Skanner mediebibliotekene dine for nye filer og oppdaterer metadata.", "TaskCleanActivityLog": "Tøm aktivitetslogg", "Undefined": "Udefinert", - "Forced": "Tvungen", + "Forced": "Tvunget", "Default": "Standard", "TaskCleanActivityLogDescription": "Sletter oppføringer i aktivitetsloggen som er eldre enn den konfigurerte alderen." } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index ffc329e35..2973c8c6e 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -3,9 +3,9 @@ "AppDeviceValues": "App: {0}, Apparaat: {1}", "Application": "Applicatie", "Artists": "Artiesten", - "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd", + "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd", "Books": "Boeken", - "CameraImageUploadedFrom": "Er is een nieuwe camera afbeelding toegevoegd via {0}", + "CameraImageUploadedFrom": "Nieuwe camera afbeelding toegevoegd vanaf {0}", "Channels": "Kanalen", "ChapterNameValue": "Hoofdstuk {0}", "Collections": "Verzamelingen", diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index 6236515b2..32d4f3a8b 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -1,25 +1,25 @@ { - "MessageServerConfigurationUpdated": "Tenar konfigurasjonen har blitt oppdatert", + "MessageServerConfigurationUpdated": "Tenarkonfigurasjonen har blitt oppdatert", "MessageNamedServerConfigurationUpdatedWithValue": "Tenar konfigurasjon seksjon {0} har blitt oppdatert", - "MessageApplicationUpdatedTo": "Jellyfin Tenaren har blitt oppdatert til {0}", - "MessageApplicationUpdated": "Jellyfin Tenaren har blitt oppdatert", + "MessageApplicationUpdatedTo": "Jellyfin-tenaren har blitt oppdatert til {0}", + "MessageApplicationUpdated": "Jellyfin-tenaren har blitt oppdatert", "Latest": "Nyaste", "LabelRunningTimeValue": "Speletid: {0}", - "LabelIpAddressValue": "IP adresse: {0}", + "LabelIpAddressValue": "IP-adresse: {0}", "ItemRemovedWithName": "{0} vart fjerna frå biblioteket", "ItemAddedWithName": "{0} vart lagt til i biblioteket", - "Inherit": "Arv", - "HomeVideos": "Heime Videoar", + "Inherit": "Arve", + "HomeVideos": "Heimevideoar", "HeaderRecordingGroups": "Innspelingsgrupper", "HeaderNextUp": "Neste", "HeaderLiveTV": "Direkte TV", - "HeaderFavoriteSongs": "Favoritt Songar", - "HeaderFavoriteShows": "Favoritt Seriar", - "HeaderFavoriteEpisodes": "Favoritt Episodar", - "HeaderFavoriteArtists": "Favoritt Artistar", - "HeaderFavoriteAlbums": "Favoritt Album", + "HeaderFavoriteSongs": "Favorittsongar", + "HeaderFavoriteShows": "Favorittseriar", + "HeaderFavoriteEpisodes": "Favorittepisodar", + "HeaderFavoriteArtists": "Favorittartistar", + "HeaderFavoriteAlbums": "Favorittalbum", "HeaderContinueWatching": "Fortsett å sjå", - "HeaderAlbumArtists": "Album Artist", + "HeaderAlbumArtists": "Albumartist", "Genres": "Sjangrar", "Folders": "Mapper", "Favorites": "Favorittar", @@ -29,18 +29,18 @@ "Collections": "Samlingar", "ChapterNameValue": "Kapittel {0}", "Channels": "Kanalar", - "CameraImageUploadedFrom": "Eit nytt kamera bilete har blitt lasta opp frå {0}", + "CameraImageUploadedFrom": "Eit nytt kamerabilete har blitt lasta opp frå {0}", "Books": "Bøker", - "AuthenticationSucceededWithUserName": "{0} Har logga inn", + "AuthenticationSucceededWithUserName": "{0} har logga inn", "Artists": "Artistar", "Application": "Program", "AppDeviceValues": "App: {0}, Eining: {1}", "Albums": "Album", "NotificationOptionServerRestartRequired": "Tenaren krev omstart", - "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert", - "NotificationOptionPluginUninstalled": "Tilleggsprogram avinstallert", - "NotificationOptionPluginInstalled": "Tilleggsprogram installert", - "NotificationOptionPluginError": "Tilleggsprogram feila", + "NotificationOptionPluginUpdateInstalled": "Programvaretilleggoppdatering vart installert", + "NotificationOptionPluginUninstalled": "Programvaretillegg avinstallert", + "NotificationOptionPluginInstalled": "Programvaretillegg installert", + "NotificationOptionPluginError": "Programvaretillegg feila", "NotificationOptionNewLibraryContent": "Nytt innhald er lagt til", "NotificationOptionInstallationFailed": "Installasjonsfeil", "NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp", @@ -48,33 +48,33 @@ "NotificationOptionAudioPlayback": "Lydavspilling påbyrja", "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering er installert", "NotificationOptionApplicationUpdateAvailable": "Applikasjonsoppdatering er tilgjengeleg", - "NewVersionIsAvailable": "Ein ny versjon av Jellyfin serveren er tilgjengeleg for nedlasting.", + "NewVersionIsAvailable": "Ein ny versjon av Jellyfin-tjenaren er tilgjengeleg for nedlasting.", "NameSeasonUnknown": "Ukjend sesong", "NameSeasonNumber": "Sesong {0}", - "NameInstallFailed": "{0} Installasjonen feila", + "NameInstallFailed": "Installasjonen av {0} feila", "MusicVideos": "Musikkvideoar", "Music": "Musikk", "Movies": "Filmar", "MixedContent": "Blanda innhald", - "Sync": "Synkronisera", + "Sync": "Synkroniser", "TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.", "TaskDownloadMissingSubtitles": "Last ned manglande undertekstar", "TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.", "TaskRefreshChannels": "Oppdater kanalar", - "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.", - "TaskCleanTranscode": "Reins transkodemappe", + "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gammal.", + "TaskCleanTranscode": "Fjern transkodemappe", "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.", - "TaskUpdatePlugins": "Oppdaterer programtillegg", + "TaskUpdatePlugins": "Oppdaterer programvaretillegg", "TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.", "TaskRefreshPeople": "Oppdater personar", "TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.", - "TaskCleanLogs": "Reins loggmappe", + "TaskCleanLogs": "Slett loggmappa", "TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.", "TaskRefreshLibrary": "Skann mediebibliotek", "TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.", "TaskRefreshChapterImages": "Trekk ut kapittelbilete", - "TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.", - "TaskCleanCache": "Rens mappe for hurtiglager", + "TaskCleanCacheDescription": "Sletter mellomlagra filer som ikkje lengre trengst av systemet.", + "TaskCleanCache": "Fjern hurtigbuffer", "TasksChannelsCategory": "Internettkanalar", "TasksApplicationCategory": "Applikasjon", "TasksLibraryCategory": "Bibliotek", @@ -96,9 +96,9 @@ "TvShows": "TV-seriar", "System": "System", "SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}", - "StartupEmbyServerIsLoading": "Jellyfintenaren laster. Prøv igjen om litt.", - "Songs": "Songar", - "Shows": "Program", + "StartupEmbyServerIsLoading": "Jellyfin-tenaren laster. Prøv igjen seinare.", + "Songs": "Sangar", + "Shows": "Seriar", "ServerNameNeedsToBeRestarted": "{0} må omstartast", "ScheduledTaskStartedWithName": "{0} starta", "ScheduledTaskFailedWithName": "{0} feila", @@ -106,11 +106,16 @@ "PluginUpdatedWithName": "{0} blei oppdatert", "PluginUninstalledWithName": "{0} blei avinstallert", "PluginInstalledWithName": "{0} blei installert", - "Plugin": "Programtillegg", - "Playlists": "Speleliste", - "Photos": "Foto", + "Plugin": "Programvaretillegg", + "Playlists": "Spelelister", + "Photos": "Bilete", "NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa", "NotificationOptionVideoPlayback": "Videoavspeling starta", "NotificationOptionUserLockedOut": "Brukar er utestengd", - "NotificationOptionTaskFailed": "Planlagt oppgåve feila" + "NotificationOptionTaskFailed": "Planlagt oppgåve feila", + "TaskCleanActivityLogDescription": "Sletter aktivitetslogginnlegg som er eldre enn den konfigurerte alderen.", + "TaskCleanActivityLog": "Slett aktivitetslogg", + "Undefined": "Udefinert", + "Forced": "Tvungen", + "Default": "Standard" } diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json index 469fa89b6..d1db09232 100644 --- a/Emby.Server.Implementations/Localization/Core/pa.json +++ b/Emby.Server.Implementations/Localization/Core/pa.json @@ -1,5 +1,5 @@ { - "TaskRefreshChapterImages": "ਐਬਸਟਰੈਕਟ ਅਧਿਆਇ ਅਧਿਆਇ", + "TaskRefreshChapterImages": "ਐਕਸਟਰੈਕਟ ਚੈਪਟਰ ਚਿੱਤਰ", "TaskDownloadMissingSubtitlesDescription": "ਮੈਟਾਡੇਟਾ ਕੌਂਫਿਗਰੇਸ਼ਨ ਦੇ ਅਧਾਰ ਤੇ ਗਾਇਬ ਉਪਸਿਰਲੇਖਾਂ ਲਈ ਇੰਟਰਨੈਟ ਦੀ ਭਾਲ ਕਰਦਾ ਹੈ.", "TaskDownloadMissingSubtitles": "ਗਾਇਬ ਉਪਸਿਰਲੇਖ ਡਾ Download ਨਲੋਡ ਕਰੋ", "TaskRefreshChannelsDescription": "ਇੰਟਰਨੈੱਟ ਚੈਨਲ ਦੀ ਜਾਣਕਾਰੀ ਨੂੰ ਤਾਜ਼ਾ ਕਰਦਾ ਹੈ.", diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 46b47cf4a..e58f8c39d 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -39,7 +39,7 @@ "MixedContent": "Смешанное содержимое", "Movies": "Кино", "Music": "Музыка", - "MusicVideos": "Музыкальные клипы", + "MusicVideos": "Муз. видео", "NameInstallFailed": "Установка {0} неудачна", "NameSeasonNumber": "Сезон {0}", "NameSeasonUnknown": "Сезон неопознан", @@ -75,7 +75,7 @@ "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}", - "Sync": "Синхронизация", + "Sync": "Синхро", "System": "Система", "TvShows": "ТВ", "User": "Пользователь", diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index b5a7fa5b8..d992bf79b 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -39,7 +39,7 @@ "MixedContent": "Blandat innehåll", "Movies": "Filmer", "Music": "Musik", - "MusicVideos": "Musikvideos", + "MusicVideos": "Musikvideor", "NameInstallFailed": "{0} installationen misslyckades", "NameSeasonNumber": "Säsong {0}", "NameSeasonUnknown": "Okänd säsong", diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index c737ba42b..129986ed0 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -69,7 +69,7 @@ "NameSeasonUnknown": "அறியப்படாத பருவம்", "NameSeasonNumber": "பருவம் {0}", "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது", - "MusicVideos": "இசைப்படங்கள்", + "MusicVideos": "இசை கானொளி", "Music": "இசை", "Movies": "திரைப்படங்கள்", "Latest": "புதியவை", diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 5bf58baf8..e26010423 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -113,5 +113,9 @@ "Sync": "ซิงค์", "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้", "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่", - "Default": "ค่าเริ่มต้น" + "Default": "ค่าเริ่มต้น", + "TaskCleanActivityLogDescription": "ลบบันทึกกิจกรรมที่เก่ากว่าค่าที่กำหนดไว้", + "TaskCleanActivityLog": "ล้างบันทึกกิจกรรม", + "Undefined": "ไม่ได้กำหนด", + "Forced": "บังคับใช้" } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index b6073bf6a..5a2069df5 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -1,5 +1,5 @@ { - "MusicVideos": "Музичні кліпи", + "MusicVideos": "Музичні відеокліпи", "Music": "Музика", "Movies": "Фільми", "MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}", @@ -113,7 +113,7 @@ "MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено", "Inherit": "Успадкувати", "HeaderRecordingGroups": "Групи запису", - "Forced": "Примусово", + "Forced": "Форсовані", "TaskCleanActivityLogDescription": "Видаляє старші за встановлений термін записи з журналу активності.", "TaskCleanActivityLog": "Очистити журнал активності", "Undefined": "Не визначено", diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index affb0e099..c3b223f63 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -37,7 +37,7 @@ "MixedContent": "混合內容", "Movies": "電影", "Music": "音樂", - "MusicVideos": "音樂MV", + "MusicVideos": "音樂錄影帶", "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知季數", diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 220e423bf..b1ff28c2c 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -1,9 +1,10 @@ +#nullable disable + using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading.Tasks; @@ -167,12 +168,22 @@ namespace Emby.Server.Implementations.Localization /// <inheritdoc /> public CultureDto FindLanguageInfo(string language) - => GetCultures() - .FirstOrDefault(i => - string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase) - || string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase) - || i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase) - || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase)); + { + // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs + for (var i = 0; i < _cultures.Count; i++) + { + var culture = _cultures[i]; + if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase) + || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase) + || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase) + || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)) + { + return culture; + } + } + + return default; + } /// <inheritdoc /> public IEnumerable<CountryInfo> GetCountries() @@ -222,7 +233,7 @@ namespace Emby.Server.Implementations.Localization throw new ArgumentNullException(nameof(rating)); } - if (_unratedValues.Contains(rating, StringComparer.OrdinalIgnoreCase)) + if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase)) { return null; } @@ -250,11 +261,11 @@ namespace Emby.Server.Implementations.Localization var index = rating.IndexOf(':', StringComparison.Ordinal); if (index != -1) { - rating = rating.Substring(index).TrimStart(':').Trim(); + var trimmedRating = rating.AsSpan(index).TrimStart(':').Trim(); - if (!string.IsNullOrWhiteSpace(rating)) + if (!trimmedRating.IsEmpty) { - return GetRatingLevel(rating); + return GetRatingLevel(trimmedRating.ToString()); } } @@ -316,7 +327,8 @@ namespace Emby.Server.Implementations.Localization return _dictionaries.GetOrAdd( culture, - f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); + (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(), + this); } private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename) diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 031b5d2e7..8aaa1f7bb 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 0781a0e33..137728616 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs index 4e25768cf..a8b18d292 100644 --- a/Emby.Server.Implementations/Net/UdpSocket.cs +++ b/Emby.Server.Implementations/Net/UdpSocket.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 2d1a559f1..9a1ca9946 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 14df20936..8fd61f2bc 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.Globalization; @@ -396,7 +394,7 @@ namespace Emby.Server.Implementations.Plugins Category = packageInfo.Category, Changelog = versionInfo.Changelog ?? string.Empty, Description = packageInfo.Description, - Id = new Guid(packageInfo.Id), + Id = packageInfo.Id, Name = packageInfo.Name, Overview = packageInfo.Overview, Owner = packageInfo.Owner, @@ -457,7 +455,8 @@ namespace Emby.Server.Implementations.Plugins try { _logger.LogDebug("Creating instance of {Type}", type); - var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type); + // _appHost.ServiceProvider is already assigned when we create the plugins + var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider!, type); if (plugin == null) { // Create a dummy record for the providers. diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 0259dc436..7cfd1fced 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Concurrent; using System.Globalization; diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 101d9b537..d7e320754 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -709,11 +711,7 @@ namespace Emby.Server.Implementations.ScheduledTasks throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); } - return new DailyTrigger - { - TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value), - TaskOptions = options - }; + return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options); } if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase)) @@ -728,12 +726,7 @@ namespace Emby.Server.Implementations.ScheduledTasks throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info)); } - return new WeeklyTrigger - { - TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value), - DayOfWeek = info.DayOfWeek.Value, - TaskOptions = options - }; + return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options); } if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase)) @@ -743,16 +736,12 @@ namespace Emby.Server.Implementations.ScheduledTasks throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info)); } - return new IntervalTrigger - { - Interval = TimeSpan.FromTicks(info.IntervalTicks.Value), - TaskOptions = options - }; + return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options); } if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase)) { - return new StartupTrigger(); + return new StartupTrigger(options); } throw new ArgumentException("Unrecognized trigger type: " + info.Type); diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index af316e108..4f0df75bf 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 2312c85d9..baeb86a22 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -140,8 +140,10 @@ namespace Emby.Server.Implementations.ScheduledTasks previouslyFailedImages.Add(key); var parentPath = Path.GetDirectoryName(failHistoryPath); - - Directory.CreateDirectory(parentPath); + if (parentPath != null) + { + Directory.CreateDirectory(parentPath); + } string text = string.Join('|', previouslyFailedImages); File.WriteAllText(failHistoryPath, text); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs index 4abbf784b..50ba9bc89 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -75,4 +75,4 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return Enumerable.Empty<TaskTriggerInfo>(); } } -}
\ No newline at end of file +} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs new file mode 100644 index 000000000..1ad1d0f50 --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Server.Implementations; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks +{ + /// <summary> + /// Optimizes Jellyfin's database by issuing a VACUUM command. + /// </summary> + public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask + { + private readonly ILogger<OptimizeDatabaseTask> _logger; + private readonly ILocalizationManager _localization; + private readonly JellyfinDbProvider _provider; + + /// <summary> + /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class. + /// </summary> + public OptimizeDatabaseTask( + ILogger<OptimizeDatabaseTask> logger, + ILocalizationManager localization, + JellyfinDbProvider provider) + { + _logger = logger; + _localization = localization; + _provider = provider; + } + + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskOptimizeDatabase"); + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskOptimizeDatabaseDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public string Key => "OptimizeDatabaseTask"; + + /// <inheritdoc /> + public bool IsHidden => false; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> + public bool IsLogged => true; + + /// <summary> + /// Creates the triggers that define when the task will run. + /// </summary> + /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return new[] + { + // Every so often + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } + }; + } + + /// <summary> + /// Returns the task to be executed. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task.</returns> + public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + { + _logger.LogInformation("Optimizing and vacuuming jellyfin.db..."); + + try + { + using var context = _provider.CreateContext(); + if (context.Database.IsSqlite()) + { + context.Database.ExecuteSqlRaw("PRAGMA optimize"); + context.Database.ExecuteSqlRaw("VACUUM"); + _logger.LogInformation("jellyfin.db optimized successfully!"); + } + else + { + _logger.LogInformation("This database doesn't support optimization"); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error while optimizing jellyfin.db"); + } + + return Task.CompletedTask; + } + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index 3b40320ab..29ab6a73d 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -8,29 +8,31 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// Represents a task trigger that fires everyday. /// </summary> - public class DailyTrigger : ITaskTrigger + public sealed class DailyTrigger : ITaskTrigger { - /// <summary> - /// Occurs when [triggered]. - /// </summary> - public event EventHandler<EventArgs> Triggered; + private readonly TimeSpan _timeOfDay; + private Timer? _timer; /// <summary> - /// Gets or sets the time of day to trigger the task to run. + /// Initializes a new instance of the <see cref="DailyTrigger"/> class. /// </summary> - /// <value>The time of day.</value> - public TimeSpan TimeOfDay { get; set; } + /// <param name="timeofDay">The time of day to trigger the task to run.</param> + /// <param name="taskOptions">The options of this task.</param> + public DailyTrigger(TimeSpan timeofDay, TaskOptions taskOptions) + { + _timeOfDay = timeofDay; + TaskOptions = taskOptions; + } /// <summary> - /// Gets or sets the options of this task. + /// Occurs when [triggered]. /// </summary> - public TaskOptions TaskOptions { get; set; } + public event EventHandler<EventArgs>? Triggered; /// <summary> - /// Gets or sets the timer. + /// Gets the options of this task. /// </summary> - /// <value>The timer.</value> - private Timer Timer { get; set; } + public TaskOptions TaskOptions { get; } /// <summary> /// Stars waiting for the trigger action. @@ -45,14 +47,14 @@ namespace Emby.Server.Implementations.ScheduledTasks var now = DateTime.Now; - var triggerDate = now.TimeOfDay > TimeOfDay ? now.Date.AddDays(1) : now.Date; - triggerDate = triggerDate.Add(TimeOfDay); + var triggerDate = now.TimeOfDay > _timeOfDay ? now.Date.AddDays(1) : now.Date; + triggerDate = triggerDate.Add(_timeOfDay); var dueTime = triggerDate - now; logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime); - Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); + _timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); } /// <summary> @@ -68,10 +70,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> private void DisposeTimer() { - if (Timer != null) - { - Timer.Dispose(); - } + _timer?.Dispose(); } /// <summary> diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index b04fd7c7e..30568e809 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -9,31 +9,32 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// Represents a task trigger that runs repeatedly on an interval. /// </summary> - public class IntervalTrigger : ITaskTrigger + public sealed class IntervalTrigger : ITaskTrigger { + private readonly TimeSpan _interval; private DateTime _lastStartDate; + private Timer? _timer; /// <summary> - /// Occurs when [triggered]. + /// Initializes a new instance of the <see cref="IntervalTrigger"/> class. /// </summary> - public event EventHandler<EventArgs> Triggered; - - /// <summary> - /// Gets or sets the interval. - /// </summary> - /// <value>The interval.</value> - public TimeSpan Interval { get; set; } + /// <param name="interval">The interval.</param> + /// <param name="taskOptions">The options of this task.</param> + public IntervalTrigger(TimeSpan interval, TaskOptions taskOptions) + { + _interval = interval; + TaskOptions = taskOptions; + } /// <summary> - /// Gets or sets the options of this task. + /// Occurs when [triggered]. /// </summary> - public TaskOptions TaskOptions { get; set; } + public event EventHandler<EventArgs>? Triggered; /// <summary> - /// Gets or sets the timer. + /// Gets the options of this task. /// </summary> - /// <value>The timer.</value> - private Timer Timer { get; set; } + public TaskOptions TaskOptions { get; } /// <summary> /// Stars waiting for the trigger action. @@ -55,7 +56,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } else { - triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(Interval); + triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(_interval); } if (DateTime.UtcNow > triggerDate) @@ -71,7 +72,7 @@ namespace Emby.Server.Implementations.ScheduledTasks dueTime = maxDueTime; } - Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); + _timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); } /// <summary> @@ -87,10 +88,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> private void DisposeTimer() { - if (Timer != null) - { - Timer.Dispose(); - } + _timer?.Dispose(); } /// <summary> diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs index 7cd5493da..18b9a8b75 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs @@ -10,24 +10,28 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// Class StartupTaskTrigger. /// </summary> - public class StartupTrigger : ITaskTrigger + public sealed class StartupTrigger : ITaskTrigger { + public const int DelayMs = 3000; + /// <summary> - /// Occurs when [triggered]. + /// Initializes a new instance of the <see cref="StartupTrigger"/> class. /// </summary> - public event EventHandler<EventArgs> Triggered; - - public int DelayMs { get; set; } + /// <param name="taskOptions">The options of this task.</param> + public StartupTrigger(TaskOptions taskOptions) + { + TaskOptions = taskOptions; + } /// <summary> - /// Gets or sets the options of this task. + /// Occurs when [triggered]. /// </summary> - public TaskOptions TaskOptions { get; set; } + public event EventHandler<EventArgs>? Triggered; - public StartupTrigger() - { - DelayMs = 3000; - } + /// <summary> + /// Gets the options of this task. + /// </summary> + public TaskOptions TaskOptions { get; } /// <summary> /// Stars waiting for the trigger action. diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs index 0c0ebec08..36ae190b0 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs @@ -8,35 +8,34 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <summary> /// Represents a task trigger that fires on a weekly basis. /// </summary> - public class WeeklyTrigger : ITaskTrigger + public sealed class WeeklyTrigger : ITaskTrigger { - /// <summary> - /// Occurs when [triggered]. - /// </summary> - public event EventHandler<EventArgs> Triggered; - - /// <summary> - /// Gets or sets the time of day to trigger the task to run. - /// </summary> - /// <value>The time of day.</value> - public TimeSpan TimeOfDay { get; set; } + private readonly TimeSpan _timeOfDay; + private readonly DayOfWeek _dayOfWeek; + private Timer? _timer; /// <summary> - /// Gets or sets the day of week. + /// Initializes a new instance of the <see cref="WeeklyTrigger"/> class. /// </summary> - /// <value>The day of week.</value> - public DayOfWeek DayOfWeek { get; set; } + /// <param name="timeofDay">The time of day to trigger the task to run.</param> + /// <param name="dayOfWeek">The day of week.</param> + /// <param name="taskOptions">The options of this task.</param> + public WeeklyTrigger(TimeSpan timeofDay, DayOfWeek dayOfWeek, TaskOptions taskOptions) + { + _timeOfDay = timeofDay; + _dayOfWeek = dayOfWeek; + TaskOptions = taskOptions; + } /// <summary> - /// Gets or sets the options of this task. + /// Occurs when [triggered]. /// </summary> - public TaskOptions TaskOptions { get; set; } + public event EventHandler<EventArgs>? Triggered; /// <summary> - /// Gets or sets the timer. + /// Gets the options of this task. /// </summary> - /// <value>The timer.</value> - private Timer Timer { get; set; } + public TaskOptions TaskOptions { get; } /// <summary> /// Stars waiting for the trigger action. @@ -51,7 +50,7 @@ namespace Emby.Server.Implementations.ScheduledTasks var triggerDate = GetNextTriggerDateTime(); - Timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1)); + _timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1)); } /// <summary> @@ -63,22 +62,22 @@ namespace Emby.Server.Implementations.ScheduledTasks var now = DateTime.Now; // If it's on the same day - if (now.DayOfWeek == DayOfWeek) + if (now.DayOfWeek == _dayOfWeek) { // It's either later today, or a week from now - return now.TimeOfDay < TimeOfDay ? now.Date.Add(TimeOfDay) : now.Date.AddDays(7).Add(TimeOfDay); + return now.TimeOfDay < _timeOfDay ? now.Date.Add(_timeOfDay) : now.Date.AddDays(7).Add(_timeOfDay); } var triggerDate = now.Date; // Walk the date forward until we get to the trigger day - while (triggerDate.DayOfWeek != DayOfWeek) + while (triggerDate.DayOfWeek != _dayOfWeek) { triggerDate = triggerDate.AddDays(1); } // Return the trigger date plus the time offset - return triggerDate.Add(TimeOfDay); + return triggerDate.Add(_timeOfDay); } /// <summary> @@ -94,10 +93,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> private void DisposeTimer() { - if (Timer != null) - { - Timer.Dispose(); - } + _timer?.Dispose(); } /// <summary> diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 4bc12f44a..e8eac315b 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -289,7 +291,7 @@ namespace Emby.Server.Implementations.Security return result; } - private static AuthenticationInfo Get(IReadOnlyList<IResultSetValue> reader) + private static AuthenticationInfo Get(IReadOnlyList<ResultSetValue> reader) { var info = new AuthenticationInfo { @@ -297,50 +299,49 @@ namespace Emby.Server.Implementations.Security AccessToken = reader[1].ToString() }; - if (reader[2].SQLiteType != SQLiteType.Null) - { - info.DeviceId = reader[2].ToString(); - } - - if (reader[3].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(2, out var deviceId)) { - info.AppName = reader[3].ToString(); + info.DeviceId = deviceId; } - if (reader[4].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(3, out var appName)) { - info.AppVersion = reader[4].ToString(); + info.AppName = appName; } - if (reader[5].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(4, out var appVersion)) { - info.DeviceName = reader[5].ToString(); + info.AppVersion = appVersion; } - if (reader[6].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(6, out var userId)) { - info.UserId = new Guid(reader[6].ToString()); + info.UserId = new Guid(userId); } - if (reader[7].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(7, out var userName)) { - info.UserName = reader[7].ToString(); + info.UserName = userName; } info.DateCreated = reader[8].ReadDateTime(); - if (reader[9].SQLiteType != SQLiteType.Null) + if (reader.TryReadDateTime(9, out var dateLastActivity)) { - info.DateLastActivity = reader[9].ReadDateTime(); + info.DateLastActivity = dateLastActivity; } else { info.DateLastActivity = info.DateCreated; } - if (reader[10].SQLiteType != SQLiteType.Null) + if (reader.TryGetString(10, out var customName)) + { + info.DeviceName = customName; + } + else if (reader.TryGetString(5, out var deviceName)) { - info.DeviceName = reader[10].ToString(); + info.DeviceName = deviceName; } return info; @@ -361,9 +362,9 @@ namespace Emby.Server.Implementations.Security foreach (var row in statement.ExecuteQuery()) { - if (row[0].SQLiteType != SQLiteType.Null) + if (row.TryGetString(0, out var customName)) { - result.CustomName = row[0].ToString(); + result.CustomName = customName; } } diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs index 27024e4e1..5ff73de81 100644 --- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs +++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs @@ -19,7 +19,10 @@ namespace Emby.Server.Implementations.Serialization new ConcurrentDictionary<string, XmlSerializer>(); private static XmlSerializer GetSerializer(Type type) - => _serializers.GetOrAdd(type.FullName, _ => new XmlSerializer(type)); + => _serializers.GetOrAdd( + type.FullName ?? throw new ArgumentException($"Invalid type {type}."), + (_, t) => new XmlSerializer(t), + type); /// <summary> /// Serializes to writer. @@ -38,7 +41,7 @@ namespace Emby.Server.Implementations.Serialization /// <param name="type">The type.</param> /// <param name="stream">The stream.</param> /// <returns>System.Object.</returns> - public object DeserializeFromStream(Type type, Stream stream) + public object? DeserializeFromStream(Type type, Stream stream) { using (var reader = XmlReader.Create(stream)) { @@ -81,7 +84,7 @@ namespace Emby.Server.Implementations.Serialization /// <param name="type">The type.</param> /// <param name="file">The file.</param> /// <returns>System.Object.</returns> - public object DeserializeFromFile(Type type, string file) + public object? DeserializeFromFile(Type type, string file) { using (var stream = File.OpenRead(file)) { @@ -95,7 +98,7 @@ namespace Emby.Server.Implementations.Serialization /// <param name="type">The type.</param> /// <param name="buffer">The buffer.</param> /// <returns>System.Object.</returns> - public object DeserializeFromBytes(Type type, byte[] buffer) + public object? DeserializeFromBytes(Type type, byte[] buffer) { using (var stream = new MemoryStream(buffer, 0, buffer.Length, false, true)) { diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index ac589b03c..6cf9a8f71 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -25,6 +25,10 @@ namespace Emby.Server.Implementations cacheDirectoryPath, webDirectoryPath) { + // ProgramDataPath cannot change when the server is running, so cache these to avoid allocations. + RootFolderPath = Path.Join(ProgramDataPath, "root"); + DefaultUserViewsPath = Path.Combine(RootFolderPath, "default"); + DefaultInternalMetadataPath = Path.Combine(ProgramDataPath, "metadata"); InternalMetadataPath = DefaultInternalMetadataPath; } @@ -32,13 +36,13 @@ namespace Emby.Server.Implementations /// Gets the path to the base root media directory. /// </summary> /// <value>The root folder path.</value> - public string RootFolderPath => Path.Combine(ProgramDataPath, "root"); + public string RootFolderPath { get; } /// <summary> /// Gets the path to the default user view directory. Used if no specific user view is defined. /// </summary> /// <value>The default user views path.</value> - public string DefaultUserViewsPath => Path.Combine(RootFolderPath, "default"); + public string DefaultUserViewsPath { get; } /// <summary> /// Gets the path to the People directory. @@ -98,7 +102,7 @@ namespace Emby.Server.Implementations public string UserConfigurationDirectoryPath => Path.Combine(ConfigurationDirectoryPath, "users"); /// <inheritdoc/> - public string DefaultInternalMetadataPath => Path.Combine(ProgramDataPath, "metadata"); + public string DefaultInternalMetadataPath { get; } /// <inheritdoc /> public string InternalMetadataPath { get; set; } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6844152ea..62df354fd 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -1540,23 +1542,26 @@ namespace Emby.Server.Implementations.Session Limit = 1 }).Items.FirstOrDefault(); - var allExistingForDevice = _authRepo.Get( - new AuthenticationInfoQuery - { - DeviceId = deviceId - }).Items; - - foreach (var auth in allExistingForDevice) + if (!string.IsNullOrEmpty(deviceId)) { - if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal)) - { - try + var allExistingForDevice = _authRepo.Get( + new AuthenticationInfoQuery { - Logout(auth); - } - catch (Exception ex) + DeviceId = deviceId + }).Items; + + foreach (var auth in allExistingForDevice) + { + if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal)) { - _logger.LogError(ex, "Error while logging out."); + try + { + Logout(auth); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while logging out."); + } } } } diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 39c369a01..e9e3ca7f4 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index a653b58c2..9fa92a53a 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -1,6 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 -#nullable enable using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs index 60698e803..2b0ab536f 100644 --- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs +++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { if (x == null) { diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs index 7657cc74e..42e644970 100644 --- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs @@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); } @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Sorting /// </summary> /// <param name="x">The x.</param> /// <returns>System.String.</returns> - private static string GetValue(BaseItem x) + private static string? GetValue(BaseItem? x) { var audio = x as IHasAlbumArtist; diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs index 7dfdd9ecf..1db3f5e9c 100644 --- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs +++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs @@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); } @@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Sorting /// </summary> /// <param name="x">The x.</param> /// <returns>System.String.</returns> - private static string GetValue(BaseItem x) + private static string? GetValue(BaseItem? x) { var audio = x as Audio; diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs index 756d3c5b6..98bee3fd9 100644 --- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting public string Name => ItemSortBy.Artist; /// <inheritdoc /> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); } @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting /// </summary> /// <param name="x">The x.</param> /// <returns>System.String.</returns> - private static string GetValue(BaseItem x) + private static string? GetValue(BaseItem? x) { if (!(x is Audio audio)) { diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs index 980954ba0..5f142fa4b 100644 --- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { if (x == null) { diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs index fa136c36d..d20dedc2d 100644 --- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs @@ -15,14 +15,14 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return GetValue(x).CompareTo(GetValue(y)); } - private static float GetValue(BaseItem x) + private static float GetValue(BaseItem? x) { - return x.CriticRating ?? 0; + return x?.CriticRating ?? 0; } /// <summary> diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs index cbca300d2..d3f10f78c 100644 --- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { if (x == null) { diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs index 03ff19d21..b1cb123ce 100644 --- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs index 16bd2aff8..08a44319f 100644 --- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs index 0c4e82d01..73e628cf7 100644 --- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using Jellyfin.Data.Entities; diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs index a35192eff..3c5ddeefa 100644 --- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs @@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return GetValue(x).CompareTo(GetValue(y)); } @@ -30,9 +30,9 @@ namespace Emby.Server.Implementations.Sorting /// </summary> /// <param name="x">The x.</param> /// <returns>System.String.</returns> - private static int GetValue(BaseItem x) + private static int GetValue(BaseItem? x) { - return x.IsFolder ? 0 : 1; + return x?.IsFolder ?? true ? 0 : 1; } } } diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs index d95948406..7d77a8bc5 100644 --- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using Jellyfin.Data.Entities; diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs index 1632c5a7a..926835f90 100644 --- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using Jellyfin.Data.Entities; diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs index da020d8d8..4de81a69e 100644 --- a/Emby.Server.Implementations/Sorting/NameComparer.cs +++ b/Emby.Server.Implementations/Sorting/NameComparer.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { if (x == null) { diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs index 76bb798b5..a81f78ebf 100644 --- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs @@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { if (x == null) { diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs index 5c2830322..04e4865cb 100644 --- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs +++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs @@ -1,3 +1,5 @@ +#nullable disable + using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs index 92ac04dc6..c98f97bf1 100644 --- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return GetDate(x).CompareTo(GetDate(y)); } @@ -26,8 +26,13 @@ namespace Emby.Server.Implementations.Sorting /// </summary> /// <param name="x">The x.</param> /// <returns>DateTime.</returns> - private static DateTime GetDate(BaseItem x) + private static DateTime GetDate(BaseItem? x) { + if (x == null) + { + return DateTime.MinValue; + } + if (x.PremiereDate.HasValue) { return x.PremiereDate.Value; diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs index e2857df0b..df9f9957d 100644 --- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs +++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return GetValue(x).CompareTo(GetValue(y)); } @@ -25,8 +25,13 @@ namespace Emby.Server.Implementations.Sorting /// </summary> /// <param name="x">The x.</param> /// <returns>DateTime.</returns> - private static int GetValue(BaseItem x) + private static int GetValue(BaseItem? x) { + if (x == null) + { + return 0; + } + if (x.ProductionYear.HasValue) { return x.ProductionYear.Value; diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs index 7739d0418..af3bc2750 100644 --- a/Emby.Server.Implementations/Sorting/RandomComparer.cs +++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting /// <param name="x">The x.</param> /// <param name="y">The y.</param> /// <returns>System.Int32.</returns> - public int Compare(BaseItem x, BaseItem y) + public int Compare(BaseItem? x, BaseItem? y) { return Guid.NewGuid().CompareTo(Guid.NewGuid()); } diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs index dde44333d..129315303 100644 --- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs +++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index b9205ee07..4123a59f8 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs index f745e193b..8d30716d3 100644 --- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs index 558a3d351..c3df7c47e 100644 --- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs index 5766dc542..01445c525 100644 --- a/Emby.Server.Implementations/Sorting/StudioComparer.cs +++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs index 7c2ad2477..12efff261 100644 --- a/Emby.Server.Implementations/SyncPlay/Group.cs +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 72c0a838e..993456196 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 829df64bf..af453d148 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -1,3 +1,5 @@ +#nullable disable + #pragma warning disable CS1591 using System; @@ -152,7 +154,7 @@ namespace Emby.Server.Implementations.TV return i.Item1 != DateTime.MinValue; } - if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue) + if (alwaysEnableFirstEpisode || (i.Item1 != DateTime.MinValue && i.Item1.Date >= request.NextUpDateCutoff)) { anyFound = true; return true; diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index db5265e79..8179e26c5 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -18,17 +18,17 @@ namespace Emby.Server.Implementations.Udp public sealed class UdpServer : IDisposable { /// <summary> + /// Address Override Configuration Key. + /// </summary> + public const string AddressOverrideConfigKey = "PublishedServerUrl"; + + /// <summary> /// The _logger. /// </summary> private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; private readonly IConfiguration _config; - /// <summary> - /// Address Override Configuration Key. - /// </summary> - public const string AddressOverrideConfigKey = "PublishedServerUrl"; - private Socket _udpSocket; private IPEndPoint _endpoint; private readonly byte[] _receiveBuffer = new byte[8192]; @@ -38,49 +38,58 @@ namespace Emby.Server.Implementations.Udp /// <summary> /// Initializes a new instance of the <see cref="UdpServer" /> class. /// </summary> - public UdpServer(ILogger logger, IServerApplicationHost appHost, IConfiguration configuration) + /// <param name="logger">The logger.</param> + /// <param name="appHost">The application host.</param> + /// <param name="configuration">The configuration manager.</param> + /// <param name="port">The port.</param> + public UdpServer( + ILogger logger, + IServerApplicationHost appHost, + IConfiguration configuration, + int port) { _logger = logger; _appHost = appHost; _config = configuration; + + _endpoint = new IPEndPoint(IPAddress.Any, port); + + _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken) { - string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey]) - ? _config[AddressOverrideConfigKey] - : _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address); + string? localUrl = _config[AddressOverrideConfigKey]; + if (string.IsNullOrEmpty(localUrl)) + { + localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address); + } - if (!string.IsNullOrEmpty(localUrl)) + if (string.IsNullOrEmpty(localUrl)) { - var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName); + _logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined."); + return; + } - try - { - await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false); - } - catch (SocketException ex) - { - _logger.LogError(ex, "Error sending response message"); - } + var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName); + + try + { + await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false); } - else + catch (SocketException ex) { - _logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined."); + _logger.LogError(ex, "Error sending response message"); } } /// <summary> /// Starts the specified port. /// </summary> - /// <param name="port">The port.</param> /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - public void Start(int port, CancellationToken cancellationToken) + public void Start(CancellationToken cancellationToken) { - _endpoint = new IPEndPoint(IPAddress.Any, port); - - _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); _udpSocket.Bind(_endpoint); _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); @@ -88,9 +97,9 @@ namespace Emby.Server.Implementations.Udp private async Task BeginReceiveAsync(CancellationToken cancellationToken) { + var infiniteTask = Task.Delay(-1, cancellationToken); while (!cancellationToken.IsCancellationRequested) { - var infiniteTask = Task.Delay(-1, cancellationToken); try { var task = _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint); diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 653b1381b..b0921cbd8 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -101,12 +99,12 @@ namespace Emby.Server.Implementations.Updates public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal; /// <inheritdoc /> - public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default) + public async Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default) { try { - List<PackageInfo>? packages = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); + PackageInfo[]? packages = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetFromJsonAsync<PackageInfo[]>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false); if (packages == null) { @@ -179,20 +177,14 @@ namespace Emby.Server.Implementations.Updates // Where repositories have the same content, the details from the first is taken. foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true)) { - if (!Guid.TryParse(package.Id, out var packageGuid)) - { - // Package doesn't have a valid GUID, skip. - continue; - } - - var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault(); + var existing = FilterPackages(result, package.Name, package.Id).FirstOrDefault(); // Remove invalid versions from the valid package. for (var i = package.Versions.Count - 1; i >= 0; i--) { var version = package.Versions[i]; - var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber); + var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber); if (plugin != null) { await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false); @@ -231,7 +223,7 @@ namespace Emby.Server.Implementations.Updates public IEnumerable<PackageInfo> FilterPackages( IEnumerable<PackageInfo> availablePackages, string? name = null, - Guid? id = default, + Guid id = default, Version? specificVersion = null) { if (name != null) @@ -239,9 +231,9 @@ namespace Emby.Server.Implementations.Updates availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } - if (id != Guid.Empty) + if (id != default) { - availablePackages = availablePackages.Where(x => Guid.Parse(x.Id) == id); + availablePackages = availablePackages.Where(x => x.Id == id); } if (specificVersion != null) @@ -256,7 +248,7 @@ namespace Emby.Server.Implementations.Updates public IEnumerable<InstallationInfo> GetCompatibleVersions( IEnumerable<PackageInfo> availablePackages, string? name = null, - Guid? id = default, + Guid id = default, Version? minVersion = null, Version? specificVersion = null) { @@ -286,7 +278,7 @@ namespace Emby.Server.Implementations.Updates yield return new InstallationInfo { Changelog = v.Changelog, - Id = new Guid(package.Id), + Id = package.Id, Name = package.Name, Version = v.VersionNumber, SourceUrl = v.SourceUrl, diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 4b2e5e7ea..154a56702 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -77,6 +77,8 @@ namespace Jellyfin.Api.Controllers /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Total record count.</param> /// <response code="200">Artists returned.</response> @@ -112,6 +114,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -150,7 +154,8 @@ namespace Jellyfin.Api.Controllers MinCommunityRating = minCommunityRating, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) @@ -276,6 +281,8 @@ namespace Jellyfin.Api.Controllers /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Total record count.</param> /// <response code="200">Album artists returned.</response> @@ -311,6 +318,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -349,7 +358,8 @@ namespace Jellyfin.Api.Controllers MinCommunityRating = minCommunityRating, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b4154b361..62283d038 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -28,7 +28,6 @@ using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -545,7 +544,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new VideoRequestDto { Id = itemId, @@ -710,7 +709,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new StreamingRequestDto { Id = itemId, @@ -1138,7 +1137,7 @@ namespace Jellyfin.Api.Controllers var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase); var hlsVersion = isHlsInFmp4 ? "7" : "3"; - var builder = new StringBuilder(); + var builder = new StringBuilder(128); builder.AppendLine("#EXTM3U") .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") @@ -1191,6 +1190,7 @@ namespace Jellyfin.Api.Controllers throw new ArgumentException("StartTimeTicks is not allowed."); } + // CTS lifecycle is managed internally. var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; @@ -1208,7 +1208,7 @@ namespace Jellyfin.Api.Controllers _deviceManager, _transcodingJobHelper, TranscodingJobType, - cancellationTokenSource.Token) + cancellationToken) .ConfigureAwait(false); var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); @@ -1227,7 +1227,7 @@ namespace Jellyfin.Api.Controllers } var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); - await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); + await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); var released = false; var startTranscoding = false; @@ -1323,24 +1323,28 @@ namespace Jellyfin.Api.Controllers return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } - private double[] GetSegmentLengths(StreamState state) - { - var result = new List<double>(); + private static double[] GetSegmentLengths(StreamState state) + => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength); - var ticks = state.RunTimeTicks ?? 0; - - var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks; + internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength) + { + var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks; + var wholeSegments = runtimeTicks / segmentLengthTicks; + var remainingTicks = runtimeTicks % segmentLengthTicks; - while (ticks > 0) + var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1); + var segments = new double[segmentsLen]; + for (int i = 0; i < wholeSegments; i++) { - var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks; - - result.Add(TimeSpan.FromTicks(length).TotalSeconds); + segments[i] = segmentlength; + } - ticks -= length; + if (remainingTicks != 0) + { + segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds; } - return result.ToArray(); + return segments; } private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber) @@ -1376,18 +1380,13 @@ namespace Jellyfin.Api.Controllers } else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) { - var outputFmp4HeaderArg = string.Empty; - var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - if (isWindows) + var outputFmp4HeaderArg = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) switch { // on Windows, the path of fmp4 header file needs to be configured - outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\""; - } - else - { + true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"", // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder - outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""; - } + false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"" + }; segmentFormat = "fmp4" + outputFmp4HeaderArg; } @@ -1762,9 +1761,9 @@ namespace Jellyfin.Api.Controllers private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem) { - var folder = Path.GetDirectoryName(playlist); + var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist)); - var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty; + var filePrefix = Path.GetFileNameWithoutExtension(playlist); try { diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 7bcf4674c..5aa457153 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -63,6 +63,8 @@ namespace Jellyfin.Api.Controllers /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Optional. Include total record count.</param> /// <response code="200">Genres returned.</response> @@ -84,6 +86,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -107,7 +111,8 @@ namespace Jellyfin.Api.Controllers NameStartsWithOrGreater = nameStartsWithOrGreater, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 74cf3b162..35c27dd0e 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -143,7 +143,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Items")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetItems( - [FromQuery] Guid? userId, + [FromQuery] Guid userId, [FromQuery] string? maxOfficialRating, [FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeVideo, @@ -224,8 +224,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) + var user = !userId.Equals(Guid.Empty) + ? _userManager.GetUserById(userId) : null; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index d0a2358ae..010a3b19a 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -18,6 +18,7 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers @@ -300,9 +301,8 @@ namespace Jellyfin.Api.Controllers private IEnumerable<string> GetActors(IEnumerable<BaseItem> items) { - var people = _libraryManager.GetPeople(new InternalPeopleQuery + var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director }) { - ExcludePersonTypes = new[] { PersonType.Director }, MaxListOrder = 3 }); @@ -316,10 +316,9 @@ namespace Jellyfin.Api.Controllers private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items) { - var people = _libraryManager.GetPeople(new InternalPeopleQuery - { - PersonTypes = new[] { PersonType.Director } - }); + var people = _libraryManager.GetPeople(new InternalPeopleQuery( + new[] { PersonType.Director }, + Array.Empty<string>())); var itemIds = items.Select(i => i.Id).ToList(); diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 7f7058b5e..27eec2b9a 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -63,6 +63,8 @@ namespace Jellyfin.Api.Controllers /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Optional. Include total record count.</param> /// <response code="200">Music genres returned.</response> @@ -84,6 +86,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -107,7 +111,8 @@ namespace Jellyfin.Api.Controllers NameStartsWithOrGreater = nameStartsWithOrGreater, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 70a94e27c..b98307f87 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -94,10 +94,10 @@ namespace Jellyfin.Api.Controllers } var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); - var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery + var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery( + personTypes, + excludePersonTypes) { - PersonTypes = personTypes, - ExcludePersonTypes = excludePersonTypes, NameContains = searchTerm, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 1669a659d..b473574e0 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param> /// <response code="200">File returned.</response> /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns> - [HttpGet("Videos/{routeItemId}/routeMediaSourceId/Subtitles/{routeIndex}/Stream.{routeFormat}")] + [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile("text/*")] public async Task<ActionResult> GetSubtitle( diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index dd3836551..5cb7468b2 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -114,7 +114,7 @@ namespace Jellyfin.Api.Controllers [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetTrailers( - [FromQuery] Guid? userId, + [FromQuery] Guid userId, [FromQuery] string? maxOfficialRating, [FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeVideo, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 59400db2a..ffb726fab 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -65,6 +65,7 @@ namespace Jellyfin.Api.Controllers /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="enableUserData">Optional. Include user data.</param> + /// <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> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> @@ -81,6 +82,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, + [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool disableFirstEpisode = false) { @@ -97,7 +99,8 @@ namespace Jellyfin.Api.Controllers StartIndex = startIndex, UserId = userId ?? Guid.Empty, EnableTotalRecordCount = enableTotalRecordCount, - DisableFirstEpisode = disableFirstEpisode + DisableFirstEpisode = disableFirstEpisode, + NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue }, options); diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 800cfa81f..34a1d1842 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -265,6 +265,7 @@ namespace Jellyfin.Api.Controllers EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true }; + // CTS lifecycle is managed internally. var cancellationTokenSource = new CancellationTokenSource(); using var state = await StreamingHelpers.GetStreamingState( streamingRequest, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index e544d001e..dc64a0f1b 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -373,6 +373,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] Dictionary<string, string> streamOptions) { var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; + // CTS lifecycle is managed internally. var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new VideoRequestDto { diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs index cf35ee23a..264131905 100644 --- a/Jellyfin.Api/Helpers/AudioHelper.cs +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -97,6 +97,8 @@ namespace Jellyfin.Api.Helpers } bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head; + + // CTS lifecycle is managed internally. var cancellationTokenSource = new CancellationTokenSource(); using var state = await StreamingHelpers.GetStreamingState( diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index fcada0e77..dc5d6715b 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -106,6 +106,7 @@ namespace Jellyfin.Api.Helpers bool enableAdaptiveBitrateStreaming) { var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head; + // CTS lifecycle is managed internally. var cancellationTokenSource = new CancellationTokenSource(); return await GetMasterPlaylistInternal( streamingRequest, diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 0879cbd18..c295af7eb 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -22,7 +22,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Helpers @@ -270,7 +269,7 @@ namespace Jellyfin.Api.Helpers { _activeTranscodingJobs.Remove(job); - if (!job.CancellationTokenSource!.IsCancellationRequested) + if (job.CancellationTokenSource?.IsCancellationRequested == false) { job.CancellationTokenSource.Cancel(); } @@ -380,7 +379,9 @@ namespace Jellyfin.Api.Helpers /// <param name="outputFilePath">The output file path.</param> private void DeleteHlsPartialStreamFiles(string outputFilePath) { - var directory = Path.GetDirectoryName(outputFilePath); + var directory = Path.GetDirectoryName(outputFilePath) + ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath)); + var name = Path.GetFileNameWithoutExtension(outputFilePath); var filesToDelete = _fileSystem.GetFilePaths(directory) @@ -750,7 +751,7 @@ namespace Jellyfin.Api.Helpers _logger.LogError("FFmpeg exited with code {0}", process.ExitCode); } - process.Dispose(); + job.Dispose(); } private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index c10c34b59..bd7da9b06 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -15,7 +15,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.5" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.7" /> <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.4" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.1.4" /> diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs index 9edc19bb6..291e571dc 100644 --- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs +++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs @@ -11,7 +11,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos /// <summary> /// Class TranscodingJob. /// </summary> - public class TranscodingJobDto + public class TranscodingJobDto : IDisposable { /// <summary> /// The process lock. @@ -249,5 +249,31 @@ namespace Jellyfin.Api.Models.PlaybackDtos } } } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Dispose all resources. + /// </summary> + /// <param name="disposing">Whether to dispose all resources.</param> + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Process?.Dispose(); + Process = null; + KillTimer?.Dispose(); + KillTimer = null; + CancellationTokenSource?.Dispose(); + CancellationTokenSource = null; + TranscodingThrottler?.Dispose(); + TranscodingThrottler = null; + } + } } } diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs index e33e552ed..7b32d76ba 100644 --- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs +++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs @@ -145,7 +145,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; var downloadPositionTicks = job.DownloadPositionTicks ?? 0; - var path = job.Path; + var path = job.Path ?? throw new ArgumentException("Path can't be null."); + var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks; if (downloadPositionTicks > 0 && transcodingPositionTicks > 0) diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index e9f9aad57..09a370238 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -68,7 +68,7 @@ namespace Jellyfin.Drawing.Skia /// <param name="outputPath">The path at which to place the resulting collage image.</param> /// <param name="width">The desired width of the collage.</param> /// <param name="height">The desired height of the collage.</param> - public void BuildSquareCollage(string[] paths, string outputPath, int width, int height) + public void BuildSquareCollage(IReadOnlyList<string> paths, string outputPath, int width, int height) { using var bitmap = BuildSquareCollageBitmap(paths, width, height); using var outputStream = new SKFileWStream(outputPath); @@ -84,7 +84,7 @@ namespace Jellyfin.Drawing.Skia /// <param name="width">The desired width of the collage.</param> /// <param name="height">The desired height of the collage.</param> /// <param name="libraryName">The name of the library to draw on the collage.</param> - public void BuildThumbCollage(string[] paths, string outputPath, int width, int height, string? libraryName) + public void BuildThumbCollage(IReadOnlyList<string> paths, string outputPath, int width, int height, string? libraryName) { using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName); using var outputStream = new SKFileWStream(outputPath); @@ -92,7 +92,7 @@ namespace Jellyfin.Drawing.Skia pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); } - private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height, string? libraryName) + private SKBitmap BuildThumbCollageBitmap(IReadOnlyList<string> paths, int width, int height, string? libraryName) { var bitmap = new SKBitmap(width, height); @@ -152,14 +152,14 @@ namespace Jellyfin.Drawing.Skia return bitmap; } - private SKBitmap? GetNextValidImage(string[] paths, int currentIndex, out int newIndex) + private SKBitmap? GetNextValidImage(IReadOnlyList<string> paths, int currentIndex, out int newIndex) { var imagesTested = new Dictionary<int, int>(); SKBitmap? bitmap = null; - while (imagesTested.Count < paths.Length) + while (imagesTested.Count < paths.Count) { - if (currentIndex >= paths.Length) + if (currentIndex >= paths.Count) { currentIndex = 0; } @@ -180,7 +180,7 @@ namespace Jellyfin.Drawing.Skia return bitmap; } - private SKBitmap BuildSquareCollageBitmap(string[] paths, int width, int height) + private SKBitmap BuildSquareCollageBitmap(IReadOnlyList<string> paths, int width, int height) { var bitmap = new SKBitmap(width, height); var imageIndex = 0; diff --git a/Jellyfin.Server.Implementations/Events/EventManager.cs b/Jellyfin.Server.Implementations/Events/EventManager.cs index 707002442..8c5d8f2ce 100644 --- a/Jellyfin.Server.Implementations/Events/EventManager.cs +++ b/Jellyfin.Server.Implementations/Events/EventManager.cs @@ -30,7 +30,7 @@ namespace Jellyfin.Server.Implementations.Events public void Publish<T>(T eventArgs) where T : EventArgs { - Task.WaitAll(PublishInternal(eventArgs)); + PublishInternal(eventArgs).GetAwaiter().GetResult(); } /// <inheritdoc /> @@ -43,7 +43,12 @@ namespace Jellyfin.Server.Implementations.Events private async Task PublishInternal<T>(T eventArgs) where T : EventArgs { - using var scope = _appHost.ServiceProvider.CreateScope(); + using var scope = _appHost.ServiceProvider?.CreateScope(); + if (scope == null) + { + return; + } + foreach (var service in scope.ServiceProvider.GetServices<IEventConsumer<T>>()) { try diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 2c6a176b6..eeeb1d19b 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -27,13 +27,13 @@ <ItemGroup> <PackageReference Include="System.Linq.Async" Version="5.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.5" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.5"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.7" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.7"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.5"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.7"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 88e2b4152..e29167747 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -79,6 +79,16 @@ namespace Jellyfin.Server.Extensions } /// <summary> + /// Enables url decoding before binding to the application pipeline. + /// </summary> + /// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param> + /// <returns>The updated application builder.</returns> + public static IApplicationBuilder UseQueryStringDecoding(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware<QueryStringDecodingMiddleware>(); + } + + /// <summary> /// Adds base url redirection to the application pipeline. /// </summary> /// <param name="appBuilder">The application builder.</param> diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 3496cabe8..ea782cb66 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -38,8 +38,8 @@ <PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.5" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.5" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.7" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.7" /> <PackageReference Include="prometheus-net" Version="4.1.1" /> <PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs index c23da2fd6..2eef223e5 100644 --- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs @@ -45,15 +45,33 @@ namespace Jellyfin.Server.Middleware var localPath = httpContext.Request.Path.ToString(); var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl; - if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) - || string.IsNullOrEmpty(localPath) - || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(baseUrlPrefix)) { - // Always redirect back to the default path if the base prefix is invalid or missing + var startsWithBaseUrl = localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase); + + if (!startsWithBaseUrl + && (localPath.Equals("/health", StringComparison.OrdinalIgnoreCase) + || localPath.Equals("/health/", StringComparison.OrdinalIgnoreCase))) + { + _logger.LogDebug("Redirecting /health check"); + httpContext.Response.Redirect(baseUrlPrefix + "/health"); + return; + } + + if (!startsWithBaseUrl) + { + // Always redirect back to the default path if the base prefix is invalid or missing + _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); + httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]); + return; + } + } + else if (string.IsNullOrEmpty(localPath) + || localPath.Equals("/", StringComparison.Ordinal)) + { + // Always redirect back to the default path if root is requested. _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); - httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]); + httpContext.Response.Redirect("/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]); return; } diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs new file mode 100644 index 000000000..fd0ebbf43 --- /dev/null +++ b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// URL decodes the querystring before binding. + /// </summary> + public class QueryStringDecodingMiddleware + { + private readonly RequestDelegate _next; + + /// <summary> + /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + public QueryStringDecodingMiddleware(RequestDelegate next) + { + _next = next; + } + + /// <summary> + /// Executes the middleware action. + /// </summary> + /// <param name="httpContext">The current HTTP context.</param> + /// <returns>The async task.</returns> + public async Task Invoke(HttpContext httpContext) + { + httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(httpContext.Features.Get<IQueryFeature>())); + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs new file mode 100644 index 000000000..310a3d31a --- /dev/null +++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using MediaBrowser.Common.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// Defines the <see cref="UrlDecodeQueryFeature"/>. + /// </summary> + public class UrlDecodeQueryFeature : IQueryFeature + { + private IQueryCollection? _store; + + /// <summary> + /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class. + /// </summary> + /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param> + public UrlDecodeQueryFeature(IQueryFeature feature) + { + Query = feature.Query; + } + + /// <summary> + /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>. + /// </summary> + public IQueryCollection Query + { + get + { + return _store ?? QueryCollection.Empty; + } + + set + { + // Only interested in where the querystring is encoded which shows up as one key with nothing in the value. + if (value.Count != 1) + { + _store = value; + return; + } + + // Encoded querystrings have no value, so don't process anything if a value is present. + var (key, stringValues) = value.First(); + if (!string.IsNullOrEmpty(stringValues)) + { + _store = value; + return; + } + + // Unencode and re-parse querystring. + var unencodedKey = HttpUtility.UrlDecode(key); + + if (string.Equals(unencodedKey, key, StringComparison.Ordinal)) + { + // Don't do anything if it's not encoded. + _store = value; + return; + } + + var pairs = new Dictionary<string, StringValues>(); + var queryString = unencodedKey.SpanSplit('&'); + + foreach (var pair in queryString) + { + var i = pair.IndexOf('='); + if (i == -1) + { + // encoded is an equals. + // We use TryAdd so duplicate keys get ignored + pairs.TryAdd(pair.ToString(), StringValues.Empty); + continue; + } + + var k = pair[..i].ToString(); + var v = pair[(i + 1)..].ToString(); + if (!pairs.TryAdd(k, new StringValues(v))) + { + pairs[k] = StringValues.Concat(pairs[k], v); + } + } + + _store = new QueryCollection(pairs); + } + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index a15a38177..96bd2ccc4 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -82,11 +82,14 @@ namespace Jellyfin.Server.Migrations.Routines var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name); - var config = File.Exists(Path.Combine(userDataDir, "config.xml")) - ? (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml")) + var configPath = Path.Combine(userDataDir, "config.xml"); + var config = File.Exists(configPath) + ? (UserConfiguration?)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), configPath) ?? new UserConfiguration() : new UserConfiguration(); - var policy = File.Exists(Path.Combine(userDataDir, "policy.xml")) - ? (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml")) + + var policyPath = Path.Combine(userDataDir, "policy.xml"); + var policy = File.Exists(policyPath) + ? (UserPolicy?)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), policyPath) ?? new UserPolicy() : new UserPolicy(); policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace( "Emby.Server.Implementations.Library", diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index c10b2ddb3..3a3d7415b 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -12,10 +12,12 @@ using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; +using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -220,6 +222,14 @@ namespace Jellyfin.Server } finally { + _logger.LogInformation("Running query planner optimizations in the database... This might take a while"); + // Run before disposing the application + using var context = new JellyfinDbProvider(appHost.ServiceProvider, appPaths).CreateContext(); + if (context.Database.IsSqlite()) + { + context.Database.ExecuteSqlRaw("PRAGMA optimize"); + } + appHost.Dispose(); } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index f75139884..60cdc2f6f 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -160,6 +160,7 @@ namespace Jellyfin.Server mainApp.UseAuthentication(); mainApp.UseJellyfinApiSwagger(_serverConfigurationManager); + mainApp.UseQueryStringDecoding(); mainApp.UseRouting(); mainApp.UseAuthorization(); diff --git a/Jellyfin.sln b/Jellyfin.sln index 8626a4b1b..9fbd9d266 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -81,6 +81,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Tests", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Integration.Tests", "tests\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj", "{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Providers.Tests", "tests\Jellyfin.Providers.Tests\Jellyfin.Providers.Tests.csproj", "{A964008C-2136-4716-B6CB-B3426C22320A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -223,6 +225,10 @@ Global {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Debug|Any CPU.Build.0 = Debug|Any CPU {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Release|Any CPU.ActiveCfg = Release|Any CPU {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E}.Release|Any CPU.Build.0 = Release|Any CPU + {A964008C-2136-4716-B6CB-B3426C22320A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A964008C-2136-4716-B6CB-B3426C22320A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A964008C-2136-4716-B6CB-B3426C22320A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A964008C-2136-4716-B6CB-B3426C22320A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -240,6 +246,7 @@ Global {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {68B0B823-A5AC-4E8B-82EA-965AAC7BF76E} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {A964008C-2136-4716-B6CB-B3426C22320A} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/MediaBrowser.Common/Extensions/EnumerableExtensions.cs b/MediaBrowser.Common/Extensions/EnumerableExtensions.cs new file mode 100644 index 000000000..2b8a6c395 --- /dev/null +++ b/MediaBrowser.Common/Extensions/EnumerableExtensions.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Static extensions for the <see cref="IEnumerable{T}"/> interface. + /// </summary> + public static class EnumerableExtensions + { + /// <summary> + /// Determines whether the value is contained in the source collection. + /// </summary> + /// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param> + /// <param name="value">The value to look for in the collection.</param> + /// <param name="stringComparison">The string comparison.</param> + /// <returns>A value indicating whether the value is contained in the collection.</returns> + /// <exception cref="ArgumentNullException">The source is null.</exception> + public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (source is IList<string> list) + { + int len = list.Count; + for (int i = 0; i < len; i++) + { + if (value.Equals(list[i], stringComparison)) + { + return true; + } + } + + return false; + } + + foreach (string element in source) + { + if (value.Equals(element, stringComparison)) + { + return true; + } + } + + return false; + } + } +} diff --git a/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs new file mode 100644 index 000000000..75d654f23 --- /dev/null +++ b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Text; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Extension methods for the <see cref="StringBuilder"/> class. + /// </summary> + public static class StringBuilderExtensions + { + /// <summary> + /// Concatenates and appends the members of a collection in single quotes using the specified delimiter. + /// </summary> + /// <param name="builder">The string builder.</param> + /// <param name="delimiter">The character delimiter.</param> + /// <param name="values">The collection of strings to concatenate.</param> + /// <returns>The updated string builder.</returns> + public static StringBuilder AppendJoinInSingleQuotes(this StringBuilder builder, char delimiter, IReadOnlyList<string> values) + { + var len = values.Count; + for (var i = 0; i < len; i++) + { + builder.Append('\'') + .Append(values[i]) + .Append('\'') + .Append(delimiter); + } + + // remove last , + builder.Length--; + + return builder; + } + } +} diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 46d93e494..192a77611 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Reflection; @@ -12,7 +10,7 @@ namespace MediaBrowser.Common /// </summary> /// <param name="type">Type to create.</param> /// <returns>New instance of type <param>type</param>.</returns> - public delegate object CreationDelegateFactory(Type type); + public delegate object? CreationDelegateFactory(Type type); /// <summary> /// An interface to be implemented by the applications hosting a kernel. @@ -22,7 +20,7 @@ namespace MediaBrowser.Common /// <summary> /// Occurs when [has pending restart changed]. /// </summary> - event EventHandler HasPendingRestartChanged; + event EventHandler? HasPendingRestartChanged; /// <summary> /// Gets the name. @@ -63,7 +61,7 @@ namespace MediaBrowser.Common /// <summary> /// Gets or sets the service provider. /// </summary> - IServiceProvider ServiceProvider { get; set; } + IServiceProvider? ServiceProvider { get; set; } /// <summary> /// Gets the application version. diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index c2a28e0a2..458494bdc 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Updates /// <param name="filterIncompatible">Filter out incompatible plugins.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{IReadOnlyList{PackageInfo}}.</returns> - Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default); + Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default); /// <summary> /// Gets all available packages that are supported by this version. @@ -45,7 +45,7 @@ namespace MediaBrowser.Common.Updates IEnumerable<PackageInfo> FilterPackages( IEnumerable<PackageInfo> availablePackages, string? name = null, - Guid? id = default, + Guid id = default, Version? specificVersion = null); /// <summary> @@ -60,7 +60,7 @@ namespace MediaBrowser.Common.Updates IEnumerable<InstallationInfo> GetCompatibleVersions( IEnumerable<PackageInfo> availablePackages, string? name = null, - Guid? id = default, + Guid id = default, Version? minVersion = null, Version? specificVersion = null); diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs index 68119cfed..ffc274c5d 100644 --- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs +++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.Threading; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -52,7 +53,7 @@ namespace MediaBrowser.Controller.BaseItemManager var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name); if (typeOptions != null) { - return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } if (!libraryOptions.EnableInternetProviders) @@ -62,7 +63,7 @@ namespace MediaBrowser.Controller.BaseItemManager var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); - return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } /// <inheritdoc /> @@ -83,7 +84,7 @@ namespace MediaBrowser.Controller.BaseItemManager var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name); if (typeOptions != null) { - return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } if (!libraryOptions.EnableInternetProviders) @@ -93,7 +94,7 @@ namespace MediaBrowser.Controller.BaseItemManager var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase)); - return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase); + return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase); } /// <summary> diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index 26c64e0da..26a936be0 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -84,7 +84,7 @@ namespace MediaBrowser.Controller.Channels internal static bool IsChannelVisible(BaseItem channelItem, User user) { - var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString("")); + var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(string.Empty)); return channel.IsVisible(user); } diff --git a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs index fa7aff647..4d1e35f9e 100644 --- a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs +++ b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs @@ -13,6 +13,19 @@ namespace MediaBrowser.Controller.Channels { public class ChannelItemInfo : IHasProviderIds { + public ChannelItemInfo() + { + MediaSources = new List<MediaSourceInfo>(); + TrailerTypes = new List<TrailerType>(); + Genres = new List<string>(); + Studios = new List<string>(); + People = new List<PersonInfo>(); + Tags = new List<string>(); + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + Artists = new List<string>(); + AlbumArtists = new List<string>(); + } + public string Name { get; set; } public string SeriesName { get; set; } @@ -80,18 +93,5 @@ namespace MediaBrowser.Controller.Channels public bool IsLiveStream { get; set; } public string Etag { get; set; } - - public ChannelItemInfo() - { - MediaSources = new List<MediaSourceInfo>(); - TrailerTypes = new List<TrailerType>(); - Genres = new List<string>(); - Studios = new List<string>(); - People = new List<PersonInfo>(); - Tags = new List<string>(); - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - Artists = new List<string>(); - AlbumArtists = new List<string>(); - } } } diff --git a/MediaBrowser.Controller/Channels/ChannelItemResult.cs b/MediaBrowser.Controller/Channels/ChannelItemResult.cs index 8e937852f..6b2077662 100644 --- a/MediaBrowser.Controller/Channels/ChannelItemResult.cs +++ b/MediaBrowser.Controller/Channels/ChannelItemResult.cs @@ -8,13 +8,13 @@ namespace MediaBrowser.Controller.Channels { public class ChannelItemResult { - public List<ChannelItemInfo> Items { get; set; } - - public int? TotalRecordCount { get; set; } - public ChannelItemResult() { Items = new List<ChannelItemInfo>(); } + + public List<ChannelItemInfo> Items { get; set; } + + public int? TotalRecordCount { get; set; } } } diff --git a/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs b/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs new file mode 100644 index 000000000..6f0761e64 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ChannelLatestMediaSearch.cs @@ -0,0 +1,11 @@ +#nullable disable + +#pragma warning disable CS1591 + +namespace MediaBrowser.Controller.Channels +{ + public class ChannelLatestMediaSearch + { + public string UserId { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs index 53a73d62a..990b025bc 100644 --- a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs +++ b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs @@ -10,9 +10,4 @@ namespace MediaBrowser.Controller.Channels public string UserId { get; set; } } - - public class ChannelLatestMediaSearch - { - public string UserId { get; set; } - } } diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs index 4c5626338..49be897ef 100644 --- a/MediaBrowser.Controller/Channels/IChannelManager.cs +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -51,32 +51,47 @@ namespace MediaBrowser.Controller.Channels /// Gets the channels internal. /// </summary> /// <param name="query">The query.</param> + /// <returns>The channels.</returns> QueryResult<Channel> GetChannelsInternal(ChannelQuery query); /// <summary> /// Gets the channels. /// </summary> /// <param name="query">The query.</param> + /// <returns>The channels.</returns> QueryResult<BaseItemDto> GetChannels(ChannelQuery query); /// <summary> - /// Gets the latest media. + /// Gets the latest channel items. /// </summary> + /// <param name="query">The item query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The latest channels.</returns> Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken); /// <summary> - /// Gets the latest media. + /// Gets the latest channel items. /// </summary> + /// <param name="query">The item query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The latest channels.</returns> Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken); /// <summary> /// Gets the channel items. /// </summary> + /// <param name="query">The query.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The channel items.</returns> Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken); /// <summary> - /// Gets the channel items internal. + /// Gets the channel items. /// </summary> + /// <param name="query">The query.</param> + /// <param name="progress">The progress to report to.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The channel items.</returns> Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken); /// <summary> @@ -84,9 +99,14 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <param name="item">The item.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns> + /// <returns>The item media sources.</returns> IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken); + /// <summary> + /// Whether the item supports media probe. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>Whether media probe should be enabled.</returns> bool EnableMediaProbe(BaseItem item); } } diff --git a/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs new file mode 100644 index 000000000..0539b9048 --- /dev/null +++ b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs @@ -0,0 +1,12 @@ +namespace MediaBrowser.Controller.Channels +{ + /// <summary> + /// Disable media source display. + /// </summary> + /// <remarks> + /// <see cref="Channel"/> can inherit this interface to disable being displayed. + /// </remarks> + public interface IDisableMediaSourceDisplay + { + } +} diff --git a/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs b/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs new file mode 100644 index 000000000..47277a8cc --- /dev/null +++ b/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Controller.Channels +{ + public interface IHasFolderAttributes + { + string[] Attributes { get; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs index 589295543..eeaa6b622 100644 --- a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs +++ b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -7,11 +5,17 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Channels { + /// <summary> + /// The channel requires a media info callback. + /// </summary> public interface IRequiresMediaInfoCallback { /// <summary> /// Gets the channel item media information. /// </summary> + /// <param name="id">The channel item id.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The enumerable of media source info.</returns> Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaInfo(string id, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Channels/ISearchableChannel.cs b/MediaBrowser.Controller/Channels/ISearchableChannel.cs index b58446fc4..b87943a6e 100644 --- a/MediaBrowser.Controller/Channels/ISearchableChannel.cs +++ b/MediaBrowser.Controller/Channels/ISearchableChannel.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Channels { @@ -19,35 +18,4 @@ namespace MediaBrowser.Controller.Channels /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns> Task<IEnumerable<ChannelItemInfo>> Search(ChannelSearchInfo searchInfo, CancellationToken cancellationToken); } - - public interface ISupportsLatestMedia - { - /// <summary> - /// Gets the latest media. - /// </summary> - /// <param name="request">The request.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns> - Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken); - } - - public interface ISupportsDelete - { - bool CanDelete(BaseItem item); - - Task DeleteItem(string id, CancellationToken cancellationToken); - } - - public interface IDisableMediaSourceDisplay - { - } - - public interface ISupportsMediaProbe - { - } - - public interface IHasFolderAttributes - { - string[] Attributes { get; } - } } diff --git a/MediaBrowser.Controller/Channels/ISupportsDelete.cs b/MediaBrowser.Controller/Channels/ISupportsDelete.cs new file mode 100644 index 000000000..204054374 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ISupportsDelete.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 + +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Channels +{ + public interface ISupportsDelete + { + bool CanDelete(BaseItem item); + + Task DeleteItem(string id, CancellationToken cancellationToken); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs new file mode 100644 index 000000000..dbba7cba2 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs @@ -0,0 +1,21 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Channels +{ + public interface ISupportsLatestMedia + { + /// <summary> + /// Gets the latest media. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The latest media.</returns> + Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs new file mode 100644 index 000000000..bc7683125 --- /dev/null +++ b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Channels +{ + /// <summary> + /// Channel supports media probe. + /// </summary> + public interface ISupportsMediaProbe + { + } +} diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs index 152c653dc..45cd08173 100644 --- a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs +++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs @@ -30,7 +30,7 @@ namespace MediaBrowser.Controller.Channels public List<ChannelMediaContentType> ContentTypes { get; set; } /// <summary> - /// Represents the maximum number of records the channel allows retrieving at a time. + /// Gets or sets the maximum number of records the channel allows retrieving at a time. /// </summary> public int? MaxPageSize { get; set; } @@ -41,7 +41,7 @@ namespace MediaBrowser.Controller.Channels public List<ChannelItemSortField> DefaultSortFields { get; set; } /// <summary> - /// Indicates if a sort ascending/descending toggle is supported or not. + /// Gets or sets a value indicating whether a sort ascending/descending toggle is supported or not. /// </summary> public bool SupportsSortOrderToggle { get; set; } diff --git a/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs b/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs new file mode 100644 index 000000000..82b3a4977 --- /dev/null +++ b/MediaBrowser.Controller/Collections/CollectionCreatedEventArgs.cs @@ -0,0 +1,24 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using MediaBrowser.Controller.Entities.Movies; + +namespace MediaBrowser.Controller.Collections +{ + public class CollectionCreatedEventArgs : EventArgs + { + /// <summary> + /// Gets or sets the collection. + /// </summary> + /// <value>The collection.</value> + public BoxSet Collection { get; set; } + + /// <summary> + /// Gets or sets the options. + /// </summary> + /// <value>The options.</value> + public CollectionCreationOptions Options { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs index 94e7541f8..30f5f4efa 100644 --- a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs +++ b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs @@ -25,8 +25,8 @@ namespace MediaBrowser.Controller.Collections public Dictionary<string, string> ProviderIds { get; set; } - public string[] ItemIdList { get; set; } + public IReadOnlyList<string> ItemIdList { get; set; } - public Guid[] UserIds { get; set; } + public IReadOnlyList<Guid> UserIds { get; set; } } } diff --git a/MediaBrowser.Controller/Collections/CollectionEvents.cs b/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs index 821318ffc..8155cf3db 100644 --- a/MediaBrowser.Controller/Collections/CollectionEvents.cs +++ b/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs @@ -9,23 +9,14 @@ using MediaBrowser.Controller.Entities.Movies; namespace MediaBrowser.Controller.Collections { - public class CollectionCreatedEventArgs : EventArgs - { - /// <summary> - /// Gets or sets the collection. - /// </summary> - /// <value>The collection.</value> - public BoxSet Collection { get; set; } - - /// <summary> - /// Gets or sets the options. - /// </summary> - /// <value>The options.</value> - public CollectionCreationOptions Options { get; set; } - } - public class CollectionModifiedEventArgs : EventArgs { + public CollectionModifiedEventArgs(BoxSet collection, IReadOnlyCollection<BaseItem> itemsChanged) + { + Collection = collection; + ItemsChanged = itemsChanged; + } + /// <summary> /// Gets or sets the collection. /// </summary> @@ -36,6 +27,6 @@ namespace MediaBrowser.Controller.Collections /// Gets or sets the items changed. /// </summary> /// <value>The items changed.</value> - public List<BaseItem> ItemsChanged { get; set; } + public IReadOnlyCollection<BaseItem> ItemsChanged { get; set; } } } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index ef17c8fb3..8096be1bd 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -20,7 +20,6 @@ namespace MediaBrowser.Controller.Devices /// </summary> /// <param name="reportedId">The reported identifier.</param> /// <param name="capabilities">The capabilities.</param> - /// <returns>Task.</returns> void SaveCapabilities(string reportedId, ClientCapabilities capabilities); /// <summary> @@ -47,6 +46,9 @@ namespace MediaBrowser.Controller.Devices /// <summary> /// Determines whether this instance [can access device] the specified user identifier. /// </summary> + /// <param name="user">The user to test.</param> + /// <param name="deviceId">The device id to test.</param> + /// <returns>Whether the user can access the device.</returns> bool CanAccessDevice(User user, string deviceId); void UpdateDeviceOptions(string deviceId, DeviceOptions options); diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 800f7a8bb..4e640d421 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -1,7 +1,4 @@ -#nullable disable - #pragma warning disable CS1591 -#nullable enable using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 9bfead8b3..c7f61a90b 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -1,7 +1,4 @@ -#nullable disable - #pragma warning disable CS1591 -#nullable enable using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs index f06bbe4d0..e9c88ffb5 100644 --- a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs @@ -1,5 +1,7 @@ #nullable disable +using System.Collections.Generic; + #pragma warning disable CS1591 namespace MediaBrowser.Controller.Drawing @@ -10,7 +12,7 @@ namespace MediaBrowser.Controller.Drawing /// Gets or sets the input paths. /// </summary> /// <value>The input paths.</value> - public string[] InputPaths { get; set; } + public IReadOnlyList<string> InputPaths { get; set; } /// <summary> /// Gets or sets the output path. diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs index 204175ed5..9ef92bc98 100644 --- a/MediaBrowser.Controller/Drawing/ImageHelper.cs +++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs @@ -1,7 +1,4 @@ -#nullable disable - #pragma warning disable CS1591 -#nullable enable using MediaBrowser.Model.Drawing; diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs index 591cc53d1..5ee781ffa 100644 --- a/MediaBrowser.Controller/Drawing/ImageStream.cs +++ b/MediaBrowser.Controller/Drawing/ImageStream.cs @@ -22,9 +22,15 @@ namespace MediaBrowser.Controller.Drawing public void Dispose() { - if (Stream != null) + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) { - Stream.Dispose(); + Stream?.Dispose(); } } } diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index 758e841a7..ecc833154 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -18,37 +18,17 @@ namespace MediaBrowser.Controller.Dto ItemFields.RefreshState }; - public IReadOnlyList<ItemFields> Fields { get; set; } - - public IReadOnlyList<ImageType> ImageTypes { get; set; } - - public int ImageTypeLimit { get; set; } - - public bool EnableImages { get; set; } - - public bool AddProgramRecordingInfo { get; set; } - - public bool EnableUserData { get; set; } + private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>(); - public bool AddCurrentProgram { get; set; } + private static readonly ItemFields[] AllItemFields = Enum.GetValues<ItemFields>() + .Except(DefaultExcludedFields) + .ToArray(); public DtoOptions() : this(true) { } - private static readonly ImageType[] AllImageTypes = Enum.GetNames(typeof(ImageType)) - .Select(i => (ImageType)Enum.Parse(typeof(ImageType), i, true)) - .ToArray(); - - private static readonly ItemFields[] AllItemFields = Enum.GetNames(typeof(ItemFields)) - .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) - .Except(DefaultExcludedFields) - .ToArray(); - - public bool ContainsField(ItemFields field) - => Fields.Contains(field); - public DtoOptions(bool allFields) { ImageTypeLimit = int.MaxValue; @@ -60,6 +40,23 @@ namespace MediaBrowser.Controller.Dto ImageTypes = AllImageTypes; } + public IReadOnlyList<ItemFields> Fields { get; set; } + + public IReadOnlyList<ImageType> ImageTypes { get; set; } + + public int ImageTypeLimit { get; set; } + + public bool EnableImages { get; set; } + + public bool AddProgramRecordingInfo { get; set; } + + public bool EnableUserData { get; set; } + + public bool AddCurrentProgram { get; set; } + + public bool ContainsField(ItemFields field) + => Fields.Contains(field); + public int GetImageLimit(ImageType type) { if (EnableImages && ImageTypes.Contains(type)) diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index 7f4bbead0..61d796235 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -36,11 +36,17 @@ namespace MediaBrowser.Controller.Dto /// <param name="options">The options.</param> /// <param name="user">The user.</param> /// <param name="owner">The owner.</param> + /// <returns>The <see cref="IReadOnlyList{T}"/> of <see cref="BaseItemDto"/>.</returns> IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null); /// <summary> /// Gets the item by name dto. /// </summary> + /// <param name="item">The item.</param> + /// <param name="options">The dto options.</param> + /// <param name="taggedItems">The list of tagged items.</param> + /// <param name="user">The user.</param> + /// <returns>The item dto.</returns> BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null); } } diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index f1944a7d3..fe1bc62ab 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -22,6 +22,8 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class AggregateFolder : Folder { + private bool _requiresRefresh; + public AggregateFolder() { PhysicalLocationsList = Array.Empty<string>(); @@ -85,7 +87,6 @@ namespace MediaBrowser.Controller.Entities } } - private bool _requiresRefresh; public override bool RequiresRefresh() { var changed = base.RequiresRefresh() || _requiresRefresh; @@ -105,11 +106,11 @@ namespace MediaBrowser.Controller.Entities return changed; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { ClearCache(); - var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh; + var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh; _requiresRefresh = false; return changed; } @@ -154,11 +155,11 @@ namespace MediaBrowser.Controller.Entities return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren); } - protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { ClearCache(); - await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken) .ConfigureAwait(false); ClearCache(); @@ -184,7 +185,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="id">The id.</param> /// <returns>BaseItem.</returns> - /// <exception cref="ArgumentNullException">id</exception> + /// <exception cref="ArgumentNullException">The id is empty.</exception> public BaseItem FindVirtualChild(Guid id) { if (id.Equals(Guid.Empty)) diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 4c2b7cb7c..576ab67a2 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; @@ -82,19 +83,19 @@ namespace MediaBrowser.Controller.Entities.Audio /// <returns>System.String.</returns> protected override string CreateSortName() { - return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "") - + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name; + return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name; } public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); - var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty; + var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : string.Empty; if (ParentIndexNumber.HasValue) { - songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey; + songKey = ParentIndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) + "-" + songKey; } songKey += Name; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 6101d3016..c0cd81110 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -94,7 +94,7 @@ namespace MediaBrowser.Controller.Entities.Audio return base.IsSaveLocalMetadataEnabled(); } - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (IsAccessedByName) { @@ -102,7 +102,7 @@ namespace MediaBrowser.Controller.Entities.Audio return Task.CompletedTask; } - return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); + return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken); } public override List<string> GetUserDataKeys() @@ -114,7 +114,7 @@ namespace MediaBrowser.Controller.Entities.Audio } /// <summary> - /// Returns the folder containing the item. + /// Gets the folder containing the item. /// If the item is a folder, it returns the folder itself. /// </summary> /// <value>The containing folder path.</value> @@ -208,9 +208,9 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (IsAccessedByName) { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index b07d47ffd..a682a2e58 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.Entities.Audio public override bool IsDisplayedAsFolder => true; /// <summary> - /// Returns the folder containing the item. + /// Gets the folder containing the item. /// If the item is a folder, it returns the folder itself. /// </summary> /// <value>The containing folder path.</value> @@ -106,9 +106,9 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index ca5213273..6137ddbf7 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -92,7 +92,8 @@ namespace MediaBrowser.Controller.Entities public const string ShortsFolderName = "shorts"; public const string FeaturettesFolderName = "featurettes"; - public static readonly string[] AllExtrasTypesFolderNames = { + public static readonly string[] AllExtrasTypesFolderNames = + { ExtrasFolderName, BehindTheScenesFolderName, DeletedScenesFolderName, @@ -177,7 +178,7 @@ namespace MediaBrowser.Controller.Entities public virtual bool AlwaysScanInternalMetadataPath => false; /// <summary> - /// Gets a value indicating whether this instance is in mixed folder. + /// Gets or sets a value indicating whether this instance is in mixed folder. /// </summary> /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value> [JsonIgnore] @@ -244,7 +245,7 @@ namespace MediaBrowser.Controller.Entities public ProgramAudio? Audio { get; set; } /// <summary> - /// Return the id that should be used to key display prefs for this item. + /// Gets the id that should be used to key display prefs for this item. /// Default is based on the type for everything except actual generic folders. /// </summary> /// <value>The display prefs id.</value> @@ -280,7 +281,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Returns the folder containing the item. + /// Gets the folder containing the item. /// If the item is a folder, it returns the folder itself. /// </summary> [JsonIgnore] @@ -305,8 +306,11 @@ namespace MediaBrowser.Controller.Entities public string ServiceName { get; set; } /// <summary> - /// If this content came from an external service, the id of the content on that service. + /// Gets or sets the external id. /// </summary> + /// <remarks> + /// If this content came from an external service, the id of the content on that service. + /// </remarks> [JsonIgnore] public string ExternalId { get; set; } @@ -330,7 +334,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Gets or sets the type of the location. + /// Gets the type of the location. /// </summary> /// <value>The type of the location.</value> [JsonIgnore] @@ -339,9 +343,9 @@ namespace MediaBrowser.Controller.Entities get { // if (IsOffline) - //{ + // { // return LocationType.Offline; - //} + // } var path = Path; if (string.IsNullOrEmpty(path)) @@ -449,8 +453,11 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// This is just a helper for convenience. + /// Gets the primary image path. /// </summary> + /// <remarks> + /// This is just a helper for convenience. + /// </remarks> /// <value>The primary image path.</value> [JsonIgnore] public string PrimaryImagePath => this.GetImagePath(ImageType.Primary); @@ -541,7 +548,7 @@ namespace MediaBrowser.Controller.Entities public DateTime DateLastRefreshed { get; set; } /// <summary> - /// The logger. + /// Gets or sets the logger. /// </summary> public static ILogger<BaseItem> Logger { get; set; } @@ -613,7 +620,11 @@ namespace MediaBrowser.Controller.Entities public string ForcedSortName { get => _forcedSortName; - set { _forcedSortName = value; _sortName = null; } + set + { + _forcedSortName = value; + _sortName = null; + } } private string _sortName; @@ -621,7 +632,7 @@ namespace MediaBrowser.Controller.Entities private Guid[] _themeVideoIds; /// <summary> - /// Gets the name of the sort. + /// Gets or sets the name of the sort. /// </summary> /// <value>The name of the sort.</value> [JsonIgnore] @@ -659,14 +670,12 @@ namespace MediaBrowser.Controller.Entities { if (SourceType == SourceType.Channel) { - return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture)); + return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture)); } ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture); - basePath = System.IO.Path.Combine(basePath, "library"); - - return System.IO.Path.Join(basePath, idString.Slice(0, 2), idString); + return System.IO.Path.Join(basePath, "library", idString.Slice(0, 2), idString); } /// <summary> @@ -848,7 +857,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// When the item first debuted. For movies this could be premiere date, episodes would be first aired + /// Gets or sets the date that the item first debuted. For movies this could be premiere date, episodes would be first aired. /// </summary> /// <value>The premiere date.</value> [JsonIgnore] @@ -945,7 +954,7 @@ namespace MediaBrowser.Controller.Entities public int? ProductionYear { get; set; } /// <summary> - /// If the item is part of a series, this is it's number in the series. + /// Gets or sets the index number. If the item is part of a series, this is it's number in the series. /// This could be episode number, album track number, etc. /// </summary> /// <value>The index number.</value> @@ -953,7 +962,7 @@ namespace MediaBrowser.Controller.Entities public int? IndexNumber { get; set; } /// <summary> - /// For an episode this could be the season number, or for a song this could be the disc number. + /// Gets or sets the parent index number. For an episode this could be the season number, or for a song this could be the disc number. /// </summary> /// <value>The parent index number.</value> [JsonIgnore] @@ -1017,9 +1026,9 @@ namespace MediaBrowser.Controller.Entities } // if (!user.IsParentalScheduleAllowed()) - //{ + // { // return PlayAccess.None; - //} + // } return PlayAccess.Full; } @@ -1251,7 +1260,7 @@ namespace MediaBrowser.Controller.Entities // Support plex/xbmc convention files.AddRange(fileSystemChildren - .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase))); + .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFilename, StringComparison.OrdinalIgnoreCase))); return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) .OfType<Audio.Audio>() @@ -1312,14 +1321,16 @@ namespace MediaBrowser.Controller.Entities { var extras = new List<Video>(); - var folders = fileSystemChildren.Where(i => i.IsDirectory).ToArray(); + var libraryOptions = new LibraryOptions(); + var folders = fileSystemChildren.Where(i => i.IsDirectory).ToList(); foreach (var extraFolderName in AllExtrasTypesFolderNames) { var files = folders .Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => FileSystem.GetFiles(i.FullName)); - extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) + // Re-using the same instance of LibraryOptions since it looks like it's never being altered. + extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, libraryOptions) .OfType<Video>() .Select(item => { @@ -1330,7 +1341,7 @@ namespace MediaBrowser.Controller.Entities } // Use some hackery to get the extra type based on foldername - item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty), true, out ExtraType extraType) + item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty, StringComparison.Ordinal), true, out ExtraType extraType) ? extraType : Model.Entities.ExtraType.Unknown; @@ -1420,10 +1431,10 @@ namespace MediaBrowser.Controller.Entities /// Refreshes owned items such as trailers, theme videos, special features, etc. /// Returns true or false indicating if changes were found. /// </summary> - /// <param name="options"></param> - /// <param name="fileSystemChildren"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> + /// <param name="options">The metadata refresh options.</param> + /// <param name="fileSystemChildren">The list of filesystem children.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns> protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var themeSongsChanged = false; @@ -1765,7 +1776,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="user">The user.</param> /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns> - /// <exception cref="ArgumentNullException">user</exception> + /// <exception cref="ArgumentNullException">If user is null.</exception> public bool IsParentalAllowed(User user) { if (user == null) @@ -1910,7 +1921,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="user">The user.</param> /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns> - /// <exception cref="ArgumentNullException">user</exception> + /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception> public virtual bool IsVisible(User user) { if (user == null) @@ -2208,7 +2219,7 @@ namespace MediaBrowser.Controller.Entities /// <param name="type">The type.</param> /// <param name="imageIndex">Index of the image.</param> /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns> - /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops</exception> + /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception> public bool HasImage(ImageType type, int imageIndex) { return GetImageInfo(type, imageIndex) != null; @@ -2316,7 +2327,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => i.IsLocalFile) .Select(i => System.IO.Path.GetDirectoryName(i.Path)) .Distinct(StringComparer.OrdinalIgnoreCase) - .SelectMany(directoryService.GetFilePaths) + .SelectMany(path => directoryService.GetFilePaths(path)) .ToList(); var deletedImages = ImageInfos @@ -2337,9 +2348,8 @@ namespace MediaBrowser.Controller.Entities /// <param name="imageType">Type of the image.</param> /// <param name="imageIndex">Index of the image.</param> /// <returns>System.String.</returns> - /// <exception cref="InvalidOperationException"> - /// </exception> - /// <exception cref="ArgumentNullException">item</exception> + /// <exception cref="InvalidOperationException"> </exception> + /// <exception cref="ArgumentNullException">Item is null.</exception> public string GetImagePath(ImageType imageType, int imageIndex) => GetImageInfo(imageType, imageIndex)?.Path; @@ -2426,7 +2436,15 @@ namespace MediaBrowser.Controller.Entities throw new ArgumentException("No image info for chapter images"); } - return ImageInfos.Where(i => i.Type == imageType); + // Yield return is more performant than LINQ Where on an Array + for (var i = 0; i < ImageInfos.Length; i++) + { + var imageInfo = ImageInfos[i]; + if (imageInfo.Type == imageType) + { + yield return imageInfo; + } + } } /// <summary> @@ -2435,7 +2453,7 @@ namespace MediaBrowser.Controller.Entities /// <param name="imageType">Type of the image.</param> /// <param name="images">The images.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - /// <exception cref="ArgumentException">Cannot call AddImages with chapter images</exception> + /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception> public bool AddImages(ImageType imageType, List<FileSystemMetadata> images) { if (imageType == ImageType.Chapter) @@ -2458,7 +2476,7 @@ namespace MediaBrowser.Controller.Entities } var existing = existingImages - .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); + .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase)); if (existing == null) { @@ -2489,8 +2507,7 @@ namespace MediaBrowser.Controller.Entities var newImagePaths = images.Select(i => i.FullName).ToList(); var deleted = existingImages - .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path)) - .ToList(); + .FindAll(i => i.IsLocalFile && !newImagePaths.Contains(i.Path.AsSpan(), StringComparison.OrdinalIgnoreCase) && !File.Exists(i.Path)); if (deleted.Count > 0) { @@ -2519,10 +2536,11 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Gets the file system path to delete when the item is to be deleted. /// </summary> - /// <returns></returns> + /// <returns>The metadata for the deleted paths.</returns> public virtual IEnumerable<FileSystemMetadata> GetDeletePaths() { - return new[] { + return new[] + { new FileSystemMetadata { FullName = Path, @@ -2629,6 +2647,7 @@ namespace MediaBrowser.Controller.Entities MetadataCountryCode = GetPreferredMetadataCountryCode(), MetadataLanguage = GetPreferredMetadataLanguage(), Name = GetNameForMetadataLookup(), + OriginalTitle = OriginalTitle, ProviderIds = ProviderIds, IndexNumber = IndexNumber, ParentIndexNumber = ParentIndexNumber, @@ -2645,7 +2664,9 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// This is called before any metadata refresh and returns true if changes were made. /// </summary> - public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Whether to replace all metadata.</param> + /// <returns>true if the item has change, else false.</returns> + public virtual bool BeforeMetadataRefresh(bool replaceAllMetadata) { _sortName = null; @@ -2769,11 +2790,11 @@ namespace MediaBrowser.Controller.Entities // var parentId = Id; // if (!video.IsOwnedItem || video.ParentId != parentId) - //{ + // { // video.IsOwnedItem = true; // video.ParentId = parentId; // newOptions.ForceSave = true; - //} + // } if (video == null) { @@ -2880,7 +2901,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Updates the official rating based on content and returns true or false indicating if it changed. /// </summary> - /// <returns></returns> + /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns> public bool UpdateRatingToItems(IList<BaseItem> children) { var currentOfficialRating = OfficialRating; @@ -2896,7 +2917,9 @@ namespace MediaBrowser.Controller.Entities OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating; - return !string.Equals(currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty, + return !string.Equals( + currentOfficialRating ?? string.Empty, + OfficialRating ?? string.Empty, StringComparison.OrdinalIgnoreCase); } @@ -2993,7 +3016,7 @@ namespace MediaBrowser.Controller.Entities } /// <inheritdoc /> - public bool Equals(BaseItem item) => Object.Equals(Id, item?.Id); + public bool Equals(BaseItem other) => object.Equals(Id, other?.Id); /// <inheritdoc /> public override int GetHashCode() => HashCode.Combine(Id); diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index c39b18891..89ad392a4 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -1,6 +1,3 @@ -#nullable disable - -#nullable enable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 3d0370248..d75beb06d 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -12,6 +12,11 @@ namespace MediaBrowser.Controller.Entities { public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries { + public Book() + { + this.RunTimeTicks = TimeSpan.TicksPerSecond; + } + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Book; @@ -28,11 +33,6 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public Guid SeriesId { get; set; } - public Book() - { - this.RunTimeTicks = TimeSpan.TicksPerSecond; - } - public string FindSeriesSortName() { return SeriesName; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index a86da29ce..4a721ca44 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -29,30 +29,45 @@ namespace MediaBrowser.Controller.Entities public class CollectionFolder : Folder, ICollectionFolder { private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - public static IXmlSerializer XmlSerializer { get; set; } - - public static IServerApplicationHost ApplicationHost { get; set; } + private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>(); + private bool _requiresRefresh; + /// <summary> + /// Initializes a new instance of the <see cref="CollectionFolder"/> class. + /// </summary> public CollectionFolder() { PhysicalLocationsList = Array.Empty<string>(); PhysicalFolderIds = Array.Empty<Guid>(); } + public static IXmlSerializer XmlSerializer { get; set; } + + public static IServerApplicationHost ApplicationHost { get; set; } + [JsonIgnore] public override bool SupportsPlayedStatus => false; [JsonIgnore] public override bool SupportsInheritedParentImages => false; + public string CollectionType { get; set; } + + /// <summary> + /// Gets the item's children. + /// </summary> + /// <remarks> + /// Our children are actually just references to the ones in the physical root... + /// </remarks> + /// <value>The actual children.</value> + [JsonIgnore] + public override IEnumerable<BaseItem> Children => GetActualChildren(); + public override bool CanDelete() { return false; } - public string CollectionType { get; set; } - - private static readonly Dictionary<string, LibraryOptions> LibraryOptions = new Dictionary<string, LibraryOptions>(); public LibraryOptions GetLibraryOptions() { return GetLibraryOptions(Path); @@ -106,12 +121,12 @@ namespace MediaBrowser.Controller.Entities public static LibraryOptions GetLibraryOptions(string path) { - lock (LibraryOptions) + lock (_libraryOptions) { - if (!LibraryOptions.TryGetValue(path, out var options)) + if (!_libraryOptions.TryGetValue(path, out var options)) { options = LoadLibraryOptions(path); - LibraryOptions[path] = options; + _libraryOptions[path] = options; } return options; @@ -120,9 +135,9 @@ namespace MediaBrowser.Controller.Entities public static void SaveLibraryOptions(string path, LibraryOptions options) { - lock (LibraryOptions) + lock (_libraryOptions) { - LibraryOptions[path] = options; + _libraryOptions[path] = options; var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions); foreach (var mediaPath in clone.PathInfos) @@ -139,15 +154,18 @@ namespace MediaBrowser.Controller.Entities public static void OnCollectionFolderChange() { - lock (LibraryOptions) + lock (_libraryOptions) { - LibraryOptions.Clear(); + _libraryOptions.Clear(); } } /// <summary> - /// Allow different display preferences for each collection folder. + /// Gets the display preferences id. /// </summary> + /// <remarks> + /// Allow different display preferences for each collection folder. + /// </remarks> /// <value>The display prefs id.</value> [JsonIgnore] public override Guid DisplayPreferencesId => Id; @@ -155,21 +173,20 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public override string[] PhysicalLocations => PhysicalLocationsList; + public string[] PhysicalLocationsList { get; set; } + + public Guid[] PhysicalFolderIds { get; set; } + public override bool IsSaveLocalMetadataEnabled() { return true; } - public string[] PhysicalLocationsList { get; set; } - - public Guid[] PhysicalFolderIds { get; set; } - protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService) { return CreateResolveArgs(directoryService, true).FileSystemChildren; } - private bool _requiresRefresh; public override bool RequiresRefresh() { var changed = base.RequiresRefresh() || _requiresRefresh; @@ -201,9 +218,9 @@ namespace MediaBrowser.Controller.Entities return changed; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh; + var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh; _requiresRefresh = false; return changed; } @@ -298,27 +315,20 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes - /// ***Currently does not contain logic to maintain items that are unavailable in the file system*** + /// ***Currently does not contain logic to maintain items that are unavailable in the file system***. /// </summary> /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param> /// <param name="refreshOptions">The refresh options.</param> /// <param name="directoryService">The directory service.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { return Task.CompletedTask; } - /// <summary> - /// Our children are actually just references to the ones in the physical root... - /// </summary> - /// <value>The actual children.</value> - [JsonIgnore] - public override IEnumerable<BaseItem> Children => GetActualChildren(); - public IEnumerable<BaseItem> GetActualChildren() { return GetPhysicalFolders(true).SelectMany(c => c.Children); diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index a59f5c6e4..541747422 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -37,6 +37,11 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class Folder : BaseItem { + public Folder() + { + LinkedChildren = Array.Empty<LinkedChild>(); + } + public static IUserViewManager UserViewManager { get; set; } /// <summary> @@ -50,11 +55,6 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public DateTime? DateLastMediaAdded { get; set; } - public Folder() - { - LinkedChildren = Array.Empty<LinkedChild>(); - } - [JsonIgnore] public override bool SupportsThemeMedia => true; @@ -86,6 +86,85 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public virtual bool SupportsDateLastMediaAdded => false; + [JsonIgnore] + public override string FileNameWithoutExtension + { + get + { + if (IsFileProtocol) + { + return System.IO.Path.GetFileName(Path); + } + + return null; + } + } + + /// <summary> + /// Gets the actual children. + /// </summary> + /// <value>The actual children.</value> + [JsonIgnore] + public virtual IEnumerable<BaseItem> Children => LoadChildren(); + + /// <summary> + /// Gets thread-safe access to all recursive children of this folder - without regard to user. + /// </summary> + /// <value>The recursive children.</value> + [JsonIgnore] + public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren(); + + [JsonIgnore] + protected virtual bool SupportsShortcutChildren => false; + + protected virtual bool FilterLinkedChildrenPerUser => false; + + [JsonIgnore] + protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren; + + [JsonIgnore] + public virtual bool SupportsUserDataFromChildren + { + get + { + // These are just far too slow. + if (this is ICollectionFolder) + { + return false; + } + + if (this is UserView) + { + return false; + } + + if (this is UserRootFolder) + { + return false; + } + + if (this is Channel) + { + return false; + } + + if (SourceType != SourceType.Library) + { + return false; + } + + if (this is IItemByName) + { + if (this is not IHasDualAccess hasDualAccess || hasDualAccess.IsAccessedByName) + { + return false; + } + } + + return true; + } + } + public override bool CanDelete() { if (IsRoot) @@ -108,20 +187,6 @@ namespace MediaBrowser.Controller.Entities return baseResult; } - [JsonIgnore] - public override string FileNameWithoutExtension - { - get - { - if (IsFileProtocol) - { - return System.IO.Path.GetFileName(Path); - } - - return null; - } - } - protected override bool IsAllowTagFilterEnforced() { if (this is ICollectionFolder) @@ -137,16 +202,12 @@ namespace MediaBrowser.Controller.Entities return true; } - [JsonIgnore] - protected virtual bool SupportsShortcutChildren => false; - /// <summary> /// Adds the child. /// </summary> /// <param name="item">The item.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="InvalidOperationException">Unable to add + item.Name</exception> + /// <exception cref="InvalidOperationException">Unable to add + item.Name.</exception> public void AddChild(BaseItem item, CancellationToken cancellationToken) { item.SetParent(this); @@ -169,20 +230,6 @@ namespace MediaBrowser.Controller.Entities LibraryManager.CreateItem(item, this); } - /// <summary> - /// Gets the actual children. - /// </summary> - /// <value>The actual children.</value> - [JsonIgnore] - public virtual IEnumerable<BaseItem> Children => LoadChildren(); - - /// <summary> - /// thread-safe access to all recursive children of this folder - without regard to user. - /// </summary> - /// <value>The recursive children.</value> - [JsonIgnore] - public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren(); - public override bool IsVisible(User user) { if (this is ICollectionFolder && !(this is BasePluginFolder)) @@ -226,20 +273,20 @@ namespace MediaBrowser.Controller.Entities public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken) { - return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(FileSystem))); + return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken: cancellationToken); } /// <summary> /// Validates that the children of the folder still exist. /// </summary> /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="metadataRefreshOptions">The metadata refresh options.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true) + public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, CancellationToken cancellationToken = default) { - return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService); + return ValidateChildrenInternal(progress, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken); } private Dictionary<Guid, BaseItem> GetActualChildrenDictionary() @@ -279,13 +326,13 @@ namespace MediaBrowser.Controller.Entities /// Validates the children internal. /// </summary> /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param> /// <param name="refreshOptions">The refresh options.</param> /// <param name="directoryService">The directory service.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (recursive) { @@ -294,7 +341,7 @@ namespace MediaBrowser.Controller.Entities try { - await ValidateChildrenInternal2(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false); + await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false); } finally { @@ -305,7 +352,7 @@ namespace MediaBrowser.Controller.Entities } } - private async Task ValidateChildrenInternal2(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -527,7 +574,7 @@ namespace MediaBrowser.Controller.Entities private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) { return RunTasks( - (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService), + (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, null, directoryService, cancellationToken), children, progress, cancellationToken); @@ -940,7 +987,13 @@ namespace MediaBrowser.Controller.Entities } else { - items = GetChildren(user, true).Where(filter); + // need to pass this param to the children. + var childQuery = new InternalItemsQuery + { + DisplayAlbumFolders = query.DisplayAlbumFolders + }; + + items = GetChildren(user, true, childQuery).Where(filter); } return PostFilterAndSort(items, query, true); @@ -965,7 +1018,7 @@ namespace MediaBrowser.Controller.Entities if (!string.IsNullOrEmpty(query.NameStartsWith)) { - items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.CurrentCultureIgnoreCase)); } if (!string.IsNullOrEmpty(query.NameLessThan)) @@ -1276,10 +1329,23 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Adds the children to list. /// </summary> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query) { - foreach (var child in GetEligibleChildrenForRecursiveChildren(user)) + // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums. + IEnumerable<BaseItem> children = null; + if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum)) + { + children = Children; + query = null; + } + + // If there are not sub-folders, proceed as normal. + if (children == null) + { + children = GetEligibleChildrenForRecursiveChildren(user); + } + + foreach (var child in children) { bool? isVisibleToUser = null; @@ -1428,8 +1494,6 @@ namespace MediaBrowser.Controller.Entities return list; } - protected virtual bool FilterLinkedChildrenPerUser => false; - public bool ContainsLinkedChildByItemId(Guid itemId) { var linkedChildren = LinkedChildren; @@ -1530,9 +1594,6 @@ namespace MediaBrowser.Controller.Entities .Where(i => i.Item2 != null); } - [JsonIgnore] - protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren; - protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) { var changesFound = false; @@ -1553,7 +1614,8 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Refreshes the linked children. /// </summary> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <param name="fileSystemChildren">The enumerable of file system metadata.</param> + /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns> protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren) { if (SupportsShortcutChildren) @@ -1696,51 +1758,6 @@ namespace MediaBrowser.Controller.Entities return !IsPlayed(user); } - [JsonIgnore] - public virtual bool SupportsUserDataFromChildren - { - get - { - // These are just far too slow. - if (this is ICollectionFolder) - { - return false; - } - - if (this is UserView) - { - return false; - } - - if (this is UserRootFolder) - { - return false; - } - - if (this is Channel) - { - return false; - } - - if (SourceType != SourceType.Library) - { - return false; - } - - var iItemByName = this as IItemByName; - if (iItemByName != null) - { - var hasDualAccess = this as IHasDualAccess; - if (hasDualAccess == null || hasDualAccess.IsAccessedByName) - { - return false; - } - } - - return true; - } - } - public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) { if (!SupportsUserDataFromChildren) diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 7987f38a0..b80a5be3b 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -16,6 +16,23 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class Genre : BaseItem, IItemByName { + /// <summary> + /// Gets the folder containing the item. + /// If the item is a folder, it returns the folder itself. + /// </summary> + /// <value>The containing folder path.</value> + [JsonIgnore] + public override string ContainingFolderPath => Path; + + [JsonIgnore] + public override bool IsDisplayedAsFolder => true; + + [JsonIgnore] + public override bool SupportsAncestors => false; + + [JsonIgnore] + public override bool SupportsPeople => false; + public override List<string> GetUserDataKeys() { var list = base.GetUserDataKeys(); @@ -34,20 +51,6 @@ namespace MediaBrowser.Controller.Entities return 1; } - /// <summary> - /// Returns the folder containing the item. - /// If the item is a folder, it returns the folder itself. - /// </summary> - /// <value>The containing folder path.</value> - [JsonIgnore] - public override string ContainingFolderPath => Path; - - [JsonIgnore] - public override bool IsDisplayedAsFolder => true; - - [JsonIgnore] - public override bool SupportsAncestors => false; - public override bool IsSaveLocalMetadataEnabled() { return true; @@ -72,9 +75,6 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.GetItemList(query); } - [JsonIgnore] - public override bool SupportsPeople => false; - public static string GetPath(string name) { return GetPath(name, true); @@ -108,11 +108,13 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made. + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Whether to replace all metadata.</param> + /// <returns>true if the item has change, else false.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs index b027a0cb1..ae01c223e 100644 --- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs +++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Entities { /// <summary> - /// Interface IHasScreenshots. + /// The item has screenshots. /// </summary> public interface IHasScreenshots { diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs index 64d769d5b..5f774bbde 100644 --- a/MediaBrowser.Controller/Entities/IHasSeries.cs +++ b/MediaBrowser.Controller/Entities/IHasSeries.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Entities public interface IHasSeries { /// <summary> - /// Gets the name of the series. + /// Gets or sets the name of the series. /// </summary> /// <value>The name of the series.</value> string SeriesName { get; set; } diff --git a/MediaBrowser.Controller/Entities/IHasShares.cs b/MediaBrowser.Controller/Entities/IHasShares.cs new file mode 100644 index 000000000..bdde744a3 --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHasShares.cs @@ -0,0 +1,11 @@ +#nullable disable + +#pragma warning disable CS1591 + +namespace MediaBrowser.Controller.Entities +{ + public interface IHasShares + { + Share[] Shares { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index c06021029..ebaf5506d 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -20,9 +18,9 @@ namespace MediaBrowser.Controller.Entities public int? Limit { get; set; } - public User User { get; set; } + public User? User { get; set; } - public BaseItem SimilarTo { get; set; } + public BaseItem? SimilarTo { get; set; } public bool? IsFolder { get; set; } @@ -58,23 +56,23 @@ namespace MediaBrowser.Controller.Entities public bool? CollapseBoxSetItems { get; set; } - public string NameStartsWithOrGreater { get; set; } + public string? NameStartsWithOrGreater { get; set; } - public string NameStartsWith { get; set; } + public string? NameStartsWith { get; set; } - public string NameLessThan { get; set; } + public string? NameLessThan { get; set; } - public string NameContains { get; set; } + public string? NameContains { get; set; } - public string MinSortName { get; set; } + public string? MinSortName { get; set; } - public string PresentationUniqueKey { get; set; } + public string? PresentationUniqueKey { get; set; } - public string Path { get; set; } + public string? Path { get; set; } - public string Name { get; set; } + public string? Name { get; set; } - public string Person { get; set; } + public string? Person { get; set; } public Guid[] PersonIds { get; set; } @@ -82,7 +80,7 @@ namespace MediaBrowser.Controller.Entities public Guid[] ExcludeItemIds { get; set; } - public string AdjacentTo { get; set; } + public string? AdjacentTo { get; set; } public string[] PersonTypes { get; set; } @@ -182,13 +180,13 @@ namespace MediaBrowser.Controller.Entities public Guid ParentId { get; set; } - public string ParentType { get; set; } + public string? ParentType { get; set; } public Guid[] AncestorIds { get; set; } public Guid[] TopParentIds { get; set; } - public BaseItem Parent + public BaseItem? Parent { set { @@ -213,9 +211,9 @@ namespace MediaBrowser.Controller.Entities public SeriesStatus[] SeriesStatuses { get; set; } - public string ExternalSeriesId { get; set; } + public string? ExternalSeriesId { get; set; } - public string ExternalId { get; set; } + public string? ExternalId { get; set; } public Guid[] AlbumIds { get; set; } @@ -223,9 +221,9 @@ namespace MediaBrowser.Controller.Entities public Guid[] ExcludeArtistIds { get; set; } - public string AncestorWithPresentationUniqueKey { get; set; } + public string? AncestorWithPresentationUniqueKey { get; set; } - public string SeriesPresentationUniqueKey { get; set; } + public string? SeriesPresentationUniqueKey { get; set; } public bool GroupByPresentationUniqueKey { get; set; } @@ -235,7 +233,7 @@ namespace MediaBrowser.Controller.Entities public bool ForceDirect { get; set; } - public Dictionary<string, string> ExcludeProviderIds { get; set; } + public Dictionary<string, string>? ExcludeProviderIds { get; set; } public bool EnableGroupByMetadataKey { get; set; } @@ -253,13 +251,13 @@ namespace MediaBrowser.Controller.Entities public int MinSimilarityScore { get; set; } - public string HasNoAudioTrackWithLanguage { get; set; } + public string? HasNoAudioTrackWithLanguage { get; set; } - public string HasNoInternalSubtitleTrackWithLanguage { get; set; } + public string? HasNoInternalSubtitleTrackWithLanguage { get; set; } - public string HasNoExternalSubtitleTrackWithLanguage { get; set; } + public string? HasNoExternalSubtitleTrackWithLanguage { get; set; } - public string HasNoSubtitleTrackWithLanguage { get; set; } + public string? HasNoSubtitleTrackWithLanguage { get; set; } public bool? IsDeadArtist { get; set; } @@ -267,6 +265,11 @@ namespace MediaBrowser.Controller.Entities public bool? IsDeadPerson { get; set; } + /// <summary> + /// Gets or sets a value indicating whether album sub-folders should be returned if they exist. + /// </summary> + public bool? DisplayAlbumFolders { get; set; } + public InternalItemsQuery() { AlbumArtistIds = Array.Empty<Guid>(); @@ -283,12 +286,10 @@ namespace MediaBrowser.Controller.Entities ExcludeInheritedTags = Array.Empty<string>(); ExcludeItemIds = Array.Empty<Guid>(); ExcludeItemTypes = Array.Empty<string>(); - ExcludeProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); ExcludeTags = Array.Empty<string>(); GenreIds = Array.Empty<Guid>(); Genres = Array.Empty<string>(); GroupByPresentationUniqueKey = true; - HasAnyProviderId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); ImageTypes = Array.Empty<ImageType>(); IncludeItemTypes = Array.Empty<string>(); ItemIds = Array.Empty<Guid>(); @@ -309,32 +310,33 @@ namespace MediaBrowser.Controller.Entities Years = Array.Empty<int>(); } - public InternalItemsQuery(User user) + public InternalItemsQuery(User? user) : this() { - SetUser(user); + if (user != null) + { + SetUser(user); + } } public void SetUser(User user) { - if (user != null) - { - MaxParentalRating = user.MaxParentalAgeRating; + MaxParentalRating = user.MaxParentalAgeRating; - if (MaxParentalRating.HasValue) - { - BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems) - .Where(i => i != UnratedItem.Other.ToString()) - .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray(); - } + if (MaxParentalRating.HasValue) + { + string other = UnratedItem.Other.ToString(); + BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems) + .Where(i => i != other) + .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray(); + } - ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); + ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); - User = user; - } + User = user; } - public Dictionary<string, string> HasAnyProviderId { get; set; } + public Dictionary<string, string>? HasAnyProviderId { get; set; } public Guid[] AlbumArtistIds { get; set; } @@ -356,8 +358,8 @@ namespace MediaBrowser.Controller.Entities public int? MinWidth { get; set; } - public string SearchTerm { get; set; } + public string? SearchTerm { get; set; } - public string SeriesTimerId { get; set; } + public string? SeriesTimerId { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index b2d6a4609..3e1d89274 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -3,12 +3,24 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; using Jellyfin.Data.Entities; namespace MediaBrowser.Controller.Entities { public class InternalPeopleQuery { + public InternalPeopleQuery() + : this(Array.Empty<string>(), Array.Empty<string>()) + { + } + + public InternalPeopleQuery(IReadOnlyList<string> personTypes, IReadOnlyList<string> excludePersonTypes) + { + PersonTypes = personTypes; + ExcludePersonTypes = excludePersonTypes; + } + /// <summary> /// Gets or sets the maximum number of items the query should return. /// </summary> @@ -16,9 +28,9 @@ namespace MediaBrowser.Controller.Entities public Guid ItemId { get; set; } - public string[] PersonTypes { get; set; } + public IReadOnlyList<string> PersonTypes { get; } - public string[] ExcludePersonTypes { get; set; } + public IReadOnlyList<string> ExcludePersonTypes { get; } public int? MaxListOrder { get; set; } @@ -29,11 +41,5 @@ namespace MediaBrowser.Controller.Entities public User User { get; set; } public bool? IsFavorite { get; set; } - - public InternalPeopleQuery() - { - PersonTypes = Array.Empty<string>(); - ExcludePersonTypes = Array.Empty<string>(); - } } } diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index 01c0a9339..fd5fef3dc 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -3,15 +3,18 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; using System.Globalization; using System.Text.Json.Serialization; -using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.Entities { public class LinkedChild { + public LinkedChild() + { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + } + public string Path { get; set; } public LinkedChildType Type { get; set; } @@ -22,7 +25,7 @@ namespace MediaBrowser.Controller.Entities public string Id { get; set; } /// <summary> - /// Serves as a cache. + /// Gets or sets the linked item id. /// </summary> public Guid? ItemId { get; set; } @@ -41,41 +44,5 @@ namespace MediaBrowser.Controller.Entities return child; } - - public LinkedChild() - { - Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - } - } - - public enum LinkedChildType - { - Manual = 0, - Shortcut = 1 - } - - public class LinkedChildComparer : IEqualityComparer<LinkedChild> - { - private readonly IFileSystem _fileSystem; - - public LinkedChildComparer(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - - public bool Equals(LinkedChild x, LinkedChild y) - { - if (x.Type == y.Type) - { - return _fileSystem.AreEqual(x.Path, y.Path); - } - - return false; - } - - public int GetHashCode(LinkedChild obj) - { - return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(); - } } } diff --git a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs new file mode 100644 index 000000000..4e58e2942 --- /dev/null +++ b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs @@ -0,0 +1,35 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Entities +{ + public class LinkedChildComparer : IEqualityComparer<LinkedChild> + { + private readonly IFileSystem _fileSystem; + + public LinkedChildComparer(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public bool Equals(LinkedChild x, LinkedChild y) + { + if (x.Type == y.Type) + { + return _fileSystem.AreEqual(x.Path, y.Path); + } + + return false; + } + + public int GetHashCode(LinkedChild obj) + { + return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(StringComparison.Ordinal); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/LinkedChildType.cs b/MediaBrowser.Controller/Entities/LinkedChildType.cs new file mode 100644 index 000000000..9ddb7b620 --- /dev/null +++ b/MediaBrowser.Controller/Entities/LinkedChildType.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Controller.Entities +{ + /// <summary> + /// The linked child type. + /// </summary> + public enum LinkedChildType + { + /// <summary> + /// Manually linked child. + /// </summary> + Manual = 0, + + /// <summary> + /// Shortcut linked child. + /// </summary> + Shortcut = 1 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 64d60c2e9..b54bbf5eb 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -144,9 +144,9 @@ namespace MediaBrowser.Controller.Entities.Movies } /// <inheritdoc /> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!ProductionYear.HasValue) { diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index f42e7723c..237ad5198 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -13,15 +13,15 @@ namespace MediaBrowser.Controller.Entities { public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasLookupInfo<MusicVideoInfo> { - /// <inheritdoc /> - [JsonIgnore] - public IReadOnlyList<string> Artists { get; set; } - public MusicVideo() { Artists = Array.Empty<string>(); } + /// <inheritdoc /> + [JsonIgnore] + public IReadOnlyList<string> Artists { get; set; } + public override UnratedItem GetBlockUnratedType() { return UnratedItem.Music; @@ -36,9 +36,9 @@ namespace MediaBrowser.Controller.Entities return info; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!ProductionYear.HasValue) { diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index d9ff55362..913f76d3b 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Returns the folder containing the item. + /// Gets the folder containing the item. /// If the item is a folder, it returns the folder itself. /// </summary> /// <value>The containing folder path.</value> @@ -67,6 +67,9 @@ namespace MediaBrowser.Controller.Entities return true; } + /// <summary> + /// Gets a value indicating whether to enable alpha numeric sorting. + /// </summary> [JsonIgnore] public override bool EnableAlphaNumericSorting => false; @@ -126,9 +129,9 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs index 7e4ec1830..64f446eef 100644 --- a/MediaBrowser.Controller/Entities/Share.cs +++ b/MediaBrowser.Controller/Entities/Share.cs @@ -4,11 +4,6 @@ namespace MediaBrowser.Controller.Entities { - public interface IHasShares - { - Share[] Shares { get; set; } - } - public class Share { public string UserId { get; set; } diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index ae1d10447..6fd0a6c6c 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -29,7 +29,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Returns the folder containing the item. + /// Gets the folder containing the item. /// If the item is a folder, it returns the folder itself. /// </summary> /// <value>The containing folder path.</value> @@ -105,9 +105,9 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 2724bd9b3..31c179bca 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Entities.TV public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> - /// Gets the season in which it aired. + /// Gets or sets the season in which it aired. /// </summary> /// <value>The aired season.</value> public int? AirsBeforeSeasonNumber { get; set; } @@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.Entities.TV public int? AirsBeforeEpisodeNumber { get; set; } /// <summary> - /// This is the ending episode number for double episodes. + /// Gets or sets the ending episode number for double episodes. /// </summary> /// <value>The index number.</value> public int? IndexNumberEnd { get; set; } @@ -116,7 +116,7 @@ namespace MediaBrowser.Controller.Entities.TV } /// <summary> - /// This Episode's Series Instance. + /// Gets the Episode's Series Instance. /// </summary> /// <value>The series.</value> [JsonIgnore] @@ -218,8 +218,8 @@ namespace MediaBrowser.Controller.Entities.TV /// <returns>System.String.</returns> protected override string CreateSortName() { - return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ") : "") - + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name; + return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ", CultureInfo.InvariantCulture) : string.Empty) + + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name; } /// <summary> @@ -261,6 +261,7 @@ namespace MediaBrowser.Controller.Entities.TV [JsonIgnore] public Guid SeasonId { get; set; } + [JsonIgnore] public Guid SeriesId { get; set; } @@ -286,7 +287,8 @@ namespace MediaBrowser.Controller.Entities.TV public override IEnumerable<FileSystemMetadata> GetDeletePaths() { - return new[] { + return new[] + { new FileSystemMetadata { FullName = Path, @@ -318,9 +320,9 @@ namespace MediaBrowser.Controller.Entities.TV return id; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!IsLocked) { @@ -328,7 +330,7 @@ namespace MediaBrowser.Controller.Entities.TV { try { - if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetdata)) + if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetadata)) { hasChanges = true; } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index ad3e0fe8d..aa62bb35b 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -81,7 +81,7 @@ namespace MediaBrowser.Controller.Entities.TV } /// <summary> - /// This Episode's Series Instance. + /// Gets this Episode's Series Instance. /// </summary> /// <value>The series.</value> [JsonIgnore] @@ -122,7 +122,7 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; if (series != null) { - return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000"); + return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); } } @@ -135,7 +135,7 @@ namespace MediaBrowser.Controller.Entities.TV /// <returns>System.String.</returns> protected override string CreateSortName() { - return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name; + return IndexNumber != null ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : Name; } protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) @@ -242,9 +242,9 @@ namespace MediaBrowser.Controller.Entities.TV /// This is called before any metadata refresh and returns true or false indicating if changes were made. /// </summary> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) { diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index ded825abc..44d07b4a4 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -59,8 +59,11 @@ namespace MediaBrowser.Controller.Entities.TV public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> - /// airdate, dvd or absolute. + /// Gets or sets the display order. /// </summary> + /// <remarks> + /// Valid options are airdate, dvd or absolute. + /// </remarks> public string DisplayOrder { get; set; } /// <summary> diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index b086b5906..732b45521 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -45,9 +45,9 @@ namespace MediaBrowser.Controller.Entities return info; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (!ProductionYear.HasValue) { diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs index f60359c01..6ab2116d7 100644 --- a/MediaBrowser.Controller/Entities/UserItemData.cs +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -96,7 +96,7 @@ namespace MediaBrowser.Controller.Entities public const double MinLikeValue = 6.5; /// <summary> - /// This is an interpreted property to indicate likes or dislikes + /// Gets or sets a value indicating whether the item is liked or not. /// This should never be serialized. /// </summary> /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value> diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index e492740ed..2b15a52f0 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -23,6 +23,7 @@ namespace MediaBrowser.Controller.Entities { private List<Guid> _childrenIds = null; private readonly object _childIdsLock = new object(); + protected override List<BaseItem> LoadChildren() { lock (_childIdsLock) @@ -87,10 +88,10 @@ namespace MediaBrowser.Controller.Entities return list; } - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { ClearCache(); - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase)) { @@ -108,11 +109,11 @@ namespace MediaBrowser.Controller.Entities return base.GetNonCachedChildren(directoryService); } - protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { ClearCache(); - await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken) .ConfigureAwait(false); ClearCache(); diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 0dfde2766..57dc9b59b 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -15,13 +15,19 @@ namespace MediaBrowser.Controller.Entities { public class UserView : Folder, IHasCollectionType { - /// <inheritdoc /> + /// <summary> + /// Gets or sets the view type. + /// </summary> public string ViewType { get; set; } - /// <inheritdoc /> + /// <summary> + /// Gets or sets the display parent id. + /// </summary> public new Guid DisplayParentId { get; set; } - /// <inheritdoc /> + /// <summary> + /// Gets or sets the user id. + /// </summary> public Guid? UserId { get; set; } public static ITVSeriesManager TVSeriesManager; @@ -110,10 +116,10 @@ namespace MediaBrowser.Controller.Entities return GetChildren(user, false); } - private static string[] UserSpecificViewTypes = new string[] - { - Model.Entities.CollectionType.Playlists - }; + private static readonly string[] UserSpecificViewTypes = new string[] + { + Model.Entities.CollectionType.Playlists + }; public static bool IsUserSpecific(Folder folder) { @@ -166,7 +172,7 @@ namespace MediaBrowser.Controller.Entities return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); } - protected override Task ValidateChildrenInternal(IProgress<double> progress, System.Threading.CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken) { return Task.CompletedTask; } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 15a4573c2..add734f62 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -55,12 +55,12 @@ namespace MediaBrowser.Controller.Entities // if (query.IncludeItemTypes != null && // query.IncludeItemTypes.Length == 1 && // string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase)) - //{ + // { // if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) // { // return await FindPlaylists(queryParent, user, query).ConfigureAwait(false); // } - //} + // } switch (viewType) { @@ -344,12 +344,14 @@ namespace MediaBrowser.Controller.Entities var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty }); var result = _tvSeriesManager.GetNextUp( - new NextUpQuery - { - Limit = query.Limit, - StartIndex = query.StartIndex, - UserId = query.User.Id - }, parentFolders, query.DtoOptions); + new NextUpQuery + { + Limit = query.Limit, + StartIndex = query.StartIndex, + UserId = query.User.Id + }, + parentFolders, + query.DtoOptions); return result; } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 723027a88..d05b5df2f 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -482,7 +482,8 @@ namespace MediaBrowser.Controller.Entities { if (!IsInMixedFolder) { - return new[] { + return new[] + { new FileSystemMetadata { FullName = ContainingFolderPath, diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index 4d84a151a..f268bc939 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Returns the folder containing the item. + /// Gets the folder containing the item. /// If the item is a folder, it returns the folder itself. /// </summary> /// <value>The containing folder path.</value> @@ -112,11 +112,13 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made. + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> - public override bool BeforeMetadataRefresh(bool replaceAllMetdata) + /// <param name="replaceAllMetadata">Whether to replace all metadata.</param> + /// <returns>true if the item has change, else false.</returns> + public override bool BeforeMetadataRefresh(bool replaceAllMetadata) { - var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); + var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); var newPath = GetRebasedPath(); if (!string.Equals(Path, newPath, StringComparison.Ordinal)) diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs index 8441a3171..f1af01345 100644 --- a/MediaBrowser.Controller/Extensions/StringExtensions.cs +++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs @@ -21,6 +21,27 @@ namespace MediaBrowser.Controller.Extensions return Normalize(string.Concat(chars), NormalizationForm.FormC); } + /// <summary> + /// Counts the number of occurrences of [needle] in the string. + /// </summary> + /// <param name="value">The haystack to search in.</param> + /// <param name="needle">The character to search for.</param> + /// <returns>The number of occurrences of the [needle] character.</returns> + public static int Count(this ReadOnlySpan<char> value, char needle) + { + var count = 0; + var length = value.Length; + for (var i = 0; i < length; i++) + { + if (value[i] == needle) + { + count++; + } + } + + return count; + } + private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true) { if (stripStringOnFailure) @@ -33,7 +54,7 @@ namespace MediaBrowser.Controller.Extensions { // will throw if input contains invalid unicode chars // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/ - text = Regex.Replace(text, "([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])", ""); + text = Regex.Replace(text, "([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])", string.Empty); return Normalize(text, form, false); } } diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs index 3db60ae0b..b8a0bf331 100644 --- a/MediaBrowser.Controller/IO/FileData.cs +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.IO /// <param name="flattenFolderDepth">The flatten folder depth.</param> /// <param name="resolveShortcuts">if set to <c>true</c> [resolve shortcuts].</param> /// <returns>Dictionary{System.StringFileSystemInfo}.</returns> - /// <exception cref="ArgumentNullException">path</exception> + /// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c> or empty.</exception> public static FileSystemMetadata[] GetFilteredFileSystemEntries( IDirectoryService directoryService, string path, diff --git a/MediaBrowser.Controller/Library/DeleteOptions.cs b/MediaBrowser.Controller/Library/DeleteOptions.cs index b7417efcb..408e70284 100644 --- a/MediaBrowser.Controller/Library/DeleteOptions.cs +++ b/MediaBrowser.Controller/Library/DeleteOptions.cs @@ -4,13 +4,13 @@ namespace MediaBrowser.Controller.Library { public class DeleteOptions { - public bool DeleteFileLocation { get; set; } - - public bool DeleteFromExternalProvider { get; set; } - public DeleteOptions() { DeleteFromExternalProvider = true; } + + public bool DeleteFileLocation { get; set; } + + public bool DeleteFromExternalProvider { get; set; } } } diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs index 3bb1bd9a0..a74d1b9f0 100644 --- a/MediaBrowser.Controller/Library/IIntroProvider.cs +++ b/MediaBrowser.Controller/Library/IIntroProvider.cs @@ -12,6 +12,12 @@ namespace MediaBrowser.Controller.Library public interface IIntroProvider { /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> /// Gets the intros. /// </summary> /// <param name="item">The item.</param> @@ -24,11 +30,5 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <returns>IEnumerable{System.String}.</returns> IEnumerable<string> GetAllIntroFiles(); - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - string Name { get; } } } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 782e15398..7a4ba6a24 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Emby.Naming.Common; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; @@ -43,6 +44,12 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Resolves a set of files into a list of BaseItem. /// </summary> + /// <param name="files">The list of tiles.</param> + /// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param> + /// <param name="parent">The parent folder.</param> + /// <param name="libraryOptions">The library options.</param> + /// <param name="collectionType">The collection type.</param> + /// <returns>The items resolved from the paths.</returns> IEnumerable<BaseItem> ResolvePaths( IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, @@ -346,6 +353,7 @@ namespace MediaBrowser.Controller.Library /// <param name="viewType">Type of the view.</param> /// <param name="sortName">Name of the sort.</param> /// <param name="uniqueId">The unique identifier.</param> + /// <returns>The named view.</returns> UserView GetNamedView( string name, Guid parentId, @@ -359,10 +367,11 @@ namespace MediaBrowser.Controller.Library /// <param name="parent">The parent.</param> /// <param name="viewType">Type of the view.</param> /// <param name="sortName">Name of the sort.</param> + /// <returns>The shadow view.</returns> UserView GetShadowView( BaseItem parent, - string viewType, - string sortName); + string viewType, + string sortName); /// <summary> /// Determines whether [is video file] [the specified path]. @@ -587,5 +596,11 @@ namespace MediaBrowser.Controller.Library BaseItem GetParentItem(string parentId, Guid? userId); BaseItem GetParentItem(Guid? parentId, Guid? userId); + + /// <summary> + /// Gets or creates a static instance of <see cref="NamingOptions"/>. + /// </summary> + /// <returns>An instance of the <see cref="NamingOptions"/> class.</returns> + NamingOptions GetNamingOptions(); } } diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index 58499e853..e5dcfcff0 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -49,17 +49,16 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Get all user data for the given user. /// </summary> - /// <param name="userId"></param> - /// <returns></returns> + /// <param name="userId">The user id.</param> + /// <returns>The user item data.</returns> List<UserItemData> GetAllUserData(Guid userId); /// <summary> /// Save the all provided user data for the given user. /// </summary> - /// <param name="userId"></param> - /// <param name="userData"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> + /// <param name="userId">The user id.</param> + /// <param name="userData">The array of user data.</param> + /// <param name="cancellationToken">The cancellation token.</param> void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken); /// <summary> diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index c95b0ea32..1801b1c41 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -61,16 +61,16 @@ namespace MediaBrowser.Controller.Library /// <param name="user">The user.</param> /// <param name="newName">The new name.</param> /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">user</exception> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception> + /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception> Task RenameUser(User user, string newName); /// <summary> /// Updates the user. /// </summary> /// <param name="user">The user.</param> - /// <exception cref="ArgumentNullException">user</exception> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception> + /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception> void UpdateUser(User user); /// <summary> @@ -87,8 +87,8 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="name">The name of the new user.</param> /// <returns>The created user.</returns> - /// <exception cref="ArgumentNullException">name</exception> - /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception> + /// <exception cref="ArgumentException"><paramref name="name"/> already exists.</exception> Task<User> CreateUserAsync(string name); /// <summary> diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index 0e2d8fb02..521e37274 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -39,7 +39,7 @@ namespace MediaBrowser.Controller.Library public IDirectoryService DirectoryService { get; } /// <summary> - /// Gets the file system children. + /// Gets or sets the file system children. /// </summary> /// <value>The file system children.</value> public FileSystemMetadata[] FileSystemChildren { get; set; } @@ -242,14 +242,14 @@ namespace MediaBrowser.Controller.Library /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns> public override int GetHashCode() { - return Path.GetHashCode(); + return Path.GetHashCode(StringComparison.Ordinal); } /// <summary> /// Equals the specified args. /// </summary> /// <param name="args">The args.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <returns><c>true</c> if the arguments are the same, <c>false</c> otherwise.</returns> protected bool Equals(ItemResolveArgs args) { if (args != null) diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs deleted file mode 100644 index 8f42d3706..000000000 --- a/MediaBrowser.Controller/Library/Profiler.cs +++ /dev/null @@ -1,85 +0,0 @@ -#nullable disable - -using System; -using System.Diagnostics; -using System.Globalization; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Controller.Library -{ - /// <summary> - /// Class Profiler. - /// </summary> - public class Profiler : IDisposable - { - /// <summary> - /// The name. - /// </summary> - readonly string _name; - - /// <summary> - /// The stopwatch. - /// </summary> - readonly Stopwatch _stopwatch; - - /// <summary> - /// The _logger. - /// </summary> - private readonly ILogger<Profiler> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="Profiler" /> class. - /// </summary> - /// <param name="name">The name.</param> - /// <param name="logger">The logger.</param> - public Profiler(string name, ILogger<Profiler> logger) - { - this._name = name; - - _logger = logger; - - _stopwatch = new Stopwatch(); - _stopwatch.Start(); - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - _stopwatch.Stop(); - string message; - if (_stopwatch.ElapsedMilliseconds > 300000) - { - message = string.Format( - CultureInfo.InvariantCulture, - "{0} took {1} minutes.", - _name, - ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F", CultureInfo.InvariantCulture)); - } - else - { - message = string.Format( - CultureInfo.InvariantCulture, - "{0} took {1} seconds.", - _name, - ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000", CultureInfo.InvariantCulture)); - } - - _logger.LogInformation(message); - } - } - } -} diff --git a/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs b/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs new file mode 100644 index 000000000..463061e68 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/ActiveRecordingInfo.cs @@ -0,0 +1,19 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System.Threading; + +namespace MediaBrowser.Controller.LiveTv +{ + public class ActiveRecordingInfo + { + public string Id { get; set; } + + public string Path { get; set; } + + public TimerInfo Timer { get; set; } + + public CancellationTokenSource CancellationTokenSource { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs index a55fd670d..699c15f93 100644 --- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.LiveTv public string Number { get; set; } /// <summary> - /// Get or sets the Id. + /// Gets or sets the Id. /// </summary> /// <value>The id of the channel.</value> public string Id { get; set; } @@ -54,13 +54,13 @@ namespace MediaBrowser.Controller.LiveTv public string ChannelGroup { get; set; } /// <summary> - /// Supply the image path if it can be accessed directly from the file system. + /// Gets or sets the the image path if it can be accessed directly from the file system. /// </summary> /// <value>The image path.</value> public string ImagePath { get; set; } /// <summary> - /// Supply the image url if it can be downloaded. + /// Gets or sets the image url if it can be downloaded. /// </summary> /// <value>The image URL.</value> public string ImageUrl { get; set; } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index c28e0426b..f4dc18e11 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -268,16 +268,21 @@ namespace MediaBrowser.Controller.LiveTv void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user); Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken); + Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken); IListingsProvider[] ListingProviders { get; } List<NameIdPair> GetTunerHostTypes(); + Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken); event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; + event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled; + event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated; + event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated; string GetEmbyTvActiveRecordingPath(string id); @@ -288,15 +293,4 @@ namespace MediaBrowser.Controller.LiveTv List<BaseItem> GetRecordingFolders(User user); } - - public class ActiveRecordingInfo - { - public string Id { get; set; } - - public string Path { get; set; } - - public TimerInfo Timer { get; set; } - - public CancellationTokenSource CancellationTokenSource { get; set; } - } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 02678cfb6..bf759bc54 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -146,7 +146,7 @@ namespace MediaBrowser.Controller.LiveTv public bool IsNews { get; set; } /// <summary> - /// Gets or sets a value indicating whether this instance is kids. + /// Gets a value indicating whether this instance is kids. /// </summary> /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> [JsonIgnore] diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index d9634a731..9d638a0bf 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -53,6 +53,7 @@ namespace MediaBrowser.Controller.LiveTv } private static string EmbyServiceName = "Emby"; + public override double GetDefaultPrimaryImageAspectRatio() { var serviceName = ServiceName; @@ -71,7 +72,7 @@ namespace MediaBrowser.Controller.LiveTv public override SourceType SourceType => SourceType.LiveTV; /// <summary> - /// The start date of the program, in UTC. + /// Gets or sets start date of the program, in UTC. /// </summary> [JsonIgnore] public DateTime StartDate { get; set; } @@ -101,7 +102,7 @@ namespace MediaBrowser.Controller.LiveTv public bool IsMovie { get; set; } /// <summary> - /// Gets or sets a value indicating whether this instance is sports. + /// Gets a value indicating whether this instance is sports. /// </summary> /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> [JsonIgnore] @@ -115,49 +116,49 @@ namespace MediaBrowser.Controller.LiveTv public bool IsSeries { get; set; } /// <summary> - /// Gets or sets a value indicating whether this instance is live. + /// Gets a value indicating whether this instance is live. /// </summary> /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> [JsonIgnore] public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase); /// <summary> - /// Gets or sets a value indicating whether this instance is news. + /// Gets a value indicating whether this instance is news. /// </summary> /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> [JsonIgnore] public bool IsNews => Tags.Contains("News", StringComparer.OrdinalIgnoreCase); /// <summary> - /// Gets or sets a value indicating whether this instance is kids. + /// Gets a value indicating whether this instance is kids. /// </summary> /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> [JsonIgnore] public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); /// <summary> - /// Gets or sets a value indicating whether this instance is premiere. + /// Gets a value indicating whether this instance is premiere. /// </summary> /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> [JsonIgnore] public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase); /// <summary> - /// Returns the folder containing the item. + /// Gets the folder containing the item. /// If the item is a folder, it returns the folder itself. /// </summary> /// <value>The containing folder path.</value> [JsonIgnore] public override string ContainingFolderPath => Path; - //[JsonIgnore] + // [JsonIgnore] // public override string MediaType - //{ + // { // get // { // return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio; // } - //} + // } [JsonIgnore] public bool IsAiring diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index 4a977c5cc..3c3ac2471 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -10,8 +10,16 @@ namespace MediaBrowser.Controller.LiveTv { public class ProgramInfo { + public ProgramInfo() + { + Genres = new List<string>(); + + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + /// <summary> - /// Id of the program. + /// Gets or sets the id of the program. /// </summary> public string Id { get; set; } @@ -22,7 +30,7 @@ namespace MediaBrowser.Controller.LiveTv public string ChannelId { get; set; } /// <summary> - /// Name of the program. + /// Gets or sets the name of the program. /// </summary> public string Name { get; set; } @@ -45,17 +53,17 @@ namespace MediaBrowser.Controller.LiveTv public string ShortOverview { get; set; } /// <summary> - /// The start date of the program, in UTC. + /// Gets or sets the start date of the program, in UTC. /// </summary> public DateTime StartDate { get; set; } /// <summary> - /// The end date of the program, in UTC. + /// Gets or sets the end date of the program, in UTC. /// </summary> public DateTime EndDate { get; set; } /// <summary> - /// Genre of the program. + /// Gets or sets the genre of the program. /// </summary> public List<string> Genres { get; set; } @@ -71,6 +79,9 @@ namespace MediaBrowser.Controller.LiveTv /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value> public bool? IsHD { get; set; } + /// <summary> + /// Gets or sets a value indicating whether this instance is 3d. + /// </summary> public bool? Is3D { get; set; } /// <summary> @@ -100,13 +111,13 @@ namespace MediaBrowser.Controller.LiveTv public string EpisodeTitle { get; set; } /// <summary> - /// Supply the image path if it can be accessed directly from the file system. + /// Gets or sets the image path if it can be accessed directly from the file system. /// </summary> /// <value>The image path.</value> public string ImagePath { get; set; } /// <summary> - /// Supply the image url if it can be downloaded. + /// Gets or sets the image url if it can be downloaded. /// </summary> /// <value>The image URL.</value> public string ImageUrl { get; set; } @@ -212,13 +223,5 @@ namespace MediaBrowser.Controller.LiveTv public Dictionary<string, string> ProviderIds { get; set; } public Dictionary<string, string> SeriesProviderIds { get; set; } - - public ProgramInfo() - { - Genres = new List<string>(); - - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } } } diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs index 00135afa8..1dcf7a58f 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs @@ -10,8 +10,13 @@ namespace MediaBrowser.Controller.LiveTv { public class RecordingInfo { + public RecordingInfo() + { + Genres = new List<string>(); + } + /// <summary> - /// Id of the recording. + /// Gets or sets the id of the recording. /// </summary> public string Id { get; set; } @@ -28,7 +33,7 @@ namespace MediaBrowser.Controller.LiveTv public string TimerId { get; set; } /// <summary> - /// ChannelId of the recording. + /// Gets or sets the channelId of the recording. /// </summary> public string ChannelId { get; set; } @@ -39,7 +44,7 @@ namespace MediaBrowser.Controller.LiveTv public ChannelType ChannelType { get; set; } /// <summary> - /// Name of the recording. + /// Gets or sets the name of the recording. /// </summary> public string Name { get; set; } @@ -62,12 +67,12 @@ namespace MediaBrowser.Controller.LiveTv public string Overview { get; set; } /// <summary> - /// The start date of the recording, in UTC. + /// Gets or sets the start date of the recording, in UTC. /// </summary> public DateTime StartDate { get; set; } /// <summary> - /// The end date of the recording, in UTC. + /// Gets or sets the end date of the recording, in UTC. /// </summary> public DateTime EndDate { get; set; } @@ -84,7 +89,7 @@ namespace MediaBrowser.Controller.LiveTv public RecordingStatus Status { get; set; } /// <summary> - /// Genre of the program. + /// Gets or sets the genre of the program. /// </summary> public List<string> Genres { get; set; } @@ -173,13 +178,13 @@ namespace MediaBrowser.Controller.LiveTv public float? CommunityRating { get; set; } /// <summary> - /// Supply the image path if it can be accessed directly from the file system. + /// Gets or sets the image path if it can be accessed directly from the file system. /// </summary> /// <value>The image path.</value> public string ImagePath { get; set; } /// <summary> - /// Supply the image url if it can be downloaded. + /// Gets or sets the image url if it can be downloaded. /// </summary> /// <value>The image URL.</value> public string ImageUrl { get; set; } @@ -201,10 +206,5 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value>The date last updated.</value> public DateTime DateLastUpdated { get; set; } - - public RecordingInfo() - { - Genres = new List<string>(); - } } } diff --git a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs index 1bb649a99..d6811fe14 100644 --- a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs @@ -10,13 +10,20 @@ namespace MediaBrowser.Controller.LiveTv { public class SeriesTimerInfo { + public SeriesTimerInfo() + { + Days = new List<DayOfWeek>(); + SkipEpisodesInLibrary = true; + KeepUntil = KeepUntil.UntilDeleted; + } + /// <summary> - /// Id of the recording. + /// Gets or sets the id of the recording. /// </summary> public string Id { get; set; } /// <summary> - /// ChannelId of the recording. + /// Gets or sets the channelId of the recording. /// </summary> public string ChannelId { get; set; } @@ -27,24 +34,27 @@ namespace MediaBrowser.Controller.LiveTv public string ProgramId { get; set; } /// <summary> - /// Name of the recording. + /// Gets or sets the name of the recording. /// </summary> public string Name { get; set; } + /// <summary> + /// Gets or sets the service name. + /// </summary> public string ServiceName { get; set; } /// <summary> - /// Description of the recording. + /// Gets or sets the description of the recording. /// </summary> public string Overview { get; set; } /// <summary> - /// The start date of the recording, in UTC. + /// Gets or sets the start date of the recording, in UTC. /// </summary> public DateTime StartDate { get; set; } /// <summary> - /// The end date of the recording, in UTC. + /// Gets or sets the end date of the recording, in UTC. /// </summary> public DateTime EndDate { get; set; } @@ -113,12 +123,5 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value>The series identifier.</value> public string SeriesId { get; set; } - - public SeriesTimerInfo() - { - Days = new List<DayOfWeek>(); - SkipEpisodesInLibrary = true; - KeepUntil = KeepUntil.UntilDeleted; - } } } diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs index 728387c56..92eb0be9c 100644 --- a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs @@ -1,6 +1,3 @@ -#nullable disable - -#nullable enable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs index e54dc967c..1a2e8acb3 100644 --- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -28,18 +28,17 @@ namespace MediaBrowser.Controller.LiveTv public string[] Tags { get; set; } /// <summary> - /// Id of the recording. + /// Gets or sets the id of the recording. /// </summary> public string Id { get; set; } /// <summary> /// Gets or sets the series timer identifier. /// </summary> - /// <value>The series timer identifier.</value> public string SeriesTimerId { get; set; } /// <summary> - /// ChannelId of the recording. + /// Gets or sets the channelId of the recording. /// </summary> public string ChannelId { get; set; } @@ -52,24 +51,24 @@ namespace MediaBrowser.Controller.LiveTv public string ShowId { get; set; } /// <summary> - /// Name of the recording. + /// Gets or sets the name of the recording. /// </summary> public string Name { get; set; } /// <summary> - /// Description of the recording. + /// Gets or sets the description of the recording. /// </summary> public string Overview { get; set; } public string SeriesId { get; set; } /// <summary> - /// The start date of the recording, in UTC. + /// Gets or sets the start date of the recording, in UTC. /// </summary> public DateTime StartDate { get; set; } /// <summary> - /// The end date of the recording, in UTC. + /// Gets or sets the end date of the recording, in UTC. /// </summary> public DateTime EndDate { get; set; } @@ -133,7 +132,7 @@ namespace MediaBrowser.Controller.LiveTv public bool IsSeries { get; set; } /// <summary> - /// Gets or sets a value indicating whether this instance is live. + /// Gets a value indicating whether this instance is live. /// </summary> /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> [JsonIgnore] diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 37ce35fc2..ee76ff080 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -16,13 +16,14 @@ <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="System.Threading.Tasks.Dataflow" Version="5.0.0" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + <ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" /> + <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" /> + <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs index 88de5b292..745ee6bdb 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs @@ -1,63 +1,13 @@ -#nullable disable +#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Generic; -using System.Linq; using MediaBrowser.Model.Dlna; namespace MediaBrowser.Controller.MediaEncoding { - public class EncodingJobOptions : BaseEncodingJobOptions - { - public string OutputDirectory { get; set; } - - public string ItemId { get; set; } - - public string TempDirectory { get; set; } - - public bool ReadInputAtNativeFramerate { get; set; } - - /// <summary> - /// Gets a value indicating whether this instance has fixed resolution. - /// </summary> - /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value> - public bool HasFixedResolution => Width.HasValue || Height.HasValue; - - public DeviceProfile DeviceProfile { get; set; } - - public EncodingJobOptions(StreamInfo info, DeviceProfile deviceProfile) - { - Container = info.Container; - StartTimeTicks = info.StartPositionTicks; - MaxWidth = info.MaxWidth; - MaxHeight = info.MaxHeight; - MaxFramerate = info.MaxFramerate; - Id = info.ItemId; - MediaSourceId = info.MediaSourceId; - AudioCodec = info.TargetAudioCodec.FirstOrDefault(); - MaxAudioChannels = info.GlobalMaxAudioChannels; - AudioBitRate = info.AudioBitrate; - AudioSampleRate = info.TargetAudioSampleRate; - DeviceProfile = deviceProfile; - VideoCodec = info.TargetVideoCodec.FirstOrDefault(); - VideoBitRate = info.VideoBitrate; - AudioStreamIndex = info.AudioStreamIndex; - SubtitleMethod = info.SubtitleDeliveryMethod; - Context = info.Context; - TranscodingMaxAudioChannels = info.TranscodingMaxAudioChannels; - - if (info.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) - { - SubtitleStreamIndex = info.SubtitleStreamIndex; - } - - StreamOptions = info.StreamOptions; - } - } - - // For now until api and media encoding layers are unified public class BaseEncodingJobOptions { /// <summary> @@ -251,4 +201,4 @@ namespace MediaBrowser.Controller.MediaEncoding StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 97cb8d63b..26b0bc3de 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -596,7 +596,8 @@ namespace MediaBrowser.Controller.MediaEncoding && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecDecoder) { - arg.Append("-hwaccel_output_format cuda -autorotate 0 "); + // Fix for 'No decoder surfaces left' error. https://trac.ffmpeg.org/ticket/7562 + arg.Append("-hwaccel_output_format cuda -extra_hw_frames 3 -autorotate 0 "); } if (state.IsVideoRequest @@ -1070,7 +1071,6 @@ namespace MediaBrowser.Controller.MediaEncoding else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc) { - // following preset will be deprecated in ffmpeg 4.4, use p1~p7 instead. switch (encodingOptions.EncoderPreset) { case "veryslow": @@ -1166,7 +1166,9 @@ namespace MediaBrowser.Controller.MediaEncoding profileScore = Math.Min(profileScore, 2); // http://www.webmproject.org/docs/encoder-parameters/ - param += string.Format(CultureInfo.InvariantCulture, " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}", + param += string.Format( + CultureInfo.InvariantCulture, + " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}", profileScore.ToString(_usCulture), crf, qmin, @@ -1251,7 +1253,7 @@ namespace MediaBrowser.Controller.MediaEncoding } if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) - && profile.Contains("constrainedbaseline", StringComparison.OrdinalIgnoreCase)) + && profile.Contains("baseline", StringComparison.OrdinalIgnoreCase)) { profile = "constrained_baseline"; } @@ -1296,7 +1298,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hevc_qsv use -level 51 instead of -level 153. if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel)) { - param += " -level " + hevcLevel / 3; + param += " -level " + (hevcLevel / 3); } } else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) @@ -1392,7 +1394,7 @@ namespace MediaBrowser.Controller.MediaEncoding var requestedProfile = requestedProfiles[0]; // strip spaces because they may be stripped out on the query string as well if (!string.IsNullOrEmpty(videoStream.Profile) - && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", "", StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase)) + && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", string.Empty, StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase)) { var currentScore = GetVideoProfileScore(videoStream.Profile); var requestedScore = GetVideoProfileScore(requestedProfile); @@ -1801,7 +1803,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (isTranscodingAudio && state.TranscodingType != TranscodingJobType.Progressive && resultChannels.HasValue - && (resultChannels.Value > 2 && resultChannels.Value < 6 || resultChannels.Value == 7)) + && ((resultChannels.Value > 2 && resultChannels.Value < 6) || resultChannels.Value == 7)) { resultChannels = 2; } @@ -2129,8 +2131,8 @@ namespace MediaBrowser.Controller.MediaEncoding return (null, null); } - decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth); - decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight); + decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth, CultureInfo.InvariantCulture); + decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight, CultureInfo.InvariantCulture); decimal outputWidth = requestedWidth.HasValue ? Convert.ToDecimal(requestedWidth.Value) : inputWidth; decimal outputHeight = requestedHeight.HasValue ? Convert.ToDecimal(requestedHeight.Value) : inputHeight; decimal maximumWidth = requestedMaxWidth.HasValue ? Convert.ToDecimal(requestedMaxWidth.Value) : outputWidth; @@ -2197,12 +2199,11 @@ namespace MediaBrowser.Controller.MediaEncoding var isQsvHevcEncoder = videoEncoder.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase); var isTonemappingSupported = IsTonemappingSupported(state, options); var isVppTonemappingSupported = IsVppTonemappingSupported(state, options); - var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)&& isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); + var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder); var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder); var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported)) || (isTonemappingSupportedOnQsv && isVppTonemappingSupported); - var outputPixFmt = "format=nv12"; if (isP010PixFmtRequired) { @@ -2933,6 +2934,7 @@ namespace MediaBrowser.Controller.MediaEncoding return threads; } + #nullable disable public void TryStreamCopy(EncodingJobInfo state) { @@ -3174,8 +3176,8 @@ namespace MediaBrowser.Controller.MediaEncoding state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; if (state.ReadInputAtNativeFramerate - || mediaSource.Protocol == MediaProtocol.File - && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)) + || (mediaSource.Protocol == MediaProtocol.File + && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))) { state.InputVideoSync = "-1"; state.InputAudioSync = "1"; @@ -3548,7 +3550,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Gets a hw decoder name + /// Gets a hw decoder name. /// </summary> public string GetHwDecoderName(EncodingOptions options, string decoder, string videoCodec, bool isColorDepth10) { @@ -3566,7 +3568,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system + /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system. /// </summary> public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec, bool isColorDepth10) { @@ -3692,7 +3694,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (flags.Count > 0) { - return " -fflags " + string.Join("", flags); + return " -fflags " + string.Join(string.Empty, flags); } return string.Empty; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 1e13382b7..bc0318ad7 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -69,6 +69,7 @@ namespace MediaBrowser.Controller.MediaEncoding } private TranscodeReason[] _transcodeReasons = null; + public TranscodeReason[] TranscodeReasons { get @@ -274,6 +275,16 @@ namespace MediaBrowser.Controller.MediaEncoding public int? GetRequestedAudioChannels(string codec) { + if (!string.IsNullOrEmpty(codec)) + { + var value = BaseRequest.GetOption(codec, "audiochannels"); + if (!string.IsNullOrEmpty(value) + && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + } + if (BaseRequest.MaxAudioChannels.HasValue) { return BaseRequest.MaxAudioChannels; @@ -289,16 +300,6 @@ namespace MediaBrowser.Controller.MediaEncoding return BaseRequest.TranscodingMaxAudioChannels; } - if (!string.IsNullOrEmpty(codec)) - { - var value = BaseRequest.GetOption(codec, "audiochannels"); - if (!string.IsNullOrEmpty(value) - && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - } - return null; } @@ -430,7 +431,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Predicts the audio sample rate that will be in the output stream. + /// Gets the target video level. /// </summary> public double? TargetVideoLevel { @@ -453,7 +454,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Predicts the audio sample rate that will be in the output stream. + /// Gets the target video bit depth. /// </summary> public int? TargetVideoBitDepth { @@ -488,7 +489,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Predicts the audio sample rate that will be in the output stream. + /// Gets the target framerate. /// </summary> public float? TargetFramerate { @@ -520,7 +521,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Predicts the audio sample rate that will be in the output stream. + /// Gets the target packet length. /// </summary> public int? TargetPacketLength { @@ -536,7 +537,7 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Predicts the audio sample rate that will be in the output stream. + /// Gets the target video profile. /// </summary> public string TargetVideoProfile { @@ -700,25 +701,4 @@ namespace MediaBrowser.Controller.MediaEncoding Progress.Report(percentComplete.Value); } } - - /// <summary> - /// Enum TranscodingJobType. - /// </summary> - public enum TranscodingJobType - { - /// <summary> - /// The progressive. - /// </summary> - Progressive, - - /// <summary> - /// The HLS. - /// </summary> - Hls, - - /// <summary> - /// The dash. - /// </summary> - Dash - } } diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index d3260280a..76a9fd7c7 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.MediaEncoding public interface IMediaEncoder : ITranscoderSupport { /// <summary> - /// The location of the discovered FFmpeg tool. + /// Gets location of the discovered FFmpeg tool. /// </summary> FFmpegLocation EncoderLocation { get; } diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index aa5e2c403..b23c95112 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -6,6 +6,7 @@ using System; using System.Globalization; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs new file mode 100644 index 000000000..66b628371 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJobType.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Controller.MediaEncoding +{ + /// <summary> + /// Enum TranscodingJobType. + /// </summary> + public enum TranscodingJobType + { + /// <summary> + /// The progressive. + /// </summary> + Progressive, + + /// <summary> + /// The HLS. + /// </summary> + Hls, + + /// <summary> + /// The dash. + /// </summary> + Dash + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 855467e8e..d8995ce74 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -270,13 +270,4 @@ namespace MediaBrowser.Controller.Net GC.SuppressFinalize(this); } } - - public class WebSocketListenerState - { - public DateTime DateLastSendUtc { get; set; } - - public long InitialDelayMs { get; set; } - - public long IntervalMs { get; set; } - } } diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs index a60dc2ea1..6b896b41f 100644 --- a/MediaBrowser.Controller/Net/ISessionContext.cs +++ b/MediaBrowser.Controller/Net/ISessionContext.cs @@ -10,10 +10,10 @@ namespace MediaBrowser.Controller.Net { SessionInfo GetSession(object requestContext); - User GetUser(object requestContext); + User? GetUser(object requestContext); SessionInfo GetSession(HttpContext requestContext); - User GetUser(HttpContext requestContext); + User? GetUser(HttpContext requestContext); } } diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index 5e9fce550..c8c5caf80 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -1,9 +1,5 @@ -#nullable disable - #pragma warning disable CS1591 -#nullable enable - using System; using System.Net; using System.Net.WebSockets; @@ -34,7 +30,7 @@ namespace MediaBrowser.Controller.Net DateTime LastKeepAliveDate { get; set; } /// <summary> - /// Gets or sets the query string. + /// Gets the query string. /// </summary> /// <value>The query string.</value> IQueryCollection QueryString { get; } @@ -60,11 +56,11 @@ namespace MediaBrowser.Controller.Net /// <summary> /// Sends a message asynchronously. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">The type of websocket message data.</typeparam> /// <param name="message">The message.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">message</exception> + /// <exception cref="ArgumentNullException">The message is null.</exception> Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken); Task ProcessAsync(CancellationToken cancellationToken = default); diff --git a/MediaBrowser.Controller/Net/WebSocketListenerState.cs b/MediaBrowser.Controller/Net/WebSocketListenerState.cs new file mode 100644 index 000000000..70604d60a --- /dev/null +++ b/MediaBrowser.Controller/Net/WebSocketListenerState.cs @@ -0,0 +1,17 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; + +namespace MediaBrowser.Controller.Net +{ + public class WebSocketListenerState + { + public DateTime DateLastSendUtc { get; set; } + + public long InitialDelayMs { get; set; } + + public long IntervalMs { get; set; } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 56fb36af2..0a9073e7f 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -49,21 +49,23 @@ namespace MediaBrowser.Controller.Persistence /// <summary> /// Gets chapters for an item. /// </summary> - /// <param name="id"></param> - /// <returns></returns> + /// <param name="id">The item.</param> + /// <returns>The list of chapter info.</returns> List<ChapterInfo> GetChapters(BaseItem id); /// <summary> /// Gets a single chapter for an item. /// </summary> - /// <param name="id"></param> - /// <param name="index"></param> - /// <returns></returns> + /// <param name="id">The item.</param> + /// <param name="index">The chapter index.</param> + /// <returns>The chapter info at the specified index.</returns> ChapterInfo GetChapter(BaseItem id, int index); /// <summary> /// Saves the chapters. /// </summary> + /// <param name="id">The item id.</param> + /// <param name="chapters">The list of chapters to save.</param> void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters); /// <summary> diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs index 6f5f02123..5fa5834c8 100644 --- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs +++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs @@ -40,17 +40,16 @@ namespace MediaBrowser.Controller.Persistence /// <summary> /// Return all user data associated with the given user. /// </summary> - /// <param name="userId"></param> - /// <returns></returns> + /// <param name="userId">The user id.</param> + /// <returns>The list of user item data.</returns> List<UserItemData> GetAllUserData(long userId); /// <summary> /// Save all user data associated with the given user. /// </summary> - /// <param name="userId"></param> - /// <param name="userData"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> + /// <param name="userId">The user id.</param> + /// <param name="userData">The user item data.</param> + /// <param name="cancellationToken">The cancellation token.</param> void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index a80c11643..3eaf23515 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -22,14 +22,14 @@ namespace MediaBrowser.Controller.Playlists { public class Playlist : Folder, IHasShares { - public static string[] SupportedExtensions = - { - ".m3u", - ".m3u8", - ".pls", - ".wpl", - ".zpl" - }; + public static readonly IReadOnlyList<string> SupportedExtensions = new[] + { + ".m3u", + ".m3u8", + ".pls", + ".wpl", + ".zpl" + }; public Guid OwnerUserId { get; set; } @@ -101,7 +101,7 @@ namespace MediaBrowser.Controller.Playlists return new List<BaseItem>(); } - protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { return Task.CompletedTask; } diff --git a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs new file mode 100644 index 000000000..2b831103a --- /dev/null +++ b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Controller.Plugins +{ + /// <summary> + /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task. + /// </summary> + public interface IRunBeforeStartup + { + } +} diff --git a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs index b44e2531e..6024661e1 100644 --- a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs +++ b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs @@ -14,13 +14,7 @@ namespace MediaBrowser.Controller.Plugins /// <summary> /// Run the initialization for this module. This method is invoked at application start. /// </summary> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> Task RunAsync(); } - - /// <summary> - /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task. - /// </summary> - public interface IRunBeforeStartup - { - } } diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs index 276bcf125..c7fad5974 100644 --- a/MediaBrowser.Controller/Providers/AlbumInfo.cs +++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs @@ -7,6 +7,13 @@ namespace MediaBrowser.Controller.Providers { public class AlbumInfo : ItemLookupInfo { + public AlbumInfo() + { + ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + SongInfos = new List<SongInfo>(); + AlbumArtists = Array.Empty<string>(); + } + /// <summary> /// Gets or sets the album artist. /// </summary> @@ -20,12 +27,5 @@ namespace MediaBrowser.Controller.Providers public Dictionary<string, string> ArtistProviderIds { get; set; } public List<SongInfo> SongInfos { get; set; } - - public AlbumInfo() - { - ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - SongInfos = new List<SongInfo>(); - AlbumArtists = Array.Empty<string>(); - } } } diff --git a/MediaBrowser.Controller/Providers/ArtistInfo.cs b/MediaBrowser.Controller/Providers/ArtistInfo.cs index adf885baa..e9181f476 100644 --- a/MediaBrowser.Controller/Providers/ArtistInfo.cs +++ b/MediaBrowser.Controller/Providers/ArtistInfo.cs @@ -6,11 +6,11 @@ namespace MediaBrowser.Controller.Providers { public class ArtistInfo : ItemLookupInfo { - public List<SongInfo> SongInfos { get; set; } - public ArtistInfo() { SongInfos = new List<SongInfo>(); } + + public List<SongInfo> SongInfos { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 291a26883..b31270270 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -25,15 +25,16 @@ namespace MediaBrowser.Controller.Providers public FileSystemMetadata[] GetFileSystemEntries(string path) { - return _cache.GetOrAdd(path, p => _fileSystem.GetFileSystemEntries(p).ToArray()); + return _cache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem); } public List<FileSystemMetadata> GetFiles(string path) { var list = new List<FileSystemMetadata>(); var items = GetFileSystemEntries(path); - foreach (var item in items) + for (var i = 0; i < items.Length; i++) { + var item = items[i]; if (!item.IsDirectory) { list.Add(item); @@ -48,10 +49,9 @@ namespace MediaBrowser.Controller.Providers if (!_fileCache.TryGetValue(path, out var result)) { var file = _fileSystem.GetFileInfo(path); - var res = file != null && file.Exists ? file : null; - if (res != null) + if (file.Exists) { - result = res; + result = file; _fileCache.TryAdd(path, result); } } @@ -62,14 +62,21 @@ namespace MediaBrowser.Controller.Providers public IReadOnlyList<string> GetFilePaths(string path) => GetFilePaths(path, false); - public IReadOnlyList<string> GetFilePaths(string path, bool clearCache) + public IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false) { if (clearCache) { _filePathCache.TryRemove(path, out _); } - return _filePathCache.GetOrAdd(path, p => _fileSystem.GetFilePaths(p).ToList()); + var filePaths = _filePathCache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem); + + if (sort) + { + filePaths.Sort(); + } + + return filePaths; } } } diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index 341bf6936..0c932fa87 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -9,6 +9,11 @@ namespace MediaBrowser.Controller.Providers { public class EpisodeInfo : ItemLookupInfo { + public EpisodeInfo() + { + SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + public Dictionary<string, string> SeriesProviderIds { get; set; } public int? IndexNumberEnd { get; set; } @@ -16,10 +21,5 @@ namespace MediaBrowser.Controller.Providers public bool IsMissingEpisode { get; set; } public string SeriesDisplayOrder { get; set; } - - public EpisodeInfo() - { - SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } } } diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs index 9cee06a4c..b1a36e102 100644 --- a/MediaBrowser.Controller/Providers/IDirectoryService.cs +++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs @@ -15,6 +15,6 @@ namespace MediaBrowser.Controller.Providers IReadOnlyList<string> GetFilePaths(string path); - IReadOnlyList<string> GetFilePaths(string path, bool clearCache); + IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false); } } diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs index 5f3d4274e..05fbb18ee 100644 --- a/MediaBrowser.Controller/Providers/IMetadataService.cs +++ b/MediaBrowser.Controller/Providers/IMetadataService.cs @@ -11,6 +11,12 @@ namespace MediaBrowser.Controller.Providers public interface IMetadataService { /// <summary> + /// Gets the order. + /// </summary> + /// <value>The order.</value> + int Order { get; } + + /// <summary> /// Determines whether this instance can refresh the specified item. /// </summary> /// <param name="item">The item.</param> @@ -27,11 +33,5 @@ namespace MediaBrowser.Controller.Providers /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken); - - /// <summary> - /// Gets the order. - /// </summary> - /// <value>The order.</value> - int Order { get; } } } diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index b4d91f396..684bd9e68 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -191,11 +191,4 @@ namespace MediaBrowser.Controller.Providers double? GetRefreshProgress(Guid id); } - - public enum RefreshPriority - { - High = 0, - Normal = 1, - Low = 2 - } } diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs index f16669a78..2fd89e3bb 100644 --- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs @@ -10,6 +10,12 @@ namespace MediaBrowser.Controller.Providers { public class ItemLookupInfo : IHasProviderIds { + public ItemLookupInfo() + { + IsAutomated = true; + ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + /// <summary> /// Gets or sets the name. /// </summary> @@ -17,6 +23,12 @@ namespace MediaBrowser.Controller.Providers public string Name { get; set; } /// <summary> + /// Gets or sets the original title + /// </summary> + /// <value>The original title of the item.</value> + public string OriginalTitle { get; set; } + + /// <summary> /// Gets or sets the path. /// </summary> /// <value>The path.</value> @@ -53,11 +65,5 @@ namespace MediaBrowser.Controller.Providers public DateTime? PremiereDate { get; set; } public bool IsAutomated { get; set; } - - public ItemLookupInfo() - { - IsAutomated = true; - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } } } diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index 5afc358ba..2cf536779 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -11,21 +11,6 @@ namespace MediaBrowser.Controller.Providers { public class MetadataRefreshOptions : ImageRefreshOptions { - /// <summary> - /// When paired with MetadataRefreshMode=FullRefresh, all existing data will be overwritten with new data from the providers. - /// </summary> - public bool ReplaceAllMetadata { get; set; } - - public MetadataRefreshMode MetadataRefreshMode { get; set; } - - public RemoteSearchResult SearchResult { get; set; } - - public string[] RefreshPaths { get; set; } - - public bool ForceSave { get; set; } - - public bool EnableRemoteContentProbe { get; set; } - public MetadataRefreshOptions(IDirectoryService directoryService) : base(directoryService) { @@ -53,6 +38,22 @@ namespace MediaBrowser.Controller.Providers } } + /// <summary> + /// Gets or sets a value indicating whether all existing data should be overwritten with new data from providers + /// when paired with MetadataRefreshMode=FullRefresh. + /// </summary> + public bool ReplaceAllMetadata { get; set; } + + public MetadataRefreshMode MetadataRefreshMode { get; set; } + + public RemoteSearchResult SearchResult { get; set; } + + public string[] RefreshPaths { get; set; } + + public bool ForceSave { get; set; } + + public bool EnableRemoteContentProbe { get; set; } + public bool RefreshItem(BaseItem item) { if (RefreshPaths != null && RefreshPaths.Length > 0) diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index 8b0967a6e..7ec1eefcd 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -12,16 +12,26 @@ namespace MediaBrowser.Controller.Providers { public class MetadataResult<T> { + // Images aren't always used so the allocation is a waste a lot of the time + private List<LocalImageInfo> _images; + private List<(string url, ImageType type)> _remoteImages; + public MetadataResult() { - Images = new List<LocalImageInfo>(); - RemoteImages = new List<(string url, ImageType type)>(); ResultLanguage = "en"; } - public List<LocalImageInfo> Images { get; set; } + public List<LocalImageInfo> Images + { + get => _images ??= new List<LocalImageInfo>(); + set => _images = value; + } - public List<(string url, ImageType type)> RemoteImages { get; set; } + public List<(string url, ImageType type)> RemoteImages + { + get => _remoteImages ??= new List<(string url, ImageType type)>(); + set => _remoteImages = value; + } public List<UserItemData> UserDataList { get; set; } diff --git a/MediaBrowser.Controller/Providers/RefreshPriority.cs b/MediaBrowser.Controller/Providers/RefreshPriority.cs new file mode 100644 index 000000000..3619f679d --- /dev/null +++ b/MediaBrowser.Controller/Providers/RefreshPriority.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Controller.Providers +{ + /// <summary> + /// Provider refresh priority. + /// </summary> + public enum RefreshPriority + { + /// <summary> + /// High priority. + /// </summary> + High = 0, + + /// <summary> + /// Normal priority. + /// </summary> + Normal = 1, + + /// <summary> + /// Low priority. + /// </summary> + Low = 2 + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs index d830231cf..d4df5fa0d 100644 --- a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs +++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Controller.Providers public Guid ItemId { get; set; } /// <summary> - /// Will only search within the given provider when set. + /// Gets or sets the provider name to search within if set. /// </summary> public string SearchProviderName { get; set; } diff --git a/MediaBrowser.Controller/Providers/SeasonInfo.cs b/MediaBrowser.Controller/Providers/SeasonInfo.cs index 2a4c1f03c..7e39bc37a 100644 --- a/MediaBrowser.Controller/Providers/SeasonInfo.cs +++ b/MediaBrowser.Controller/Providers/SeasonInfo.cs @@ -7,11 +7,11 @@ namespace MediaBrowser.Controller.Providers { public class SeasonInfo : ItemLookupInfo { - public Dictionary<string, string> SeriesProviderIds { get; set; } - public SeasonInfo() { SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } + + public Dictionary<string, string> SeriesProviderIds { get; set; } } } diff --git a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs index bb80e6025..a07b3e898 100644 --- a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs +++ b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.Resolvers { /// <summary> - /// Provides a base "rule" that anyone can use to have paths ignored by the resolver + /// Provides a base "rule" that anyone can use to have paths ignored by the resolver. /// </summary> public interface IResolverIgnoreRule { diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs index e77593a03..7fd54fcc6 100644 --- a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Resolvers /// <summary> /// Class ItemResolver. /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="T">The type of BaseItem.</typeparam> public abstract class ItemResolver<T> : IItemResolver where T : BaseItem, new() { diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 7eda49c60..4c3cf5ffe 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -346,21 +346,19 @@ namespace MediaBrowser.Controller.Session /// Logouts the specified access token. /// </summary> /// <param name="accessToken">The access token.</param> - /// <returns>Task.</returns> void Logout(string accessToken); + void Logout(AuthenticationInfo accessToken); /// <summary> /// Revokes the user tokens. /// </summary> - /// <returns>Task.</returns> void RevokeUserTokens(Guid userId, string currentAccessToken); /// <summary> /// Revokes the token. /// </summary> /// <param name="id">The identifier.</param> - /// <returns>Task.</returns> void RevokeToken(string id); void CloseIfNeeded(SessionInfo session); diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 5da3783bf..6134c0cf3 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -54,7 +54,7 @@ namespace MediaBrowser.Controller.Session public string RemoteEndPoint { get; set; } /// <summary> - /// Gets or sets the playable media types. + /// Gets the playable media types. /// </summary> /// <value>The playable media types.</value> public IReadOnlyList<string> PlayableMediaTypes @@ -230,7 +230,7 @@ namespace MediaBrowser.Controller.Session public string UserPrimaryImageTag { get; set; } /// <summary> - /// Gets or sets the supported commands. + /// Gets the supported commands. /// </summary> /// <value>The supported commands.</value> public IReadOnlyList<GeneralCommandType> SupportedCommands diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs index 4d9b98889..e00cadca2 100644 --- a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs +++ b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs @@ -121,7 +121,9 @@ namespace MediaBrowser.Controller.Sorting return result; } } +#pragma warning disable SA1500 // TODO remove with StyleCop.Analyzers v1.2.0 https://github.com/DotNetAnalyzers/StyleCopAnalyzers/pull/3196 } while (pos1 < len1 && pos2 < len2); +#pragma warning restore SA1500 return len1 - len2; } diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs index 727cbe639..07fe1ea8a 100644 --- a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs @@ -6,7 +6,7 @@ namespace MediaBrowser.Controller.Sorting /// <summary> /// Interface IBaseItemComparer. /// </summary> - public interface IBaseItemComparer : IComparer<BaseItem> + public interface IBaseItemComparer : IComparer<BaseItem?> { /// <summary> /// Gets the name. diff --git a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs deleted file mode 100644 index 3d3e44da0..000000000 --- a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs +++ /dev/null @@ -1,22 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Sync; - -namespace MediaBrowser.Controller.Sync -{ - public interface IHasDynamicAccess - { - /// <summary> - /// Gets the synced file information. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="target">The target.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task<SyncedFileInfo>.</returns> - Task<SyncedFileInfo> GetSyncedFileInfo(string id, SyncTarget target, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs deleted file mode 100644 index b2c53365c..000000000 --- a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MediaBrowser.Controller.Sync -{ - /// <summary> - /// A marker interface. - /// </summary> - public interface IRemoteSyncProvider - { - } -} diff --git a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs deleted file mode 100644 index 3891ac0a6..000000000 --- a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs +++ /dev/null @@ -1,32 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Sync; - -namespace MediaBrowser.Controller.Sync -{ - public interface IServerSyncProvider : ISyncProvider - { - /// <summary> - /// Transfers the file. - /// </summary> - Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, Stream inputStream, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken); - - Task<QueryResult<FileSystemMetadata>> GetFiles(string[] directoryPathParts, SyncTarget target, CancellationToken cancellationToken); - } - - public interface ISupportsDirectCopy - { - /// <summary> - /// Sends the file. - /// </summary> - Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, string inputPath, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Controller/Sync/ISyncProvider.cs b/MediaBrowser.Controller/Sync/ISyncProvider.cs deleted file mode 100644 index ea20014c7..000000000 --- a/MediaBrowser.Controller/Sync/ISyncProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Model.Sync; - -namespace MediaBrowser.Controller.Sync -{ - public interface ISyncProvider - { - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - string Name { get; } - - /// <summary> - /// Gets the synchronize targets. - /// </summary> - /// <param name="userId">The user identifier.</param> - /// <returns>IEnumerable<SyncTarget>.</returns> - List<SyncTarget> GetSyncTargets(string userId); - - /// <summary> - /// Gets all synchronize targets. - /// </summary> - /// <returns>IEnumerable<SyncTarget>.</returns> - List<SyncTarget> GetAllSyncTargets(); - } -} diff --git a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs deleted file mode 100644 index 7eac52299..000000000 --- a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Model.MediaInfo; - -namespace MediaBrowser.Controller.Sync -{ - public class SyncedFileInfo - { - public SyncedFileInfo() - { - RequiredHttpHeaders = new Dictionary<string, string>(); - } - - /// <summary> - /// Gets or sets the path. - /// </summary> - /// <value>The path.</value> - public string Path { get; set; } - - public string[] PathParts { get; set; } - - /// <summary> - /// Gets or sets the protocol. - /// </summary> - /// <value>The protocol.</value> - public MediaProtocol Protocol { get; set; } - - /// <summary> - /// Gets or sets the required HTTP headers. - /// </summary> - /// <value>The required HTTP headers.</value> - public Dictionary<string, string> RequiredHttpHeaders { get; set; } - - /// <summary> - /// Gets or sets the identifier. - /// </summary> - /// <value>The identifier.</value> - public string Id { get; set; } - } -} diff --git a/MediaBrowser.Controller/TV/ITVSeriesManager.cs b/MediaBrowser.Controller/TV/ITVSeriesManager.cs index 291dea04e..e066c03fd 100644 --- a/MediaBrowser.Controller/TV/ITVSeriesManager.cs +++ b/MediaBrowser.Controller/TV/ITVSeriesManager.cs @@ -6,16 +6,26 @@ using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.TV { + /// <summary> + /// The TV Series manager. + /// </summary> public interface ITVSeriesManager { /// <summary> /// Gets the next up. /// </summary> + /// <param name="query">The next up query.</param> + /// <param name="options">The dto options.</param> + /// <returns>The next up items.</returns> QueryResult<BaseItem> GetNextUp(NextUpQuery query, DtoOptions options); /// <summary> /// Gets the next up. /// </summary> + /// <param name="request">The next up request.</param> + /// <param name="parentsFolders">The list of parent folders.</param> + /// <param name="options">The dto options.</param> + /// <returns>The next up items.</returns> QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions options); } } diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs index bc62ca4d5..fdba64c4a 100644 --- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; @@ -15,17 +16,6 @@ namespace MediaBrowser.LocalMetadata.Images /// </summary> public class EpisodeLocalImageProvider : ILocalImageProvider, IHasOrder { - private readonly IFileSystem _fileSystem; - - /// <summary> - /// Initializes a new instance of the <see cref="EpisodeLocalImageProvider"/> class. - /// </summary> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - public EpisodeLocalImageProvider(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - /// <inheritdoc /> public string Name => "Local Images"; @@ -49,14 +39,14 @@ namespace MediaBrowser.LocalMetadata.Images var parentPathFiles = directoryService.GetFiles(parentPath); - var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path); + var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()); return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles); } - private List<LocalImageInfo> GetFilesFromParentFolder(string filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles) + private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles) { - var thumbName = filenameWithoutExtension + "-thumb"; + var thumbName = string.Concat(filenameWithoutExtension, "-thumb"); var list = new List<LocalImageInfo>(1); @@ -67,15 +57,15 @@ namespace MediaBrowser.LocalMetadata.Images continue; } - if (BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)) + if (BaseItem.SupportedImageExtensions.Contains(i.Extension.AsSpan(), StringComparison.OrdinalIgnoreCase)) { - var currentNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(i); + var currentNameWithoutExtension = Path.GetFileNameWithoutExtension(i.FullName.AsSpan()); - if (string.Equals(filenameWithoutExtension, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + if (filenameWithoutExtension.Equals(currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) { list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary }); } - else if (string.Equals(thumbName, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + else if (currentNameWithoutExtension.Equals(thumbName, StringComparison.OrdinalIgnoreCase)) { list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary }); } diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 2e3ea91bf..db9c65a60 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -466,7 +466,7 @@ namespace MediaBrowser.LocalMetadata.Images return added; } - private bool AddImage(IEnumerable<FileSystemMetadata> files, List<LocalImageInfo> images, string name, ImageType type) + private bool AddImage(List<FileSystemMetadata> files, List<LocalImageInfo> images, string name, ImageType type) { var image = GetImage(files, name); @@ -484,9 +484,20 @@ namespace MediaBrowser.LocalMetadata.Images return false; } - private FileSystemMetadata? GetImage(IEnumerable<FileSystemMetadata> files, string name) + private static FileSystemMetadata? GetImage(IReadOnlyList<FileSystemMetadata> files, string name) { - return files.FirstOrDefault(i => !i.IsDirectory && string.Equals(name, _fileSystem.GetFileNameWithoutExtension(i), StringComparison.OrdinalIgnoreCase) && i.Length > 0); + for (var i = 0; i < files.Count; i++) + { + var file = files[i]; + if (!file.IsDirectory + && file.Length > 0 + && Path.GetFileNameWithoutExtension(file.FullName.AsSpan()).Equals(name, StringComparison.OrdinalIgnoreCase)) + { + return file; + } + } + + return null; } } } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index e8aeabf9d..a0ec3bd90 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs index ef9943722..e86e518be 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -9,9 +9,9 @@ namespace MediaBrowser.MediaEncoding.BdInfo { public class BdInfoDirectoryInfo : IDirectoryInfo { - private readonly IFileSystem _fileSystem = null; + private readonly IFileSystem _fileSystem; - private readonly FileSystemMetadata _impl = null; + private readonly FileSystemMetadata _impl; public BdInfoDirectoryInfo(IFileSystem fileSystem, string path) { @@ -29,7 +29,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo public string FullName => _impl.FullName; - public IDirectoryInfo Parent + public IDirectoryInfo? Parent { get { diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs index 0a8af8e9c..41143c259 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs @@ -7,7 +7,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo { public class BdInfoFileInfo : BDInfo.IO.IFileInfo { - private FileSystemMetadata _impl = null; + private FileSystemMetadata _impl; public BdInfoFileInfo(FileSystemMetadata impl) { diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 9e2417603..f782e65bd 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -121,11 +121,11 @@ namespace MediaBrowser.MediaEncoding.Encoder // When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions public static Version MinVersion { get; } = new Version(4, 0); - public static Version MaxVersion { get; } = null; + public static Version? MaxVersion { get; } = null; public bool ValidateVersion() { - string output = null; + string output; try { output = GetProcessOutput(_encoderPath, "-version"); @@ -133,6 +133,7 @@ namespace MediaBrowser.MediaEncoding.Encoder catch (Exception ex) { _logger.LogError(ex, "Error validating encoder"); + return false; } if (string.IsNullOrWhiteSpace(output)) @@ -207,7 +208,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="output">The output from "ffmpeg -version".</param> /// <returns>The FFmpeg version.</returns> - internal Version GetFFmpegVersion(string output) + internal Version? GetFFmpegVersion(string output) { // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)"); @@ -275,7 +276,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private IEnumerable<string> GetHwaccelTypes() { - string output = null; + string? output = null; try { output = GetProcessOutput(_encoderPath, "-hwaccels"); @@ -303,7 +304,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } - string output = null; + string output; try { output = GetProcessOutput(_encoderPath, "-h filter=" + filter); @@ -311,6 +312,7 @@ namespace MediaBrowser.MediaEncoding.Encoder catch (Exception ex) { _logger.LogError(ex, "Error detecting the given filter"); + return false; } if (output.Contains("Filter " + filter, StringComparison.Ordinal)) @@ -331,7 +333,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private IEnumerable<string> GetCodecs(Codec codec) { string codecstr = codec == Codec.Encoder ? "encoders" : "decoders"; - string output = null; + string output; try { output = GetProcessOutput(_encoderPath, "-" + codecstr); @@ -339,6 +341,7 @@ namespace MediaBrowser.MediaEncoding.Encoder catch (Exception ex) { _logger.LogError(ex, "Error detecting available {Codec}", codecstr); + return Enumerable.Empty<string>(); } if (string.IsNullOrWhiteSpace(output)) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 62c0c0bb1..3af618af8 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; @@ -366,7 +367,8 @@ namespace MediaBrowser.MediaEncoding.Encoder public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) { var prefix = "file"; - if (mediaSource.VideoType == VideoType.BluRay) + if (mediaSource.VideoType == VideoType.BluRay + || mediaSource.IsoType == IsoType.BluRay) { prefix = "bluray"; } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 39fb0b47c..7733e715f 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -10,6 +10,7 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> <AnalysisMode>AllEnabledByDefault</AnalysisMode> <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs index da37687e8..1fa90bb21 100644 --- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs +++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs index 0e319c1a8..d4d153b08 100644 --- a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs +++ b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs index de062d06b..a1cef7a9f 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaChapter.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System.Collections.Generic; diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs index 8af122ef9..d50da37b8 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs index 7b7744163..c9c8c34c2 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -1,3 +1,5 @@ +#nullable disable + using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 884ec0a29..bbff5daca 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1,3 +1,4 @@ +#nullable disable #pragma warning disable CS1591 using System; @@ -123,19 +124,67 @@ namespace MediaBrowser.MediaEncoding.Probing { info.Name = title; } + else + { + title = FFProbeHelpers.GetDictionaryValue(tags, "title-eng"); + if (!string.IsNullOrWhiteSpace(title)) + { + info.Name = title; + } + } + + var titleSort = FFProbeHelpers.GetDictionaryValue(tags, "titlesort"); + if (!string.IsNullOrWhiteSpace(titleSort)) + { + info.ForcedSortName = titleSort; + } info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort"); info.ParentIndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "season_number"); info.ShowName = FFProbeHelpers.GetDictionaryValue(tags, "show_name"); info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date"); - // Several different forms of retaildate - info.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? + // Several different forms of retail/premiere date + info.PremiereDate = + FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "date_released") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "date"); + // Set common metadata for music (audio) and music videos (video) + info.Album = FFProbeHelpers.GetDictionaryValue(tags, "album"); + + var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists"); + + if (!string.IsNullOrWhiteSpace(artists)) + { + info.Artists = SplitArtists(artists, new[] { '/', ';' }, false) + .DistinctNames() + .ToArray(); + } + else + { + var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist"); + if (string.IsNullOrWhiteSpace(artist)) + { + info.Artists = Array.Empty<string>(); + } + else + { + info.Artists = SplitArtists(artist, _nameDelimiters, true) + .DistinctNames() + .ToArray(); + } + } + + // If we don't have a ProductionYear try and get it from PremiereDate + if (!info.ProductionYear.HasValue && info.PremiereDate.HasValue) + { + info.ProductionYear = info.PremiereDate.Value.Year; + } + + // Set mediaType-specific metadata if (isAudio) { SetAudioRuntimeTicks(data, info); @@ -1078,13 +1127,13 @@ namespace MediaBrowser.MediaEncoding.Probing private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags) { - var peoples = new List<BaseItemPerson>(); + var people = new List<BaseItemPerson>(); var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer"); if (!string.IsNullOrWhiteSpace(composer)) { foreach (var person in Split(composer, false)) { - peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer }); + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer }); } } @@ -1093,7 +1142,7 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(conductor, false)) { - peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor }); + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor }); } } @@ -1102,46 +1151,21 @@ namespace MediaBrowser.MediaEncoding.Probing { foreach (var person in Split(lyricist, false)) { - peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist }); + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist }); } } // Check for writer some music is tagged that way as alternative to composer/lyricist var writer = FFProbeHelpers.GetDictionaryValue(tags, "writer"); - if (!string.IsNullOrWhiteSpace(writer)) { foreach (var person in Split(writer, false)) { - peoples.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer }); + people.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer }); } } - audio.People = peoples.ToArray(); - audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album"); - - var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists"); - - if (!string.IsNullOrWhiteSpace(artists)) - { - audio.Artists = SplitArtists(artists, new[] { '/', ';' }, false) - .DistinctNames() - .ToArray(); - } - else - { - var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist"); - if (string.IsNullOrWhiteSpace(artist)) - { - audio.Artists = Array.Empty<string>(); - } - else - { - audio.Artists = SplitArtists(artist, _nameDelimiters, true) - .DistinctNames() - .ToArray(); - } - } + audio.People = people.ToArray(); var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist"); if (string.IsNullOrWhiteSpace(albumArtist)) @@ -1176,12 +1200,6 @@ namespace MediaBrowser.MediaEncoding.Probing // Disc number audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc"); - // If we don't have a ProductionYear try and get it from PremiereDate - if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue) - { - audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year; - } - // There's several values in tags may or may not be present FetchStudios(audio, tags, "organization"); FetchStudios(audio, tags, "ensemble"); diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs index 8219aa7b4..08ee5c72e 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs @@ -1,5 +1,3 @@ -#nullable enable - using Microsoft.Extensions.Logging; using Nikse.SubtitleEdit.Core.SubtitleFormats; diff --git a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs deleted file mode 100644 index dca5c1e8a..000000000 --- a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable enable -#pragma warning disable CS1591 - -namespace MediaBrowser.MediaEncoding.Subtitles -{ - public static class ParserValues - { - public const string NewLine = "\r\n"; - } -} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs index 19fb951dc..78d54ca51 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs @@ -1,5 +1,3 @@ -#nullable enable - using Microsoft.Extensions.Logging; using Nikse.SubtitleEdit.Core.SubtitleFormats; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs index 36dc2e01f..17c2ae40e 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs @@ -1,5 +1,3 @@ -#nullable enable - using Microsoft.Extensions.Logging; using Nikse.SubtitleEdit.Core.SubtitleFormats; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs index 82ec6ca21..639a34d99 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Globalization; using System.IO; using System.Linq; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 39bec8da1..608ebf443 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -71,8 +72,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - var reader = GetReader(inputFormat, true); - + var reader = GetReader(inputFormat); var trackInfo = reader.Parse(stream, cancellationToken); FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); @@ -139,10 +139,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles .ConfigureAwait(false); var inputFormat = subtitle.format; - var writer = TryGetWriter(outputFormat); // Return the original if we don't have any way of converting it - if (writer == null) + if (!TryGetWriter(outputFormat, out var writer)) { return subtitle.stream; } @@ -239,7 +238,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) .TrimStart('.'); - if (GetReader(currentFormat, false) == null) + if (TryGetReader(currentFormat, out _)) { // Convert var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt"); @@ -257,37 +256,41 @@ namespace MediaBrowser.MediaEncoding.Subtitles return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true); } - private ISubtitleParser GetReader(string format, bool throwIfMissing) + private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value) { - if (string.IsNullOrEmpty(format)) - { - throw new ArgumentNullException(nameof(format)); - } - if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) { - return new SrtParser(_logger); + value = new SrtParser(_logger); + return true; } if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)) { - return new SsaParser(_logger); + value = new SsaParser(_logger); + return true; } if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) { - return new AssParser(_logger); + value = new AssParser(_logger); + return true; } - if (throwIfMissing) + value = null; + return false; + } + + private ISubtitleParser GetReader(string format) + { + if (TryGetReader(format, out var reader)) { - throw new ArgumentException("Unsupported format: " + format); + return reader; } - return null; + throw new ArgumentException("Unsupported format: " + format); } - private ISubtitleWriter TryGetWriter(string format) + private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value) { if (string.IsNullOrEmpty(format)) { @@ -296,32 +299,35 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase)) { - return new JsonWriter(); + value = new JsonWriter(); + return true; } if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) { - return new SrtWriter(); + value = new SrtWriter(); + return true; } if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase)) { - return new VttWriter(); + value = new VttWriter(); + return true; } if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase)) { - return new TtmlWriter(); + value = new TtmlWriter(); + return true; } - return null; + value = null; + return false; } private ISubtitleWriter GetWriter(string format) { - var writer = TryGetWriter(format); - - if (writer != null) + if (TryGetWriter(format, out var writer)) { return writer; } @@ -391,7 +397,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw new ArgumentNullException(nameof(outputPath)); } - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath))); var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); @@ -549,7 +555,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw new ArgumentNullException(nameof(outputPath)); } - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath))); var processArgs = string.Format( CultureInfo.InvariantCulture, @@ -715,7 +721,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) { - var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName; + var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty; // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal)) @@ -725,7 +731,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles charset = string.Empty; } - _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path); + _logger.LogDebug("charset {0} detected for {Path}", charset, path); return charset; } diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs index bffaa8594..2c9b5f005 100644 --- a/MediaBrowser.Model/Configuration/PathSubstitution.cs +++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace MediaBrowser.Model.Configuration { /// <summary> diff --git a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs index e7fe8d6af..9b39f9e11 100644 --- a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs +++ b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs @@ -2,25 +2,28 @@ namespace MediaBrowser.Model.Dlna { + /// <summary> + /// Delivery method to use during playback of a specific subtitle format. + /// </summary> public enum SubtitleDeliveryMethod { /// <summary> - /// The encode. + /// Burn the subtitles in the video track. /// </summary> Encode = 0, /// <summary> - /// The embed. + /// Embed the subtitles in the file or stream. /// </summary> Embed = 1, /// <summary> - /// The external. + /// Serve the subtitles as an external file. /// </summary> External = 2, /// <summary> - /// The HLS. + /// Serve the subtitles as a separate HLS stream. /// </summary> Hls = 3 } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index e644c9ba7..c67f30d04 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -104,6 +104,19 @@ namespace MediaBrowser.Model.Entities return "HDR"; } + // For some Dolby Vision files, no color transfer is provided, so check the codec + + var codecTag = CodecTag; + + if (string.Equals(codecTag, "dva1", StringComparison.OrdinalIgnoreCase) + || string.Equals(codecTag, "dvav", StringComparison.OrdinalIgnoreCase) + || string.Equals(codecTag, "dvh1", StringComparison.OrdinalIgnoreCase) + || string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase) + || string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase)) + { + return "HDR"; + } + return "SDR"; } } diff --git a/MediaBrowser.Model/Extensions/StringHelper.cs b/MediaBrowser.Model/Extensions/StringHelper.cs index 2d9a6c4db..77cbef00f 100644 --- a/MediaBrowser.Model/Extensions/StringHelper.cs +++ b/MediaBrowser.Model/Extensions/StringHelper.cs @@ -17,7 +17,8 @@ namespace MediaBrowser.Model.Extensions return str; } - if (char.IsUpper(str[0])) + // We check IsLower instead of IsUpper because both return false for non-letters + if (!char.IsLower(str[0])) { return str; } diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index e5c26430a..be4f1e16b 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CS1591 using System; @@ -25,7 +24,7 @@ namespace MediaBrowser.Model.IO /// </summary> /// <param name="filename">The filename.</param> /// <returns>System.String.</returns> - string ResolveShortcut(string filename); + string? ResolveShortcut(string filename); /// <summary> /// Creates the shortcut. @@ -160,7 +159,7 @@ namespace MediaBrowser.Model.IO /// <returns>All found files.</returns> IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false); - IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive); + IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive); /// <summary> /// Gets the file system entries. @@ -186,7 +185,7 @@ namespace MediaBrowser.Model.IO /// <returns>IEnumerable<System.String>.</returns> IEnumerable<string> GetFilePaths(string path, bool recursive = false); - IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive); + IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive); /// <summary> /// Gets the file system entry paths. diff --git a/MediaBrowser.Model/IO/IShortcutHandler.cs b/MediaBrowser.Model/IO/IShortcutHandler.cs index 14d5c4b62..2c364a962 100644 --- a/MediaBrowser.Model/IO/IShortcutHandler.cs +++ b/MediaBrowser.Model/IO/IShortcutHandler.cs @@ -15,7 +15,7 @@ namespace MediaBrowser.Model.IO /// </summary> /// <param name="shortcutPath">The shortcut path.</param> /// <returns>System.String.</returns> - string Resolve(string shortcutPath); + string? Resolve(string shortcutPath); /// <summary> /// Creates the specified shortcut path. diff --git a/MediaBrowser.Model/IO/IStreamHelper.cs b/MediaBrowser.Model/IO/IStreamHelper.cs index 0e09db16e..f900da556 100644 --- a/MediaBrowser.Model/IO/IStreamHelper.cs +++ b/MediaBrowser.Model/IO/IStreamHelper.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Model.IO { public interface IStreamHelper { - Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken); + Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken); Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken); diff --git a/MediaBrowser.Model/MediaInfo/MediaInfo.cs b/MediaBrowser.Model/MediaInfo/MediaInfo.cs index a268a4fa6..453aeb028 100644 --- a/MediaBrowser.Model/MediaInfo/MediaInfo.cs +++ b/MediaBrowser.Model/MediaInfo/MediaInfo.cs @@ -51,6 +51,8 @@ namespace MediaBrowser.Model.MediaInfo public string ShowName { get; set; } + public string ForcedSortName { get; set; } + public int? IndexNumber { get; set; } public int? ParentIndexNumber { get; set; } diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs index b3db57b6d..d5c3a6aec 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Model/Plugins/PluginInfo.cs b/MediaBrowser.Model/Plugins/PluginInfo.cs index 25216610d..8eb90bdb0 100644 --- a/MediaBrowser.Model/Plugins/PluginInfo.cs +++ b/MediaBrowser.Model/Plugins/PluginInfo.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; namespace MediaBrowser.Model.Plugins diff --git a/MediaBrowser.Model/Plugins/PluginPageInfo.cs b/MediaBrowser.Model/Plugins/PluginPageInfo.cs index 85c0aa204..f4d83c28b 100644 --- a/MediaBrowser.Model/Plugins/PluginPageInfo.cs +++ b/MediaBrowser.Model/Plugins/PluginPageInfo.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace MediaBrowser.Model.Plugins { /// <summary> diff --git a/MediaBrowser.Model/Querying/EpisodeQuery.cs b/MediaBrowser.Model/Querying/EpisodeQuery.cs deleted file mode 100644 index 56a7f3320..000000000 --- a/MediaBrowser.Model/Querying/EpisodeQuery.cs +++ /dev/null @@ -1,75 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Querying -{ - public class EpisodeQuery - { - public EpisodeQuery() - { - Fields = Array.Empty<ItemFields>(); - } - - /// <summary> - /// Gets or sets the user identifier. - /// </summary> - /// <value>The user identifier.</value> - public string UserId { get; set; } - - /// <summary> - /// Gets or sets the season identifier. - /// </summary> - /// <value>The season identifier.</value> - public string SeasonId { get; set; } - - /// <summary> - /// Gets or sets the series identifier. - /// </summary> - /// <value>The series identifier.</value> - public string SeriesId { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is missing. - /// </summary> - /// <value><c>null</c> if [is missing] contains no value, <c>true</c> if [is missing]; otherwise, <c>false</c>.</value> - public bool? IsMissing { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is virtual unaired. - /// </summary> - /// <value><c>null</c> if [is virtual unaired] contains no value, <c>true</c> if [is virtual unaired]; otherwise, <c>false</c>.</value> - public bool? IsVirtualUnaired { get; set; } - - /// <summary> - /// Gets or sets the season number. - /// </summary> - /// <value>The season number.</value> - public int? SeasonNumber { get; set; } - - /// <summary> - /// Gets or sets the fields. - /// </summary> - /// <value>The fields.</value> - public ItemFields[] Fields { get; set; } - - /// <summary> - /// Gets or sets the start index. - /// </summary> - /// <value>The start index.</value> - public int? StartIndex { get; set; } - - /// <summary> - /// Gets or sets the limit. - /// </summary> - /// <value>The limit.</value> - public int? Limit { get; set; } - - /// <summary> - /// Gets or sets the start item identifier. - /// </summary> - /// <value>The start item identifier.</value> - public string StartItemId { get; set; } - } -} diff --git a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs b/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs deleted file mode 100644 index b800f5de5..000000000 --- a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs +++ /dev/null @@ -1,47 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Querying -{ - public class MovieRecommendationQuery - { - public MovieRecommendationQuery() - { - ItemLimit = 10; - CategoryLimit = 6; - Fields = Array.Empty<ItemFields>(); - } - - /// <summary> - /// Gets or sets the user identifier. - /// </summary> - /// <value>The user identifier.</value> - public string UserId { get; set; } - - /// <summary> - /// Gets or sets the parent identifier. - /// </summary> - /// <value>The parent identifier.</value> - public string ParentId { get; set; } - - /// <summary> - /// Gets or sets the item limit. - /// </summary> - /// <value>The item limit.</value> - public int ItemLimit { get; set; } - - /// <summary> - /// Gets or sets the category limit. - /// </summary> - /// <value>The category limit.</value> - public int CategoryLimit { get; set; } - - /// <summary> - /// Gets or sets the fields. - /// </summary> - /// <value>The fields.</value> - public ItemFields[] Fields { get; set; } - } -} diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index 0555afc00..fa8aa829d 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Model.Querying EnableImageTypes = Array.Empty<ImageType>(); EnableTotalRecordCount = true; DisableFirstEpisode = false; + NextUpDateCutoff = DateTime.MinValue; } /// <summary> @@ -75,5 +76,10 @@ namespace MediaBrowser.Model.Querying /// Gets or sets a value indicating whether do disable sending first episode as next up. /// </summary> public bool DisableFirstEpisode { get; set; } + + /// <summary> + /// Gets or sets a value indicating the oldest date for a show to appear in Next Up. + /// </summary> + public DateTime NextUpDateCutoff { get; set; } } } diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs deleted file mode 100644 index 2cf0f0d5f..000000000 --- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs +++ /dev/null @@ -1,64 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Model.Querying -{ - public class UpcomingEpisodesQuery - { - public UpcomingEpisodesQuery() - { - EnableImageTypes = Array.Empty<ImageType>(); - } - - /// <summary> - /// Gets or sets the user id. - /// </summary> - /// <value>The user id.</value> - public string UserId { get; set; } - - /// <summary> - /// Gets or sets the parent identifier. - /// </summary> - /// <value>The parent identifier.</value> - public string ParentId { get; set; } - - /// <summary> - /// Gets or sets the start index. Use for paging. - /// </summary> - /// <value>The start index.</value> - public int? StartIndex { get; set; } - - /// <summary> - /// Gets or sets the maximum number of items to return. - /// </summary> - /// <value>The limit.</value> - public int? Limit { get; set; } - - /// <summary> - /// Gets or sets the fields to return within the items, in addition to basic information. - /// </summary> - /// <value>The fields.</value> - public ItemFields[] Fields { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [enable images]. - /// </summary> - /// <value><c>null</c> if [enable images] contains no value, <c>true</c> if [enable images]; otherwise, <c>false</c>.</value> - public bool? EnableImages { get; set; } - - /// <summary> - /// Gets or sets the image type limit. - /// </summary> - /// <value>The image type limit.</value> - public int? ImageTypeLimit { get; set; } - - /// <summary> - /// Gets or sets the enable image types. - /// </summary> - /// <value>The enable image types.</value> - public ImageType[] EnableImageTypes { get; set; } - } -} diff --git a/MediaBrowser.Model/Sync/SyncCategory.cs b/MediaBrowser.Model/Sync/SyncCategory.cs deleted file mode 100644 index 1248c2f73..000000000 --- a/MediaBrowser.Model/Sync/SyncCategory.cs +++ /dev/null @@ -1,22 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Sync -{ - public enum SyncCategory - { - /// <summary> - /// The latest. - /// </summary> - Latest = 0, - - /// <summary> - /// The next up. - /// </summary> - NextUp = 1, - - /// <summary> - /// The resume. - /// </summary> - Resume = 2 - } -} diff --git a/MediaBrowser.Model/Sync/SyncJob.cs b/MediaBrowser.Model/Sync/SyncJob.cs deleted file mode 100644 index 3e396e5d1..000000000 --- a/MediaBrowser.Model/Sync/SyncJob.cs +++ /dev/null @@ -1,135 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Sync -{ - public class SyncJob - { - public SyncJob() - { - RequestedItemIds = Array.Empty<Guid>(); - } - - /// <summary> - /// Gets or sets the identifier. - /// </summary> - /// <value>The identifier.</value> - public string Id { get; set; } - - /// <summary> - /// Gets or sets the device identifier. - /// </summary> - /// <value>The device identifier.</value> - public string TargetId { get; set; } - - /// <summary> - /// Gets or sets the name of the target. - /// </summary> - /// <value>The name of the target.</value> - public string TargetName { get; set; } - - /// <summary> - /// Gets or sets the quality. - /// </summary> - /// <value>The quality.</value> - public string Quality { get; set; } - - /// <summary> - /// Gets or sets the bitrate. - /// </summary> - /// <value>The bitrate.</value> - public int? Bitrate { get; set; } - - /// <summary> - /// Gets or sets the profile. - /// </summary> - /// <value>The profile.</value> - public string Profile { get; set; } - - /// <summary> - /// Gets or sets the category. - /// </summary> - /// <value>The category.</value> - public SyncCategory? Category { get; set; } - - /// <summary> - /// Gets or sets the parent identifier. - /// </summary> - /// <value>The parent identifier.</value> - public string ParentId { get; set; } - - /// <summary> - /// Gets or sets the current progress. - /// </summary> - /// <value>The current progress.</value> - public double? Progress { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the status. - /// </summary> - /// <value>The status.</value> - public SyncJobStatus Status { get; set; } - - /// <summary> - /// Gets or sets the user identifier. - /// </summary> - /// <value>The user identifier.</value> - public string UserId { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [unwatched only]. - /// </summary> - /// <value><c>true</c> if [unwatched only]; otherwise, <c>false</c>.</value> - public bool UnwatchedOnly { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [synchronize new content]. - /// </summary> - /// <value><c>true</c> if [synchronize new content]; otherwise, <c>false</c>.</value> - public bool SyncNewContent { get; set; } - - /// <summary> - /// Gets or sets the item limit. - /// </summary> - /// <value>The item limit.</value> - public int? ItemLimit { get; set; } - - /// <summary> - /// Gets or sets the requested item ids. - /// </summary> - /// <value>The requested item ids.</value> - public Guid[] RequestedItemIds { get; set; } - - /// <summary> - /// Gets or sets the date created. - /// </summary> - /// <value>The date created.</value> - public DateTime DateCreated { get; set; } - - /// <summary> - /// Gets or sets the date last modified. - /// </summary> - /// <value>The date last modified.</value> - public DateTime DateLastModified { get; set; } - - /// <summary> - /// Gets or sets the item count. - /// </summary> - /// <value>The item count.</value> - public int ItemCount { get; set; } - - public string ParentName { get; set; } - - public string PrimaryImageItemId { get; set; } - - public string PrimaryImageTag { get; set; } - } -} diff --git a/MediaBrowser.Model/Sync/SyncJobStatus.cs b/MediaBrowser.Model/Sync/SyncJobStatus.cs deleted file mode 100644 index 226a47d4c..000000000 --- a/MediaBrowser.Model/Sync/SyncJobStatus.cs +++ /dev/null @@ -1,15 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Sync -{ - public enum SyncJobStatus - { - Queued = 0, - Converting = 1, - ReadyToTransfer = 2, - Transferring = 3, - Completed = 4, - CompletedWithError = 5, - Failed = 6 - } -} diff --git a/MediaBrowser.Model/Sync/SyncTarget.cs b/MediaBrowser.Model/Sync/SyncTarget.cs deleted file mode 100644 index 9e6bbbc00..000000000 --- a/MediaBrowser.Model/Sync/SyncTarget.cs +++ /dev/null @@ -1,20 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Sync -{ - public class SyncTarget - { - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the identifier. - /// </summary> - /// <value>The identifier.</value> - public string Id { get; set; } - } -} diff --git a/MediaBrowser.Model/Tasks/ITaskTrigger.cs b/MediaBrowser.Model/Tasks/ITaskTrigger.cs index cbd60cca1..999db9605 100644 --- a/MediaBrowser.Model/Tasks/ITaskTrigger.cs +++ b/MediaBrowser.Model/Tasks/ITaskTrigger.cs @@ -11,12 +11,12 @@ namespace MediaBrowser.Model.Tasks /// <summary> /// Fires when the trigger condition is satisfied and the task should run. /// </summary> - event EventHandler<EventArgs> Triggered; + event EventHandler<EventArgs>? Triggered; /// <summary> - /// Gets or sets the options of this task. + /// Gets the options of this task. /// </summary> - TaskOptions TaskOptions { get; set; } + TaskOptions TaskOptions { get; } /// <summary> /// Stars waiting for the trigger action. diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs index 7a82685f0..aeaaa8b35 100644 --- a/MediaBrowser.Model/Updates/PackageInfo.cs +++ b/MediaBrowser.Model/Updates/PackageInfo.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -16,7 +15,6 @@ namespace MediaBrowser.Model.Updates public PackageInfo() { Versions = Array.Empty<VersionInfo>(); - Id = string.Empty; Category = string.Empty; Name = string.Empty; Overview = string.Empty; @@ -65,7 +63,7 @@ namespace MediaBrowser.Model.Updates /// </summary> /// <value>The name.</value> [JsonPropertyName("guid")] - public string Id { get; set; } + public Guid Id { get; set; } /// <summary> /// Gets or sets the versions. diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index 209092265..03a540dde 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Text.Json.Serialization; using SysVersion = System.Version; diff --git a/MediaBrowser.Model/Users/UserAction.cs b/MediaBrowser.Model/Users/UserAction.cs deleted file mode 100644 index 7646db4a8..000000000 --- a/MediaBrowser.Model/Users/UserAction.cs +++ /dev/null @@ -1,24 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Users -{ - public class UserAction - { - public string Id { get; set; } - - public string ServerId { get; set; } - - public Guid UserId { get; set; } - - public Guid ItemId { get; set; } - - public UserActionType Type { get; set; } - - public DateTime Date { get; set; } - - public long? PositionTicks { get; set; } - } -} diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 4471a25b2..966a3d822 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Manager /// <summary> /// Image types that are only one per item. /// </summary> - private readonly ImageType[] _singularImages = + private static readonly ImageType[] _singularImages = { ImageType.Primary, ImageType.Art, @@ -208,9 +208,14 @@ namespace MediaBrowser.Providers.Manager /// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns> private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit, int screenshotLimit) { - if (_singularImages.Any(i => images.Contains(i) && !HasImage(item, i) && savedOptions.GetLimit(i) > 0)) + // Using .Any causes the creation of a DisplayClass aka. variable capture + for (var i = 0; i < _singularImages.Length; i++) { - return false; + var type = _singularImages[i]; + if (images.Contains(type) && !HasImage(item, type) && savedOptions.GetLimit(type) > 0) + { + return false; + } } if (images.Contains(ImageType.Backdrop) && item.GetImages(ImageType.Backdrop).Count() < backdropLimit) @@ -329,7 +334,7 @@ namespace MediaBrowser.Providers.Manager var deleted = false; var deletedImages = new List<ItemImageInfo>(); - foreach (var image in item.GetImages(type).ToList()) + foreach (var image in item.GetImages(type)) { if (!image.IsLocalFile) { @@ -359,9 +364,10 @@ namespace MediaBrowser.Providers.Manager { var changed = false; - foreach (var type in _singularImages) + for (var i = 0; i < _singularImages.Length; i++) { - var image = images.FirstOrDefault(i => i.Type == type); + var type = _singularImages[i]; + var image = GetFirstLocalImageInfoByType(images, type); if (image != null) { @@ -423,15 +429,29 @@ namespace MediaBrowser.Providers.Manager return changed; } + private static LocalImageInfo GetFirstLocalImageInfoByType(IReadOnlyList<LocalImageInfo> images, ImageType type) + { + var len = images.Count; + for (var i = 0; i < len; i++) + { + var image = images[i]; + if (image.Type == type) + { + return image; + } + } + + return null; + } + private bool UpdateMultiImages(BaseItem item, List<LocalImageInfo> images, ImageType type) { var changed = false; - var newImages = images.Where(i => i.Type == type).ToList(); - - var newImageFileInfos = newImages - .Select(i => i.FileInfo) - .ToList(); + var newImageFileInfos = images + .FindAll(i => i.Type == type) + .Select(i => i.FileInfo) + .ToList(); if (item.AddImages(type, newImageFileInfos)) { diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 401c7e99f..827cb69b9 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -28,8 +28,11 @@ namespace MediaBrowser.Providers.Manager ProviderManager = providerManager; FileSystem = fileSystem; LibraryManager = libraryManager; + ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem); } + protected ItemImageProvider ImageProvider { get; } + protected IServerConfigurationManager ServerConfigurationManager { get; } protected ILogger<MetadataService<TItemType, TIdType>> Logger { get; } @@ -88,7 +91,6 @@ namespace MediaBrowser.Providers.Manager } } - var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem); var localImagesFailed = false; var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList(); @@ -97,7 +99,7 @@ namespace MediaBrowser.Providers.Manager try { // Always validate images and check for new locally stored ones. - if (itemImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService)) + if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService)) { updateType |= ItemUpdateType.ImageUpdate; } @@ -143,7 +145,7 @@ namespace MediaBrowser.Providers.Manager // await FindIdentities(id, cancellationToken).ConfigureAwait(false); id.IsAutomated = refreshOptions.IsAutomated; - var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, itemImageProvider, cancellationToken).ConfigureAwait(false); + var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false); updateType |= result.UpdateType; if (result.Failures > 0) @@ -160,7 +162,7 @@ namespace MediaBrowser.Providers.Manager if (providers.Count > 0) { - var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false); + var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false); updateType |= result.UpdateType; if (result.Failures > 0) @@ -211,9 +213,23 @@ namespace MediaBrowser.Providers.Manager private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result) { - lookupInfo.ProviderIds = result.ProviderIds; - lookupInfo.Name = result.Name; - lookupInfo.Year = result.ProductionYear; + // Episode and Season do not support Identify, so the search results are the Series' + switch (lookupInfo) + { + case EpisodeInfo episodeInfo: + episodeInfo.SeriesProviderIds = result.ProviderIds; + episodeInfo.ProviderIds.Clear(); + break; + case SeasonInfo seasonInfo: + seasonInfo.SeriesProviderIds = result.ProviderIds; + seasonInfo.ProviderIds.Clear(); + break; + default: + lookupInfo.ProviderIds = result.ProviderIds; + lookupInfo.Name = result.Name; + lookupInfo.Year = result.ProductionYear; + break; + } } protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken) @@ -563,7 +579,7 @@ namespace MediaBrowser.Providers.Manager protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options) { // Get providers to refresh - var providers = allImageProviders.Where(i => !(i is ILocalImageProvider)).ToList(); + var providers = allImageProviders.Where(i => !(i is ILocalImageProvider)); var dateLastImageRefresh = item.DateLastRefreshed; @@ -575,15 +591,13 @@ namespace MediaBrowser.Providers.Manager providers = providers .Where(i => { - var hasFileChangeMonitor = i as IHasItemChangeMonitor; - if (hasFileChangeMonitor != null) + if (i is IHasItemChangeMonitor hasFileChangeMonitor) { return HasChanged(item, hasFileChangeMonitor, options.DirectoryService); } return false; - }) - .ToList(); + }); } return providers; diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index dd497845d..2dfaa372c 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1111,7 +1111,7 @@ namespace MediaBrowser.Providers.Manager await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false); break; case Folder folder: - await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false); + await folder.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false); break; } } @@ -1122,7 +1122,7 @@ namespace MediaBrowser.Providers.Manager { await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); - await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false); + await child.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false); } } @@ -1144,7 +1144,7 @@ namespace MediaBrowser.Providers.Manager .Select(i => i.MusicArtist) .Where(i => i != null); - var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true)); + var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), options, true, cancellationToken)); await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs index 5621d2b86..e5aa64b28 100644 --- a/MediaBrowser.Providers/Manager/ProviderUtils.cs +++ b/MediaBrowser.Providers/Manager/ProviderUtils.cs @@ -55,7 +55,7 @@ namespace MediaBrowser.Providers.Manager } } - if (replaceData || !target.CommunityRating.HasValue || (source.CommunityRating.HasValue && string.Equals(sourceResult.Provider, "The Open Movie Database", StringComparison.OrdinalIgnoreCase))) + if (replaceData || !target.CommunityRating.HasValue) { target.CommunityRating = source.CommunityRating; } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs index 945463666..cf271e7db 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs @@ -111,6 +111,11 @@ namespace MediaBrowser.Providers.MediaInfo audio.Name = data.Name; } + if (!string.IsNullOrEmpty(data.ForcedSortName)) + { + audio.ForcedSortName = data.ForcedSortName; + } + if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { var people = new List<PersonInfo>(); diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index f049cc81f..12e1fbea5 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -147,7 +147,8 @@ namespace MediaBrowser.Providers.MediaInfo { Path = path, Protocol = protocol, - VideoType = item.VideoType + VideoType = item.VideoType, + IsoType = item.IsoType } }, cancellationToken); @@ -391,6 +392,12 @@ namespace MediaBrowser.Providers.MediaInfo } } + if (video is MusicVideo musicVideo) + { + musicVideo.Album = data.Album; + musicVideo.Artists = data.Artists; + } + if (data.ProductionYear.HasValue) { if (!video.ProductionYear.HasValue || isFullRefresh) @@ -433,6 +440,11 @@ namespace MediaBrowser.Providers.MediaInfo video.Name = data.Name; } } + + if (!string.IsNullOrWhiteSpace(data.ForcedSortName)) + { + video.ForcedSortName = data.ForcedSortName; + } } // If we don't have a ProductionYear try and get it from PremiereDate diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index e9f999c6d..3cd7ec772 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -15,17 +14,6 @@ namespace MediaBrowser.Providers.MediaInfo { private readonly ILocalizationManager _localization; - private static readonly HashSet<string> SubtitleExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) - { - ".srt", - ".ssa", - ".ass", - ".sub", - ".smi", - ".sami", - ".vtt" - }; - public SubtitleResolver(ILocalizationManager localization) { _localization = localization; @@ -66,138 +54,142 @@ namespace MediaBrowser.Providers.MediaInfo return streams; } - public List<string> GetExternalSubtitleFiles( + public IEnumerable<string> GetExternalSubtitleFiles( Video video, IDirectoryService directoryService, bool clearCache) { - var list = new List<string>(); - if (!video.IsFileProtocol) { - return list; + yield break; } var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache); foreach (var stream in streams) { - list.Add(stream.Path); + yield return stream.Path; } - - return list; - } - - private void AddExternalSubtitleStreams( - List<MediaStream> streams, - string folder, - string videoPath, - int startIndex, - IDirectoryService directoryService, - bool clearCache) - { - var files = directoryService.GetFilePaths(folder, clearCache).OrderBy(i => i).ToArray(); - - AddExternalSubtitleStreams(streams, videoPath, startIndex, files); } public void AddExternalSubtitleStreams( List<MediaStream> streams, string videoPath, int startIndex, - string[] files) + IReadOnlyList<string> files) { - var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(videoPath); - videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoFileNameWithoutExtension); + var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath); - foreach (var fullName in files) + for (var i = 0; i < files.Count; i++) { - var extension = Path.GetExtension(fullName); - - if (!SubtitleExtensions.Contains(extension)) - { - continue; - } - - var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullName); - fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fileNameWithoutExtension); - - if (!string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase) && - !fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase)) + var fullName = files[i]; + var extension = Path.GetExtension(fullName.AsSpan()); + if (!IsSubtitleExtension(extension)) { continue; } - var codec = Path.GetExtension(fullName).ToLowerInvariant().TrimStart('.'); + var fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fullName); - if (string.Equals(codec, "txt", StringComparison.OrdinalIgnoreCase)) - { - codec = "srt"; - } + MediaStream mediaStream; - // If the subtitle file matches the video file name - if (string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + // The subtitle filename must either be equal to the video filename or start with the video filename followed by a dot + if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) { - streams.Add(new MediaStream + mediaStream = new MediaStream { Index = startIndex++, Type = MediaStreamType.Subtitle, IsExternal = true, - Path = fullName, - Codec = codec - }); + Path = fullName + }; } - else if (fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase)) + else if (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length + && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.' + && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) { - var isForced = fullName.IndexOf(".forced.", StringComparison.OrdinalIgnoreCase) != -1 || - fullName.IndexOf(".foreign.", StringComparison.OrdinalIgnoreCase) != -1; + var isForced = fullName.Contains(".forced.", StringComparison.OrdinalIgnoreCase) + || fullName.Contains(".foreign.", StringComparison.OrdinalIgnoreCase); - var isDefault = fullName.IndexOf(".default.", StringComparison.OrdinalIgnoreCase) != -1; + var isDefault = fullName.Contains(".default.", StringComparison.OrdinalIgnoreCase); // Support xbmc naming conventions - 300.spanish.srt - var language = fileNameWithoutExtension - .Replace(".forced", string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(".foreign", string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(".default", string.Empty, StringComparison.OrdinalIgnoreCase) - .Split('.') - .LastOrDefault(); + var languageSpan = fileNameWithoutExtension; + while (languageSpan.Length > 0) + { + var lastDot = languageSpan.LastIndexOf('.'); + var currentSlice = languageSpan[lastDot..]; + if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase) + || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase) + || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase)) + { + languageSpan = languageSpan[..lastDot]; + continue; + } + + languageSpan = languageSpan[(lastDot + 1)..]; + break; + } // Try to translate to three character code // Be flexible and check against both the full and three character versions + var language = languageSpan.ToString(); var culture = _localization.FindLanguageInfo(language); - if (culture != null) - { - language = culture.ThreeLetterISOLanguageName; - } + language = culture == null ? language : culture.ThreeLetterISOLanguageName; - streams.Add(new MediaStream + mediaStream = new MediaStream { Index = startIndex++, Type = MediaStreamType.Subtitle, IsExternal = true, Path = fullName, - Codec = codec, Language = language, IsForced = isForced, IsDefault = isDefault - }); + }; + } + else + { + continue; } + + mediaStream.Codec = extension.TrimStart('.').ToString().ToLowerInvariant(); + + streams.Add(mediaStream); } } - private string NormalizeFilenameForSubtitleComparison(string filename) + private static bool IsSubtitleExtension(ReadOnlySpan<char> extension) + { + return extension.Equals(".srt", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".ssa", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".ass", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".sub", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".vtt", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".smi", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".sami", StringComparison.OrdinalIgnoreCase); + } + + private static ReadOnlySpan<char> NormalizeFilenameForSubtitleComparison(string filename) { // Try to account for sloppy file naming filename = filename.Replace("_", string.Empty, StringComparison.Ordinal); filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal); + return Path.GetFileNameWithoutExtension(filename.AsSpan()); + } - // can't normalize this due to languages such as pt-br - // filename = filename.Replace("-", string.Empty); - - // filename = filename.Replace(".", string.Empty); + private void AddExternalSubtitleStreams( + List<MediaStream> streams, + string folder, + string videoPath, + int startIndex, + IDirectoryService directoryService, + bool clearCache) + { + var files = directoryService.GetFilePaths(folder, clearCache, true); - return filename; + AddExternalSubtitleStreams(streams, videoPath, startIndex, files); } } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs index ce9392402..2eab95294 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs @@ -69,58 +69,52 @@ namespace MediaBrowser.Providers.Music private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream) { - using (var oReader = new StreamReader(stream, Encoding.UTF8)) + using var oReader = new StreamReader(stream, Encoding.UTF8); + var settings = new XmlReaderSettings() { - var settings = new XmlReaderSettings() - { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true - }; + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; - using (var reader = XmlReader.Create(oReader, settings)) - { - reader.MoveToContent(); - reader.Read(); + using var reader = XmlReader.Create(oReader, settings); + reader.MoveToContent(); + reader.Read(); - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) { - if (reader.NodeType == XmlNodeType.Element) + case "artist-list": { - switch (reader.Name) + if (reader.IsEmptyElement) { - case "artist-list": - { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subReader = reader.ReadSubtree()) - { - return ParseArtistList(subReader).ToList(); - } - } - - default: - { - reader.Skip(); - break; - } + reader.Read(); + continue; } + + using var subReader = reader.ReadSubtree(); + return ParseArtistList(subReader).ToList(); } - else + + default: { - reader.Read(); + reader.Skip(); + break; } } - - return Enumerable.Empty<RemoteSearchResult>(); + } + else + { + reader.Read(); } } + + return Enumerable.Empty<RemoteSearchResult>(); } private IEnumerable<RemoteSearchResult> ParseArtistList(XmlReader reader) @@ -145,13 +139,11 @@ namespace MediaBrowser.Providers.Music var mbzId = reader.GetAttribute("id"); - using (var subReader = reader.ReadSubtree()) + using var subReader = reader.ReadSubtree(); + var artist = ParseArtist(subReader, mbzId); + if (artist != null) { - var artist = ParseArtist(subReader, mbzId); - if (artist != null) - { - yield return artist; - } + yield return artist; } break; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index e5ad0f3e0..0023d5959 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -128,53 +128,49 @@ namespace MediaBrowser.Providers.Music private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream) { - using (var oReader = new StreamReader(stream, Encoding.UTF8)) + using var oReader = new StreamReader(stream, Encoding.UTF8); + var settings = new XmlReaderSettings() + { + ValidationType = ValidationType.None, + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true + }; + + using var reader = XmlReader.Create(oReader, settings); + var results = ReleaseResult.Parse(reader); + + return results.Select(i => { - var settings = new XmlReaderSettings() + var result = new RemoteSearchResult { - ValidationType = ValidationType.None, - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true + Name = i.Title, + ProductionYear = i.Year }; - using (var reader = XmlReader.Create(oReader, settings)) + if (i.Artists.Count > 0) { - var results = ReleaseResult.Parse(reader); - - return results.Select(i => + result.AlbumArtist = new RemoteSearchResult { - var result = new RemoteSearchResult - { - Name = i.Title, - ProductionYear = i.Year - }; - - if (i.Artists.Count > 0) - { - result.AlbumArtist = new RemoteSearchResult - { - SearchProviderName = Name, - Name = i.Artists[0].Item1 - }; + SearchProviderName = Name, + Name = i.Artists[0].Item1 + }; - result.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, i.Artists[0].Item2); - } - - if (!string.IsNullOrWhiteSpace(i.ReleaseId)) - { - result.SetProviderId(MetadataProvider.MusicBrainzAlbum, i.ReleaseId); - } + result.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, i.Artists[0].Item2); + } - if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId)) - { - result.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, i.ReleaseGroupId); - } + if (!string.IsNullOrWhiteSpace(i.ReleaseId)) + { + result.SetProviderId(MetadataProvider.MusicBrainzAlbum, i.ReleaseId); + } - return result; - }); + if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId)) + { + result.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, i.ReleaseGroupId); } - } + + return result; + }); } /// <inheritdoc /> @@ -339,10 +335,8 @@ namespace MediaBrowser.Providers.Music continue; } - using (var subReader = reader.ReadSubtree()) - { - return ParseReleaseList(subReader).ToList(); - } + using var subReader = reader.ReadSubtree(); + return ParseReleaseList(subReader).ToList(); } default: @@ -383,13 +377,11 @@ namespace MediaBrowser.Providers.Music var releaseId = reader.GetAttribute("id"); - using (var subReader = reader.ReadSubtree()) + using var subReader = reader.ReadSubtree(); + var release = ParseRelease(subReader, releaseId); + if (release != null) { - var release = ParseRelease(subReader, releaseId); - if (release != null) - { - yield return release; - } + yield return release; } break; @@ -460,14 +452,12 @@ namespace MediaBrowser.Providers.Music case "artist-credit": { - using (var subReader = reader.ReadSubtree()) - { - var artist = ParseArtistCredit(subReader); + using var subReader = reader.ReadSubtree(); + var artist = ParseArtistCredit(subReader); - if (!string.IsNullOrEmpty(artist.Item1)) - { - result.Artists.Add(artist); - } + if (!string.IsNullOrEmpty(artist.Item1)) + { + result.Artists.Add(artist); } break; @@ -505,12 +495,10 @@ namespace MediaBrowser.Providers.Music switch (reader.Name) { case "name-credit": - { - using (var subReader = reader.ReadSubtree()) - { - return ParseArtistNameCredit(subReader); - } - } + { + using var subReader = reader.ReadSubtree(); + return ParseArtistNameCredit(subReader); + } default: { @@ -545,10 +533,8 @@ namespace MediaBrowser.Providers.Music case "artist": { var id = reader.GetAttribute("id"); - using (var subReader = reader.ReadSubtree()) - { - return ParseArtistArtistCredit(subReader, id); - } + using var subReader = reader.ReadSubtree(); + return ParseArtistArtistCredit(subReader, id); } default: @@ -647,47 +633,43 @@ namespace MediaBrowser.Providers.Music IgnoreComments = true }; - using (var reader = XmlReader.Create(oReader, settings)) - { - reader.MoveToContent(); - reader.Read(); + using var reader = XmlReader.Create(oReader, settings); + reader.MoveToContent(); + reader.Read(); - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) { - if (reader.NodeType == XmlNodeType.Element) + switch (reader.Name) { - switch (reader.Name) + case "release-group-list": { - case "release-group-list": + if (reader.IsEmptyElement) { - if (reader.IsEmptyElement) - { - reader.Read(); - continue; - } - - using (var subReader = reader.ReadSubtree()) - { - return GetFirstReleaseGroupId(subReader); - } + reader.Read(); + continue; } - default: - { - reader.Skip(); - break; - } + using var subReader = reader.ReadSubtree(); + return GetFirstReleaseGroupId(subReader); + } + + default: + { + reader.Skip(); + break; } - } - else - { - reader.Read(); } } - - return null; + else + { + reader.Read(); + } } + + return null; } private string GetFirstReleaseGroupId(XmlReader reader) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index d22c1b50a..4a0884c07 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -206,12 +206,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies if (ourRelease != null) { - var ratingPrefix = string.Equals(info.MetadataCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? string.Empty : info.MetadataCountryCode + "-"; - var newRating = ratingPrefix + ourRelease.Certification; - - newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase); - - movie.OfficialRating = newRating; + movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification); } else if (usRelease != null) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index bf42ceade..e4c908a62 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 -using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -55,14 +54,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return Enumerable.Empty<RemoteImageInfo>(); } - var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), cancellationToken).ConfigureAwait(false); + var language = item.GetPreferredMetadataLanguage(); + var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), language, cancellationToken).ConfigureAwait(false); if (personResult?.Images?.Profiles == null) { return Enumerable.Empty<RemoteImageInfo>(); } var remoteImages = new RemoteImageInfo[personResult.Images.Profiles.Count]; - var language = item.GetPreferredMetadataLanguage(); for (var i = 0; i < personResult.Images.Profiles.Count; i++) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 1757c8267..6db550b1d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -32,7 +31,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People { if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId)) { - var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), cancellationToken).ConfigureAwait(false); + var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); if (personResult != null) { @@ -96,7 +95,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People if (personTmdbId > 0) { - var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false); + var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false); result.HasMetadata = true; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 496e1ae25..da76345b5 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -300,7 +300,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (ourRelease != null) { - series.OfficialRating = ourRelease.Rating; + series.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Rating); } else if (usRelease != null) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index 05e5d3ced..79ec6139d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -276,11 +276,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id. /// </summary> /// <param name="personTmdbId">The person's TMDb id.</param> + /// <param name="language">The episode's language.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb person information or null if not found.</returns> - public async Task<Person> GetPersonAsync(int personTmdbId, CancellationToken cancellationToken) + public async Task<Person> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken) { - var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}"; + var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}-{language}"; if (_memoryCache.TryGetValue(key, out Person person)) { return person; @@ -290,6 +291,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb person = await _tmDbClient.GetPersonAsync( personTmdbId, + TmdbUtils.NormalizeLanguage(language), PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 2498ce9c4..b713736a0 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -148,6 +148,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb if (parts.Length == 2) { + // TMDB doesn't support Switzerland (de-CH, it-CH or fr-CH) so use the language (de, it or fr) without country code + if (string.Equals(parts[1], "CH", StringComparison.OrdinalIgnoreCase)) + { + return parts[0]; + } + language = parts[0] + "-" + parts[1].ToUpperInvariant(); } @@ -173,5 +179,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb return imageLanguage; } + + /// <summary> + /// Combines the metadata country code and the parental rating from the Api into the value we store in our database. + /// </summary> + /// <param name="countryCode">The Iso 3166-1 country code of the rating country.</param> + /// <param name="ratingValue">The rating value returned by the Tmdb Api.</param> + /// <returns>The combined parental rating of country code+rating value.</returns> + public static string BuildParentalRating(string countryCode, string ratingValue) + { + // exclude US because we store us values as TV-14 without the country code. + var ratingPrefix = string.Equals(countryCode, "US", StringComparison.OrdinalIgnoreCase) ? string.Empty : countryCode + "-"; + var newRating = ratingPrefix + ratingValue; + + return newRating.Replace("DE-", "FSK-", StringComparison.OrdinalIgnoreCase); + } } } diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index 5ec9a02cb..f6153dd53 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -172,23 +172,19 @@ namespace MediaBrowser.Providers.Studios public IEnumerable<string> GetAvailableImages(string file) { - using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); + using var reader = new StreamReader(fileStream); + var lines = new List<string>(); + + foreach (var line in reader.ReadAllLines()) { - using (var reader = new StreamReader(fileStream)) + if (!string.IsNullOrWhiteSpace(line)) { - var lines = new List<string>(); - - foreach (var line in reader.ReadAllLines()) - { - if (!string.IsNullOrWhiteSpace(line)) - { - lines.Add(line); - } - } - - return lines; + lines.Add(line); } } + + return lines; } } } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index bf0c853ae..6aacaa15d 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -187,48 +187,46 @@ namespace MediaBrowser.Providers.Subtitles { var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia; - using (var stream = response.Stream) - using (var memoryStream = new MemoryStream()) - { - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; + using var stream = response.Stream; + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; - var savePaths = new List<string>(); - var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); + var savePaths = new List<string>(); + var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); - if (response.IsForced) - { - saveFileName += ".forced"; - } + if (response.IsForced) + { + saveFileName += ".forced"; + } - saveFileName += "." + response.Format.ToLowerInvariant(); + saveFileName += "." + response.Format.ToLowerInvariant(); - if (saveInMediaFolder) + if (saveInMediaFolder) + { + var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName)); + // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path."); + if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)) { - var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName)); - // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path."); - if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)) - { - savePaths.Add(mediaFolderPath); - } + savePaths.Add(mediaFolderPath); } + } - var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); + var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); - // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path."); - if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) - { - savePaths.Add(internalPath); - } + // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path."); + if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) + { + savePaths.Add(internalPath); + } - if (savePaths.Count > 0) - { - await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); - } - else - { - _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid."); - } + if (savePaths.Count > 0) + { + await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); + } + else + { + _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid."); } } @@ -247,10 +245,8 @@ namespace MediaBrowser.Providers.Subtitles Directory.CreateDirectory(Path.GetDirectoryName(savePath)); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true)) - { - await stream.CopyToAsync(fs).ConfigureAwait(false); - } + using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true); + await stream.CopyToAsync(fs).ConfigureAwait(false); return; } diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index f448ad38b..e49c0e77b 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; @@ -42,7 +43,7 @@ namespace Rssdp.Infrastructure private HttpResponseParser _ResponseParser; private readonly ILogger _logger; private ISocketFactory _SocketFactory; - private readonly INetworkManager _networkManager; + private readonly INetworkManager _networkManager; private int _LocalPort; private int _MulticastTtl; @@ -68,7 +69,7 @@ namespace Rssdp.Infrastructure INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding) { - + } /// <summary> @@ -358,7 +359,7 @@ namespace Rssdp.Infrastructure { // Not support IPv6 right now continue; - } + } try { diff --git a/debian/postinst b/debian/postinst index 860222e05..2f9c4cffb 100644 --- a/debian/postinst +++ b/debian/postinst @@ -9,6 +9,8 @@ if [[ -f $DEFAULT_FILE ]]; then . $DEFAULT_FILE fi +JELLYFIN_USER=${JELLYFIN_USER:-jellyfin} + # Data directories for program data (cache, db), configs, and logs PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME} CONFIGDATA=${JELLYFIN_CONFIG_DIRECTORY-/etc/$NAME} @@ -18,12 +20,12 @@ CACHEDATA=${JELLYFIN_CACHE_DIRECTORY-/var/cache/$NAME} case "$1" in configure) # create jellyfin group if it does not exist - if [[ -z "$(getent group jellyfin)" ]]; then - addgroup --quiet --system jellyfin > /dev/null 2>&1 + if [[ -z "$(getent group ${JELLYFIN_USER})" ]]; then + addgroup --quiet --system ${JELLYFIN_USER} > /dev/null 2>&1 fi # create jellyfin user if it does not exist - if [[ -z "$(getent passwd jellyfin)" ]]; then - adduser --system --ingroup jellyfin --shell /bin/false jellyfin --no-create-home --home ${PROGRAMDATA} \ + if [[ -z "$(getent passwd ${JELLYFIN_USER})" ]]; then + adduser --system --ingroup ${JELLYFIN_USER} --shell /bin/false ${JELLYFIN_USER} --no-create-home --home ${PROGRAMDATA} \ --gecos "Jellyfin default user" > /dev/null 2>&1 fi # ensure $PROGRAMDATA exists @@ -43,7 +45,7 @@ case "$1" in mkdir $CACHEDATA fi # Ensure permissions are correct on all config directories - chown -R jellyfin $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA + chown -R ${JELLYFIN_USER} $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA chgrp adm $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA chmod 0750 $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64 index ec0321f47..4c426b6d5 100644 --- a/deployment/Dockerfile.debian.amd64 +++ b/deployment/Dockerfile.debian.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64 index 8fd5ddb93..7ed6d52bc 100644 --- a/deployment/Dockerfile.debian.arm64 +++ b/deployment/Dockerfile.debian.arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf index 14615d19f..b46cceaa4 100644 --- a/deployment/Dockerfile.debian.armhf +++ b/deployment/Dockerfile.debian.armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64 index 1f6ca1558..a0e23557a 100644 --- a/deployment/Dockerfile.linux.amd64 +++ b/deployment/Dockerfile.linux.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl index 6af5d8baf..af0f55f8e 100644 --- a/deployment/Dockerfile.linux.amd64-musl +++ b/deployment/Dockerfile.linux.amd64-musl @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64 index 15b59e29d..ba004bb6a 100644 --- a/deployment/Dockerfile.linux.arm64 +++ b/deployment/Dockerfile.linux.arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf index 71a0fda21..0d1114c01 100644 --- a/deployment/Dockerfile.linux.armhf +++ b/deployment/Dockerfile.linux.armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos index 9291bcbb9..b57dc53f5 100644 --- a/deployment/Dockerfile.macos +++ b/deployment/Dockerfile.macos @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable index e98ba74f8..3783dfacf 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -15,7 +15,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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 d1fd8818e..663a7af9e 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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 8e79d417c..83eb24e42 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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 627caa95a..1187f37b9 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64 index 5723abcae..8b2361f0b 100644 --- a/deployment/Dockerfile.windows.amd64 +++ b/deployment/Dockerfile.windows.amd64 @@ -15,7 +15,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/5f0f07ab-cd9a-4498-a9f7-67d90d582180/2a3db6698751e6cbb93ec244cb81cc5f/dotnet-sdk-5.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 000000000..652de0a45 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1 @@ +Findings diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj new file mode 100644 index 000000000..791cb140d --- /dev/null +++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net5.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Emby.Server.Implementations"> + <HintPath>Emby.Server.Implementations.dll</HintPath> + </Reference> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="SharpFuzz" Version="1.6.2" /> + </ItemGroup> + +</Project> diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs new file mode 100644 index 000000000..a4a6f5f54 --- /dev/null +++ b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs @@ -0,0 +1,32 @@ +using System; +using Emby.Server.Implementations.Library; +using SharpFuzz; + +namespace Emby.Server.Implementations.Fuzz +{ + public static class Program + { + public static void Main(string[] args) + { + switch (args[0]) + { + case "PathExtensions.TryReplaceSubPath": Run(PathExtensions_TryReplaceSubPath); return; + default: throw new ArgumentException($"Unknown fuzzing function: {args[0]}"); + } + } + + private static void Run(Action<string> action) => Fuzzer.OutOfProcess.Run(action); + + private static void PathExtensions_TryReplaceSubPath(string data) + { + // Stupid, but it worked + var parts = data.Split(':'); + if (parts.Length != 3) + { + return; + } + + _ = PathExtensions.TryReplaceSubPath(parts[0], parts[1], parts[2], out _); + } + } +} diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt b/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt new file mode 100644 index 000000000..aacf973d6 --- /dev/null +++ b/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt @@ -0,0 +1 @@ +/fuzz/Emby.Server.Implementations.Fuzz/Testcases/PathExtensions.TryReplaceSubPath/test1.txt/:/home/bond/dev/jellyfin/:/srv/jellyfin/ diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh new file mode 100755 index 000000000..244f73402 --- /dev/null +++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -e + +dotnet build -c Release ../../Emby.Server.Implementations/Emby.Server.Implementations.csproj --output bin +sharpfuzz bin/Emby.Server.Implementations.dll +cp bin/Emby.Server.Implementations.dll . + +dotnet build +mkdir -p Findings +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 -m 10240 dotnet bin/Debug/net5.0/Emby.Server.Implementations.Fuzz.dll "$1" diff --git a/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj new file mode 100644 index 000000000..6fcfbae0e --- /dev/null +++ b/fuzz/Jellyfin.Server.Fuzz/Jellyfin.Server.Fuzz.csproj @@ -0,0 +1,22 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net5.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Jellyfin.Server"> + <HintPath>jellyfin.dll</HintPath> + </Reference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="SharpFuzz" Version="1.6.2" /> + </ItemGroup> + +</Project> diff --git a/fuzz/Jellyfin.Server.Fuzz/Program.cs b/fuzz/Jellyfin.Server.Fuzz/Program.cs new file mode 100644 index 000000000..e47286c13 --- /dev/null +++ b/fuzz/Jellyfin.Server.Fuzz/Program.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Server.Middleware; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; +using SharpFuzz; + +namespace Emby.Server.Implementations.Fuzz +{ + public static class Program + { + public static void Main(string[] args) + { + switch (args[0]) + { + case "UrlDecodeQueryFeature": Run(UrlDecodeQueryFeature); return; + default: throw new ArgumentException($"Unknown fuzzing function: {args[0]}"); + } + } + + private static void Run(Action<string> action) => Fuzzer.OutOfProcess.Run(action); + + private static void UrlDecodeQueryFeature(string data) + { + var dict = new Dictionary<string, StringValues> + { + { data, StringValues.Empty } + }; + _ = new UrlDecodeQueryFeature(new QueryFeature(new QueryCollection(dict))); + } + } +} diff --git a/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt b/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt new file mode 100644 index 000000000..73f356b93 --- /dev/null +++ b/fuzz/Jellyfin.Server.Fuzz/Testcases/UrlDecodeQueryFeature/test1.txt @@ -0,0 +1 @@ +a%3D1%26b%3D2%26c%3D3 diff --git a/fuzz/Jellyfin.Server.Fuzz/fuzz.sh b/fuzz/Jellyfin.Server.Fuzz/fuzz.sh new file mode 100755 index 000000000..ad81e2c35 --- /dev/null +++ b/fuzz/Jellyfin.Server.Fuzz/fuzz.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -e + +dotnet build -c Release ../../Jellyfin.Server/Jellyfin.Server.csproj --output bin +sharpfuzz bin/jellyfin.dll +cp bin/jellyfin.dll . + +dotnet build +mkdir -p Findings +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 -m 10240 dotnet bin/Debug/net5.0/Jellyfin.Server.Fuzz.dll "$1" diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 19c0a08b2..44bc34369 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -1,12 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0"> <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers"> - <!-- disable warning SA1202: 'public' members must come before 'private' members --> - <Rule Id="SA1202" Action="Info" /> - <!-- disable warning SA1204: Static members must appear before non-static members --> - <Rule Id="SA1204" Action="Info" /> - <!-- disable warning SA1404: Code analysis suppression should have justification --> - <Rule Id="SA1404" Action="Info" /> + <!-- disable warning CA1040: Avoid empty interfaces --> + <Rule Id="CA1040" Action="Info" /> <!-- disable warning SA1009: Closing parenthesis should be followed by a space. --> <Rule Id="SA1009" Action="None" /> @@ -22,6 +18,10 @@ <Rule Id="SA1130" Action="None" /> <!-- disable warning SA1200: 'using' directive must appear within a namespace declaration --> <Rule Id="SA1200" Action="None" /> + <!-- disable warning SA1202: 'public' members must come before 'private' members --> + <Rule Id="SA1202" Action="None" /> + <!-- disable warning SA1204: Static members must appear before non-static members --> + <Rule Id="SA1204" Action="None" /> <!-- disable warning SA1309: Fields must not begin with an underscore --> <Rule Id="SA1309" Action="None" /> <!-- disable warning SA1413: Use trailing comma in multi-line initializers --> @@ -43,6 +43,8 @@ or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token --> <Rule Id="CA2016" Action="Error" /> + <!-- disable warning CA1024: Use properties where appropriate --> + <Rule Id="CA1024" Action="Info" /> <!-- disable warning CA1031: Do not catch general exception types --> <Rule Id="CA1031" Action="Info" /> <!-- disable warning CA1032: Implement standard exception constructors --> diff --git a/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs new file mode 100644 index 000000000..117083815 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using AutoFixture; +using AutoFixture.AutoMoq; +using Jellyfin.Api.Controllers; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests.Controllers +{ + public class DynamicHlsControllerTests + { + [Theory] + [MemberData(nameof(GetSegmentLengths_Success_TestData))] + public void GetSegmentLengths_Success(long runtimeTicks, int segmentlength, double[] expected) + { + var res = DynamicHlsController.GetSegmentLengthsInternal(runtimeTicks, segmentlength); + Assert.Equal(expected.Length, res.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], res[i]); + } + } + + public static IEnumerable<object[]> GetSegmentLengths_Success_TestData() + { + yield return new object[] { 0, 6, Array.Empty<double>() }; + yield return new object[] + { + TimeSpan.FromSeconds(3).Ticks, + 6, + new double[] { 3 } + }; + yield return new object[] + { + TimeSpan.FromSeconds(6).Ticks, + 6, + new double[] { 6 } + }; + yield return new object[] + { + TimeSpan.FromSeconds(3.3333333).Ticks, + 6, + new double[] { 3.3333333 } + }; + yield return new object[] + { + TimeSpan.FromSeconds(9.3333333).Ticks, + 6, + new double[] { 6, 3.3333333 } + }; + } + } +} diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 397b863b7..d4ea91872 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -18,9 +18,9 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.5" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" /> <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 8018b2966..546b2487e 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -15,10 +15,11 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> + <PackageReference Include="FsCheck.Xunit" Version="2.15.3" /> </ItemGroup> <!-- Code Analyzers --> diff --git a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs index 9ded01f2b..7629d9912 100644 --- a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs +++ b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs @@ -1,34 +1,45 @@ -using System.Text.Json; +using System.Globalization; +using System.Text.Json; +using FsCheck; +using FsCheck.Xunit; using MediaBrowser.Common.Json.Converters; using Xunit; namespace Jellyfin.Common.Tests.Json { - public static class JsonBoolNumberTests + public class JsonBoolNumberTests { + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() + { + Converters = + { + new JsonBoolNumberConverter() + } + }; + [Theory] [InlineData("1", true)] [InlineData("0", false)] [InlineData("2", true)] [InlineData("true", true)] [InlineData("false", false)] - public static void Deserialize_Number_Valid_Success(string input, bool? output) + public void Deserialize_Number_Valid_Success(string input, bool? output) { - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonBoolNumberConverter()); - var value = JsonSerializer.Deserialize<bool>(input, options); + var value = JsonSerializer.Deserialize<bool>(input, _jsonOptions); Assert.Equal(value, output); } [Theory] [InlineData(true, "true")] [InlineData(false, "false")] - public static void Serialize_Bool_Success(bool input, string output) + public void Serialize_Bool_Success(bool input, string output) { - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonBoolNumberConverter()); - var value = JsonSerializer.Serialize(input, options); + var value = JsonSerializer.Serialize(input, _jsonOptions); Assert.Equal(value, output); } + + [Property] + public Property Deserialize_NonZeroInt_True(NonZeroInt input) + => JsonSerializer.Deserialize<bool>(input.ToString(), _jsonOptions).ToProperty(); } -}
\ No newline at end of file +} diff --git a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs index fd77694b3..2b23c6705 100644 --- a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs +++ b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs @@ -6,14 +6,13 @@ namespace Jellyfin.Common.Tests.Json { public class JsonStringConverterTests { - private readonly JsonSerializerOptions _jsonSerializerOptions - = new () + private readonly JsonSerializerOptions _jsonSerializerOptions = new () + { + Converters = { - Converters = - { - new JsonStringConverter() - } - }; + new JsonStringConverter() + } + }; [Theory] [InlineData("\"test\"", "test")] @@ -36,4 +35,4 @@ namespace Jellyfin.Common.Tests.Json Assert.Equal(deserialized, output); } } -}
\ No newline at end of file +} diff --git a/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs b/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs new file mode 100644 index 000000000..576c0a49b --- /dev/null +++ b/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs @@ -0,0 +1,19 @@ +using System; +using MediaBrowser.Controller.Extensions; +using Xunit; + +namespace Jellyfin.Controller.Extensions.Tests +{ + public class StringExtensionsTests + { + [Theory] + [InlineData("", '_', 0)] + [InlineData("___", '_', 3)] + [InlineData("test\x00", '\x00', 1)] + [InlineData("Imdb=tt0119567|Tmdb=330|TmdbCollection=328", '|', 2)] + public void ReadOnlySpan_Count_Success(string str, char needle, int count) + { + Assert.Equal(count, str.AsSpan().Count(needle)); + } + } +} diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index ad1627698..9a8ddafa0 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -15,7 +15,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj index f7c21f072..1f6cd541c 100644 --- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj +++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj @@ -10,7 +10,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index 8321d0255..6b828e113 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -21,7 +21,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 69e2aa437..98fbb00d5 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Globalization; using System.IO; using System.Text.Json; using MediaBrowser.Common.Json; @@ -17,9 +19,9 @@ namespace Jellyfin.MediaEncoding.Tests.Probing [Fact] public void GetMediaInfo_MetaData_Success() { - var bytes = File.ReadAllBytes("Test Data/Probing/some_matadata.json"); + var bytes = File.ReadAllBytes("Test Data/Probing/video_metadata.json"); var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions); - MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/some_matadata.mkv", MediaProtocol.File); + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File); Assert.Single(res.MediaStreams); @@ -52,5 +54,22 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Empty(res.Chapters); Assert.Equal("Just color bars", res.Overview); } + + [Fact] + public void GetMediaInfo_MusicVideo_Success() + { + var bytes = File.ReadAllBytes("Test Data/Probing/music_video_metadata.json"); + var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions); + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/music_video.mkv", MediaProtocol.File); + + Assert.Equal("The Title", res.Name); + Assert.Equal("Title, The", res.ForcedSortName); + Assert.Single(res.Artists); + Assert.Equal("The Artist", res.Artists[0]); + Assert.Equal("Album", res.Album); + Assert.Equal(2021, res.ProductionYear); + Assert.True(res.PremiereDate.HasValue); + Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate); + } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json new file mode 100644 index 000000000..97d6600a4 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_video_metadata.json @@ -0,0 +1,111 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "High", + "codec_type": "video", + "codec_time_base": "1001/48000", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 1920, + "height": 1080, + "coded_width": 1920, + "coded_height": 1088, + "closed_captions": 0, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "16:9", + "pix_fmt": "yuv420p", + "level": 42, + "chroma_location": "left", + "field_order": "progressive", + "refs": 1, + "is_avc": "true", + "nal_length_size": "4", + "r_frame_rate": "24000/1001", + "avg_frame_rate": "24000/1001", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "bits_per_raw_sample": "8", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_time_base": "1/48000", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0 + }, + "tags": { + "language": "eng" + } + } + ], + "chapters": [ + ], + "format": { + "filename": "music_video.mkv", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "matroska,webm", + "format_long_name": "Matroska / WebM", + "start_time": "0.000000", + "duration": "180.000000", + "size": "500000000", + "bit_rate": "22222222", + "probe_score": 100, + "tags": { + "TITLE-eng": "The Title", + "TITLESORT": "Title, The", + "ARTIST": "The Artist", + "ARTISTSORT": "Artist, The", + "ALBUM": "Album", + "DATE_RELEASED": "2021-01-01" + } + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/some_matadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json index 720fc5c8f..720fc5c8f 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/some_matadata.json +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json diff --git a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs index 5864a0509..0a4e060df 100644 --- a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs +++ b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs @@ -1,3 +1,6 @@ +using System; +using FsCheck; +using FsCheck.Xunit; using MediaBrowser.Model.Extensions; using Xunit; @@ -10,9 +13,20 @@ namespace Jellyfin.Model.Tests.Extensions [InlineData("banana", "Banana")] [InlineData("Banana", "Banana")] [InlineData("ä", "Ä")] + [InlineData("\027", "\027")] public void StringHelper_ValidArgs_Success(string input, string expectedResult) { Assert.Equal(expectedResult, StringHelper.FirstToUpper(input)); } + + [Property] + public Property FirstToUpper_RandomArg_Correct(NonEmptyString input) + { + var result = StringHelper.FirstToUpper(input.Item); + + // We check IsLower instead of IsUpper because both return false for non-letters + return (!char.IsLower(result[0])).Label("First char is uppercase") + .And(input.Item.Length == 1 || result[1..].Equals(input.Item[1..], StringComparison.Ordinal)).Label("Remaining chars are unmodified"); + } } } diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj index c5b51ef76..40c51e524 100644 --- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -10,10 +10,11 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> + <PackageReference Include="FsCheck.Xunit" Version="2.15.3" /> </ItemGroup> <!-- Code Analyzers --> diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index ebb134fc3..e386cb8c1 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -15,7 +15,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index 950899d7e..b1141df47 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -58,7 +58,7 @@ namespace Jellyfin.Naming.Tests.Video { input = Path.GetFileName(input); - var result = new VideoResolver(_namingOptions).CleanDateTime(input); + var result = VideoResolver.CleanDateTime(input, _namingOptions); Assert.Equal(expectedName, result.Name, true); Assert.Equal(expectedYear, result.Year); diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs index a720bdade..fb050cf5a 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs @@ -7,7 +7,7 @@ namespace Jellyfin.Naming.Tests.Video { public sealed class CleanStringTests { - private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions()); + private readonly NamingOptions _namingOptions = new NamingOptions(); [Theory] [InlineData("Super movie 480p.mp4", "Super movie")] @@ -26,7 +26,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")] public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName) { - Assert.True(_videoResolver.TryCleanString(input, out ReadOnlySpan<char> newName)); + Assert.True(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName)); // TODO: compare spans when XUnit supports it Assert.Equal(expectedName, newName.ToString()); } @@ -41,7 +41,7 @@ namespace Jellyfin.Naming.Tests.Video [InlineData("Run lola run (lola rennt) (2009).mp4")] public void CleanStringTest_DoesntNeedCleaning_False(string? input) { - Assert.False(_videoResolver.TryCleanString(input, out ReadOnlySpan<char> newName)); + Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName)); Assert.True(newName.IsEmpty); } } diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index 2f173b0ce..f872f94f8 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -104,13 +104,6 @@ namespace Jellyfin.Naming.Tests.Video Assert.Equal(rule, res.Rule); } - [Fact] - public void TestFlagsParser() - { - var flags = new FlagParser(_videoOptions).GetFlags(string.Empty); - Assert.Empty(flags); - } - private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions) { return new ExtraResolver(videoOptions); diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs index 69de96a47..1762b91b9 100644 --- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs @@ -22,8 +22,7 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void Test3DName() { - var result = - new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv"); + var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions); Assert.Equal("hsbs", result?.Format3D); Assert.Equal("Oblivion", result?.Name); @@ -58,15 +57,13 @@ namespace Jellyfin.Naming.Tests.Video private void Test(string input, bool is3D, string? format3D) { - var parser = new Format3DParser(_namingOptions); - - var result = parser.Parse(input); + var result = Format3DParser.Parse(input, _namingOptions); Assert.Equal(is3D, result.Is3D); if (format3D == null) { - Assert.Null(result.Format3D); + Assert.Null(result?.Format3D); } else { diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 6e803593e..d02f8ae92 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video { public class MultiVersionTests { - private readonly VideoListResolver _videoListResolver = new VideoListResolver(new NamingOptions()); + private readonly NamingOptions _namingOptions = new NamingOptions(); [Fact] public void TestMultiEdition1() @@ -22,11 +22,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Single(result[0].Extras); @@ -43,11 +45,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Single(result[0].Extras); @@ -63,11 +67,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Single(result[0].AlternateVersions); @@ -87,11 +93,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/M/Movie 7.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(7, result.Count); Assert.Empty(result[0].Extras); @@ -113,11 +121,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Movie/Movie-8.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -140,11 +150,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Mo/Movie 9.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(9, result.Count); Assert.Empty(result[0].Extras); @@ -163,11 +175,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Movie/Movie 5.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(5, result.Count); Assert.Empty(result[0].Extras); @@ -188,11 +202,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man (2011).mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(5, result.Count); Assert.Empty(result[0].Extras); @@ -214,11 +230,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man[test].mkv", }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -243,11 +261,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man [test].mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -266,11 +286,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man - C (2007).mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(2, result.Count); } @@ -289,11 +311,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man_3d.hsbs.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(7, result.Count); Assert.Empty(result[0].Extras); @@ -314,11 +338,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Iron Man/Iron Man (2011).mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(5, result.Count); Assert.Empty(result[0].Extras); @@ -334,11 +360,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -354,11 +382,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -374,11 +404,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); Assert.Empty(result[0].Extras); @@ -394,11 +426,13 @@ namespace Jellyfin.Naming.Tests.Video @"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(2, result.Count); } @@ -406,7 +440,7 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void TestEmptyList() { - var result = _videoListResolver.Resolve(new List<FileSystemMetadata>()).ToList(); + var result = VideoListResolver.Resolve(new List<FileSystemMetadata>(), _namingOptions).ToList(); Assert.Empty(result); } diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs index 6e759c6d6..1d50df7a6 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs @@ -29,8 +29,7 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void TestStubName() { - var result = - new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc"); + var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions); Assert.Equal("Oblivion", result?.Name); } diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index 08af76669..9e0776c3c 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video { public class VideoListResolverTests { - private readonly VideoListResolver _videoListResolver = new VideoListResolver(new NamingOptions()); + private readonly NamingOptions _namingOptions = new NamingOptions(); [Fact] public void TestStackAndExtras() @@ -40,11 +40,13 @@ namespace Jellyfin.Naming.Tests.Video "WillyWonka-trailer.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(5, result.Count); var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal)); @@ -67,11 +69,13 @@ namespace Jellyfin.Naming.Tests.Video "300.nfo" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -85,11 +89,13 @@ namespace Jellyfin.Naming.Tests.Video "300 trailer.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -103,11 +109,13 @@ namespace Jellyfin.Naming.Tests.Video "X-Men Days of Future Past-trailer.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -122,11 +130,13 @@ namespace Jellyfin.Naming.Tests.Video "X-Men Days of Future Past-trailer2.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -140,11 +150,13 @@ namespace Jellyfin.Naming.Tests.Video "Looper.2012.bluray.720p.x264.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -162,11 +174,13 @@ namespace Jellyfin.Naming.Tests.Video "My video 5.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(5, result.Count); } @@ -180,11 +194,13 @@ namespace Jellyfin.Naming.Tests.Video @"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = true, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = true, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -199,11 +215,13 @@ namespace Jellyfin.Naming.Tests.Video @"My movie #2.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = true, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = true, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(2, result.Count); } @@ -218,11 +236,13 @@ namespace Jellyfin.Naming.Tests.Video @"No (2012) part1-trailer.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -237,11 +257,13 @@ namespace Jellyfin.Naming.Tests.Video @"No (2012)-trailer.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -257,11 +279,13 @@ namespace Jellyfin.Naming.Tests.Video @"trailer.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -277,11 +301,13 @@ namespace Jellyfin.Naming.Tests.Video @"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(2, result.Count); } @@ -294,11 +320,13 @@ namespace Jellyfin.Naming.Tests.Video @"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -311,11 +339,13 @@ namespace Jellyfin.Naming.Tests.Video @"The Colony.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -329,11 +359,13 @@ namespace Jellyfin.Naming.Tests.Video @"Four Sisters and a Wedding - B.avi" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -347,11 +379,13 @@ namespace Jellyfin.Naming.Tests.Video @"Four Rooms - A.mp4" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(2, result.Count); } @@ -365,11 +399,13 @@ namespace Jellyfin.Naming.Tests.Video @"/Server/Despicable Me/movie-trailer.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } @@ -385,11 +421,13 @@ namespace Jellyfin.Naming.Tests.Video @"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Equal(4, result.Count); } @@ -403,11 +441,13 @@ namespace Jellyfin.Naming.Tests.Video @"/Movies/Despicable Me/trailers/trailer.mkv" }; - var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata - { - IsDirectory = false, - FullName = i - }).ToList()).ToList(); + var result = VideoListResolver.Resolve( + files.Select(i => new FileSystemMetadata + { + IsDirectory = false, + FullName = i + }).ToList(), + _namingOptions).ToList(); Assert.Single(result); } diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs index 9bbbe2970..ac5a7a21e 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video { public class VideoResolverTests { - private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions()); + private static NamingOptions _namingOptions = new NamingOptions(); public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData() { @@ -148,7 +148,7 @@ namespace Jellyfin.Naming.Tests.Video yield return new object[] { new VideoFileInfo( - path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", + path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4", container: "mp4", name: "Rain Man", year: 1988) @@ -159,27 +159,27 @@ namespace Jellyfin.Naming.Tests.Video [MemberData(nameof(ResolveFile_ValidFileNameTestData))] public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult) { - var result = _videoResolver.ResolveFile(expectedResult.Path); + var result = VideoResolver.ResolveFile(expectedResult.Path, _namingOptions); Assert.NotNull(result); - Assert.Equal(result?.Path, expectedResult.Path); - Assert.Equal(result?.Container, expectedResult.Container); - Assert.Equal(result?.Name, expectedResult.Name); - Assert.Equal(result?.Year, expectedResult.Year); - Assert.Equal(result?.ExtraType, expectedResult.ExtraType); - Assert.Equal(result?.Format3D, expectedResult.Format3D); - Assert.Equal(result?.Is3D, expectedResult.Is3D); - Assert.Equal(result?.IsStub, expectedResult.IsStub); - Assert.Equal(result?.StubType, expectedResult.StubType); - Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory); - Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension); - Assert.Equal(result?.ToString(), expectedResult.ToString()); + Assert.Equal(result!.Path, expectedResult.Path); + Assert.Equal(result.Container, expectedResult.Container); + Assert.Equal(result.Name, expectedResult.Name); + Assert.Equal(result.Year, expectedResult.Year); + Assert.Equal(result.ExtraType, expectedResult.ExtraType); + Assert.Equal(result.Format3D, expectedResult.Format3D); + Assert.Equal(result.Is3D, expectedResult.Is3D); + Assert.Equal(result.IsStub, expectedResult.IsStub); + Assert.Equal(result.StubType, expectedResult.StubType); + Assert.Equal(result.IsDirectory, expectedResult.IsDirectory); + Assert.Equal(result.FileNameWithoutExtension.ToString(), expectedResult.FileNameWithoutExtension.ToString()); + Assert.Equal(result.ToString(), expectedResult.ToString()); } [Fact] public void ResolveFile_EmptyPath() { - var result = _videoResolver.ResolveFile(string.Empty); + var result = VideoResolver.ResolveFile(string.Empty, _namingOptions); Assert.Null(result); } @@ -194,12 +194,16 @@ namespace Jellyfin.Naming.Tests.Video string.Empty }; - var results = paths.Select(path => _videoResolver.ResolveDirectory(path)).ToList(); + var results = paths.Select(path => VideoResolver.ResolveDirectory(path, _namingOptions)).ToList(); Assert.Equal(3, results.Count); Assert.NotNull(results[0]); Assert.NotNull(results[1]); Assert.Null(results[2]); + foreach (var result in results) + { + Assert.Null(result?.Container); + } } } } diff --git a/tests/Jellyfin.Networking.Tests/IPHostTests.cs b/tests/Jellyfin.Networking.Tests/IPHostTests.cs new file mode 100644 index 000000000..ec3a1300c --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/IPHostTests.cs @@ -0,0 +1,53 @@ +using FsCheck; +using FsCheck.Xunit; +using MediaBrowser.Common.Net; +using Xunit; + +namespace Jellyfin.Networking.Tests +{ + public static class IPHostTests + { + /// <summary> + /// Checks IP address formats. + /// </summary> + /// <param name="address">IP Address.</param> + [Theory] + [InlineData("127.0.0.1")] + [InlineData("127.0.0.1:123")] + [InlineData("localhost")] + [InlineData("localhost:1345")] + [InlineData("www.google.co.uk")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")] + [InlineData("fe80::7add:12ff:febb:c67b%16")] + [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] + [InlineData("fe80::7add:12ff:febb:c67b%16:123")] + [InlineData("[fe80::7add:12ff:febb:c67b%16]")] + [InlineData("192.168.1.2/255.255.255.0")] + [InlineData("192.168.1.2/24")] + public static void TryParse_ValidHostStrings_True(string address) + => Assert.True(IPHost.TryParse(address, out _)); + + [Property] + public static Property TryParse_IPv4Address_True(IPv4Address address) + => IPHost.TryParse(address.Item.ToString(), out _).ToProperty(); + + [Property] + public static Property TryParse_IPv6Address_True(IPv6Address address) + => IPHost.TryParse(address.Item.ToString(), out _).ToProperty(); + + /// <summary> + /// All should be invalid address strings. + /// </summary> + /// <param name="address">Invalid address strings.</param> + [Theory] + [InlineData("256.128.0.0.0.1")] + [InlineData("127.0.0.1#")] + [InlineData("localhost!")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] + public static void TryParse_InvalidAddressString_False(string address) + => Assert.False(IPHost.TryParse(address, out _)); + } +} diff --git a/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs b/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs new file mode 100644 index 000000000..aa2dbc57a --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs @@ -0,0 +1,49 @@ +using FsCheck; +using FsCheck.Xunit; +using MediaBrowser.Common.Net; +using Xunit; + +namespace Jellyfin.Networking.Tests +{ + public static class IPNetAddressTests + { + /// <summary> + /// Checks IP address formats. + /// </summary> + /// <param name="address">IP Address.</param> + [Theory] + [InlineData("127.0.0.1")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")] + [InlineData("fe80::7add:12ff:febb:c67b%16")] + [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] + [InlineData("fe80::7add:12ff:febb:c67b%16:123")] + [InlineData("[fe80::7add:12ff:febb:c67b%16]")] + [InlineData("192.168.1.2/255.255.255.0")] + [InlineData("192.168.1.2/24")] + public static void TryParse_ValidIPStrings_True(string address) + => Assert.True(IPNetAddress.TryParse(address, out _)); + + [Property] + public static Property TryParse_IPv4Address_True(IPv4Address address) + => IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty(); + + [Property] + public static Property TryParse_IPv6Address_True(IPv6Address address) + => IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty(); + + /// <summary> + /// All should be invalid address strings. + /// </summary> + /// <param name="address">Invalid address strings.</param> + [Theory] + [InlineData("256.128.0.0.0.1")] + [InlineData("127.0.0.1#")] + [InlineData("localhost!")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] + public static void TryParse_InvalidAddressString_False(string address) + => Assert.False(IPNetAddress.TryParse(address, out _)); + } +} diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index d5268facc..97bf673ae 100644 --- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -15,10 +15,11 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> + <PackageReference Include="FsCheck.Xunit" Version="2.15.3" /> <PackageReference Include="Moq" Version="4.16.1" /> </ItemGroup> diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 671b8598d..97c14d463 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -57,66 +57,6 @@ namespace Jellyfin.Networking.Tests } /// <summary> - /// Checks IP address formats. - /// </summary> - /// <param name="address">IP Address.</param> - [Theory] - [InlineData("127.0.0.1")] - [InlineData("127.0.0.1:123")] - [InlineData("localhost")] - [InlineData("localhost:1345")] - [InlineData("www.google.co.uk")] - [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] - [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] - [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")] - [InlineData("fe80::7add:12ff:febb:c67b%16")] - [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] - [InlineData("fe80::7add:12ff:febb:c67b%16:123")] - [InlineData("[fe80::7add:12ff:febb:c67b%16]")] - [InlineData("192.168.1.2/255.255.255.0")] - [InlineData("192.168.1.2/24")] - public void ValidHostStrings(string address) - { - Assert.True(IPHost.TryParse(address, out _)); - } - - /// <summary> - /// Checks IP address formats. - /// </summary> - /// <param name="address">IP Address.</param> - [Theory] - [InlineData("127.0.0.1")] - [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] - [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] - [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")] - [InlineData("fe80::7add:12ff:febb:c67b%16")] - [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] - [InlineData("fe80::7add:12ff:febb:c67b%16:123")] - [InlineData("[fe80::7add:12ff:febb:c67b%16]")] - [InlineData("192.168.1.2/255.255.255.0")] - [InlineData("192.168.1.2/24")] - public void ValidIPStrings(string address) - { - Assert.True(IPNetAddress.TryParse(address, out _)); - } - - /// <summary> - /// All should be invalid address strings. - /// </summary> - /// <param name="address">Invalid address strings.</param> - [Theory] - [InlineData("256.128.0.0.0.1")] - [InlineData("127.0.0.1#")] - [InlineData("localhost!")] - [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] - [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] - public void InvalidAddressString(string address) - { - Assert.False(IPNetAddress.TryParse(address, out _)); - Assert.False(IPHost.TryParse(address, out _)); - } - - /// <summary> /// Test collection parsing. /// </summary> /// <param name="settings">Collection to parse.</param> diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj new file mode 100644 index 000000000..14bd53db5 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj @@ -0,0 +1,37 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <IsPackable>false</IsPackable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <Nullable>enable</Nullable> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> + <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="coverlet.collector" Version="3.0.3"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + </ItemGroup> + + <!-- Code Analyzers --> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" /> + </ItemGroup> + +</Project> diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs new file mode 100644 index 000000000..b160e676e --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs @@ -0,0 +1,96 @@ +#pragma warning disable CA1002 // Do not expose generic lists + +using System.Collections.Generic; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Providers.MediaInfo; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.MediaInfo +{ + public class SubtitleResolverTests + { + public static IEnumerable<object[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData() + { + var index = 0; + yield return new object[] + { + new List<MediaStream>(), + "/video/My.Video.mkv", + index, + new[] + { + "/video/My.Video.mp3", + "/video/My.Video.png", + "/video/My.Video.srt", + "/video/My.Video.txt", + "/video/My.Video.vtt", + "/video/My.Video.ass", + "/video/My.Video.sub", + "/video/My.Video.ssa", + "/video/My.Video.smi", + "/video/My.Video.sami", + "/video/My.Video.en.srt", + "/video/My.Video.default.en.srt", + "/video/My.Video.default.forced.en.srt", + "/video/My.Video.en.default.forced.srt", + "/video/My.Video.With.Additional.Garbage.en.srt", + "/video/My.Video With Additional Garbage.srt" + }, + new[] + { + CreateMediaStream("/video/My.Video.srt", "srt", null, index++), + CreateMediaStream("/video/My.Video.vtt", "vtt", null, index++), + CreateMediaStream("/video/My.Video.ass", "ass", null, index++), + CreateMediaStream("/video/My.Video.sub", "sub", null, index++), + CreateMediaStream("/video/My.Video.ssa", "ssa", null, index++), + CreateMediaStream("/video/My.Video.smi", "smi", null, index++), + CreateMediaStream("/video/My.Video.sami", "sami", null, index++), + CreateMediaStream("/video/My.Video.en.srt", "srt", "en", index++), + CreateMediaStream("/video/My.Video.default.en.srt", "srt", "en", index++, isDefault: true), + CreateMediaStream("/video/My.Video.default.forced.en.srt", "srt", "en", index++, isForced: true, isDefault: true), + CreateMediaStream("/video/My.Video.en.default.forced.srt", "srt", "en", index++, isForced: true, isDefault: true), + CreateMediaStream("/video/My.Video.With.Additional.Garbage.en.srt", "srt", "en", index), + } + }; + } + + [Theory] + [MemberData(nameof(AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData))] + public void AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles(List<MediaStream> streams, string videoPath, int startIndex, string[] files, MediaStream[] expectedResult) + { + new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, startIndex, files); + + Assert.Equal(expectedResult.Length, streams.Count); + for (var i = 0; i < expectedResult.Length; i++) + { + var expected = expectedResult[i]; + var actual = streams[i]; + + Assert.Equal(expected.Index, actual.Index); + Assert.Equal(expected.Type, actual.Type); + Assert.Equal(expected.IsExternal, actual.IsExternal); + Assert.Equal(expected.Path, actual.Path); + Assert.Equal(expected.IsDefault, actual.IsDefault); + Assert.Equal(expected.IsForced, actual.IsForced); + Assert.Equal(expected.Language, actual.Language); + } + } + + private static MediaStream CreateMediaStream(string path, string codec, string? language, int index, bool isForced = false, bool isDefault = false) + { + return new () + { + Index = index, + Codec = codec, + Type = MediaStreamType.Subtitle, + IsExternal = true, + Path = path, + IsDefault = isDefault, + IsForced = isForced, + Language = language + }; + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs b/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs new file mode 100644 index 000000000..f6a7c676f --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs @@ -0,0 +1,27 @@ +using MediaBrowser.Providers.Plugins.Tmdb; +using Xunit; + +namespace Jellyfin.Providers.Tests.Tmdb +{ + public static class TmdbUtilsTests + { + [Theory] + [InlineData("de", "de")] + [InlineData("En", "En")] + [InlineData("de-de", "de-DE")] + [InlineData("en-US", "en-US")] + [InlineData("de-CH", "de")] + public static void NormalizeLanguage_Valid_Success(string input, string expected) + { + Assert.Equal(expected, TmdbUtils.NormalizeLanguage(input)); + } + + [Theory] + [InlineData(null, null)] + [InlineData("", "")] + public static void NormalizeLanguage_Invalid_Equal(string? input, string? expected) + { + Assert.Equal(expected, TmdbUtils.NormalizeLanguage(input!)); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs index 71f8c5181..f312933fb 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs @@ -166,6 +166,38 @@ namespace Jellyfin.Server.Implementations.Tests.Data }; } + public static IEnumerable<object[]> DeserializeImages_ValidAndInvalid_TestData() + { + yield return new object[] + { + string.Empty, + Array.Empty<ItemImageInfo>() + }; + + yield return new object[] + { + "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN|test|1234||ss", + new ItemImageInfo[] + { + new () + { + Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg", + Type = ImageType.Primary, + DateModified = new DateTime(637452096478512963, DateTimeKind.Utc), + Width = 1920, + Height = 1080, + BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN" + } + } + }; + + yield return new object[] + { + "|", + Array.Empty<ItemImageInfo>() + }; + } + [Theory] [MemberData(nameof(DeserializeImages_Valid_TestData))] public void DeserializeImages_Valid_Success(string value, ItemImageInfo[] expected) @@ -184,6 +216,23 @@ namespace Jellyfin.Server.Implementations.Tests.Data } [Theory] + [MemberData(nameof(DeserializeImages_ValidAndInvalid_TestData))] + public void DeserializeImages_ValidAndInvalid_Success(string value, ItemImageInfo[] expected) + { + var result = _sqliteItemRepository.DeserializeImages(value); + Assert.Equal(expected.Length, result.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i].Path, result[i].Path); + Assert.Equal(expected[i].Type, result[i].Type); + Assert.Equal(expected[i].DateModified, result[i].DateModified); + Assert.Equal(expected[i].Width, result[i].Width); + Assert.Equal(expected[i].Height, result[i].Height); + Assert.Equal(expected[i].BlurHash, result[i].BlurHash); + } + } + + [Theory] [MemberData(nameof(DeserializeImages_Valid_TestData))] public void SerializeImages_Valid_Success(string expected, ItemImageInfo[] value) { diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 27713d58a..adbca8344 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -24,7 +24,7 @@ <ItemGroup> <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> @@ -42,6 +42,7 @@ <ItemGroup> <ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> <ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" /> + <ProjectReference Include="..\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj" /> </ItemGroup> </Project> diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs index 8847239d9..c859d11c6 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using AutoFixture; @@ -15,8 +16,6 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv { public class HdHomerunHostTests { - private const string TestIp = "http://192.168.1.182"; - private readonly Fixture _fixture; private readonly HdHomerunHost _hdHomerunHost; @@ -30,7 +29,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv { return Task.FromResult(new HttpResponseMessage() { - Content = new StreamContent(File.OpenRead("Test Data/LiveTv/" + m.RequestUri?.Segments[^1])) + Content = new StreamContent(File.OpenRead(Path.Combine("Test Data/LiveTv", m.RequestUri!.Host, m.RequestUri.Segments[^1]))) }); }); @@ -50,7 +49,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv { var host = new TunerHostInfo() { - Url = TestIp + Url = "192.168.1.182" }; var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false); @@ -66,6 +65,26 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv } [Fact] + public async Task GetModelInfo_Legacy_Success() + { + var host = new TunerHostInfo() + { + Url = "10.10.10.100" + }; + + var modelInfo = await _hdHomerunHost.GetModelInfo(host, true, CancellationToken.None).ConfigureAwait(false); + Assert.Equal("HDHomeRun DUAL", modelInfo.FriendlyName); + Assert.Equal("HDHR3-US", modelInfo.ModelNumber); + Assert.Equal("hdhomerun3_atsc", modelInfo.FirmwareName); + Assert.Equal("20200225", modelInfo.FirmwareVersion); + Assert.Equal("10xxxxx5", modelInfo.DeviceID); + Assert.Null(modelInfo.DeviceAuth); + Assert.Equal(2, modelInfo.TunerCount); + Assert.Equal("http://10.10.10.100:80", modelInfo.BaseURL); + Assert.Null(modelInfo.LineupURL); + } + + [Fact] public async Task GetModelInfo_EmptyUrl_ArgumentException() { var host = new TunerHostInfo() @@ -81,7 +100,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv { var host = new TunerHostInfo() { - Url = TestIp + Url = "192.168.1.182" }; var channels = await _hdHomerunHost.GetLineup(host, CancellationToken.None).ConfigureAwait(false); @@ -94,11 +113,23 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv } [Fact] + public async Task GetLineup_Legacy_Success() + { + var host = new TunerHostInfo() + { + Url = "10.10.10.100" + }; + + // Placeholder json is invalid, just need to make sure we can reach it + await Assert.ThrowsAsync<JsonException>(() => _hdHomerunHost.GetLineup(host, CancellationToken.None)); + } + + [Fact] public async Task GetLineup_ImportFavoritesOnly_Success() { var host = new TunerHostInfo() { - Url = TestIp, + Url = "192.168.1.182", ImportFavoritesOnly = true }; @@ -114,9 +145,9 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv [Fact] public async Task TryGetTunerHostInfo_Valid_Success() { - var host = await _hdHomerunHost.TryGetTunerHostInfo(TestIp, CancellationToken.None).ConfigureAwait(false); + var host = await _hdHomerunHost.TryGetTunerHostInfo("192.168.1.182", CancellationToken.None).ConfigureAwait(false); Assert.Equal(_hdHomerunHost.Type, host.Type); - Assert.Equal(TestIp, host.Url); + Assert.Equal("192.168.1.182", host.Url); Assert.Equal("HDHomeRun PRIME", host.FriendlyName); Assert.Equal("FFFFFFFF", host.DeviceId); Assert.Equal(3, host.TunerCount); diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs new file mode 100644 index 000000000..e8b93b437 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using Emby.Server.Implementations.LiveTv.EmbyTV; +using MediaBrowser.Controller.LiveTv; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.LiveTv +{ + public static class RecordingHelperTests + { + public static IEnumerable<object[]> GetRecordingName_Success_TestData() + { + yield return new object[] + { + "The Incredibles 2020_04_20_21_06_00", + new TimerInfo + { + Name = "The Incredibles", + StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local), + IsMovie = true + } + }; + + yield return new object[] + { + "The Incredibles (2004)", + new TimerInfo + { + Name = "The Incredibles", + IsMovie = true, + ProductionYear = 2004 + } + }; + + yield return new object[] + { + "The Big Bang Theory 2020_04_20_21_06_00", + new TimerInfo + { + Name = "The Big Bang Theory", + StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local), + IsProgramSeries = true, + } + }; + + yield return new object[] + { + "The Big Bang Theory S12E10", + new TimerInfo + { + Name = "The Big Bang Theory", + IsProgramSeries = true, + SeasonNumber = 12, + EpisodeNumber = 10 + } + }; + + yield return new object[] + { + "The Big Bang Theory S12E10 The VCR Illumination", + new TimerInfo + { + Name = "The Big Bang Theory", + IsProgramSeries = true, + SeasonNumber = 12, + EpisodeNumber = 10, + EpisodeTitle = "The VCR Illumination" + } + }; + + yield return new object[] + { + "The Big Bang Theory 2018-12-06", + new TimerInfo + { + Name = "The Big Bang Theory", + IsProgramSeries = true, + OriginalAirDate = new DateTime(2018, 12, 6) + } + }; + + yield return new object[] + { + "The Big Bang Theory 2018-12-06 - The VCR Illumination", + new TimerInfo + { + Name = "The Big Bang Theory", + IsProgramSeries = true, + OriginalAirDate = new DateTime(2018, 12, 6), + EpisodeTitle = "The VCR Illumination" + } + }; + + yield return new object[] + { + "The Big Bang Theory 2018_12_06_21_06_00 - The VCR Illumination", + new TimerInfo + { + Name = "The Big Bang Theory", + StartDate = new DateTime(2018, 12, 6, 21, 6, 0, DateTimeKind.Local), + IsProgramSeries = true, + OriginalAirDate = new DateTime(2018, 12, 6), + EpisodeTitle = "The VCR Illumination" + } + }; + } + + [Theory] + [MemberData(nameof(GetRecordingName_Success_TestData))] + public static void GetRecordingName_Success(string expected, TimerInfo timerInfo) + { + Assert.Equal(expected, RecordingHelper.GetRecordingName(timerInfo)); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json new file mode 100644 index 000000000..a4ad4ed44 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json @@ -0,0 +1 @@ +{"FriendlyName":"HDHomeRun DUAL","ModelNumber":"HDHR3-US","Legacy":1,"FirmwareName":"hdhomerun3_atsc","FirmwareVersion":"20200225","DeviceID":"10xxxxx5","TunerCount":2,"BaseURL":"http://10.10.10.100:80"} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json @@ -0,0 +1 @@ +{} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/discover.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/discover.json index 851f17bb2..851f17bb2 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/discover.json +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/discover.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/lineup.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/lineup.json index 4cb5ebc8e..4cb5ebc8e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/lineup.json +++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/lineup.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs index 4fa64d8a2..70acbfc40 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System; using System.IO; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -46,12 +47,36 @@ namespace Jellyfin.Server.Implementations.Tests.Updates [Fact] public async Task GetPackages_Valid_Success() { - IList<PackageInfo> packages = await _installationManager.GetPackages( + PackageInfo[] packages = await _installationManager.GetPackages( "Jellyfin Stable", "https://repo.jellyfin.org/releases/plugin/manifest-stable.json", false); - Assert.Equal(25, packages.Count); + Assert.Equal(25, packages.Length); + } + + [Fact] + public async Task FilterPackages_NameOnly_Success() + { + PackageInfo[] packages = await _installationManager.GetPackages( + "Jellyfin Stable", + "https://repo.jellyfin.org/releases/plugin/manifest-stable.json", + false); + + packages = _installationManager.FilterPackages(packages, "Anime").ToArray(); + Assert.Single(packages); + } + + [Fact] + public async Task FilterPackages_GuidOnly_Success() + { + PackageInfo[] packages = await _installationManager.GetPackages( + "Jellyfin Stable", + "https://repo.jellyfin.org/releases/plugin/manifest-stable.json", + false); + + packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray(); + Assert.Single(packages); } } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs new file mode 100644 index 000000000..9db8689a7 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/BaseJellyfinTestController.cs @@ -0,0 +1,14 @@ +using Jellyfin.Api; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Server.Integration.Tests.Controllers +{ + /// <summary> + /// Base controller for testing infrastructure. + /// Automatically ignored in generated openapi spec. + /// </summary> + [ApiExplorerSettings(IgnoreApi = true)] + public class BaseJellyfinTestController : BaseJellyfinApiController + { + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs new file mode 100644 index 000000000..1a720c2f6 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Server.Integration.Tests.Controllers +{ + /// <summary> + /// Controller for testing the encoded url. + /// </summary> + public class EncoderController : BaseJellyfinTestController + { + /// <summary> + /// Tests the url decoding. + /// </summary> + /// <param name="params">Parameters to echo back in the response.</param> + /// <returns>An <see cref="OkResult"/>.</returns> + /// <response code="200">Information retrieved.</response> + [HttpGet("UrlDecode")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ContentResult TestUrlDecoding([FromQuery] Dictionary<string, string>? @params = null) + { + return new ContentResult() + { + Content = (@params != null && @params.Count > 0) + ? string.Join("&", @params.Select(x => x.Key + "=" + x.Value)) + : string.Empty, + ContentType = "text/plain; charset=utf-8", + StatusCode = 200 + }; + } + + /// <summary> + /// Tests the url decoding. + /// </summary> + /// <param name="params">Parameters to echo back in the response.</param> + /// <returns>An <see cref="OkResult"/>.</returns> + /// <response code="200">Information retrieved.</response> + [HttpGet("UrlArrayDecode")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ContentResult TestUrlArrayDecoding([FromQuery] Dictionary<string, string[]>? @params = null) + { + return new ContentResult() + { + Content = (@params != null && @params.Count > 0) + ? string.Join("&", @params.Select(x => x.Key + "=" + string.Join(',', x.Value))) + : string.Empty, + ContentType = "text/plain; charset=utf-8", + StatusCode = 200 + }; + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs new file mode 100644 index 000000000..732b4f050 --- /dev/null +++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs @@ -0,0 +1,48 @@ +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Jellyfin.Server.Integration.Tests +{ + /// <summary> + /// Defines the test for encoded querystrings in the url. + /// </summary> + public class EncodedQueryStringTest : IClassFixture<JellyfinApplicationFactory> + { + private readonly JellyfinApplicationFactory _factory; + + public EncodedQueryStringTest(JellyfinApplicationFactory factory) + { + _factory = factory; + } + + [Theory] + [InlineData("a=1&b=2&c=3", "a=1&b=2&c=3")] // won't be processed as there is more than 1. + [InlineData("a=1", "a=1")] // won't be processed as it has a value + [InlineData("a%3D1%26b%3D2%26c%3D3", "a=1&b=2&c=3")] // will be processed. + [InlineData("a=b&a=c", "a=b")] + [InlineData("a%3Db%26a%3Dc", "a=b")] + public async Task Ensure_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl) + { + var client = _factory.CreateClient(); + + var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + Assert.Equal(unencodedUrl, reply); + } + + [Theory] + [InlineData("a=b&a=c", "a=b,c")] + [InlineData("a%3Db%26a%3Dc", "a=b,c")] + public async Task Ensure_Array_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl) + { + var client = _factory.CreateClient(); + + var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl).ConfigureAwait(false); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + Assert.Equal(unencodedUrl, reply); + } + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj index 938385a2a..59f125cd0 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -12,9 +12,9 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.5" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" /> <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="Xunit.Priority" Version="1.1.6" /> diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index 72e40ebcb..c8e72c10d 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -13,9 +13,9 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.5" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" /> <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="3.0.3" /> diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs new file mode 100644 index 000000000..419afb2dc --- /dev/null +++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using Jellyfin.Server.Middleware; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Jellyfin.Server.Tests +{ + public static class UrlDecodeQueryFeatureTests + { + [Theory] + [InlineData("e0a72cb2a2c7", "e0a72cb2a2c7")] // isn't encoded + [InlineData("random+test", "random test")] // encoded + [InlineData("random%20test", "random test")] // encoded + [InlineData("++", " ")] // encoded + public static void EmptyValueTest(string query, string key) + { + var dict = new Dictionary<string, StringValues> + { + { query, StringValues.Empty } + }; + var test = new UrlDecodeQueryFeature(new QueryFeature(new QueryCollection(dict))); + Assert.Single(test.Query); + var (k, v) = test.Query.First(); + Assert.Equal(key, k); + Assert.Empty(v); + } + } +} diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj index 4132205c3..0a04a5c54 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj +++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj @@ -16,7 +16,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index 9ad093a2b..3e726f23d 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -14,8 +14,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; -#pragma warning disable CA5369 - namespace Jellyfin.XbmcMetadata.Tests.Parsers { public class EpisodeNfoProviderTests diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs index 2129f3422..eea8cb50a 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs @@ -1,6 +1,4 @@ -#pragma warning disable CA5369 - -using System; +using System; using System.Threading; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities.Audio; diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs index 0e61fa2a1..31110dbd7 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs @@ -1,6 +1,4 @@ -#pragma warning disable CA5369 - -using System; +using System; using System.Linq; using System.Threading; using MediaBrowser.Common.Configuration; |
