diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index ea2675a3d0..7cf60e92f7 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "9.0.2", + "version": "9.0.7", "commands": [ "dotnet-ef" ] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 228d4a17c8..8b6b12c31e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,8 @@ { "name": "Development Jellyfin Server", - "image":"mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", + "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", // restores nuget packages, installs the dotnet workloads and installs the dev https certificate "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", // reads the extensions list and installs them @@ -11,9 +13,11 @@ "dotnetRuntimeVersions": "9.0", "aspNetCoreRuntimeVersions": "9.0" }, - "ghcr.io/devcontainers-contrib/features/apt-packages:1": { + "ghcr.io/devcontainers-extra/features/apt-packages:1": { "preserve_apt_list": false, - "packages": ["libfontconfig1"] + "packages": [ + "libfontconfig1" + ] }, "ghcr.io/devcontainers/features/docker-in-docker:2": { "dockerDashComposeVersion": "v2" diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index 4f58c5bc50..2699688395 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -1,6 +1,7 @@ name: Issue Report description: File an issue report labels: [bug, triage] +type: Bug body: - type: markdown id: introduction @@ -140,7 +141,9 @@ body: - **Reverse Proxy**: [e.g. none, nginx, apache, etc.] - **Base URL**: [e.g. none, yes: /example] - **Networking**: [e.g. Host, Bridge/NAT] - - **Storage**: [e.g. local, NFS, cloud] + - **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD] + - **Media Storage**: [e.g. Local HDD, SMB Share] + - **External Integrations**: [e.g. Jellystat, Jellyseerr] value: | - OS: - Linux Kernel: @@ -155,7 +158,9 @@ body: - Reverse Proxy: - Base URL: - Networking: - - Storage: + - Jellyfin Data Storage: + - Media Storage: + - External Integrations: render: markdown validations: required: true diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 00f7e9e6d2..f30192641c 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -22,16 +22,16 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 + uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 + uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 + uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 07e61024ee..7dfcd94773 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -17,7 +17,7 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: abi-head retention-days: 14 @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' @@ -65,7 +65,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: abi-base retention-days: 14 @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: abi-base path: abi-base @@ -105,7 +105,7 @@ jobs: run: | { echo 'body<> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: openapi-head path: openapi-head - name: Upload openapi.json (unstable) to repository server - uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 + uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" @@ -172,7 +172,7 @@ jobs: strip_components: 1 target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" - name: Move openapi.json (unstable) into place - uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1 + uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" @@ -220,12 +220,12 @@ jobs: run: |- echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: openapi-head path: openapi-head - name: Upload openapi.json (stable) to repository server - uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 + uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" @@ -234,7 +234,7 @@ jobs: strip_components: 1 target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" - name: Move openapi.json (stable) into place - uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1 + uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index ec78396db0..334c0f54a4 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: ${{ env.SDK_VERSION }} @@ -35,7 +35,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@f1927db1dbfc029b056583ee488832e939447fe6 # v5.4.4 + uses: danielpalme/ReportGenerator-GitHub-Action@c1dd332d00304c5aa5d506aab698a5224a8fa24e # 5.4.11 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 1ab7ae029d..374554ac07 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -44,9 +44,9 @@ jobs: with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' - name: install python packages run: pip install -r rename/requirements.txt diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index 3c5ba68f91..588f0a50e8 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -14,9 +14,9 @@ jobs: with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' - name: install python packages run: pip install -r main-repo-triage/requirements.txt diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml index 411ebf8290..e6a9bf0caa 100644 --- a/.github/workflows/pull-request-conflict.yml +++ b/.github/workflows/pull-request-conflict.yml @@ -12,7 +12,7 @@ jobs: label: name: Labeling runs-on: ubuntu-latest - if: ${{ github.repository == 'jellyfin/jellyfin' }} + if: ${{ github.repository == 'jellyfin/jellyfin' && github.event.issue.pull_request }} steps: - name: Apply label uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..6733d59aca --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.preferVisualStudioCodeFileSystemWatcher": true +} diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 271f1c1971..d5f6e594cf 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -27,6 +27,7 @@ - [cryptobank](https://github.com/cryptobank) - [cvium](https://github.com/cvium) - [dannymichel](https://github.com/dannymichel) + - [darioackermann](https://github.com/darioackermann) - [DaveChild](https://github.com/DaveChild) - [DavidFair](https://github.com/DavidFair) - [Delgan](https://github.com/Delgan) @@ -60,6 +61,7 @@ - [ikomhoog](https://github.com/ikomhoog) - [iwalton3](https://github.com/iwalton3) - [jftuga](https://github.com/jftuga) + - [jkhsjdhjs](https://github.com/jkhsjdhjs) - [jmshrv](https://github.com/jmshrv) - [joern-h](https://github.com/joern-h) - [joshuaboniface](https://github.com/joshuaboniface) @@ -196,6 +198,10 @@ - [benedikt257](https://github.com/benedikt257) - [revam](https://github.com/revam) - [Jxiced](https://github.com/Jxiced) + - [allesmi](https://github.com/allesmi) + - [ThunderClapLP](https://github.com/ThunderClapLP) + - [Shoham Peller](https://github.com/spellr) + - [theshoeshiner](https://github.com/theshoeshiner) # Emby Contributors @@ -270,3 +276,5 @@ - [Robert Lützner](https://github.com/rluetzner) - [Nathan McCrina](https://github.com/nfmccrina) - [Martin Reuter](https://github.com/reuterma24) + - [Michael McElroy](https://github.com/mcmcelro) + - [Soumyadip Auddy](https://github.com/SoumyadipAuddy) diff --git a/Directory.Packages.props b/Directory.Packages.props index 854c5a6df8..32ef7ffc93 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,82 +4,88 @@ - + - - + + + - + - - + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + + + - + - - - + + + + - + - - - - + + + + - + diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 48338daf48..192235baeb 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -187,7 +187,9 @@ namespace Emby.Naming.Common "disc", "disk", "vol", - "volume" + "volume", + "part", + "act" }; ArtistSubfolders = new[] @@ -238,6 +240,7 @@ namespace Emby.Naming.Common ".dsp", ".dts", ".dvf", + ".eac3", ".far", ".flac", ".gdm", diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index 45b91971bf..98ee1e4b8f 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -1,43 +1,35 @@ using System; using System.Globalization; using System.IO; +using System.Text.RegularExpressions; namespace Emby.Naming.TV { /// /// Class to parse season paths. /// - public static class SeasonPathParser + public static partial class SeasonPathParser { - /// - /// A season folder must contain one of these somewhere in the name. - /// - private static readonly string[] _seasonFolderNames = - { - "season", - "sæson", - "temporada", - "saison", - "staffel", - "series", - "сезон", - "stagione" - }; + [GeneratedRegex(@"^\s*((?(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?.*)$")] + private static partial Regex ProcessPre(); - private static readonly char[] _splitChars = ['.', '_', ' ', '-']; + [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?(?>\d+)(?!\s*[Ee]\d+))(?.*)$")] + private static partial Regex ProcessPost(); /// /// Attempts to parse season number from path. /// /// Path to season. + /// Folder name of the parent. /// Support special aliases when parsing. /// Support numeric season folders when parsing. /// Returns object. - public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders) + public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders) { var result = new SeasonPathParserResult(); + var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name; - var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders); + var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders); result.SeasonNumber = seasonNumber; @@ -54,15 +46,24 @@ namespace Emby.Naming.TV /// Gets the season number from path. /// /// The path. + /// The parent folder name. /// if set to true [support special aliases]. /// if set to true [support numeric season folders]. /// System.Nullable{System.Int32}. private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath( string path, + string? parentFolderName, bool supportSpecialAliases, bool supportNumericSeasonFolders) { string filename = Path.GetFileName(path); + filename = Regex.Replace(filename, "[ ._-]", string.Empty); + + if (parentFolderName is not null) + { + parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty); + filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase); + } if (supportSpecialAliases) { @@ -85,53 +86,38 @@ namespace Emby.Naming.TV } } - if (TryGetSeasonNumberFromPart(filename, out int seasonNumber)) + if (filename.StartsWith('s')) { + var testFilename = filename.AsSpan()[1..]; + + if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) + { + return (val, true); + } + } + + var preMatch = ProcessPre().Match(filename); + if (preMatch.Success) + { + return CheckMatch(preMatch); + } + else + { + var postMatch = ProcessPost().Match(filename); + return CheckMatch(postMatch); + } + } + + private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match) + { + var numberString = match.Groups["seasonnumber"]; + if (numberString.Success) + { + var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture); return (seasonNumber, true); } - // Look for one of the season folder names - foreach (var name in _seasonFolderNames) - { - if (filename.Contains(name, StringComparison.OrdinalIgnoreCase)) - { - var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase)); - if (result.SeasonNumber.HasValue) - { - return result; - } - - break; - } - } - - var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries); - foreach (var part in parts) - { - if (TryGetSeasonNumberFromPart(part, out seasonNumber)) - { - return (seasonNumber, true); - } - } - - return (null, true); - } - - private static bool TryGetSeasonNumberFromPart(ReadOnlySpan part, out int seasonNumber) - { - seasonNumber = 0; - if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - seasonNumber = value; - return true; - } - - return false; + return (null, false); } /// diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs index 3219472eff..5289065898 100644 --- a/Emby.Naming/Video/ExtraRuleResolver.cs +++ b/Emby.Naming/Video/ExtraRuleResolver.cs @@ -18,8 +18,9 @@ namespace Emby.Naming.Video /// /// Path to file. /// The naming options. + /// Top-level folder for the containing library. /// Returns object. - public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions) + public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "") { var result = new ExtraResult(); @@ -69,7 +70,9 @@ namespace Emby.Naming.Video else if (rule.RuleType == ExtraRuleType.DirectoryName) { var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan)); - if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) + string fullDirectory = Path.GetDirectoryName(pathSpan).ToString(); + if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase) + && !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase)) { result.ExtraType = rule.ExtraType; result.Rule = rule; diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 12bc22a6ac..a3134f3f68 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -27,8 +27,9 @@ namespace Emby.Naming.Video /// The naming options. /// Indication we should consider multi-versions of content. /// Whether to parse the name or use the filename. + /// Top-level folder for the containing library. /// Returns enumerable of which groups files together when related. - public static IReadOnlyList Resolve(IReadOnlyList videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true) + public static IReadOnlyList Resolve(IReadOnlyList videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "") { // Filter out all extras, otherwise they could cause stacks to not be resolved // See the unit test TestStackedWithTrailer @@ -65,7 +66,7 @@ namespace Emby.Naming.Video { var info = new VideoInfo(stack.Name) { - Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName)) + Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot)) .OfType() .ToList() }; diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index db5bfdbf94..afbf6f8fae 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -17,10 +17,11 @@ namespace Emby.Naming.Video /// The path. /// The naming options. /// Whether to parse the name or use the filename. + /// Top-level folder for the containing library. /// VideoFileInfo. - public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true) + public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "") { - return Resolve(path, true, namingOptions, parseName); + return Resolve(path, true, namingOptions, parseName, libraryRoot); } /// @@ -28,10 +29,11 @@ namespace Emby.Naming.Video /// /// The path. /// The naming options. + /// Top-level folder for the containing library. /// VideoFileInfo. - public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions) + public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions, string? libraryRoot = "") { - return Resolve(path, false, namingOptions); + return Resolve(path, false, namingOptions, libraryRoot: libraryRoot); } /// @@ -41,9 +43,10 @@ namespace Emby.Naming.Video /// if set to true [is folder]. /// The naming options. /// Whether or not the name should be parsed for info. + /// Top-level folder for the containing library. /// VideoFileInfo. /// path is null. - public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true) + public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "") { if (string.IsNullOrEmpty(path)) { @@ -75,7 +78,7 @@ namespace Emby.Naming.Video var format3DResult = Format3DParser.Parse(path, namingOptions); - var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions); + var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions, libraryRoot); var name = Path.GetFileNameWithoutExtension(path); diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index ac6c41ca52..e24c067d61 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -49,7 +49,7 @@ public class PhotoProvider : ICustomMetadataProvider, IForcedProvider, IH if (item.IsFileProtocol) { var file = directoryService.GetFile(item.Path); - return file is not null && file.LastWriteTimeUtc != item.DateModified; + return file is not null && item.HasChanged(file.LastWriteTimeUtc); } return false; @@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider, IForcedProvider, IH var dateTaken = image.ImageTag.DateTime; if (dateTaken.HasValue) { - item.DateCreated = dateTaken.Value; + item.DateCreated = dateTaken.Value.ToUniversalTime(); item.PremiereDate = dateTaken.Value; item.ProductionYear = dateTaken.Value.Year; } diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index dc845b2d7e..e74755ec32 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; namespace Emby.Server.Implementations.AppBase @@ -30,80 +33,91 @@ namespace Emby.Server.Implementations.AppBase ConfigurationDirectoryPath = configurationDirectoryPath; CachePath = cacheDirectoryPath; WebPath = webDirectoryPath; - DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; } - /// - /// Gets the path to the program data folder. - /// - /// The program data path. + /// public string ProgramDataPath { get; } /// public string WebPath { get; } - /// - /// Gets the path to the system folder. - /// - /// The path to the system folder. + /// public string ProgramSystemPath { get; } = AppContext.BaseDirectory; - /// - /// Gets the folder path to the data directory. - /// - /// The data directory. + /// public string DataPath { get; } /// public string VirtualDataPath => "%AppDataPath%"; - /// - /// Gets the image cache path. - /// - /// The image cache path. + /// public string ImageCachePath => Path.Combine(CachePath, "images"); - /// - /// Gets the path to the plugin directory. - /// - /// The plugins path. + /// public string PluginsPath => Path.Combine(ProgramDataPath, "plugins"); - /// - /// Gets the path to the plugin configurations directory. - /// - /// The plugin configurations path. + /// public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations"); - /// - /// Gets the path to the log directory. - /// - /// The log directory path. + /// public string LogDirectoryPath { get; } - /// - /// Gets the path to the application configuration root directory. - /// - /// The configuration directory path. + /// public string ConfigurationDirectoryPath { get; } - /// - /// Gets the path to the system configuration file. - /// - /// The system configuration file path. + /// public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml"); - /// - /// Gets or sets the folder path to the cache directory. - /// - /// The cache directory. + /// public string CachePath { get; set; } - /// - /// Gets the folder path to the temp directory within the cache folder. - /// - /// The temp directory. + /// public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin"); + + /// + public string TrickplayPath => Path.Combine(DataPath, "trickplay"); + + /// + public string BackupPath => Path.Combine(DataPath, "backups"); + + /// + public virtual void MakeSanityCheckOrThrow() + { + CreateAndCheckMarker(ConfigurationDirectoryPath, "config"); + CreateAndCheckMarker(LogDirectoryPath, "log"); + CreateAndCheckMarker(PluginsPath, "plugin"); + CreateAndCheckMarker(ProgramDataPath, "data"); + CreateAndCheckMarker(CachePath, "cache"); + CreateAndCheckMarker(DataPath, "data"); + } + + /// + public void CreateAndCheckMarker(string path, string markerName, bool recursive = false) + { + Directory.CreateDirectory(path); + + CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive); + } + + private IEnumerable GetMarkers(string path, bool recursive = false) + { + return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + } + + private void CheckOrCreateMarker(string path, string markerName, bool recursive = false) + { + var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName); + if (otherMarkers != null) + { + throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}."); + } + + var markerPath = Path.Combine(path, markerName); + if (!File.Exists(markerPath)) + { + FileHelper.CreateEmpty(markerPath); + } + } } } diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 9bc3a0204b..81ef0e5f9a 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -227,6 +227,7 @@ namespace Emby.Server.Implementations.AppBase Logger.LogInformation("Setting cache path: {Path}", cachePath); ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath; + CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache"); } /// diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 29967c6df5..cbb0f6c565 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -15,6 +15,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Emby.Naming.Common; using Emby.Photos; +using Emby.Server.Implementations.Chapters; using Emby.Server.Implementations.Collections; using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Cryptography; @@ -39,9 +40,10 @@ using Jellyfin.Drawing; using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Manager; using Jellyfin.Networking.Udp; -using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.FullSystemBackup; using Jellyfin.Server.Implementations.Item; using Jellyfin.Server.Implementations.MediaSegments; +using Jellyfin.Server.Implementations.SystemBackupService; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; @@ -57,10 +59,14 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LibraryTaskScheduler; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; @@ -91,7 +97,6 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -266,6 +271,8 @@ namespace Emby.Server.Implementations ? Environment.MachineName : ConfigurationManager.Configuration.ServerName; + public string RestoreBackupPath { get; set; } + public string ExpandVirtualPath(string path) { if (path is null) @@ -470,6 +477,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(this); serviceCollection.AddSingleton(_pluginManager); serviceCollection.AddSingleton(ApplicationPaths); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -504,10 +512,13 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy isn't required serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); @@ -542,6 +553,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -549,13 +561,14 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -572,20 +585,10 @@ namespace Emby.Server.Implementations /// /// Create services registered with the service container that need to be initialized at application startup. /// + /// The configuration used to initialise the application. /// A task representing the service initialization operation. - public async Task InitializeServices() + public async Task InitializeServices(IConfiguration startupConfig) { - var jellyfinDb = await Resolve>().CreateDbContextAsync().ConfigureAwait(false); - await using (jellyfinDb.ConfigureAwait(false)) - { - if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any()) - { - Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)"); - await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false); - Logger.LogInformation("EFCore migrations applied successfully"); - } - } - var localizationManager = (LocalizationManager)Resolve(); await localizationManager.LoadAll().ConfigureAwait(false); @@ -633,24 +636,26 @@ namespace Emby.Server.Implementations private void SetStaticProperties() { // For now there's no real way to inject these properly - BaseItem.Logger = Resolve>(); - BaseItem.ConfigurationManager = ConfigurationManager; - BaseItem.LibraryManager = Resolve(); - BaseItem.ProviderManager = Resolve(); - BaseItem.LocalizationManager = Resolve(); - BaseItem.ItemRepository = Resolve(); - BaseItem.ChapterRepository = Resolve(); - BaseItem.FileSystem = Resolve(); - BaseItem.UserDataManager = Resolve(); + BaseItem.ChapterManager = Resolve(); BaseItem.ChannelManager = Resolve(); - Video.RecordingsManager = Resolve(); - Folder.UserViewManager = Resolve(); - UserView.TVSeriesManager = Resolve(); - UserView.CollectionManager = Resolve(); - BaseItem.MediaSourceManager = Resolve(); + BaseItem.ConfigurationManager = ConfigurationManager; + BaseItem.FileSystem = Resolve(); + BaseItem.ItemRepository = Resolve(); + BaseItem.LibraryManager = Resolve(); + BaseItem.LocalizationManager = Resolve(); + BaseItem.Logger = Resolve>(); BaseItem.MediaSegmentManager = Resolve(); + BaseItem.MediaSourceManager = Resolve(); + BaseItem.ProviderManager = Resolve(); + BaseItem.UserDataManager = Resolve(); CollectionFolder.XmlSerializer = _xmlSerializer; CollectionFolder.ApplicationHost = this; + Folder.UserViewManager = Resolve(); + Folder.CollectionManager = Resolve(); + Folder.LimitedConcurrencyLibraryScheduler = Resolve(); + Episode.MediaEncoder = Resolve(); + UserView.TVSeriesManager = Resolve(); + Video.RecordingsManager = Resolve(); } /// diff --git a/Emby.Server.Implementations/Chapters/ChapterManager.cs b/Emby.Server.Implementations/Chapters/ChapterManager.cs new file mode 100644 index 0000000000..b4daa2a143 --- /dev/null +++ b/Emby.Server.Implementations/Chapters/ChapterManager.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Chapters; + +/// +/// The chapter manager. +/// +public class ChapterManager : IChapterManager +{ + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + private readonly IMediaEncoder _encoder; + private readonly IChapterRepository _chapterRepository; + private readonly ILibraryManager _libraryManager; + private readonly IPathManager _pathManager; + + /// + /// The first chapter ticks. + /// + private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks; + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + public ChapterManager( + ILogger logger, + IFileSystem fileSystem, + IMediaEncoder encoder, + IChapterRepository chapterRepository, + ILibraryManager libraryManager, + IPathManager pathManager) + { + _logger = logger; + _fileSystem = fileSystem; + _encoder = encoder; + _chapterRepository = chapterRepository; + _libraryManager = libraryManager; + _pathManager = pathManager; + } + + /// + /// Determines whether [is eligible for chapter image extraction] [the specified video]. + /// + /// The video. + /// The library options for the video. + /// true if [is eligible for chapter image extraction] [the specified video]; otherwise, false. + private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions) + { + if (video.IsPlaceHolder) + { + return false; + } + + if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction) + { + return false; + } + + if (video.IsShortcut) + { + return false; + } + + if (!video.IsCompleteMedia) + { + return false; + } + + // Can't extract images if there are no video streams + return video.DefaultVideoStreamIndex.HasValue; + } + + private long GetAverageDurationBetweenChapters(IReadOnlyList chapters) + { + if (chapters.Count < 2) + { + return 0; + } + + long sum = 0; + for (int i = 1; i < chapters.Count; i++) + { + sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks; + } + + return sum / chapters.Count; + } + + /// + public async Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) + { + if (chapters.Count == 0) + { + return true; + } + + var libraryOptions = _libraryManager.GetLibraryOptions(video); + + if (!IsEligibleForChapterImageExtraction(video, libraryOptions)) + { + extractImages = false; + } + + var averageChapterDuration = GetAverageDurationBetweenChapters(chapters); + var threshold = TimeSpan.FromSeconds(1).Ticks; + if (averageChapterDuration < threshold) + { + _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold); + extractImages = false; + } + + var success = true; + var changesMade = false; + + var runtimeTicks = video.RunTimeTicks ?? 0; + + var currentImages = GetSavedChapterImages(video, directoryService); + + foreach (var chapter in chapters) + { + if (chapter.StartPositionTicks >= runtimeTicks) + { + _logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name); + break; + } + + var path = _pathManager.GetChapterImagePath(video, chapter.StartPositionTicks); + + if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase)) + { + if (extractImages) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + // Add some time for the first chapter to make sure we don't end up with a black image + var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); + + var inputPath = video.Path; + var directoryPath = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + var container = video.Container; + var mediaSource = new MediaSourceInfo + { + VideoType = video.VideoType, + IsoType = video.IsoType, + Protocol = video.PathProtocol ?? MediaProtocol.File, + }; + + _logger.LogInformation("Extracting chapter image for {Name} at {Path}", video.Name, inputPath); + var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); + File.Copy(tempFile, path, true); + + try + { + _fileSystem.DeleteFile(tempFile); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile); + } + + chapter.ImagePath = path; + chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); + changesMade = true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path)); + success = false; + break; + } + } + else if (!string.IsNullOrEmpty(chapter.ImagePath)) + { + chapter.ImagePath = null; + changesMade = true; + } + } + else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase)) + { + chapter.ImagePath = path; + chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); + changesMade = true; + } + else if (libraryOptions?.EnableChapterImageExtraction != true) + { + // We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image + chapter.ImagePath = null; + changesMade = true; + } + } + + if (saveChapters && changesMade) + { + _chapterRepository.SaveChapters(video.Id, chapters); + } + + DeleteDeadImages(currentImages, chapters); + + return success; + } + + /// + public void SaveChapters(Video video, IReadOnlyList chapters) + { + _chapterRepository.SaveChapters(video.Id, chapters); + } + + /// + public ChapterInfo? GetChapter(Guid baseItemId, int index) + { + return _chapterRepository.GetChapter(baseItemId, index); + } + + /// + public IReadOnlyList GetChapters(Guid baseItemId) + { + return _chapterRepository.GetChapters(baseItemId); + } + + /// + public void DeleteChapterImages(Video video) + { + var path = _pathManager.GetChapterImageFolderPath(video); + try + { + if (Directory.Exists(path)) + { + _logger.LogInformation("Removing chapter images for {Name} [{Id}]", video.Name, video.Id); + Directory.Delete(path, true); + } + } + catch (Exception ex) + { + _logger.LogWarning("Failed to remove chapter image folder for {Item}: {Exception}", video.Id, ex); + } + + _chapterRepository.DeleteChapters(video.Id); + } + + private IReadOnlyList GetSavedChapterImages(Video video, IDirectoryService directoryService) + { + var path = _pathManager.GetChapterImageFolderPath(video); + if (!Directory.Exists(path)) + { + return []; + } + + try + { + return directoryService.GetFilePaths(path); + } + catch (IOException) + { + return []; + } + } + + private void DeleteDeadImages(IEnumerable images, IEnumerable chapters) + { + var existingImages = chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)); + var deadImages = images + .Except(existingImages, StringComparer.OrdinalIgnoreCase) + .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach (var image in deadImages) + { + _logger.LogDebug("Deleting dead chapter image {Path}", image); + + try + { + _fileSystem.DeleteFile(image!); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting {Path}.", image); + } + } + } +} diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index e414792ba0..0eb387ffdd 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities; @@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections var libraryOptions = new LibraryOptions { - PathInfos = new[] { new MediaPathInfo(path) }, + PathInfos = [new MediaPathInfo(path)], EnableRealtimeMonitor = false, SaveLocalMetadata = true }; @@ -150,15 +150,15 @@ namespace Emby.Server.Implementations.Collections try { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); var collection = new BoxSet { Name = name, Path = path, IsLocked = options.IsLocked, ProviderIds = options.ProviderIds, - DateCreated = DateTime.UtcNow + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc }; parentFolder.AddChild(collection); @@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.Collections { if (_libraryManager.GetItemById(collectionId) is not BoxSet collection) { - throw new ArgumentException("No collection exists with the supplied Id"); + throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId); } List? itemList = null; @@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Collections if (item is null) { - throw new ArgumentException("No item exists with the supplied Id"); + throw new ArgumentException("No item exists with the supplied Id " + id); } if (!currentLinkedChildrenIds.Contains(id)) diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 7ea863d769..31ae82d6a3 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -1,84 +1,114 @@ #pragma warning disable CS1591 using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Data +namespace Emby.Server.Implementations.Data; + +public class CleanDatabaseScheduledTask : ILibraryPostScanTask { - public class CleanDatabaseScheduledTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IDbContextFactory _dbProvider; + private readonly IPathManager _pathManager; + + public CleanDatabaseScheduledTask( + ILibraryManager libraryManager, + ILogger logger, + IDbContextFactory dbProvider, + IPathManager pathManager) { - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IDbContextFactory _dbProvider; + _libraryManager = libraryManager; + _logger = logger; + _dbProvider = dbProvider; + _pathManager = pathManager; + } - public CleanDatabaseScheduledTask( - ILibraryManager libraryManager, - ILogger logger, - IDbContextFactory dbProvider) - { - _libraryManager = libraryManager; - _logger = logger; - _dbProvider = dbProvider; - } + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); + } - public async Task Run(IProgress progress, CancellationToken cancellationToken) + private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) + { + var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery { - await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); - } + HasDeadParentId = true + }); - private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) + var numComplete = 0; + var numItems = itemIds.Count + 1; + + _logger.LogDebug("Cleaning {Number} items with dead parents", numItems); + + foreach (var itemId in itemIds) { - var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery + cancellationToken.ThrowIfCancellationRequested(); + + var item = _libraryManager.GetItemById(itemId); + if (item is not null) { - HasDeadParentId = true - }); + _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty); - var numComplete = 0; - var numItems = itemIds.Count + 1; - - _logger.LogDebug("Cleaning {0} items with dead parent links", numItems); - - foreach (var itemId in itemIds) - { - cancellationToken.ThrowIfCancellationRequested(); - - var item = _libraryManager.GetItemById(itemId); - - if (item is not null) + foreach (var mediaSource in item.GetMediaSources(false)) { - _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty); - - _libraryManager.DeleteItem(item, new DeleteOptions + // Delete extracted data + var mediaSourceItem = _libraryManager.GetItemById(mediaSource.Id); + if (mediaSourceItem is null) { - DeleteFileLocation = false - }); + continue; + } + + var extractedDataFolders = _pathManager.GetExtractedDataPaths(mediaSourceItem); + foreach (var folder in extractedDataFolders) + { + if (Directory.Exists(folder)) + { + try + { + Directory.Delete(folder, true); + } + catch (Exception e) + { + _logger.LogWarning("Failed to remove {Folder}: {Exception}", folder, e.Message); + } + } + } } - numComplete++; - double percent = numComplete; - percent /= numItems; - progress.Report(percent * 100); - } - - var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - await using (context.ConfigureAwait(false)) - { - var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await using (transaction.ConfigureAwait(false)) + // Delete item + _libraryManager.DeleteItem(item, new DeleteOptions { - await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); - } + DeleteFileLocation = false + }); } - progress.Report(100); + numComplete++; + double percent = numComplete; + percent /= numItems; + progress.Report(percent * 100); } + + var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using (transaction.ConfigureAwait(false)) + { + await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } + } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 356d1e437a..cf886ae826 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -5,8 +5,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Controller.Channels; @@ -17,7 +17,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Trickplay; @@ -41,7 +40,6 @@ namespace Emby.Server.Implementations.Dto private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserDataManager _userDataRepository; - private readonly IItemRepository _itemRepo; private readonly IImageProcessor _imageProcessor; private readonly IProviderManager _providerManager; @@ -52,13 +50,12 @@ namespace Emby.Server.Implementations.Dto private readonly Lazy _livetvManagerFactory; private readonly ITrickplayManager _trickplayManager; - private readonly IChapterRepository _chapterRepository; + private readonly IChapterManager _chapterManager; public DtoService( ILogger logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, - IItemRepository itemRepo, IImageProcessor imageProcessor, IProviderManager providerManager, IRecordingsManager recordingsManager, @@ -66,12 +63,11 @@ namespace Emby.Server.Implementations.Dto IMediaSourceManager mediaSourceManager, Lazy livetvManagerFactory, ITrickplayManager trickplayManager, - IChapterRepository chapterRepository) + IChapterManager chapterManager) { _logger = logger; _libraryManager = libraryManager; _userDataRepository = userDataRepository; - _itemRepo = itemRepo; _imageProcessor = imageProcessor; _providerManager = providerManager; _recordingsManager = recordingsManager; @@ -79,7 +75,7 @@ namespace Emby.Server.Implementations.Dto _mediaSourceManager = mediaSourceManager; _livetvManagerFactory = livetvManagerFactory; _trickplayManager = trickplayManager; - _chapterRepository = chapterRepository; + _chapterManager = chapterManager; } private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; @@ -99,11 +95,11 @@ namespace Emby.Server.Implementations.Dto if (item is LiveTvChannel tvChannel) { - (channelTuples ??= new()).Add((dto, tvChannel)); + (channelTuples ??= []).Add((dto, tvChannel)); } else if (item is LiveTvProgram) { - (programTuples ??= new()).Add((item, dto)); + (programTuples ??= []).Add((item, dto)); } if (item is IItemByName byName) @@ -590,12 +586,12 @@ namespace Emby.Server.Implementations.Dto if (dto.ImageBlurHashes is not null) { // Only add BlurHash for the person's image. - baseItemPerson.ImageBlurHashes = new Dictionary>(); + baseItemPerson.ImageBlurHashes = []; foreach (var (imageType, blurHash) in dto.ImageBlurHashes) { if (blurHash is not null) { - baseItemPerson.ImageBlurHashes[imageType] = new Dictionary(); + baseItemPerson.ImageBlurHashes[imageType] = []; foreach (var (imageId, blurHashValue) in blurHash) { if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase)) @@ -674,11 +670,11 @@ namespace Emby.Server.Implementations.Dto if (!string.IsNullOrEmpty(image.BlurHash)) { - dto.ImageBlurHashes ??= new Dictionary>(); + dto.ImageBlurHashes ??= []; if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value)) { - value = new Dictionary(); + value = []; dto.ImageBlurHashes[image.Type] = value; } @@ -709,7 +705,7 @@ namespace Emby.Server.Implementations.Dto if (hashes.Count > 0) { - dto.ImageBlurHashes ??= new Dictionary>(); + dto.ImageBlurHashes ??= []; dto.ImageBlurHashes[imageType] = hashes; } @@ -756,7 +752,7 @@ namespace Emby.Server.Implementations.Dto dto.AspectRatio = hasAspectRatio.AspectRatio; } - dto.ImageBlurHashes = new Dictionary>(); + dto.ImageBlurHashes = []; var backdropLimit = options.GetImageLimit(ImageType.Backdrop); if (backdropLimit > 0) @@ -772,7 +768,7 @@ namespace Emby.Server.Implementations.Dto if (options.EnableImages) { - dto.ImageTags = new Dictionary(); + dto.ImageTags = []; // Prevent implicitly captured closure var currentItem = item; @@ -1064,12 +1060,17 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Chapters)) { - dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList(); + dto.Chapters = _chapterManager.GetChapters(item.Id).ToList(); } if (options.ContainsField(ItemFields.Trickplay)) { - dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); + var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); + dto.Trickplay = trickplay.ToDictionary( + mediaStream => mediaStream.Key, + mediaStream => mediaStream.Value.ToDictionary( + width => width.Key, + width => new TrickplayInfoDto(width.Value))); } dto.ExtraType = video.ExtraType; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 70dd5eb9ae..15843730e9 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -18,9 +18,11 @@ + + @@ -62,10 +64,14 @@ + + + + - + diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index fb0a55135f..933cfc8cbe 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -5,8 +5,8 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 1d04f3da37..8a79cdebc1 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,7 +1,8 @@ #pragma warning disable CS1591 using System.Threading.Tasks; -using Jellyfin.Data.Enums; +using Jellyfin.Data; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index a720c86fb2..373b0994a6 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer RemoteEndPoint = remoteEndPoint; _jsonOptions = JsonDefaults.Options; - LastActivityDate = DateTime.Now; + LastActivityDate = DateTime.UtcNow; } /// diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 7378cf8851..f634084034 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.IO private void ProcessPathChanges(List paths) { IEnumerable itemsToRefresh = paths - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .Select(GetAffectedBaseItem) .Where(item => item is not null) .DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where() diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 6af2a553d6..d87ad729ee 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.IO .Where(IsLibraryMonitorEnabled) .OfType() .SelectMany(f => f.PhysicalLocations) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .Order(); foreach (var path in paths) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 66b7839f77..c9630b8945 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -160,12 +160,13 @@ namespace Emby.Server.Implementations.IO { // Cross device move requires a copy Directory.CreateDirectory(destination); - foreach (string file in Directory.GetFiles(source)) + var sourceDir = new DirectoryInfo(source); + foreach (var file in sourceDir.EnumerateFiles()) { - File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true); + file.CopyTo(Path.Combine(destination, file.Name), true); } - Directory.Delete(source, true); + sourceDir.Delete(true); } } @@ -541,8 +542,8 @@ namespace Emby.Server.Implementations.IO return DriveInfo.GetDrives() .Where( d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable) - && d.IsReady - && d.TotalSize != 0) + && d.IsReady + && d.TotalSize != 0) .Select(d => new FileSystemMetadata { Name = d.Name, @@ -560,11 +561,23 @@ namespace Emby.Server.Implementations.IO /// public virtual IEnumerable GetFiles(string path, bool recursive = false) { - return GetFiles(path, null, false, recursive); + return GetFiles(path, "*", recursive); } /// - public virtual IEnumerable GetFiles(string path, IReadOnlyList? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) + public virtual IEnumerable GetFiles(string path, string searchPattern, bool recursive = false) + { + return GetFiles(path, searchPattern, null, false, recursive); + } + + /// + public virtual IEnumerable GetFiles(string path, IReadOnlyList? extensions, bool enableCaseSensitiveExtensions, bool recursive) + { + return GetFiles(path, "*", extensions, enableCaseSensitiveExtensions, recursive); + } + + /// + public virtual IEnumerable GetFiles(string path, string searchPattern, IReadOnlyList? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var enumerationOptions = GetEnumerationOptions(recursive); @@ -572,10 +585,12 @@ namespace Emby.Server.Implementations.IO // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1) { - return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions)); + searchPattern = searchPattern.EndsWith(extensions[0], StringComparison.Ordinal) ? searchPattern : searchPattern + extensions[0]; + + return ToMetadata(new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions)); } - var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions); + var files = new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions); if (extensions is not null && extensions.Count > 0) { diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 8b28691498..4874eca8e6 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -43,13 +43,11 @@ namespace Emby.Server.Implementations.Images protected IImageProcessor ImageProcessor { get; set; } protected virtual IReadOnlyCollection SupportedImages { get; } - = new ImageType[] { ImageType.Primary }; + = [ImageType.Primary]; /// public string Name => "Dynamic Image Provider"; - protected virtual int MaxImageAgeDays => 7; - public int Order => 0; protected virtual bool Supports(BaseItem item) => true; @@ -292,8 +290,14 @@ namespace Emby.Server.Implementations.Images protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image) { - var age = DateTime.UtcNow - image.DateModified; - return age.TotalDays > MaxImageAgeDays; + var path = image.Path; + if (!string.IsNullOrEmpty(path)) + { + var modificationDate = FileSystem.GetLastWriteTimeUtc(path); + return image.DateModified != modificationDate; + } + + return false; } protected string CreateSingleImage(IEnumerable itemsWithImages, string outputPathWithoutExtension, ImageType imageType) diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs index f9c10ba098..0d63b3af7d 100644 --- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 34c722e41d..273d356a39 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index c9b41f8193..706de60a90 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs index 31f053f065..c472623e67 100644 --- a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index b01fd93a7b..f29a0b3ad7 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library { if (parent is not null) { - // Ignore extras folders but allow it at the collection level + // Ignore extras for unsupported types if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename) && parent is not AggregateFolder && parent is not UserRootFolder) @@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.Library { if (parent is not null) { - // Don't resolve these into audio files + // Don't resolve theme songs if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal) && AudioFileParser.IsAudioFile(filename, _namingOptions)) { diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs new file mode 100644 index 0000000000..401ca73b80 --- /dev/null +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.Library; + +/// +/// Resolver rule class for ignoring files via .ignore. +/// +public class DotIgnoreIgnoreRule : IResolverIgnoreRule +{ + private static FileInfo? FindIgnoreFile(DirectoryInfo directory) + { + var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore")); + if (ignoreFile.Exists) + { + return ignoreFile; + } + + var parentDir = directory.Parent; + if (parentDir is null) + { + return null; + } + + return FindIgnoreFile(parentDir); + } + + /// + public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) + { + return IsIgnored(fileInfo, parent); + } + + /// + /// Checks whether or not the file is ignored. + /// + /// The file information. + /// The parent BaseItem. + /// True if the file should be ignored. + public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent) + { + if (fileInfo.IsDirectory) + { + var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName)); + if (dirIgnoreFile is null) + { + return false; + } + + // ignore the directory only if the .ignore file is empty + // evaluate individual files otherwise + return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile)); + } + + var parentDirPath = Path.GetDirectoryName(fileInfo.FullName); + if (string.IsNullOrEmpty(parentDirPath)) + { + return false; + } + + var folder = new DirectoryInfo(parentDirPath); + var ignoreFile = FindIgnoreFile(folder); + if (ignoreFile is null) + { + return false; + } + + string ignoreFileString = GetFileContent(ignoreFile); + + if (string.IsNullOrWhiteSpace(ignoreFileString)) + { + // Ignore directory if we just have the file + return true; + } + + // If file has content, base ignoring off the content .gitignore-style rules + var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var ignore = new Ignore.Ignore(); + ignore.Add(ignoreRules); + + return ignore.IsIgnored(fileInfo.FullName); + } + + private static string GetFileContent(FileInfo dirIgnoreFile) + { + using (var reader = dirIgnoreFile.OpenText()) + { + return reader.ReadToEnd(); + } + } +} diff --git a/Emby.Server.Implementations/Library/ExternalDataManager.cs b/Emby.Server.Implementations/Library/ExternalDataManager.cs new file mode 100644 index 0000000000..d3cfa1d256 --- /dev/null +++ b/Emby.Server.Implementations/Library/ExternalDataManager.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.MediaSegments; +using MediaBrowser.Controller.Trickplay; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Library; + +/// +/// IExternalDataManager implementation. +/// +public class ExternalDataManager : IExternalDataManager +{ + private readonly IKeyframeManager _keyframeManager; + private readonly IMediaSegmentManager _mediaSegmentManager; + private readonly IPathManager _pathManager; + private readonly ITrickplayManager _trickplayManager; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The keyframe manager. + /// The media segment manager. + /// The path manager. + /// The trickplay manager. + /// The logger. + public ExternalDataManager( + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager, + IPathManager pathManager, + ITrickplayManager trickplayManager, + ILogger logger) + { + _keyframeManager = keyframeManager; + _mediaSegmentManager = mediaSegmentManager; + _pathManager = pathManager; + _trickplayManager = trickplayManager; + _logger = logger; + } + + /// + public async Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken) + { + var validPaths = _pathManager.GetExtractedDataPaths(item).Where(Directory.Exists).ToList(); + var itemId = item.Id; + if (validPaths.Count > 0) + { + foreach (var path in validPaths) + { + try + { + Directory.Delete(path, true); + } + catch (Exception ex) + { + _logger.LogWarning("Unable to prune external item data at {Path}: {Exception}", path, ex); + } + } + } + + await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false); + await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false); + await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false); + } +} diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index bb45dd87e9..25ddade829 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using DotNet.Globbing; namespace Emby.Server.Implementations.Library diff --git a/Emby.Server.Implementations/Library/KeyframeManager.cs b/Emby.Server.Implementations/Library/KeyframeManager.cs new file mode 100644 index 0000000000..18f4ce0479 --- /dev/null +++ b/Emby.Server.Implementations/Library/KeyframeManager.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.MediaEncoding.Keyframes; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library; + +/// +/// Manager for Keyframe data. +/// +public class KeyframeManager : IKeyframeManager +{ + private readonly IKeyframeRepository _repository; + + /// + /// Initializes a new instance of the class. + /// + /// The keyframe repository. + public KeyframeManager(IKeyframeRepository repository) + { + _repository = repository; + } + + /// + public IReadOnlyList GetKeyframeData(Guid itemId) + { + return _repository.GetKeyframeData(itemId); + } + + /// + public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken) + { + await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken) + { + await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false); + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index c483f3c61f..df71868b6a 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2,7 +2,6 @@ #pragma warning disable CA5394 using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -11,6 +10,7 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using BitFaster.Caching.Lru; using Emby.Naming.Common; using Emby.Naming.TV; using Emby.Server.Implementations.Library.Resolvers; @@ -18,8 +18,10 @@ using Emby.Server.Implementations.Library.Validators; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks.Tasks; using Emby.Server.Implementations.Sorting; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; @@ -32,10 +34,12 @@ using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; @@ -62,14 +66,13 @@ namespace Emby.Server.Implementations.Library private const string ShortcutFileExtension = ".mblink"; private readonly ILogger _logger; - private readonly ConcurrentDictionary _cache; private readonly ITaskManager _taskManager; private readonly IUserManager _userManager; - private readonly IUserDataManager _userDataRepository; + private readonly IUserDataManager _userDataManager; private readonly IServerConfigurationManager _configurationManager; private readonly Lazy _libraryMonitorFactory; private readonly Lazy _providerManagerFactory; - private readonly Lazy _userviewManagerFactory; + private readonly Lazy _userViewManagerFactory; private readonly IServerApplicationHost _appHost; private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; @@ -78,6 +81,8 @@ namespace Emby.Server.Implementations.Library private readonly NamingOptions _namingOptions; private readonly IPeopleRepository _peopleRepository; private readonly ExtraResolver _extraResolver; + private readonly IPathManager _pathManager; + private readonly FastConcurrentLru _cache; /// /// The _root folder sync lock. @@ -103,56 +108,61 @@ namespace Emby.Server.Implementations.Library /// The task manager. /// The user manager. /// The configuration manager. - /// The user data repository. + /// The user data manager. /// The library monitor. /// The file system. /// The provider manager. - /// The userview manager. + /// The user view manager. /// The media encoder. /// The item repository. /// The image processor. /// The naming options. /// The directory service. - /// The People Repository. + /// The people repository. + /// The path manager. public LibraryManager( IServerApplicationHost appHost, ILoggerFactory loggerFactory, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, - IUserDataManager userDataRepository, + IUserDataManager userDataManager, Lazy libraryMonitorFactory, IFileSystem fileSystem, Lazy providerManagerFactory, - Lazy userviewManagerFactory, + Lazy userViewManagerFactory, IMediaEncoder mediaEncoder, IItemRepository itemRepository, IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService, - IPeopleRepository peopleRepository) + IPeopleRepository peopleRepository, + IPathManager pathManager) { _appHost = appHost; _logger = loggerFactory.CreateLogger(); _taskManager = taskManager; _userManager = userManager; _configurationManager = configurationManager; - _userDataRepository = userDataRepository; + _userDataManager = userDataManager; _libraryMonitorFactory = libraryMonitorFactory; _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; - _userviewManagerFactory = userviewManagerFactory; + _userViewManagerFactory = userViewManagerFactory; _mediaEncoder = mediaEncoder; _itemRepository = itemRepository; _imageProcessor = imageProcessor; - _cache = new ConcurrentDictionary(); + + _cache = new FastConcurrentLru(_configurationManager.Configuration.CacheSize); + _namingOptions = namingOptions; _peopleRepository = peopleRepository; + _pathManager = pathManager; _extraResolver = new ExtraResolver(loggerFactory.CreateLogger(), namingOptions, directoryService); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; - RecordConfigurationValues(configurationManager.Configuration); + RecordConfigurationValues(_configurationManager.Configuration); } /// @@ -194,39 +204,39 @@ namespace Emby.Server.Implementations.Library private IProviderManager ProviderManager => _providerManagerFactory.Value; - private IUserViewManager UserViewManager => _userviewManagerFactory.Value; + private IUserViewManager UserViewManager => _userViewManagerFactory.Value; /// /// Gets or sets the postscan tasks. /// /// The postscan tasks. - private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty(); + private ILibraryPostScanTask[] PostScanTasks { get; set; } = []; /// /// Gets or sets the intro providers. /// /// The intro providers. - private IIntroProvider[] IntroProviders { get; set; } = Array.Empty(); + private IIntroProvider[] IntroProviders { get; set; } = []; /// /// Gets or sets the list of entity resolution ignore rules. /// /// The entity resolution ignore rules. - private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty(); + private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = []; /// /// Gets or sets the list of currently registered entity resolvers. /// /// The entity resolvers enumerable. - private IItemResolver[] EntityResolvers { get; set; } = Array.Empty(); + private IItemResolver[] EntityResolvers { get; set; } = []; - private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty(); + private IMultiItemResolver[] MultiItemResolvers { get; set; } = []; /// /// Gets or sets the comparers. /// /// The comparers. - private IBaseItemComparer[] Comparers { get; set; } = Array.Empty(); + private IBaseItemComparer[] Comparers { get; set; } = []; public bool IsScanRunning { get; private set; } @@ -237,20 +247,20 @@ namespace Emby.Server.Implementations.Library /// The resolvers. /// The intro providers. /// The item comparers. - /// The post scan tasks. + /// The post scan tasks. public void AddParts( IEnumerable rules, IEnumerable resolvers, IEnumerable introProviders, IEnumerable itemComparers, - IEnumerable postscanTasks) + IEnumerable postScanTasks) { EntityResolutionIgnoreRules = rules.ToArray(); EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); MultiItemResolvers = EntityResolvers.OfType().ToArray(); IntroProviders = introProviders.ToArray(); Comparers = itemComparers.ToArray(); - PostscanTasks = postscanTasks.ToArray(); + PostScanTasks = postScanTasks.ToArray(); } /// @@ -300,7 +310,7 @@ namespace Emby.Server.Implementations.Library } } - _cache[item.Id] = item; + _cache.AddOrUpdate(item.Id, item); } public void DeleteItem(BaseItem item, DeleteOptions options) @@ -359,7 +369,7 @@ namespace Emby.Server.Implementations.Library var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren(false) - : Array.Empty(); + : []; foreach (var metadataPath in GetMetadataPaths(item, children)) { @@ -385,7 +395,7 @@ namespace Emby.Server.Implementations.Library } } - if (options.DeleteFileLocation && item.IsFileProtocol) + if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item)) { // Assume only the first is required // Add this flag to GetDeletePaths if required in the future @@ -454,24 +464,85 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); _itemRepository.DeleteItem(item.Id); + _cache.TryRemove(item.Id, out _); foreach (var child in children) { _itemRepository.DeleteItem(child.Id); + _cache.TryRemove(child.Id, out _); } - _cache.TryRemove(item.Id, out _); - ReportItemRemoved(item, parent); } - private static List GetMetadataPaths(BaseItem item, IEnumerable children) + private bool IsInternalItem(BaseItem item) + { + if (!item.IsFileProtocol) + { + return false; + } + + var pathToCheck = item switch + { + Genre => _configurationManager.ApplicationPaths.GenrePath, + MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath, + MusicGenre => _configurationManager.ApplicationPaths.GenrePath, + Person => _configurationManager.ApplicationPaths.PeoplePath, + Studio => _configurationManager.ApplicationPaths.StudioPath, + Year => _configurationManager.ApplicationPaths.YearPath, + _ => null + }; + + var itemPath = item.Path; + if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath)) + { + var cleanPath = _fileSystem.GetValidFilename(itemPath); + var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck); + + return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal); + } + + return false; + } + + private List GetMetadataPaths(BaseItem item, IEnumerable children) + { + var list = GetInternalMetadataPaths(item); + foreach (var child in children) + { + list.AddRange(GetInternalMetadataPaths(child)); + } + + return list; + } + + private List GetInternalMetadataPaths(BaseItem item) { var list = new List { item.GetInternalMetadataPath() }; - list.AddRange(children.Select(i => i.GetInternalMetadataPath())); + if (item is Video video) + { + // Trickplay + list.Add(_pathManager.GetTrickplayDirectory(video)); + + // Subtitles and attachments + foreach (var mediaSource in item.GetMediaSources(false)) + { + var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id); + if (subtitleFolder is not null) + { + list.Add(subtitleFolder); + } + + var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id); + if (attachmentFolder is not null) + { + list.Add(attachmentFolder); + } + } + } return list; } @@ -592,7 +663,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf); - files = Array.Empty(); + files = []; } else { @@ -600,7 +671,7 @@ namespace Emby.Server.Implementations.Library } } - // Need to remove subpaths that may have been resolved from shortcuts + // Need to remove sub-paths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action if (isPhysicalRoot) { @@ -610,10 +681,11 @@ namespace Emby.Server.Implementations.Library args.FileSystemChildren = files; } - // Check to see if we should resolve based on our contents - if (args.IsDirectory && !ShouldResolvePathContents(args)) + // Filter content based on ignore rules + if (args.IsDirectory) { - return null; + var filtered = args.GetActualFileSystemChildren().ToArray(); + args.FileSystemChildren = filtered ?? []; } return ResolveItem(args, resolvers); @@ -628,10 +700,10 @@ namespace Emby.Server.Implementations.Library var list = originalList.Where(i => i.IsDirectory) .Select(i => Path.TrimEndingDirectorySeparator(i.FullName)) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .ToList(); - var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath))) + var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath))) .ToList(); foreach (var dupe in dupes) @@ -639,22 +711,11 @@ namespace Emby.Server.Implementations.Library _logger.LogInformation("Found duplicate path: {0}", dupe); } - var newList = list.Except(dupes, StringComparer.OrdinalIgnoreCase).Select(_fileSystem.GetDirectoryInfo).ToList(); + var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList(); newList.AddRange(originalList.Where(i => !i.IsDirectory)); return newList; } - /// - /// Determines whether a path should be ignored based on its contents - called after the contents have been read. - /// - /// The args. - /// true if XXXX, false otherwise. - private static bool ShouldResolvePathContents(ItemResolveArgs args) - { - // Ignore any folders containing a file called .ignore - return !args.ContainsFileSystemEntryByName(".ignore"); - } - public IEnumerable ResolvePaths(IEnumerable files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null) { return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers); @@ -729,8 +790,6 @@ namespace Emby.Server.Implementations.Library { var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath; - Directory.CreateDirectory(rootFolderPath); - var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOperationException("Something went very wong")) .DeepCopy(); @@ -745,11 +804,12 @@ namespace Emby.Server.Implementations.Library // Add in the plug-in folders var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists"); - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); Folder folder = new PlaylistsFolder { - Path = path + Path = path, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, }; if (folder.Id.IsEmpty()) @@ -835,7 +895,7 @@ namespace Emby.Server.Implementations.Library { Path = path, IsFolder = isFolder, - OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, + OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)], Limit = 1, DtoOptions = new DtoOptions(true) }; @@ -941,7 +1001,7 @@ namespace Emby.Server.Implementations.Library { var existing = GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { BaseItemKind.MusicArtist }, + IncludeItemTypes = [BaseItemKind.MusicArtist], Name = name, DtoOptions = options }).Cast() @@ -960,12 +1020,13 @@ namespace Emby.Server.Implementations.Library var item = GetItemById(id) as T; if (item is null) { + var info = Directory.CreateDirectory(path); item = new T { Name = name, Id = id, - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Path = path }; @@ -1091,7 +1152,7 @@ namespace Emby.Server.Implementations.Library /// Task. private async Task RunPostScanTasks(IProgress progress, CancellationToken cancellationToken) { - var tasks = PostscanTasks.ToList(); + var tasks = PostScanTasks.ToList(); var numComplete = 0; var numTasks = tasks.Count; @@ -1214,7 +1275,7 @@ namespace Emby.Server.Implementations.Library private CollectionTypeOptions? GetCollectionType(string path) { - var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false); + var files = _fileSystem.GetFilePaths(path, [".collection"], true, false); foreach (ReadOnlySpan file in files) { if (Enum.TryParse(Path.GetFileNameWithoutExtension(file), true, out var res)) @@ -1234,7 +1295,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("Guid can't be empty", nameof(id)); } - if (_cache.TryGetValue(id, out BaseItem? item)) + if (_cache.TryGet(id, out var item)) { return item; } @@ -1251,7 +1312,7 @@ namespace Emby.Server.Implementations.Library /// public T? GetItemById(Guid id) - where T : BaseItem + where T : BaseItem { var item = GetItemById(id); if (item is T typedItem) @@ -1285,7 +1346,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new[] { parent }); + SetTopParentIdsOrAncestors(query, [parent]); } } @@ -1316,7 +1377,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new[] { parent }); + SetTopParentIdsOrAncestors(query, [parent]); } } @@ -1343,6 +1404,36 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetItemList(query); } + public IReadOnlyList GetLatestItemList(InternalItemsQuery query, IReadOnlyList parents, CollectionType collectionType) + { + SetTopParentIdsOrAncestors(query, parents); + + if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) + { + if (query.User is not null) + { + AddUserToQuery(query, query.User); + } + } + + return _itemRepository.GetLatestItemList(query, collectionType); + } + + public IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection parents, DateTime dateCutoff) + { + SetTopParentIdsOrAncestors(query, parents); + + if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) + { + if (query.User is not null) + { + AddUserToQuery(query, query.User); + } + } + + return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff); + } + public QueryResult QueryItems(InternalItemsQuery query) { if (query.User is not null) @@ -1447,7 +1538,7 @@ namespace Emby.Server.Implementations.Library // Optimize by querying against top level views query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); - query.AncestorIds = Array.Empty(); + query.AncestorIds = []; // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) @@ -1474,7 +1565,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new[] { parent }); + SetTopParentIdsOrAncestors(query, [parent]); } } @@ -1504,7 +1595,7 @@ namespace Emby.Server.Implementations.Library // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid() }; + query.TopParentIds = [Guid.NewGuid()]; } } else @@ -1515,7 +1606,7 @@ namespace Emby.Server.Implementations.Library // Prevent searching in all libraries due to empty filter if (query.AncestorIds.Length == 0) { - query.AncestorIds = new[] { Guid.NewGuid() }; + query.AncestorIds = [Guid.NewGuid()]; } } @@ -1544,7 +1635,7 @@ namespace Emby.Server.Implementations.Library // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid() }; + query.TopParentIds = [Guid.NewGuid()]; } } } @@ -1555,7 +1646,7 @@ namespace Emby.Server.Implementations.Library { if (view.ViewType == CollectionType.livetv) { - return new[] { view.Id }; + return [view.Id]; } // Translate view into folders @@ -1567,7 +1658,7 @@ namespace Emby.Server.Implementations.Library return GetTopParentIdsForQuery(displayParent, user); } - return Array.Empty(); + return []; } if (!view.ParentId.IsEmpty()) @@ -1578,7 +1669,7 @@ namespace Emby.Server.Implementations.Library return GetTopParentIdsForQuery(displayParent, user); } - return Array.Empty(); + return []; } // Handle grouping @@ -1593,7 +1684,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => GetTopParentIdsForQuery(i, user)); } - return Array.Empty(); + return []; } if (item is CollectionFolder collectionFolder) @@ -1604,10 +1695,10 @@ namespace Emby.Server.Implementations.Library var topParent = item.GetTopParent(); if (topParent is not null) { - return new[] { topParent.Id }; + return [topParent.Id]; } - return Array.Empty(); + return []; } /// @@ -1651,7 +1742,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error getting intros"); - return Enumerable.Empty(); + return []; } } @@ -1800,7 +1891,7 @@ namespace Emby.Server.Implementations.Library userComparer.User = user; userComparer.UserManager = _userManager; - userComparer.UserDataRepository = _userDataRepository; + userComparer.UserDataManager = _userDataManager; return userComparer; } @@ -1811,7 +1902,7 @@ namespace Emby.Server.Implementations.Library /// public void CreateItem(BaseItem item, BaseItem? parent) { - CreateItems(new[] { item }, parent, CancellationToken.None); + CreateItems([item], parent, CancellationToken.None); } /// @@ -1863,7 +1954,7 @@ namespace Emby.Server.Implementations.Library try { - return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified; + return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeconds > 1; } catch (Exception ex) { @@ -1890,6 +1981,8 @@ namespace Emby.Server.Implementations.Library return; } + var anyChange = false; + foreach (var img in outdated) { var image = img; @@ -1921,6 +2014,7 @@ namespace Emby.Server.Implementations.Library try { size = _imageProcessor.GetImageDimensions(item, image); + anyChange = image.Width != size.Width || image.Height != size.Height; image.Width = size.Width; image.Height = size.Height; } @@ -1928,23 +2022,29 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path); size = default; + anyChange = image.Width != size.Width || image.Height != size.Height; image.Width = 0; image.Height = 0; } try { - image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size); + var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size); + anyChange = anyChange || !blurhash.Equals(image.BlurHash, StringComparison.Ordinal); + image.BlurHash = blurhash; } catch (Exception ex) { _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path); + anyChange = anyChange || !string.IsNullOrEmpty(image.BlurHash); image.BlurHash = string.Empty; } try { - image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path); + var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path); + anyChange = anyChange || modifiedDate != image.DateModified; + image.DateModified = modifiedDate; } catch (Exception ex) { @@ -1952,20 +2052,28 @@ namespace Emby.Server.Implementations.Library } } - _itemRepository.SaveImages(item); + if (anyChange) + { + _itemRepository.SaveImages(item); + } + RegisterItem(item); } /// public async Task UpdateItemsAsync(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - _itemRepository.SaveItems(items, cancellationToken); - foreach (var item in items) { + item.DateLastSaved = DateTime.UtcNow; await RunMetadataSavers(item, updateReason).ConfigureAwait(false); + + // Modify again, so saved value is after write time of externally saved metadata + item.DateLastSaved = DateTime.UtcNow; } + _itemRepository.SaveItems(items, cancellationToken); + if (ItemUpdated is not null) { foreach (var item in items) @@ -1997,7 +2105,7 @@ namespace Emby.Server.Implementations.Library /// public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) - => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); + => UpdateItemsAsync([item], parent, updateReason, cancellationToken); public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) { @@ -2006,8 +2114,6 @@ namespace Emby.Server.Implementations.Library await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false); } - item.DateLastSaved = DateTime.UtcNow; - await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); } @@ -2226,13 +2332,13 @@ namespace Emby.Server.Implementations.Library if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, ForcedSortName = sortName @@ -2274,13 +2380,13 @@ namespace Emby.Server.Implementations.Library if (item is null) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, ForcedSortName = sortName, @@ -2293,12 +2399,13 @@ namespace Emby.Server.Implementations.Library isNew = true; } - var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + var lastRefreshedUtc = item.DateLastRefreshed; + var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc; } if (refresh) @@ -2338,31 +2445,31 @@ namespace Emby.Server.Implementations.Library if (item is null) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, - ForcedSortName = sortName + ForcedSortName = sortName, + DisplayParentId = parentId }; - item.DisplayParentId = parentId; - CreateItem(item, null); isNew = true; } - var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + var lastRefreshedUtc = item.DateLastRefreshed; + var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc; } if (refresh) @@ -2408,20 +2515,19 @@ namespace Emby.Server.Implementations.Library if (item is null) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, - ForcedSortName = sortName + ForcedSortName = sortName, + DisplayParentId = parentId }; - item.DisplayParentId = parentId; - CreateItem(item, null); isNew = true; @@ -2433,12 +2539,13 @@ namespace Emby.Server.Implementations.Library item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); } - var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + var lastRefreshedUtc = item.DateLastRefreshed; + var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc; } if (refresh) @@ -2478,8 +2585,11 @@ namespace Emby.Server.Implementations.Library } /// - public int? GetSeasonNumberFromPath(string path) - => SeasonPathParser.Parse(path, true, true).SeasonNumber; + public int? GetSeasonNumberFromPath(string path, Guid? parentId) + { + var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null; + return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber; + } /// public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) @@ -2496,7 +2606,6 @@ namespace Emby.Server.Implementations.Library var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; - // TODO nullable - what are we trying to do there with empty episodeInfo? EpisodeInfo? episodeInfo = null; if (episode.IsFileProtocol) { @@ -2514,43 +2623,11 @@ namespace Emby.Server.Implementations.Library } } - episodeInfo ??= new EpisodeInfo(episode.Path); - - try - { - var libraryOptions = GetLibraryOptions(episode); - if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase)) - { - // Read from metadata - var mediaInfo = _mediaEncoder.GetMediaInfo( - new MediaInfoRequest - { - MediaSource = episode.GetMediaSources(false)[0], - MediaType = DlnaProfileType.Video - }, - CancellationToken.None).GetAwaiter().GetResult(); - if (mediaInfo.ParentIndexNumber > 0) - { - episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber; - } - - if (mediaInfo.IndexNumber > 0) - { - episodeInfo.EpisodeNumber = mediaInfo.IndexNumber; - } - - if (!string.IsNullOrEmpty(mediaInfo.ShowName)) - { - episodeInfo.SeriesName = mediaInfo.ShowName; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", episodeInfo.Path); - } - var changed = false; + if (episodeInfo is null) + { + return changed; + } if (episodeInfo.IsByDate) { @@ -2630,15 +2707,6 @@ namespace Emby.Server.Implementations.Library { episode.ParentIndexNumber = season.IndexNumber; } - else - { - /* - Anime series don't generally have a season in their file name, however, - TVDb needs a season to correctly get the metadata. - Hence, a null season needs to be filled with something. */ - // FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified - episode.ParentIndexNumber = 1; - } if (episode.ParentIndexNumber.HasValue) { @@ -2663,27 +2731,33 @@ namespace Emby.Server.Implementations.Library public IEnumerable FindExtras(BaseItem owner, IReadOnlyList fileSystemChildren, IDirectoryService directoryService) { - var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions); + // Apply .ignore rules + var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList(); + var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath); if (ownerVideoInfo is null) { yield break; } - var count = fileSystemChildren.Count; + var count = filtered.Count; for (var i = 0; i < count; i++) { - var current = fileSystemChildren[i]; + var current = filtered[i]; if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name)) { var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false); - foreach (var file in filesInSubFolder) + var filesInSubFolderList = filesInSubFolder.ToList(); + + bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1; + + foreach (var file in filesInSubFolderList) { if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType)) { continue; } - var extra = GetExtra(file, extraType.Value); + var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder); if (extra is not null) { yield return extra; @@ -2692,7 +2766,7 @@ namespace Emby.Server.Implementations.Library } else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType)) { - var extra = GetExtra(current, extraType.Value); + var extra = GetExtra(current, extraType.Value, false); if (extra is not null) { yield return extra; @@ -2700,7 +2774,7 @@ namespace Emby.Server.Implementations.Library } } - BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType) + BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder) { var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType)); if (extra is not Video && extra is not Audio) @@ -2723,6 +2797,7 @@ namespace Emby.Server.Implementations.Library extra.ParentId = Guid.Empty; extra.OwnerId = owner.Id; + extra.IsInMixedFolder = isInMixedFolder; return extra; } } @@ -2853,7 +2928,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(name)); } - name = _fileSystem.GetValidFilename(name); + name = _fileSystem.GetValidFilename(name.Trim()); var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; @@ -2887,7 +2962,7 @@ namespace Emby.Server.Implementations.Library { var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values? - await File.WriteAllBytesAsync(path, Array.Empty()).ConfigureAwait(false); + FileHelper.CreateEmpty(path); } CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); @@ -2930,19 +3005,28 @@ namespace Emby.Server.Implementations.Library if (personEntity is null) { - var path = Person.GetPath(person.Name); - personEntity = new Person() + try { - Name = person.Name, - Id = GetItemByNameId(path), - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - Path = path - }; + var path = Person.GetPath(person.Name); + var info = Directory.CreateDirectory(path); + personEntity = new Person() + { + Name = person.Name, + Id = GetItemByNameId(path), + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, + Path = path + }; - personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); - saveEntity = true; - createEntity = true; + personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); + saveEntity = true; + createEntity = true; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to create person {Name}", person.Name); + continue; + } } foreach (var id in person.ProviderIds) @@ -2976,6 +3060,8 @@ namespace Emby.Server.Implementations.Library } await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); + personEntity.DateLastSaved = DateTime.UtcNow; + CreateItems([personEntity], null, CancellationToken.None); } } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 5795c47ccc..ab30971e27 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -13,8 +13,10 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; @@ -425,6 +427,7 @@ namespace Emby.Server.Implementations.Library if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index)) { source.DefaultAudioStreamIndex = index; + source.DefaultAudioIndexSource = AudioIndexSource.User; return; } } @@ -432,6 +435,15 @@ namespace Emby.Server.Implementations.Library var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference); source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); + if (user.PlayDefaultAudioTrack) + { + source.DefaultAudioIndexSource |= AudioIndexSource.Default; + } + + if (preferredAudio.Count > 0) + { + source.DefaultAudioIndexSource |= AudioIndexSource.Language; + } } public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user) @@ -669,17 +681,17 @@ namespace Emby.Server.Implementations.Library mediaInfo = await _mediaEncoder.GetMediaInfo( new MediaInfoRequest - { - MediaSource = mediaSource, - MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, - ExtractChapters = false - }, + { + MediaSource = mediaSource, + MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, + ExtractChapters = false + }, cancellationToken).ConfigureAwait(false); if (cacheFilePath is not null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - FileStream createStream = File.Create(cacheFilePath); + FileStream createStream = AsyncFile.Create(cacheFilePath); await using (createStream.ConfigureAwait(false)) { await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); @@ -782,9 +794,13 @@ namespace Emby.Server.Implementations.Library { ArgumentException.ThrowIfNullOrEmpty(id); - // TODO probably shouldn't throw here but it is kept for "backwards compatibility" - var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException(); - return Task.FromResult(new Tuple(info.MediaSource, info as IDirectStreamProvider)); + var info = GetLiveStreamInfo(id); + if (info is null) + { + return Task.FromResult>(new Tuple(null, null)); + } + + return Task.FromResult>(new Tuple(info.MediaSource, info as IDirectStreamProvider)); } public ILiveStream GetLiveStreamInfo(string id) diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index ea223e3ece..631179ffcf 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.Entities; @@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library return null; } + // Sort in the following order: Default > No tag > Forced var sortedStreams = streams .Where(i => i.Type == MediaStreamType.Subtitle) .OrderByDescending(x => x.IsExternal) - .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(x => x.IsForced) .ThenByDescending(x => x.IsDefault) - .ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) + .ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) + .ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language)) + .ThenByDescending(x => x.IsForced) .ToList(); MediaStream? stream = null; + if (mode == SubtitlePlaybackMode.Default) { - // Load subtitles according to external, forced and default flags. - stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + // Load subtitles according to external, default and forced flags. + stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced); } else if (mode == SubtitlePlaybackMode.Smart) { // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages. - // If no subtitles of preferred language available, use default behaviour. + // If no subtitles of preferred language available, use none. + // If the audio language is one of the user's preferred subtitle languages behave like OnlyForced. if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ?? - sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages)); } else { - // Respect forced flag. - stream = sortedStreams.FirstOrDefault(x => x.IsForced); + stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } } else if (mode == SubtitlePlaybackMode.Always) { - // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour. - stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ?? - sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour. + stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ?? + BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // Only load subtitles that are flagged forced. - stream = sortedStreams.FirstOrDefault(x => x.IsForced); + // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language + stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } return stream?.Index; @@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library if (mode == SubtitlePlaybackMode.Default) { // Prefer embedded metadata over smart logic - filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault) + // Load subtitles according to external, default, and forced flags. + filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced) .ToList(); } else if (mode == SubtitlePlaybackMode.Smart) { // Prefer smart logic over embedded metadata + // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior. if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) + filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages)) .ToList(); } + else + { + filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages); + } } else if (mode == SubtitlePlaybackMode.Always) { - // Always load the most suitable full subtitles - filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList(); + // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior. + filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages)) + .ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // Always load the most suitable full subtitles - filteredStreams = sortedStreams.Where(s => s.IsForced).ToList(); + // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language + filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages); } - // Load forced subs if we have found no suitable full subtitles - var iterStreams = filteredStreams is null || filteredStreams.Count == 0 - ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) - : filteredStreams; + // If filteredStreams is null, initialize it as an empty list to avoid null reference errors + filteredStreams ??= new List(); - foreach (var stream in iterStreams) + foreach (var stream in filteredStreams) { stream.Score = GetStreamScore(stream, preferredLanguages); } } + private static bool MatchesPreferredLanguage(string language, IReadOnlyList preferredLanguages) + { + // If preferredLanguages is empty, treat it as "any language" (wildcard) + return preferredLanguages.Count == 0 || + preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsLanguageUndefined(string language) + { + // Check for null, empty, or known placeholders + return string.IsNullOrEmpty(language) || + language.Equals("und", StringComparison.OrdinalIgnoreCase) || + language.Equals("unknown", StringComparison.OrdinalIgnoreCase) || + language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) || + language.Equals("mul", StringComparison.OrdinalIgnoreCase) || + language.Equals("zxx", StringComparison.OrdinalIgnoreCase); + } + + private static List BehaviorOnlyForced(IEnumerable sortedStreams, IReadOnlyList preferredLanguages) + { + return sortedStreams + .Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language))) + .OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages)) + .ThenByDescending(s => IsLanguageUndefined(s.Language)) + .ToList(); + } + internal static int GetStreamScore(MediaStream stream, IReadOnlyList languagePreferences) { var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 71c69ec50a..28cf695007 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -4,8 +4,9 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs new file mode 100644 index 0000000000..a9b7a1274b --- /dev/null +++ b/Emby.Server.Implementations/Library/PathManager.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; + +namespace Emby.Server.Implementations.Library; + +/// +/// IPathManager implementation. +/// +public class PathManager : IPathManager +{ + private readonly IServerConfigurationManager _config; + private readonly IApplicationPaths _appPaths; + + /// + /// Initializes a new instance of the class. + /// + /// The server configuration manager. + /// The application paths. + public PathManager( + IServerConfigurationManager config, + IApplicationPaths appPaths) + { + _config = config; + _appPaths = appPaths; + } + + private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); + + private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments"); + + /// + public string GetAttachmentPath(string mediaSourceId, string fileName) + { + return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName); + } + + /// + public string GetAttachmentFolderPath(string mediaSourceId) + { + var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan(); + + return Path.Join(AttachmentCachePath, id[..2], id); + } + + /// + public string GetSubtitleFolderPath(string mediaSourceId) + { + var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan(); + + return Path.Join(SubtitleCachePath, id[..2], id); + } + + /// + public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension) + { + return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension); + } + + /// + public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false) + { + var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan(); + + return saveWithMedia + ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(Path.GetFileName(item.Path), ".trickplay")) + : Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id); + } + + /// + public string GetChapterImageFolderPath(BaseItem item) + { + return Path.Combine(item.GetInternalMetadataPath(), "chapters"); + } + + /// + public string GetChapterImagePath(BaseItem item, long chapterPositionTicks) + { + var filename = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg"; + + return Path.Combine(GetChapterImageFolderPath(item), filename); + } + + /// + public IReadOnlyList GetExtractedDataPaths(BaseItem item) + { + var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture); + return [ + GetAttachmentFolderPath(mediaSourceId), + GetSubtitleFolderPath(mediaSourceId), + GetTrickplayDirectory(item, false), + GetTrickplayDirectory(item, true), + GetChapterImageFolderPath(item) + ]; + } +} diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index c9e3a4daf8..06aa772bd9 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -136,23 +136,33 @@ namespace Emby.Server.Implementations.Library if (config.UseFileCreationTimeForDateAdded) { - // directoryService.getFile may return null - if (info is not null) + var fileCreationDate = info?.CreationTimeUtc; + if (fileCreationDate is not null) { - var dateCreated = info.CreationTimeUtc; - - if (dateCreated.Equals(DateTime.MinValue)) + var dateCreated = fileCreationDate; + if (dateCreated == DateTime.MinValue) { dateCreated = DateTime.UtcNow; } - item.DateCreated = dateCreated; + item.DateCreated = dateCreated.Value; } } else { item.DateCreated = DateTime.UtcNow; } + + if (info is not null && !info.IsDirectory) + { + item.Size = info.Length; + } + + var fileModificationDate = info?.LastWriteTimeUtc; + if (fileModificationDate.HasValue) + { + item.DateModified = fileModificationDate.Value; + } } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs index b4791b9456..b9f9f29723 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs @@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers _ => _videoResolvers }; - public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType) + public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "") { - var extraResult = GetExtraInfo(path, _namingOptions); + var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot); if (extraResult.ExtraType is null) { extraType = null; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 4debe722b9..b2ceee97d8 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } var videoInfos = files - .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName)) + .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath)) .Where(f => f is not null) .ToList(); - var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName); + var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath); var result = new MultiItemResolverResult { @@ -456,12 +456,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { var videoPath = result.Items[0].Path; var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name)); + var hasOtherSubfolders = multiDiscFolders.Count > 0; - if (!hasPhotos) + if (!hasPhotos && !hasOtherSubfolders) { var movie = (T)result.Items[0]; movie.IsInMixedFolder = false; - movie.Name = Path.GetFileName(movie.ContainingFolderPath); + if (collectionType == CollectionType.movies || collectionType is null) + { + movie.Name = Path.GetFileName(movie.ContainingFolderPath); + } + return movie; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index abf2d01159..6cb63a28a2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var path = args.Path; - var seasonParserResult = SeasonPathParser.Parse(path, true, true); + var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true); var season = new Season { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index fb48d7bf17..c81a0adb89 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (child.IsDirectory) { - if (IsSeasonFolder(child.FullName, isTvContentType)) + if (IsSeasonFolder(child.FullName, path, isTvContentType)) { _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); return true; @@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// Determines whether [is season folder] [the specified path]. /// /// The path. + /// The parentpath. /// if set to true [is tv content type]. /// true if [is season folder] [the specified path]; otherwise, false. - private static bool IsSeasonFolder(string path, bool isTvContentType) + private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType) { - var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber; + var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber; return seasonNumber.HasValue; } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 3ac1d02192..9d81b835ce 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -3,8 +3,9 @@ using System; using System.Collections.Generic; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index 76e564d535..71ce3b6012 100644 --- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -4,13 +4,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library; @@ -77,15 +77,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask CollapseBoxSetItems = false, Recursive = true, DtoOptions = new DtoOptions(false), - ImageTypes = new[] { imageType }, + ImageTypes = [imageType], Limit = 30, // TODO max parental rating configurable - MaxParentalRating = 10, - OrderBy = new[] - { + MaxParentalRating = new(10, null), + OrderBy = + [ (ItemSortBy.Random, SortOrder.Ascending) - }, - IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series } + ], + IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series] }); } } diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index a41ef888b0..be1d96bf0b 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -1,14 +1,13 @@ #pragma warning disable RS0030 // Do not use banned APIs using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; -using Jellyfin.Data.Entities; -using Jellyfin.Extensions; -using Jellyfin.Server.Implementations; +using BitFaster.Caching.Lru; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -26,11 +25,9 @@ namespace Emby.Server.Implementations.Library /// public class UserDataManager : IUserDataManager { - private readonly ConcurrentDictionary _userData = - new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private readonly IServerConfigurationManager _config; private readonly IDbContextFactory _repository; + private readonly FastConcurrentLru _cache; /// /// Initializes a new instance of the class. @@ -43,6 +40,7 @@ namespace Emby.Server.Implementations.Library { _config = config; _repository = repository; + _cache = new FastConcurrentLru(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase); } /// @@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.Library var userId = user.InternalId; var cacheKey = GetCacheKey(userId, item.Id); - _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData); + _cache.AddOrUpdate(cacheKey, userData); UserDataSaved?.Invoke(this, new UserDataSaveEventArgs { @@ -182,7 +180,7 @@ namespace Emby.Server.Implementations.Library { var cacheKey = GetCacheKey(user.InternalId, itemId); - if (_userData.TryGetValue(cacheKey, out var data)) + if (_cache.TryGet(cacheKey, out var data)) { return data; } @@ -197,7 +195,7 @@ namespace Emby.Server.Implementations.Library }; } - return _userData.GetOrAdd(cacheKey, data); + return _cache.GetOrAdd(cacheKey, _ => data); } private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List keys) diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index d42a0e7d28..87214c273b 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -6,8 +6,10 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; @@ -370,6 +372,21 @@ namespace Emby.Server.Implementations.Library MediaTypes = mediaTypes }; + if (request.GroupItems) + { + if (parents.OfType().All(i => i.CollectionType == CollectionType.tvshows)) + { + query.Limit = limit; + return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows); + } + + if (parents.OfType().All(i => i.CollectionType == CollectionType.music)) + { + query.Limit = limit; + return _libraryManager.GetLatestItemList(query, parents, CollectionType.music); + } + } + return _libraryManager.GetItemList(query, parents); } } diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs index d51f9aaa79..a31d5eccac 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs @@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class ArtistsPostScanTask. +/// +public class ArtistsPostScanTask : ILibraryPostScanTask { /// - /// Class ArtistsPostScanTask. + /// The _library manager. /// - public class ArtistsPostScanTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public ArtistsPostScanTask( + ILibraryManager libraryManager, + ILogger logger, + IItemRepository itemRepo) { - /// - /// The _library manager. - /// - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public ArtistsPostScanTask( - ILibraryManager libraryManager, - ILogger logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public Task Run(IProgress progress, CancellationToken cancellationToken) - { - return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 7591e8391f..7cc851b73b 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -10,102 +10,101 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class ArtistsValidator. +/// +public class ArtistsValidator { /// - /// Class ArtistsValidator. + /// The library manager. /// - public class ArtistsValidator + private readonly ILibraryManager _libraryManager; + + /// + /// The logger. + /// + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// The logger. - /// - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetAllArtistNames(); - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetAllArtistNames(); - - var numComplete = 0; - var count = names.Count; - - foreach (var name in names) + try { - try - { - var item = _libraryManager.GetArtist(name); + var item = _libraryManager.GetArtist(name); - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {ArtistName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {ArtistName}", name); } - var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.MusicArtist], + IsDeadArtist = true, + IsLocked = false + }).Cast().ToList(); + + foreach (var item in deadEntities) + { + if (!item.IsAccessedByName) { - IncludeItemTypes = new[] { BaseItemKind.MusicArtist }, - IsDeadArtist = true, - IsLocked = false - }).Cast().ToList(); - - foreach (var item in deadEntities) - { - if (!item.IsAccessedByName) - { - continue; - } - - _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); - - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); + continue; } - progress.Report(100); + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs index 89f64ee4f0..e62c638ed6 100644 --- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs @@ -4,153 +4,150 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class CollectionPostScanTask. +/// +public class CollectionPostScanTask : ILibraryPostScanTask { + private readonly ILibraryManager _libraryManager; + private readonly ICollectionManager _collectionManager; + private readonly ILogger _logger; + /// - /// Class CollectionPostScanTask. + /// Initializes a new instance of the class. /// - public class CollectionPostScanTask : ILibraryPostScanTask + /// The library manager. + /// The collection manager. + /// The logger. + public CollectionPostScanTask( + ILibraryManager libraryManager, + ICollectionManager collectionManager, + ILogger logger) { - private readonly ILibraryManager _libraryManager; - private readonly ICollectionManager _collectionManager; - private readonly ILogger _logger; + _libraryManager = libraryManager; + _collectionManager = collectionManager; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The collection manager. - /// The logger. - public CollectionPostScanTask( - ILibraryManager libraryManager, - ICollectionManager collectionManager, - ILogger logger) + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var collectionNameMoviesMap = new Dictionary>(); + + foreach (var library in _libraryManager.RootFolder.Children) { - _libraryManager = libraryManager; - _collectionManager = collectionManager; - _logger = logger; + if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection) + { + continue; + } + + var startIndex = 0; + var pagesize = 1000; + + while (true) + { + var movies = _libraryManager.GetItemList(new InternalItemsQuery + { + MediaTypes = [MediaType.Video], + IncludeItemTypes = [BaseItemKind.Movie], + IsVirtualItem = false, + OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)], + Parent = library, + StartIndex = startIndex, + Limit = pagesize, + Recursive = true + }); + + foreach (var m in movies) + { + if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName)) + { + if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList)) + { + movieList.Add(movie.Id); + } + else + { + collectionNameMoviesMap[movie.CollectionName] = new HashSet { movie.Id }; + } + } + } + + if (movies.Count < pagesize) + { + break; + } + + startIndex += pagesize; + } } - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) + var numComplete = 0; + var count = collectionNameMoviesMap.Count; + + if (count == 0) { - var collectionNameMoviesMap = new Dictionary>(); - - foreach (var library in _libraryManager.RootFolder.Children) - { - if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection) - { - continue; - } - - var startIndex = 0; - var pagesize = 1000; - - while (true) - { - var movies = _libraryManager.GetItemList(new InternalItemsQuery - { - MediaTypes = new[] { MediaType.Video }, - IncludeItemTypes = new[] { BaseItemKind.Movie }, - IsVirtualItem = false, - OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, - Parent = library, - StartIndex = startIndex, - Limit = pagesize, - Recursive = true - }); - - foreach (var m in movies) - { - if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName)) - { - if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList)) - { - movieList.Add(movie.Id); - } - else - { - collectionNameMoviesMap[movie.CollectionName] = new HashSet { movie.Id }; - } - } - } - - if (movies.Count < pagesize) - { - break; - } - - startIndex += pagesize; - } - } - - var numComplete = 0; - var count = collectionNameMoviesMap.Count; - - if (count == 0) - { - progress.Report(100); - return; - } - - var boxSets = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.BoxSet }, - CollapseBoxSetItems = false, - Recursive = true - }); - - foreach (var (collectionName, movieIds) in collectionNameMoviesMap) - { - try - { - var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet; - if (boxSet is null) - { - // won't automatically create collection if only one movie in it - if (movieIds.Count >= 2) - { - boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions - { - Name = collectionName, - IsLocked = true - }); - - await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds); - } - } - else - { - await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds); - } - } - progress.Report(100); + return; } + + var boxSets = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.BoxSet], + CollapseBoxSetItems = false, + Recursive = true + }); + + foreach (var (collectionName, movieIds) in collectionNameMoviesMap) + { + try + { + var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet; + if (boxSet is null) + { + // won't automatically create collection if only one movie in it + if (movieIds.Count >= 2) + { + boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions + { + Name = collectionName, + }).ConfigureAwait(false); + + await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false); + } + } + else + { + await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false); + } + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds); + } + } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs index d21d2887b0..5097e0073d 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs @@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class GenresPostScanTask. +/// +public class GenresPostScanTask : ILibraryPostScanTask { /// - /// Class GenresPostScanTask. + /// The _library manager. /// - public class GenresPostScanTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public GenresPostScanTask( + ILibraryManager libraryManager, + ILogger logger, + IItemRepository itemRepo) { - /// - /// The _library manager. - /// - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public GenresPostScanTask( - ILibraryManager libraryManager, - ILogger logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public Task Run(IProgress progress, CancellationToken cancellationToken) - { - return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs index e59c62e239..fbfc9f7d54 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs @@ -1,81 +1,103 @@ using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class GenresValidator. +/// +public class GenresValidator { /// - /// Class GenresValidator. + /// The library manager. /// - public class GenresValidator + private readonly ILibraryManager _libraryManager; + private readonly IItemRepository _itemRepo; + + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; - private readonly IItemRepository _itemRepo; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// The logger. - /// - private readonly ILogger _logger; + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetGenreNames(); - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetGenreNames(); - - var numComplete = 0; - var count = names.Count; - - foreach (var name in names) + try { - try - { - var item = _libraryManager.GetGenre(name); + var item = _libraryManager.GetGenre(name); - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {GenreName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {GenreName}", name); } - progress.Report(100); + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre], + IsDeadGenre = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); + } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs index be119866b1..76658a81b5 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs @@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class MusicGenresPostScanTask. +/// +public class MusicGenresPostScanTask : ILibraryPostScanTask { /// - /// Class MusicGenresPostScanTask. + /// The library manager. /// - public class MusicGenresPostScanTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public MusicGenresPostScanTask( + ILibraryManager libraryManager, + ILogger logger, + IItemRepository itemRepo) { - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public MusicGenresPostScanTask( - ILibraryManager libraryManager, - ILogger logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public Task Run(IProgress progress, CancellationToken cancellationToken) - { - return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs index 1ecf4c87c9..6203bce2bc 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs @@ -5,77 +5,76 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class MusicGenresValidator. +/// +public class MusicGenresValidator { /// - /// Class MusicGenresValidator. + /// The library manager. /// - public class MusicGenresValidator + private readonly ILibraryManager _libraryManager; + + /// + /// The logger. + /// + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// The logger. - /// - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetMusicGenreNames(); - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetMusicGenreNames(); - - var numComplete = 0; - var count = names.Count; - - foreach (var name in names) + try { - try - { - var item = _libraryManager.GetMusicGenre(name); + var item = _libraryManager.GetMusicGenre(name); - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {GenreName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {GenreName}", name); } - progress.Report(100); + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 725b8f76c7..b7fd24fa5c 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -9,119 +9,114 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class PeopleValidator. +/// +public class PeopleValidator { /// - /// Class PeopleValidator. + /// The _library manager. /// - public class PeopleValidator + private readonly ILibraryManager _libraryManager; + + /// + /// The _logger. + /// + private readonly ILogger _logger; + + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The file system. + public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem) { - /// - /// The _library manager. - /// - private readonly ILibraryManager _libraryManager; + _libraryManager = libraryManager; + _logger = logger; + _fileSystem = fileSystem; + } - /// - /// The _logger. - /// - private readonly ILogger _logger; + /// + /// Validates the people. + /// + /// The cancellation token. + /// The progress. + /// Task. + public async Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) + { + var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery()); - private readonly IFileSystem _fileSystem; + var numComplete = 0; - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The file system. - public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem) + var numPeople = people.Count; + + _logger.LogDebug("Will refresh {Amount} people", numPeople); + + foreach (var person in people) { - _libraryManager = libraryManager; - _logger = logger; - _fileSystem = fileSystem; - } + cancellationToken.ThrowIfCancellationRequested(); - /// - /// Validates the people. - /// - /// The cancellation token. - /// The progress. - /// Task. - public async Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) - { - var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery()); - - var numComplete = 0; - - var numPeople = people.Count; - - _logger.LogDebug("Will refresh {0} people", numPeople); - - foreach (var person in people) + try { - cancellationToken.ThrowIfCancellationRequested(); - - try + var item = _libraryManager.GetPerson(person); + if (item is null) { - var item = _libraryManager.GetPerson(person); - if (item is null) - { - _logger.LogWarning("Failed to get person: {Name}", person); - continue; - } - - var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ImageRefreshMode = MetadataRefreshMode.ValidationOnly, - MetadataRefreshMode = MetadataRefreshMode.ValidationOnly - }; - - await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error validating IBN entry {Person}", person); + _logger.LogWarning("Failed to get person: {Name}", person); + continue; } - // Update progress - numComplete++; - double percent = numComplete; - percent /= numPeople; + var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ImageRefreshMode = MetadataRefreshMode.ValidationOnly, + MetadataRefreshMode = MetadataRefreshMode.ValidationOnly + }; - progress.Report(100 * percent); + await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error validating IBN entry {Person}", person); } - var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = [BaseItemKind.Person], - IsDeadPerson = true, - IsLocked = false - }); + // Update progress + numComplete++; + double percent = numComplete; + percent /= numPeople; - foreach (var item in deadEntities) - { - _logger.LogInformation( - "Deleting dead {2} {0} {1}.", - item.Id.ToString("N", CultureInfo.InvariantCulture), - item.Name, - item.GetType().Name); - - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); - } - - progress.Report(100); - - _logger.LogInformation("People validation complete"); + progress.Report(100 * percent); } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Person], + IsDeadPerson = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); + } + + progress.Report(100); + + _logger.LogInformation("People validation complete"); } } diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs index c682b156b8..67c56c104d 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs @@ -5,46 +5,45 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class MusicGenresPostScanTask. +/// +public class StudiosPostScanTask : ILibraryPostScanTask { /// - /// Class MusicGenresPostScanTask. + /// The _library manager. /// - public class StudiosPostScanTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public StudiosPostScanTask( + ILibraryManager libraryManager, + ILogger logger, + IItemRepository itemRepo) { - /// - /// The _library manager. - /// - private readonly ILibraryManager _libraryManager; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; - - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public StudiosPostScanTask( - ILibraryManager libraryManager, - ILogger logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public Task Run(IProgress progress, CancellationToken cancellationToken) - { - return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 26bc49c1f0..5b87e4d9d0 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -8,98 +8,97 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class StudiosValidator. +/// +public class StudiosValidator { /// - /// Class StudiosValidator. + /// The library manager. /// - public class StudiosValidator + private readonly ILibraryManager _libraryManager; + + private readonly IItemRepository _itemRepo; + + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - private readonly IItemRepository _itemRepo; + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetStudioNames(); - /// - /// The logger. - /// - private readonly ILogger _logger; + var numComplete = 0; + var count = names.Count; - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + foreach (var name in names) { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetStudioNames(); - - var numComplete = 0; - var count = names.Count; - - foreach (var name in names) + try { - try - { - var item = _libraryManager.GetStudio(name); + var item = _libraryManager.GetStudio(name); - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {StudioName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {StudioName}", name); } - var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Studio }, - IsDeadStudio = true, - IsLocked = false - }); + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; - foreach (var item in deadEntities) - { - _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); - - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); - } - - progress.Report(100); + progress.Report(percent); } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Studio], + IsDeadStudio = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); + } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index e89ede10b4..1dce589234 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -129,5 +129,11 @@ "TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.", "TaskAudioNormalization": "Odio Normalisering", "TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon", - "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie." + "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.", + "TaskDownloadMissingLyrics": "Laai tekorte lirieke af", + "TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies", + "TaskExtractMediaSegments": "Media Segment Skandeer", + "TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.", + "TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging", + "TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings." } diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 2d29eb5bf7..a92148cafe 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -125,8 +125,8 @@ "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", "External": "خارجي", "HearingImpaired": "ضعاف السمع", - "TaskRefreshTrickplayImages": "توليد صور Trickplay", - "TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.", + "TaskRefreshTrickplayImages": "توليد صور المعاينة السريعة", + "TaskRefreshTrickplayImagesDescription": "يُولّد معاينات تنقل سريع لمقاطع الفيديو ضمن المكتبات المفعّلة.", "TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل", "TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.", "TaskAudioNormalization": "تسوية الصوت", @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "فحص مقاطع الوسائط", "TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.", "TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة", - "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة." + "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.", + "CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم", + "CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل." } diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 97aa0ca58c..dec491d08b 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -1,6 +1,6 @@ { "Sync": "Сінхранізаваць", - "Playlists": "Плэйлісты", + "Playlists": "Спісы прайгравання", "Latest": "Апошні", "LabelIpAddressValue": "IP-адрас: {0}", "ItemAddedWithName": "{0} быў дададзены ў бібліятэку", @@ -16,7 +16,7 @@ "Collections": "Калекцыі", "Default": "Па змаўчанні", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", - "Folders": "Папкі", + "Folders": "Тэчкі", "Favorites": "Абранае", "External": "Знешні", "Genres": "Жанры", @@ -135,5 +135,7 @@ "TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень", "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень", "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", - "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay" + "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", + "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка", + "CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён." } diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 72f5757531..fd3666ef1c 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.", "TaskMoveTrickplayImages": "Мигриране на Локацията за Trickplay изображения", "TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.", - "TaskExtractMediaSegments": "Сканиране за сегменти" + "TaskExtractMediaSegments": "Сканиране за сегменти", + "CleanupUserDataTask": "Задача за почистване на потребителски данни", + "CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни." } diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index 268a141ff1..fad3715f20 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -6,29 +6,29 @@ "Channels": "চ্যানেলসমূহ", "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে", "Books": "পুস্তকসমূহ", - "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল", + "AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন", "Artists": "শিল্পীগণ", "Application": "অ্যাপ্লিকেশন", "Albums": "অ্যালবামসমূহ", - "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো", + "HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো", "HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderContinueWatching": "দেখতে থাকুন", "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ", - "Genres": "শৈলীধারাসমূহ", + "Genres": "জনরা", "Folders": "ফোল্ডারসমূহ", "Favorites": "পছন্দসমূহ", "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে", - "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}", + "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}", "VersionNumber": "সংস্করণ {0}", "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}", "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে", - "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}", - "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}", + "UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}", + "UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}", "UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে", "UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে", - "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন", - "UserOfflineFromDevice": "{0} {1} থেকে বিযুক্ত হয়ে গেছে", + "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে", + "UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে", "UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না", "UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে", "UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে", @@ -36,8 +36,8 @@ "User": "ব্যবহারকারী", "TvShows": "টিভি শোগুলো", "System": "সিস্টেম", - "Sync": "সমলয় স্থাপন", - "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ", + "Sync": "সমন্বয় করুন", + "SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে", "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।", "Songs": "সঙ্গীতসমূহ", "Shows": "টিভি পর্ব", @@ -46,18 +46,18 @@ "ScheduledTaskFailedWithName": "{0} ব্যর্থ", "ProviderValue": "প্রদানকারী: {0}", "PluginUpdatedWithName": "{0} আপডেট করা হয়েছে", - "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে", - "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে", + "PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে", + "PluginInstalledWithName": "{0} ইন্সটল হয়েছে", "Plugin": "প্লাগিন", "Playlists": "প্লে লিস্ট সমূহ", - "Photos": "চিত্রসমূহ", - "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ", - "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে", + "Photos": "ছবিসমূহ", + "NotificationOptionVideoPlaybackStopped": "ভিডিও বন্ধ হয়েছে", + "NotificationOptionVideoPlayback": "ভিডিও শুরু হয়েছে", "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না", "NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ", - "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট বাধ্যতামূলক", - "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল করা হয়েছে", - "NotificationOptionPluginUninstalled": "প্লাগিন বাদ দেয়া হয়েছে", + "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট করা লাগবে", + "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল হয়েছে", + "NotificationOptionPluginUninstalled": "প্লাগিন আনইনষ্টল হয়েছে", "NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে", "NotificationOptionPluginError": "প্লাগিন ব্যর্থ", "NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে", @@ -76,8 +76,8 @@ "Movies": "চলচ্চিত্রসমূহ", "MixedContent": "মিশ্র কন্টেন্ট", "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে", - "HeaderRecordingGroups": "রেকর্ডিং দল", - "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে", + "HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো", + "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে", "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে", "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে", "Latest": "সর্বশেষ", @@ -85,51 +85,57 @@ "LabelIpAddressValue": "আইপি এড্রেস: {0}", "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে", "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে", - "Inherit": "থেকে পাওয়া", + "Inherit": "মূল থেকে গ্রহণ করুন", "HomeVideos": "হোম ভিডিও", "HeaderNextUp": "এরপরে আসছে", "HeaderLiveTV": "লাইভ টিভি", "HeaderFavoriteSongs": "প্রিয় গানগুলো", "HeaderFavoriteShows": "প্রিয় শোগুলো", - "TasksLibraryCategory": "গ্রন্থাগার", + "TasksLibraryCategory": "লাইব্রেরি", "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ", "TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি", - "TaskRefreshChapterImagesDescription": "অধ্যায়গুলিতে থাকা ভিডিওগুলির জন্য থাম্বনেইল তৈরি ।", - "TaskRefreshChapterImages": "অধ্যায়ের চিত্রগুলি বের করুন", - "TaskCleanCacheDescription": "সিস্টেমে আর প্রয়োজন নেই ক্যাশ, ফাইলগুলি মুছে ফেলুন।", + "TaskRefreshChapterImagesDescription": "যেসব ভিডিওতে চ্যাপ্টার রয়েছে, তাদের জন্য থাম্বনেইল তৈরি করবে।", + "TaskRefreshChapterImages": "চ্যাপ্টার ইমেজ বের করুন", + "TaskCleanCacheDescription": "সিস্টেমের অপ্রয়োজনীয় ক্যাশ ফাইলগুলো মুছে ফেলবে।", "TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি", "TasksChannelsCategory": "ইন্টারনেট চ্যানেল", - "TasksApplicationCategory": "আবেদন", + "TasksApplicationCategory": "অ্যাপ্লিকেশন", "TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।", "TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন", "TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।", "TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন", - "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলুন।", + "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলবে।", "TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন", "TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।", - "TaskUpdatePlugins": "প্লাগইন আপডেট করুন", - "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করুন।", - "TaskRefreshPeople": "পিপল রিফ্রেশ করুন", - "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।", - "TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন", - "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।", + "TaskUpdatePlugins": "আপডেট প্লাগইন", + "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করবে।", + "TaskRefreshPeople": "ব্যক্তিদের তথ্য রিফ্রেশ", + "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলবে।", + "TaskCleanLogs": "ক্লিন লগ ডিরেক্টরি", + "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করবে।", "Undefined": "অসঙ্গায়িত", "Forced": "জোরকরে", - "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.", - "TaskCleanActivityLog": "কাজের ফাইল খালি করুন", + "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের অ্যাক্টিভিটি লগ মুছে দিবে।", + "TaskCleanActivityLog": "অ্যাক্টিভিটি লগ মুছুন", "Default": "ডিফল্ট", - "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য", + "HearingImpaired": "শ্রবণ প্রতিবন্ধী", "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।", "External": "বাহ্যিক", "TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস", "TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক", "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।", - "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন", + "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি", "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।", "TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে", - "TaskCleanCollectionsAndPlaylists": "সংগ্রহ এবং প্লেলিস্ট পরিষ্কার করুন", - "TaskCleanCollectionsAndPlaylistsDescription": "সংগ্রহ এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।", + "TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন", + "TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।", "TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান", - "TaskExtractMediaSegmentsDescription": "MediaSegment সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।", - "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন" + "TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্রিয় প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।", + "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন", + "TaskMoveTrickplayImagesDescription": "লাইব্রেরির সেটিং অনুযায়ী বিদ্যমান ট্রিকপ্লে ফাইলগুলো সরিয়ে নেবে।", + "TaskAudioNormalizationDescription": "অডিও নর্মালাইজেশন তথ্যের জন্য ফাইল স্ক্যান করবে।", + "CleanupUserDataTaskDescription": "৯০ দিন বা তার বেশি সময় ধরে অনুপস্থিত মিডিয়া থেকে সকল ব্যবহারকারীর ডেটা (ওয়াচ স্টেট, ফেভারিট স্ট্যাটাস ইত্যাদি) মুছে ফেলবে।", + "TaskMoveTrickplayImages": "ট্রিকপ্লে ইমেজের অবস্থান পরিবর্তন", + "TaskAudioNormalization": "অডিও নর্মলাইজেশন", + "CleanupUserDataTask": "ব্যবহারকারীর ডেটা পরিষ্কারের কাজ" } diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 6cce0e0198..596df63482 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -13,10 +13,10 @@ "DeviceOnlineWithName": "{0} està connectat", "FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}", "Favorites": "Preferits", - "Folders": "Carpetes", + "Folders": "Directoris", "Genres": "Gèneres", "HeaderAlbumArtists": "Artistes de l'àlbum", - "HeaderContinueWatching": "Continua veient", + "HeaderContinueWatching": "Continueu mirant", "HeaderFavoriteAlbums": "Àlbums preferits", "HeaderFavoriteArtists": "Artistes preferits", "HeaderFavoriteEpisodes": "Episodis preferits", @@ -24,11 +24,11 @@ "HeaderFavoriteSongs": "Cançons preferides", "HeaderLiveTV": "TV en directe", "HeaderNextUp": "A continuació", - "HeaderRecordingGroups": "Grups Musicals", + "HeaderRecordingGroups": "Grups musicals", "HomeVideos": "Vídeos domèstics", "Inherit": "Heretat", - "ItemAddedWithName": "{0} s'ha afegit a la biblioteca", - "ItemRemovedWithName": "{0} s'ha eliminat de la biblioteca", + "ItemAddedWithName": "{0} s'ha afegit a la mediateca", + "ItemRemovedWithName": "{0} s'ha eliminat de la mediateca", "LabelIpAddressValue": "Adreça IP: {0}", "LabelRunningTimeValue": "Temps en marxa: {0}", "Latest": "Darrers", @@ -43,7 +43,7 @@ "NameInstallFailed": "{0} instal·lació fallida", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada desconeguda", - "NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.", + "NewVersionIsAvailable": "Hi ha disponible una versió nova del servidor de Jellyfin per a la descàrrega.", "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible", "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada", "NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada", @@ -64,7 +64,7 @@ "Playlists": "Llistes de reproducció", "Plugin": "Complement", "PluginInstalledWithName": "{0} ha estat instal·lat", - "PluginUninstalledWithName": "S'ha instalat {0}", + "PluginUninstalledWithName": "S'ha instal·lat {0}", "PluginUpdatedWithName": "S'ha actualitzat {0}", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", @@ -72,10 +72,10 @@ "ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}", "Shows": "Sèries", "Songs": "Cançons", - "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu de nou en una estona.", + "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}", - "Sync": "Sincronitzar", + "Sync": "Sincronitza", "System": "Sistema", "TvShows": "Sèries de TV", "User": "Usuari", @@ -89,52 +89,54 @@ "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}", "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}", "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}", - "ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la teva biblioteca", + "ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la mediateca", "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versió {0}", "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.", - "TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin", + "TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin", "TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.", "TaskRefreshChannels": "Actualitza els canals", - "TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.", - "TaskCleanTranscode": "Neteja les transcodificacions", - "TaskUpdatePluginsDescription": "Actualitza els complements que estan configurats per a actualitzar-se automàticament.", - "TaskUpdatePlugins": "Actualitza els complements", - "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva biblioteca de mitjans.", - "TaskRefreshPeople": "Actualitza les persones", - "TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.", - "TaskCleanLogs": "Neteja els registres", - "TaskRefreshLibraryDescription": "Escaneja la biblioteca de mitjans buscant fitxers nous i refresca les metadades.", - "TaskRefreshLibrary": "Escaneja la biblioteca de mitjans", - "TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.", - "TaskRefreshChapterImages": "Extreure les imatges dels capítols", - "TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.", - "TaskCleanCache": "Elimina la memòria cau", + "TaskCleanTranscodeDescription": "Elimina els fitxers de transcodificacions que tinguin més d'un dia.", + "TaskCleanTranscode": "Neteja de les transcodificacions", + "TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.", + "TaskUpdatePlugins": "Actualització dels complements", + "TaskRefreshPeopleDescription": "Actualització de les metadades dels actors i directors de la mediateca.", + "TaskRefreshPeople": "Actualització de les persones", + "TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.", + "TaskCleanLogs": "Neteja dels registres", + "TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.", + "TaskRefreshLibrary": "Escaneig de les mediateques", + "TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.", + "TaskRefreshChapterImages": "Extracció de les imatges dels capítols", + "TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.", + "TaskCleanCache": "Eliminació de la memòria cau", "TasksChannelsCategory": "Canals per internet", "TasksApplicationCategory": "Aplicatiu", - "TasksLibraryCategory": "Biblioteca", + "TasksLibraryCategory": "Mediateca", "TasksMaintenanceCategory": "Manteniment", - "TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.", - "TaskCleanActivityLog": "Buidar el registre d'activitat", + "TaskCleanActivityLogDescription": "Eliminació de les entrades del registre d'activitats més antigues que l'antiguitat configurada.", + "TaskCleanActivityLog": "Buidatge del registre d'activitat", "Undefined": "Indefinit", "Forced": "Forçat", "Default": "Per defecte", - "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.", - "TaskOptimizeDatabase": "Optimitzar la base de dades", - "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.", - "TaskKeyframeExtractor": "Extractor de fotogrames clau", + "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.", + "TaskOptimizeDatabase": "Optimització de la base de dades", + "TaskKeyframeExtractorDescription": "Extracció de fotogrames clau dels fitxers de vídeo per a crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.", + "TaskKeyframeExtractor": "Extracció de fotogrames clau", "External": "Extern", "HearingImpaired": "Discapacitat auditiva", - "TaskRefreshTrickplayImages": "Generar miniatures de línia de temps", - "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.", + "TaskRefreshTrickplayImages": "Generació d'imatges de previsualització", + "TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.", "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.", - "TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció", - "TaskAudioNormalization": "Estabilització d'Àudio", - "TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització d'àudio.", - "TaskDownloadMissingLyricsDescription": "Baixar les lletres de les cançons", - "TaskDownloadMissingLyrics": "Baixar les lletres que falten", + "TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció", + "TaskAudioNormalization": "Estabilització de l'àudio", + "TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.", + "TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons", + "TaskDownloadMissingLyrics": "Descàrrega de les lletres que faltin", "TaskExtractMediaSegments": "Escaneig de segments multimèdia", "TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.", - "TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay", - "TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la biblioteca." + "TaskMoveTrickplayImages": "Migració de la ubicació de la imatge de previsualització", + "TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.", + "CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.", + "CleanupUserDataTask": "Tasca de neteja de dades d'usuari" } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index ba2e2700da..e14edcffa2 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Skenování segmentů médií", "TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.", "TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay", - "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny." + "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.", + "CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.", + "CleanupUserDataTask": "Pročistit uživatelská data" } diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index d43d4097f9..bbee38ba5f 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Scan for mediesegmenter", "TaskMoveTrickplayImages": "Migrer billedelokationer for trickplay-billeder", "TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.", - "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment." + "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.", + "CleanupUserDataTask": "Brugerdata oprydningsopgave", + "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage." } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index c38af5bf43..664da8249b 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -43,7 +43,7 @@ "NameInstallFailed": "Installation von {0} fehlgeschlagen", "NameSeasonNumber": "Staffel {0}", "NameSeasonUnknown": "Staffel unbekannt", - "NewVersionIsAvailable": "Eine neue Version von Jellyfin-Server steht zum Download bereit.", + "NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.", "NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar", "NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert", "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet", @@ -72,12 +72,12 @@ "ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden", "Shows": "Serien", "Songs": "Lieder", - "StartupEmbyServerIsLoading": "Jellyfin-Server startet, bitte versuche es gleich noch einmal.", + "StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.", "SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}", "SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden", "Sync": "Synchronisation", "System": "System", - "TvShows": "TV-Sendungen", + "TvShows": "Serien", "User": "Benutzer", "UserCreatedWithName": "Benutzer {0} wurde erstellt", "UserDeletedWithName": "Benutzer {0} wurde gelöscht", @@ -90,32 +90,32 @@ "UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet", "UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet", "ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt", - "ValueSpecialEpisodeName": "Extra - {0}", + "ValueSpecialEpisodeName": "Extra – {0}", "VersionNumber": "Version {0}", - "TaskDownloadMissingSubtitlesDescription": "Suche im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.", - "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter", - "TaskRefreshChannelsDescription": "Aktualisiere Internet-Kanal-Informationen.", - "TaskRefreshChannels": "Aktualisiere Kanäle", - "TaskCleanTranscodeDescription": "Löscht Transkodierdateien, die älter als einen Tag sind.", - "TaskCleanTranscode": "Räume Transkodierungs-Verzeichnis auf", + "TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.", + "TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen", + "TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.", + "TaskRefreshChannels": "Kanäle aktualisieren", + "TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.", + "TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen", "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.", - "TaskUpdatePlugins": "Aktualisiere Plugins", + "TaskUpdatePlugins": "Plugins aktualisieren", "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.", - "TaskRefreshPeople": "Aktualisiere Personen", + "TaskRefreshPeople": "Personen aktualisieren", "TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.", - "TaskCleanLogs": "Räumt Log-Verzeichnis auf", - "TaskRefreshLibraryDescription": "Scannt alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiere Metadaten.", - "TaskRefreshLibrary": "Scanne Medien-Bibliothek", + "TaskCleanLogs": "Log-Verzeichnis aufräumen", + "TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.", + "TaskRefreshLibrary": "Medien-Bibliothek scannen", "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.", - "TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder", - "TaskCleanCacheDescription": "Löscht nicht mehr benötigte Zwischenspeicherdateien.", - "TaskCleanCache": "Leere Zwischenspeicher", + "TaskRefreshChapterImages": "Kapitel-Bilder extrahieren", + "TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.", + "TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen", "TasksChannelsCategory": "Internet-Kanäle", "TasksApplicationCategory": "Anwendung", "TasksLibraryCategory": "Bibliothek", "TasksMaintenanceCategory": "Wartung", "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.", - "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen", + "TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen", "Undefined": "Undefiniert", "Forced": "Erzwungen", "Default": "Standard", @@ -128,13 +128,15 @@ "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren", "TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.", "TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen", - "TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.", + "TaskCleanCollectionsAndPlaylistsDescription": "Löscht nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.", "TaskAudioNormalization": "Audio Normalisierung", "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.", "TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter", "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen", - "TaskExtractMediaSegments": "Scanne Mediensegmente", + "TaskExtractMediaSegments": "Mediensegmente scannen", "TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.", "TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren", - "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben." + "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.", + "CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten", + "CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Anschaustatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind." } diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 55f266032e..f3195f0ea0 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -11,7 +11,7 @@ "Collections": "Συλλογές", "DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε", "DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε", - "FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}", + "FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}", "Favorites": "Αγαπημένα", "Folders": "Φάκελοι", "Genres": "Είδη", @@ -27,8 +27,8 @@ "HeaderRecordingGroups": "Ομάδες Ηχογράφησης", "HomeVideos": "Προσωπικά Βίντεο", "Inherit": "Κληρονόμηση", - "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη", - "ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη", + "ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη", + "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη", "LabelIpAddressValue": "Διεύθυνση IP: {0}", "LabelRunningTimeValue": "Διάρκεια: {0}", "Latest": "Πρόσφατα", @@ -40,7 +40,7 @@ "Movies": "Ταινίες", "Music": "Μουσική", "MusicVideos": "Μουσικά Βίντεο", - "NameInstallFailed": "{0} η εγκατάσταση απέτυχε", + "NameInstallFailed": "H εγκατάσταση του {0} απέτυχε", "NameSeasonNumber": "Κύκλος {0}", "NameSeasonUnknown": "Άγνωστος Κύκλος", "NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.", @@ -54,7 +54,7 @@ "NotificationOptionPluginError": "Αποτυχία του πρόσθετου", "NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε", "NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε", - "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε", + "NotificationOptionPluginUpdateInstalled": "Η ενημέρωση του πρόσθετου εγκαταστάθηκε", "NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση", "NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας", "NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε", @@ -63,9 +63,9 @@ "Photos": "Φωτογραφίες", "Playlists": "Λίστες αναπαραγωγής", "Plugin": "Πρόσθετο", - "PluginInstalledWithName": "{0} εγκαταστήθηκε", - "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί", - "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί", + "PluginInstalledWithName": "Το {0} εγκαταστάθηκε", + "PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί", + "PluginUpdatedWithName": "Το {0} ενημερώθηκε", "ProviderValue": "Πάροχος: {0}", "ScheduledTaskFailedWithName": "{0} αποτυχία", "ScheduledTaskStartedWithName": "{0} ξεκίνησε", @@ -96,7 +96,7 @@ "TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.", "TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής", "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.", - "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων", + "TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων", "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.", "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου", "TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.", @@ -125,7 +125,7 @@ "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο", "External": "Εξωτερικό", "HearingImpaired": "Με προβλήματα ακοής", - "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay", + "TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay", "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.", "TaskAudioNormalization": "Ομοιομορφία ήχου", "TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.", diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index ca52ffb143..720f550b33 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Media Segment Scan", "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", - "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings." + "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.", + "CleanupUserDataTask": "User data cleanup task", + "CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favourite status etc) from media that is no longer present for at least 90 days." } diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index 9702ab7123..c09d5af96c 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -135,5 +135,7 @@ "TaskExtractMediaSegments": "Media Segment Scan", "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", - "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings." + "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.", + "CleanupUserDataTask": "User data cleanup task", + "CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favorite status etc) from media that is no longer present for at least 90 days." } diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json index 0b595c2caf..42cce1096f 100644 --- a/Emby.Server.Implementations/Localization/Core/eo.json +++ b/Emby.Server.Implementations/Localization/Core/eo.json @@ -122,5 +122,9 @@ "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis", "TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.", "TaskKeyframeExtractor": "Eltiri Ĉefkadrojn", - "External": "Ekstera" + "External": "Ekstera", + "TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.", + "TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)", + "TaskAudioNormalization": "Normaligo Sonnivela", + "HearingImpaired": "Surda" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index 661333d291..1ec5eaa2a8 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.", "TaskExtractMediaSegments": "Escaneo de segmentos de medios", "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.", - "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay" + "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay", + "CleanupUserDataTask": "Tarea de limpieza de datos del usuario", + "CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días." } diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json index 4df4b90d3a..c9a798cacd 100644 --- a/Emby.Server.Implementations/Localization/Core/eu.json +++ b/Emby.Server.Implementations/Localization/Core/eu.json @@ -135,5 +135,7 @@ "TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.", "TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua", "TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.", - "TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu." + "TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu.", + "CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.", + "CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina" } diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index c9f580cd5f..0814e62234 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -135,5 +135,7 @@ "TaskDownloadMissingLyricsDescription": "Ladataan sanoituksia", "TaskExtractMediaSegmentsDescription": "Poimii tai hankkii mediasegmenttejä MediaSegment-yhteensopivista laajennuksista.", "TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti", - "TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan." + "TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan.", + "CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä", + "CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään." } diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index a10912f011..6d079d2f57 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.", "TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes", "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay", - "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment." + "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.", + "CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.", + "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index c337d19320..8bf41c02a7 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Analyse des segments de média", "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay", "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.", - "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque." + "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.", + "CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.", + "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur" } diff --git a/Emby.Server.Implementations/Localization/Core/ga.json b/Emby.Server.Implementations/Localization/Core/ga.json index b8e787c20a..8c0ae8922a 100644 --- a/Emby.Server.Implementations/Localization/Core/ga.json +++ b/Emby.Server.Implementations/Localization/Core/ga.json @@ -135,5 +135,7 @@ "TaskUpdatePlugins": "Nuashonraigh Breiseáin", "TaskCleanTranscodeDescription": "Scriostar comhaid traschódaithe níos mó ná lá amháin d'aois.", "TaskCleanTranscode": "Eolaire Transcode Glan", - "TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh" + "TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh", + "CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora", + "CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad." } diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index 3ba3e6679e..ff6f6d232c 100644 --- a/Emby.Server.Implementations/Localization/Core/gl.json +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -123,5 +123,18 @@ "TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.", "External": "Externo", "HearingImpaired": "Problemas de audición", - "TaskKeyframeExtractor": "Extractor de fragmentos" + "TaskKeyframeExtractor": "Extractor de fragmentos", + "TaskAudioNormalization": "Normalización do audio", + "TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reprodución con truco para vídeos en bibliotecas activadas.", + "TaskDownloadMissingLyrics": "Descargar letras que faltan", + "TaskDownloadMissingLyricsDescription": "Descargas de letras das cancións", + "TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución", + "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de coleccións e listas de reprodución que xa non existen.", + "TaskExtractMediaSegmentsDescription": "Extrae ou obtén segmentos multimedia de complementos habilitados para o Segmento de medios.", + "TaskExtractMediaSegments": "Escaneo de segmentos multimedia", + "TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay", + "TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.", + "TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay", + "TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.", + "CleanupUserDataTask": "Tarefa de limpeza de datos do usuario" } diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index 34d5cf0509..90c921898f 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -32,8 +32,8 @@ "LabelIpAddressValue": "Ip כתובת: {0}", "LabelRunningTimeValue": "משך צפייה: {0}", "Latest": "אחרון", - "MessageApplicationUpdated": "שרת ג'ליפין עודכן", - "MessageApplicationUpdatedTo": "שרת ג'ליפין עודכן לגרסה {0}", + "MessageApplicationUpdated": "שרת Jellyfin עודכן", + "MessageApplicationUpdatedTo": "שרת Jellyfin עודכן לגרסה {0}", "MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן", "MessageServerConfigurationUpdated": "תצורת השרת עודכנה", "MixedContent": "תוכן מעורב", @@ -43,7 +43,7 @@ "NameInstallFailed": "התקנת {0} נכשלה", "NameSeasonNumber": "עונה {0}", "NameSeasonUnknown": "עונה לא ידועה", - "NewVersionIsAvailable": "גרסה חדשה של שרת ג'ליפין זמינה להורדה.", + "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.", "NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום", "NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן", "NotificationOptionAudioPlayback": "ניגון שמע החל", @@ -72,7 +72,7 @@ "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש", "Shows": "סדרות", "Songs": "שירים", - "StartupEmbyServerIsLoading": "שרת ג'ליפין טוען. נא לנסות שוב בקרוב.", + "StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה", "Sync": "סנכרון", @@ -100,14 +100,14 @@ "TasksLibraryCategory": "ספרייה", "TasksMaintenanceCategory": "תחזוקה", "TaskUpdatePlugins": "עדכן תוספים", - "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.", + "TaskRefreshPeopleDescription": "מעדכן מטא-דאטה עבור שחקנים ובמאים בספריית המדיה שלך.", "TaskRefreshPeople": "רענן אנשים", "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.", "TaskCleanLogs": "ניקוי תיקיית יומן", - "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.", + "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא-דאטה.", "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.", "TasksChannelsCategory": "ערוצי אינטרנט", - "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.", + "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט כתוביות חסרות בהתבסס על המטא-דאטה.", "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות", "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.", "TaskRefreshChannels": "רענן ערוץ", @@ -125,16 +125,18 @@ "TaskKeyframeExtractor": "מחלץ תמונות מפתח", "External": "חיצוני", "HearingImpaired": "לקוי שמיעה", - "TaskRefreshTrickplayImages": "יצירת תמונות המחשה", - "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.", + "TaskRefreshTrickplayImages": "יצירת תמונות Trickplay", + "TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.", "TaskAudioNormalization": "נרמול שמע", "TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.", "TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.", "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה", "TaskDownloadMissingLyrics": "הורדת מילים חסרות", "TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים", - "TaskMoveTrickplayImages": "העברת מיקום התמונות", + "TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay", "TaskExtractMediaSegments": "סריקת מדיה", "TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.", - "TaskMoveTrickplayImagesDescription": "הזזת קבצי טריקפליי קיימים בהתאם להגדרות הספרייה." + "TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה.", + "CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.", + "CleanupUserDataTask": "משימת ניקוי מידע משתמש" } diff --git a/Emby.Server.Implementations/Localization/Core/he_IL.json b/Emby.Server.Implementations/Localization/Core/he_IL.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/he_IL.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index f205e8b64c..81a996330b 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -1,6 +1,6 @@ { "Albums": "Albumok", - "AppDeviceValues": "Program: {0}, eszköz: {1}", + "AppDeviceValues": "Program: {0}, Eszköz: {1}", "Application": "Alkalmazás", "Artists": "Előadók", "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve", @@ -13,7 +13,7 @@ "DeviceOnlineWithName": "{0} belépett", "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}", "Favorites": "Kedvencek", - "Folders": "Könyvtárak", + "Folders": "Mappák", "Genres": "Műfajok", "HeaderAlbumArtists": "Albumelőadók", "HeaderContinueWatching": "Megtekintés folytatása", @@ -136,5 +136,7 @@ "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése", "TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése", "TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.", - "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből." + "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.", + "CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.", + "CleanupUserDataTask": "Felhasználói adatok tisztítása feladat" } diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index b925a482b3..2a42816852 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -129,5 +129,13 @@ "TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.", "TaskAudioNormalization": "Normalisasi Audio", "TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar", - "TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada." + "TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada.", + "TaskDownloadMissingLyricsDescription": "Unduh lirik untuk lagu", + "TaskExtractMediaSegmentsDescription": "Mengekstrak atau memperoleh segmen media dari plugin yang mendukung MediaSegment.", + "TaskMoveTrickplayImagesDescription": "Memindahkan file trickplay yang sudah ada sesuai dengan pengaturan pustaka.", + "CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (status tontonan, status favorit, dll.) dari media yang sudah tidak ada selama setidaknya 90 hari.", + "TaskExtractMediaSegments": "Scan Segmen media", + "TaskMoveTrickplayImages": "Migrasikan Lokasi Gambar Trickplay", + "TaskDownloadMissingLyrics": "Unduh Lirik yang Hilang", + "CleanupUserDataTask": "Tugas Pembersihan Data Pengguna" } diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index 672c686fab..6f94df9d79 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -131,5 +131,8 @@ "TaskCleanCollectionsAndPlaylists": "Hreinsa söfn og spilunarlista", "TaskCleanCollectionsAndPlaylistsDescription": "Fjarlægir hluti úr söfnum og spilalistum sem eru ekki lengur til.", "TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög", - "TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar" + "TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar", + "TaskExtractMediaSegments": "Skönnun efnishluta", + "CleanupUserDataTask": "Hreinsun notendagagna", + "CleanupUserDataTaskDescription": "Hreinsar öll notendagögn (spilunarstöðu, uppáhöld o.s.frv.) um gögn sem hafa ekki verið til staðar í að lámarki 90 daga." } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 297b3abce7..421c4ee306 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -58,8 +58,8 @@ "NotificationOptionServerRestartRequired": "Riavvio del server necessario", "NotificationOptionTaskFailed": "Operazione pianificata fallita", "NotificationOptionUserLockedOut": "Utente bloccato", - "NotificationOptionVideoPlayback": "La riproduzione video è iniziata", - "NotificationOptionVideoPlaybackStopped": "La riproduzione video è stata interrotta", + "NotificationOptionVideoPlayback": "Riproduzione video iniziata", + "NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta", "Photos": "Foto", "Playlists": "Playlist", "Plugin": "Plugin", @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "Sposta le immagini Trickplay", "TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.", "TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.", - "TaskExtractMediaSegments": "Scansiona Segmento Media" + "TaskExtractMediaSegments": "Scansiona Segmento Media", + "CleanupUserDataTask": "Task di pulizia dei dati utente", + "CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni." } diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index 14a5765921..d564d54cef 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -135,5 +135,7 @@ "TaskMoveTrickplayImages": "Trickplayの画像を移動", "TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。", "TaskDownloadMissingLyrics": "失われた歌詞をダウンロード", - "TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。" + "TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。", + "CleanupUserDataTask": "ユーザーデータのクリーンアップタスク", + "CleanupUserDataTaskDescription": "90日以上存在しないメディアに対して、視聴状態やお気に入り状態などのユーザーデータをすべて削除します。" } diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json index 5e2b3756bb..9f49be53b4 100644 --- a/Emby.Server.Implementations/Localization/Core/kn.json +++ b/Emby.Server.Implementations/Localization/Core/kn.json @@ -25,7 +25,7 @@ "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ", "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ", "External": "ಹೊರಗಿನ", - "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ", + "FailedLoginAttemptWithUserName": "ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ ಸಂಖ್ಯೆ {0}", "Favorites": "ಮೆಚ್ಚಿನವುಗಳು", "Folders": "ಫೋಲ್ಡರ್‌ಗಳು", "Forced": "ಬಲವಂತವಾಗಿ", @@ -123,5 +123,13 @@ "TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ", "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ", "TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ", - "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ." + "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.", + "TaskAudioNormalizationDescription": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ ಮಾಹಿತಿಗಾಗಿ ಕಡತ‌ಗಳನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ.", + "TaskDownloadMissingLyricsDescription": "ಹಾಡುಗಳಿಗೆ ಸಾಹಿತ್ಯ ಪಡೆಯಿರಿ", + "TaskExtractMediaSegments": "ಮಾಧ್ಯಮ ವಿಭಾಗದ ಹುಡುಕು", + "TaskDownloadMissingLyrics": "ಇಲ್ಲದ ಸಾಹಿತ್ಯವನ್ನು ಪಡೆಯಿರಿ", + "TaskAudioNormalization": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ", + "TaskRefreshTrickplayImages": "ಟ್ರಿಕ್‌ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ", + "TaskCleanCollectionsAndPlaylists": "ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ", + "TaskCleanCollectionsAndPlaylistsDescription": "ಇಲ್ಲದ ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ." } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 46fc49f5e4..3918ab81c6 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -8,7 +8,7 @@ "CameraImageUploadedFrom": "Nauja nuotrauka įkelta iš kameros {0}", "Channels": "Kanalai", "ChapterNameValue": "Scena{0}", - "Collections": "Kolekcijos", + "Collections": "Rinkiniai", "DeviceOfflineWithName": "{0} buvo atjungtas", "DeviceOnlineWithName": "{0} prisijungęs", "FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti", @@ -17,18 +17,18 @@ "Genres": "Žanrai", "HeaderAlbumArtists": "Albumo atlikėjai", "HeaderContinueWatching": "Žiūrėti toliau", - "HeaderFavoriteAlbums": "Mėgstami Albumai", - "HeaderFavoriteArtists": "Mėgstami Atlikėjai", + "HeaderFavoriteAlbums": "Mėgstami albumai", + "HeaderFavoriteArtists": "Mėgstami atlikėjai", "HeaderFavoriteEpisodes": "Mėgstamiausios serijos", "HeaderFavoriteShows": "Mėgstamiausios TV Laidos", "HeaderFavoriteSongs": "Mėgstamos Dainos", "HeaderLiveTV": "Tiesioginė TV", - "HeaderNextUp": "Toliau eilėje", + "HeaderNextUp": "Toliau", "HeaderRecordingGroups": "Įrašų grupės", "HomeVideos": "Namų vaizdo įrašai", "Inherit": "Paveldėti", - "ItemAddedWithName": "{0} - buvo įkeltas į mediateką", - "ItemRemovedWithName": "{0} - buvo pašalinta iš mediatekos", + "ItemAddedWithName": "{0} - buvo įkeltas į biblioteką", + "ItemRemovedWithName": "{0} - buvo pašalinta iš bibliotekos", "LabelIpAddressValue": "IP adresas: {0}", "LabelRunningTimeValue": "Trukmė: {0}", "Latest": "Naujausi", @@ -36,7 +36,7 @@ "MessageApplicationUpdatedTo": "\"Jellyfin Server\" buvo atnaujinta iki {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Serverio nustatymai (skyrius {0}) buvo atnaujinti", "MessageServerConfigurationUpdated": "Serverio nustatymai buvo atnaujinti", - "MixedContent": "Mixed content", + "MixedContent": "Mišrus turinys", "Movies": "Filmai", "Music": "Muzika", "MusicVideos": "Muzikiniai vaizdo įrašai", @@ -53,21 +53,21 @@ "NotificationOptionNewLibraryContent": "Naujas turinys įkeltas", "NotificationOptionPluginError": "Įskiepio klaida", "NotificationOptionPluginInstalled": "Įskiepis įdiegtas", - "NotificationOptionPluginUninstalled": "Įskiepis pašalintas", + "NotificationOptionPluginUninstalled": "Įskiepis išdiegtas", "NotificationOptionPluginUpdateInstalled": "Įskiepio atnaujinimas įdiegtas", "NotificationOptionServerRestartRequired": "Reikalingas serverio perleidimas", "NotificationOptionTaskFailed": "Suplanuotos užduoties klaida", - "NotificationOptionUserLockedOut": "Vartotojas užblokuotas", + "NotificationOptionUserLockedOut": "Naudotojas užblokuotas", "NotificationOptionVideoPlayback": "Vaizdo įrašo atkūrimas pradėtas", "NotificationOptionVideoPlaybackStopped": "Vaizdo įrašo atkūrimas sustabdytas", "Photos": "Nuotraukos", - "Playlists": "Grojaraštis", - "Plugin": "Plugin", + "Playlists": "Grojaraščiai", + "Plugin": "Įskiepis", "PluginInstalledWithName": "{0} buvo įdiegtas", "PluginUninstalledWithName": "{0} buvo pašalintas", "PluginUpdatedWithName": "{0} buvo atnaujintas", - "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} klaida", + "ProviderValue": "Paslaugos tiekėjas: {0}", + "ScheduledTaskFailedWithName": "{0} nepavyko", "ScheduledTaskStartedWithName": "{0} paleista", "ServerNameNeedsToBeRestarted": "{0} reikia iš naujo paleisti", "Shows": "Laidos", @@ -76,65 +76,67 @@ "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}", "Sync": "Sinchronizuoti", - "System": "System", - "TvShows": "TV Serialai", - "User": "User", - "UserCreatedWithName": "Vartotojas {0} buvo sukurtas", - "UserDeletedWithName": "Vartotojas {0} ištrintas", + "System": "Sistema", + "TvShows": "TV laidos", + "User": "Naudotojas", + "UserCreatedWithName": "Buvo sukurtas {0} naudotojas", + "UserDeletedWithName": "Naudotojas {0} ištrintas", "UserDownloadingItemWithValues": "{0} siunčiasi {1}", - "UserLockedOutWithName": "Vartotojas {0} užblokuotas", + "UserLockedOutWithName": "Naudotojas {0} užblokuotas", "UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}", "UserOnlineFromDevice": "{0} prisijungęs iš {1}", - "UserPasswordChangedWithName": "Slaptažodis pakeistas vartotojui {0}", - "UserPolicyUpdatedWithName": "Vartotojo {0} teisės buvo pakeistos", + "UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}", + "UserPolicyUpdatedWithName": "Naudotojo {0} teisės buvo pakeistos", "UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}", "UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}", "ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką", - "ValueSpecialEpisodeName": "Ypatinga - {0}", - "VersionNumber": "Version {0}", - "TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.", - "TaskUpdatePlugins": "Atnaujinti Priedus", + "ValueSpecialEpisodeName": "Ypatingų - {0}", + "VersionNumber": "Versija {0}", + "TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.", + "TaskUpdatePlugins": "Atnaujinti įskieius", "TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.", "TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.", - "TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija", - "TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.", - "TaskRefreshLibrary": "Skenuoti Mediateka", + "TaskCleanTranscode": "Išvalyti perkodavimo katalogą", + "TaskRefreshLibraryDescription": "Skenuoja medijos biblioteką, ieškodamas naujų failų, ir atnaujina metaduomenis.", + "TaskRefreshLibrary": "Skenuoti medijos biblioteką", "TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus", "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informaciją.", "TaskRefreshChannels": "Atnaujinti kanalus", - "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.", - "TaskRefreshPeople": "Atnaujinti Žmones", + "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų medijos bibliotekoje.", + "TaskRefreshPeople": "Atnaujinti žmones", "TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.", - "TaskCleanLogs": "Išvalyti Žurnalą", - "TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.", - "TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus", - "TaskCleanCache": "Išvalyti Talpyklą", + "TaskCleanLogs": "Išvalyti žurnalą", + "TaskRefreshChapterImagesDescription": "Sukuria vaizdo įrašų, kuriuose yra skyrių, miniatiūras.", + "TaskRefreshChapterImages": "Ištraukti skyrių vaizdus", + "TaskCleanCache": "Išvalyti talpyklą", "TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.", - "TasksChannelsCategory": "Internetiniai Kanalai", + "TasksChannelsCategory": "Internetiniai kanalai", "TasksApplicationCategory": "Programa", - "TasksLibraryCategory": "Mediateka", + "TasksLibraryCategory": "Biblioteka", "TasksMaintenanceCategory": "Priežiūra", "TaskCleanActivityLog": "Išvalyti veiklos žurnalą", "Undefined": "Neapibrėžtas", - "Forced": "Priverstas", + "Forced": "Priverstinis", "Default": "Numatytas", - "TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius.", + "TaskCleanActivityLogDescription": "Ištrina senesnius nei nustatytas amžius veiklos žurnalo įrašus.", "TaskOptimizeDatabase": "Optimizuoti duomenų bazę", "TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.", - "TaskKeyframeExtractor": "Pagrindinių kadrų išgavėjas", + "TaskKeyframeExtractor": "Reikšminių kadrų (KeyFrame) išgavėjas", "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazę, gali pagerinti greitaveiką.", "External": "Išorinis", "HearingImpaired": "Su klausos sutrikimais", "TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus", "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.", - "TaskCleanCollectionsAndPlaylists": "Išvalo duomenis kolekcijose ir grojaraščiuose", - "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš kolekcijų ir grojaraščių.", - "TaskAudioNormalization": "Garso Normalizavimas", - "TaskAudioNormalizationDescription": "Skenuoti garso normalizavimo informacijos failuose.", + "TaskCleanCollectionsAndPlaylists": "Išvalo duomenis rinkiniuose ir grojaraščiuose", + "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš rinkinių ir grojaraščių.", + "TaskAudioNormalization": "Garso normalizavimas", + "TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.", "TaskExtractMediaSegments": "Medijos segmentų nuskaitymas", "TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus", - "TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų papildinių.", + "TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.", "TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą", "TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.", - "TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius" + "TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius", + "CleanupUserDataTask": "Naudotojo duomenų valymo užduotis", + "CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)." } diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index 77340a57ad..55549c66d0 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -135,5 +135,7 @@ "TaskMoveTrickplayImages": "Trickplay attēlu pārvietošana", "TaskMoveTrickplayImagesDescription": "Pārvieto esošos trickplay failus atbilstoši bibliotēkas iestatījumiem.", "TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus", - "TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām" + "TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām", + "CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums", + "CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas." } diff --git a/Emby.Server.Implementations/Localization/Core/lzh.json b/Emby.Server.Implementations/Localization/Core/lzh.json index 031a4dac76..9fb53e41d5 100644 --- a/Emby.Server.Implementations/Localization/Core/lzh.json +++ b/Emby.Server.Implementations/Localization/Core/lzh.json @@ -2,5 +2,10 @@ "Albums": "辑册", "Artists": "艺人", "AuthenticationSucceededWithUserName": "{0} 授之权矣", - "Books": "册" + "Books": "册", + "Genres": "类", + "HeaderAlbumArtists": "辑者", + "Favorites": "至爱", + "Folders": "箧", + "HeaderContinueWatching": "接目未竟" } diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json index 7421d42fb4..7b44f94873 100644 --- a/Emby.Server.Implementations/Localization/Core/mn.json +++ b/Emby.Server.Implementations/Localization/Core/mn.json @@ -1,14 +1,141 @@ { "Books": "Номууд", - "HeaderNextUp": "Дараах", + "HeaderNextUp": "Дараа нь", "HeaderContinueWatching": "Үргэлжлүүлэн үзэх", "Songs": "Дуунууд", "Playlists": "Тоглуулах жагсаалт", "Movies": "Кино", "Latest": "Сүүлийн үеийн", - "Genres": "Төрөл зүйл", + "Genres": "Төрлүүд", "Favorites": "Дуртай", "Collections": "Багц", - "Artists": "Зураачуд", - "Albums": "Цомгууд" + "Artists": "Уран бүтээлчид", + "Albums": "Цомгууд", + "TaskExtractMediaSegments": "Медиа сегмент шалга", + "TaskExtractMediaSegmentsDescription": "MediaSegment идэвхжүүлсэн залгаасуудаас медиа сегментүүдийг задлах эсвэл олж авах.", + "TaskMoveTrickplayImages": "Трикплэй зургуудын байршлыг шилжүүлэх", + "TaskMoveTrickplayImagesDescription": "Одоогоор байгаа трикплэй файлуудыг сангийн тохиргоонд тохируулан шилжүүлнэ.", + "TaskDownloadMissingLyrics": "Алга болсон дууны үгийг татаж авах", + "TaskDownloadMissingLyricsDescription": "Дууны үгийг татаж авах", + "TaskOptimizeDatabase": "Датабаазыг сайжруулах", + "TaskKeyframeExtractor": "Түлхүүр кадр гаргагч", + "TaskCleanCache": "Кэш санг цэвэрлэх", + "NewVersionIsAvailable": "Jellyfin Server-н шинэ хувилбар татаж авахад нээлттэй боллоо.", + "MessageNamedServerConfigurationUpdatedWithValue": "Server-н {0}-р хэсгийн тохиргоо шинэчлэгдлээ", + "NotificationOptionAudioPlaybackStopped": "Дууг зогсоов", + "NotificationOptionNewLibraryContent": "Шинэ агуулга орлоо", + "NotificationOptionServerRestartRequired": "Server-г дахин асаана уу", + "NotificationOptionVideoPlaybackStopped": "Бичлэгийг зогсоов", + "UserPasswordChangedWithName": "Хэрэглэгч {0}-н нууц үгийг өөрчиллөө", + "TaskCleanCollectionsAndPlaylists": "Цуглуулга ба тоглуулах жагсаалтыг цэвэрлэх", + "ScheduledTaskFailedWithName": "{0} амжилтгүй", + "StartupEmbyServerIsLoading": "Jellyfin Server ачааллаж байна. Хэсэг хугацааны дараа дахин оролдоно уу.", + "TaskCleanActivityLog": "Үйл ажиллагааны бүртгэлийг цэвэрлэх", + "SubtitleDownloadFailureFromForItem": "{0}-г {1}-д зориулсан хадмал орчуулгыг татаж авч чадсангүй", + "TaskRefreshLibraryDescription": "Таны медиа санг шинэ файлуудын хувьд шалгаж, мета мэдээллийг шинэчилнэ.", + "UserOfflineFromDevice": "{0}-г {1}-с салгалаа", + "ValueHasBeenAddedToLibrary": "{0}-г медиа сан руу нэмэгдлээ", + "TaskRefreshPeopleDescription": "Таны медиа санд байгаа жүжигчид болон найруулагчдын мета мэдээллийг шинэчилнэ.", + "TaskCleanTranscodeDescription": "Нэг өдрөөс илүү настай транскодлох файлуудыг устгана.", + "TaskRefreshChannelsDescription": "Интернет сувгуудын мэдээллийг шинэчлэх.", + "TaskDownloadMissingSubtitlesDescription": "Мета мэдээллийн тохиргоонд үндэслэн интернетээс алга болсон дэд гарчгийг хайна.", + "TaskOptimizeDatabaseDescription": "Мэдээллийн сантайг шахаж, чөлөөтэй зайг багасгана. Санг шалгаж, мэдээллийн сантай холбоотой өөрчлөлт хийхийн дараа энэ үйлдлийг гүйцэтгэх нь гүйцэтгэлийг сайжруулах боломжтой.", + "TaskKeyframeExtractorDescription": "Видео файлуудаас түлхүүр кадруудыг гаргаж, илүү нарийвчилсан HLS тоглуулах жагсаалт үүсгэнэ. Энэ үйлдэл удаан хугацаанд үргэлжлэх боломжтой.", + "NotificationOptionAudioPlayback": "Дууг тоглууллаа", + "TaskRefreshTrickplayImages": "Трикплэй зургуудыг үүсгэх", + "TaskUpdatePlugins": "Plugin-уудыг шинэчлэх", + "TaskCleanCollectionsAndPlaylistsDescription": "Одоо байхгүй болсон зүйлсийг цуглуулга ба тоглуулах жагсаалтаас устгана.", + "TaskAudioNormalization": "Аудиог хэвшүүлэх", + "TaskAudioNormalizationDescription": "Файлуудаас дууны хэвийн хэмжээсийн мэдээллийг шалгана.", + "TaskRefreshTrickplayImagesDescription": "Идэвхжсэн сангуудад байгаа видеонуудын трикплэй урьдчилсан харагдацыг үүсгэнэ.", + "TaskUpdatePluginsDescription": "Автомат шинэчлэлд тохируулсан залгаасуудын шинэчлэлтийг татаж авч суулгана.", + "TaskCleanTranscode": "Транскодлох санг цэвэрлэх", + "TaskRefreshChannels": "Сувгуудыг шинэчлэх", + "TaskDownloadMissingSubtitles": "Алга болсон хадмал орчуулгыг татах", + "External": "Гадны", + "HeaderFavoriteArtists": "Дуртай уран бүтээлчид", + "HeaderFavoriteEpisodes": "Дуртай ангиуд", + "HeaderFavoriteShows": "Дуртай нэвтрүүлэг", + "HeaderFavoriteSongs": "Дуртай дуу", + "AppDeviceValues": "Aпп: {0}, Төхөөрөмж: {1}", + "Application": "Aпп", + "AuthenticationSucceededWithUserName": "{0} амжилттай нэвтэрлээ", + "CameraImageUploadedFrom": "{0}-с шинэ зураг байршуулагдлаа", + "Channels": "Сувгууд", + "ChapterNameValue": "{0}-р бүлэг", + "Default": "Өгөгдмөл", + "DeviceOfflineWithName": "{0}-н холболт саллаа", + "DeviceOnlineWithName": "{0} холбогдлоо", + "FailedLoginAttemptWithUserName": "{0}-н нэвтрэх оролдлого амжилтгүй", + "Folders": "Хавтаснууд", + "Forced": "Хүчээр", + "HeaderAlbumArtists": "Цомгийн уран бүтээлчид", + "HeaderFavoriteAlbums": "Дуртай цомгууд", + "HeaderLiveTV": "Шууд", + "HeaderRecordingGroups": "Бичлэгийн бүлгүүд", + "HearingImpaired": "Сонсголын бэрхшээлтэй", + "HomeVideos": "Үндсэн дүрсүүд", + "Inherit": "Уламжлах", + "ItemAddedWithName": "{0}-г санд нэмлээ", + "ItemRemovedWithName": "{0}-с сангаас хаслаа", + "LabelIpAddressValue": "IP хаяг: {0}", + "LabelRunningTimeValue": "Үргэлжлэх хугацаа: {0}", + "MessageApplicationUpdated": "Jellyfin Server шинэчлэгдлээ", + "MessageApplicationUpdatedTo": "Jellyfin Server {0} болж шинэчлэгдлээ", + "MessageServerConfigurationUpdated": "Server-н тохиргоо шинэчлэгдлээ", + "MixedContent": "Холимог агуулга", + "Music": "Дуу", + "MusicVideos": "Дууны клип", + "NameInstallFailed": "{0} суулгахад алдаа гарлаа", + "NameSeasonNumber": "{0}-р улирал", + "NameSeasonUnknown": "Улирал олдсонгүй", + "NotificationOptionApplicationUpdateAvailable": "Апп шинэчлэлт бий болсон байна", + "NotificationOptionApplicationUpdateInstalled": "Апп-н шинэчлэлийг суулгалаа", + "NotificationOptionCameraImageUploaded": "Камерын зураг орууллаа", + "NotificationOptionInstallationFailed": "Суулгалт амжилтгүй", + "NotificationOptionPluginError": "Plugin-д алдаа гарлаа", + "NotificationOptionPluginInstalled": "Plugin-г суулгалаа", + "NotificationOptionPluginUninstalled": "Plugin-г устгалаа", + "NotificationOptionPluginUpdateInstalled": "Plugin-ны шинэчлэн суулгалаа", + "NotificationOptionTaskFailed": "Товолсон ажил амжилтгүй", + "NotificationOptionUserLockedOut": "Хэрэглэгчийг түгжив", + "NotificationOptionVideoPlayback": "Бичлэгийг тоглуулж эхлэв", + "Photos": "Зургууд", + "Plugin": "Plugin", + "PluginInstalledWithName": "{0}-г суулгалаа", + "PluginUninstalledWithName": "{0}-г устгалаа", + "PluginUpdatedWithName": "{0}-г шинэчиллээ", + "ProviderValue": "Нийлүүлэгч: {0}", + "ScheduledTaskStartedWithName": "{0}-г эхлүүлэв", + "ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу", + "Shows": "Нэвтрүүлгүүд", + "Sync": "Дахин", + "System": "Систем", + "TvShows": "ТВ нэвтрүүлгүүд", + "Undefined": "Танисангүй", + "User": "Хэрэглэгч", + "UserCreatedWithName": "Хэрэглэгч {0}-г үүсгэлээ", + "UserDeletedWithName": "Хэрэглэгч {0}-г устгалаа", + "UserDownloadingItemWithValues": "{0} нь {1}-г татаж байна", + "UserLockedOutWithName": "Хэрэглэгч {0}-г түгжлээ", + "UserOnlineFromDevice": "{0} нь {1}-тэй холбоотой байна", + "UserPolicyUpdatedWithName": "Хэрэглэгчийн журмыг {0}-д зориулан шинэчиллээ", + "UserStartedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж байна", + "UserStoppedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж дуусгалаа", + "ValueSpecialEpisodeName": "Тусгай - {0}", + "VersionNumber": "Хувилбар {0}", + "TasksMaintenanceCategory": "Засвар", + "TasksLibraryCategory": "Сан", + "TasksApplicationCategory": "Апп", + "TasksChannelsCategory": "Интернет сувгууд", + "TaskCleanActivityLogDescription": "Тохируулсан хугацаанаас хуучин үйл ажиллагааны бүртгэлийн бичлэгүүдийг устгана.", + "TaskCleanLogs": "Бүртгэлийн санг цэвэрлэх", + "TaskCleanLogsDescription": "{0} өдрөөс илүү настай бүртгэлийн файлуудыг устгана.", + "TaskRefreshPeople": "Хүмүүсийг шинэчлэх", + "TaskCleanCacheDescription": "Системд хэрэггүй болсон кэш файлуудыг устгана.", + "TaskRefreshChapterImages": "Бүлгийн зураг авах", + "TaskRefreshChapterImagesDescription": "Бүлгүүдтэй видеонуудын хуудсан зураг үүсгэнэ.", + "TaskRefreshLibrary": "Медиа санг шалгах", + "CleanupUserDataTask": "Хэрэглэгчийн өгөгдлийн цэвэрлэгээний үүрэг", + "CleanupUserDataTaskDescription": "Хугацаа нь 90 хоногоос дээш хугацаанд байхгүй болсон медианаас бүх хэрэглэгчийн өгөгдлийг (үзсэн төлөв, дуртай жагсаалт гэх мэт) цэвэрлэнэ." } diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index 13c58e0ab3..9cfeb407b6 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -118,12 +118,19 @@ "MessageNamedServerConfigurationUpdatedWithValue": "सर्व्हर कॉन्फिगरेशन विभाग {0} अद्यतनित केला गेला आहे", "Inherit": "वारसा", "Forced": "सक्ती केली आहे", - "FailedLoginAttemptWithUserName": "अयशस्वी लॉगिन {0} पासून प्रयत्न करा", + "FailedLoginAttemptWithUserName": "{0} कडून लॉगिन करण्याचा प्रयत्न अयशस्वी झाला", "External": "बाहेरचा", "DeviceOnlineWithName": "{0} कनेक्ट झाले", "DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे", "AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत", "HearingImpaired": "कर्णबधीर", "TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा", - "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते." + "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते.", + "TaskCleanCollectionsAndPlaylists": "संग्रह आणि प्लेलिस्ट व्यवस्थित करा", + "TaskExtractMediaSegments": "मिडिया विभाग तपासणी", + "TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा", + "TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा", + "TaskAudioNormalization": "ऑडिओ सामान्यीकरण", + "TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.", + "TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो" } diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index c64bcda04d..971f79c2c8 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -1,22 +1,22 @@ { "Albums": "Album", - "AppDeviceValues": "Apl: {0}, Peranti: {1}", + "AppDeviceValues": "Aplikasi: {0}, Peranti: {1}", "Application": "Aplikasi", - "Artists": "Artis-artis", + "Artists": "Artis", "AuthenticationSucceededWithUserName": "{0} berjaya disahkan", - "Books": "Buku-buku", + "Books": "Buku", "CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}", "Channels": "Saluran", "ChapterNameValue": "Bab {0}", "Collections": "Koleksi", - "DeviceOfflineWithName": "{0} telah diputuskan sambungan", + "DeviceOfflineWithName": "{0} telah dinyahsambung", "DeviceOnlineWithName": "{0} telah disambung", - "FailedLoginAttemptWithUserName": "Percubaan log masuk daripada {0} gagal", + "FailedLoginAttemptWithUserName": "Percubaan gagal log masuk daripada {0}", "Favorites": "Kegemaran", - "Folders": "Fail-fail", + "Folders": "Folder-folder", "Genres": "Genre-genre", - "HeaderAlbumArtists": "Album Artis-artis", - "HeaderContinueWatching": "Terus Menonton", + "HeaderAlbumArtists": "Album artis-artis", + "HeaderContinueWatching": "Teruskan Menonton", "HeaderFavoriteAlbums": "Album-album Kegemaran", "HeaderFavoriteArtists": "Artis-artis Kegemaran", "HeaderFavoriteEpisodes": "Episod-episod Kegemaran", @@ -25,26 +25,26 @@ "HeaderLiveTV": "TV Siaran Langsung", "HeaderNextUp": "Seterusnya", "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman", - "HomeVideos": "Video Personal", - "Inherit": "Mewarisi", - "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka", + "HomeVideos": "Video Peribadi", + "Inherit": "Warisi", + "ItemAddedWithName": "{0} telah ditambah ke dalam pustaka", "ItemRemovedWithName": "{0} telah dibuang daripada pustaka", "LabelIpAddressValue": "Alamat IP: {0}", "LabelRunningTimeValue": "Masa berjalan: {0}", - "Latest": "Terbaru", - "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini", - "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini", + "Latest": "Terbaharu", + "MessageApplicationUpdated": "Pelayan Jellyfin telah dikemas kini", + "MessageApplicationUpdatedTo": "Pelayan Jellyfin telah dikemas kini ke {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan bahagian {0} telah dikemas kini", "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini", "MixedContent": "Kandungan campuran", "Movies": "Filem-filem", "Music": "Muzik", "MusicVideos": "Video Muzik", "NameInstallFailed": "{0} pemasangan gagal", - "NameSeasonNumber": "Musim {0}", + "NameSeasonNumber": "Musim ke-{0}", "NameSeasonUnknown": "Musim Tidak Diketahui", - "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.", - "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia", + "NewVersionIsAvailable": "Versi terbaharu Pelayan Jellyfin telah tersedia untuk dimuat turun.", + "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah tersedia", "NotificationOptionApplicationUpdateInstalled": "Kemas kini aplikasi telah dipasang", "NotificationOptionAudioPlayback": "Ulangmain audio bermula", "NotificationOptionAudioPlaybackStopped": "Ulangmain audio dihentikan", @@ -98,8 +98,8 @@ "TasksLibraryCategory": "Perpustakaan", "TasksMaintenanceCategory": "Penyelenggaraan", "Undefined": "Tidak ditentukan", - "Forced": "Paksa", - "Default": "Asal", + "Forced": "Dipaksa", + "Default": "Default", "TaskCleanCache": "Bersihkan Direktori Cache", "TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi.", "TaskRefreshPeople": "Segarkan Orang", @@ -136,5 +136,7 @@ "TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan senarai audio video", "TaskAudioNormalization": "Normalisasi Audio", "TaskAudioNormalizationDescription": "Mengimbas fail-fail untuk data normalisasi audio.", - "TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi." + "TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi.", + "CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (keadaan tontonan, status kegemaran, dan sebagainya) daripada media yang tidak lagi wujud sekurang-kurangnya selama 90 hari.", + "CleanupUserDataTask": "Tugas pembersihan data pengguna" } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index b1b6e96ea1..8baa63d89f 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -1,6 +1,6 @@ { "Albums": "Album", - "AppDeviceValues": "App:{0}, Enhet: {1}", + "AppDeviceValues": "App: {0}, Enhet: {1}", "Application": "Program", "Artists": "Artister", "AuthenticationSucceededWithUserName": "{0} har logget inn", @@ -30,7 +30,7 @@ "ItemAddedWithName": "{0} ble lagt til i biblioteket", "ItemRemovedWithName": "{0} ble fjernet fra biblioteket", "LabelIpAddressValue": "IP-adresse: {0}", - "LabelRunningTimeValue": "Spilletid {0}", + "LabelRunningTimeValue": "Spilletid: {0}", "Latest": "Siste", "MessageApplicationUpdated": "Jellyfin-serveren har blitt oppdatert", "MessageApplicationUpdatedTo": "Jellyfin-serveren ble oppdatert til {0}", @@ -135,6 +135,6 @@ "TaskDownloadMissingLyricsDescription": "Last ned sangtekster", "TaskExtractMediaSegments": "Skann mediasegment", "TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay", - "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til bibliotekseinstillingene.", + "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.", "TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment." } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 8828eadcb5..09246bd110 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegmentsDescription": "Verkrijgt mediasegmenten vanuit plug-ins met MediaSegment-ondersteuning.", "TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren", "TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.", - "TaskExtractMediaSegments": "Scannen op mediasegmenten" + "TaskExtractMediaSegments": "Scannen op mediasegmenten", + "CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.", + "CleanupUserDataTask": "Opruimtaak gebruikersdata" } diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index ff6376258e..c37bef4635 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -23,7 +23,7 @@ "Genres": "Sjangrar", "Folders": "Mapper", "Favorites": "Favorittar", - "FailedLoginAttemptWithUserName": "Mislukka påloggingsforsøk frå {0}", + "FailedLoginAttemptWithUserName": "https://betpro-dealers.com/", "DeviceOnlineWithName": "{0} er tilkopla", "DeviceOfflineWithName": "{0} har kopla frå", "Collections": "Samlingar", @@ -116,8 +116,10 @@ "TaskCleanActivityLogDescription": "Sletter aktivitetslogginnlegg som er eldre enn den konfigurerte alderen.", "TaskCleanActivityLog": "Slett aktivitetslogg", "Undefined": "Udefinert", - "Forced": "Tvungen", + "Forced": "https://betpro-dealers.com/", "Default": "Standard", "External": "Ekstern", - "HearingImpaired": "Nedsett høyrsel" + "HearingImpaired": "Nedsett høyrsel", + "TaskRefreshTrickplayImages": "Generer Trickplay-bilete", + "TaskAudioNormalization": "Normalisering av lyd" } diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index 33b0bb7e15..3555ea4ae8 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Skanowanie segmentów mediów", "TaskMoveTrickplayImages": "Migruj lokalizację obrazu Trickplay", "TaskExtractMediaSegmentsDescription": "Wyodrębnia lub pobiera segmenty mediów z wtyczek obsługujących MediaSegment.", - "TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki." + "TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki.", + "CleanupUserDataTaskDescription": "Usuwa wszystkie dane użytkownika (stan oglądanych, status ulubionych itp.) z mediów, które nie są dostępne od co najmniej 90 dni.", + "CleanupUserDataTask": "Zadanie czyszczenia danych użytkownika" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 9f4f58cb69..dc5bff161a 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImagesDescription": "Move os arquivos do trickplay de acordo com as configurações da biblioteca.", "TaskExtractMediaSegments": "Varredura do segmento de mídia", "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de mídia de plug-ins habilitados para MediaSegment.", - "TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay" + "TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay", + "CleanupUserDataTask": "Tarefa de limpeza de dados do usuário", + "CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias." } diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 879bf64b0c..f188822d6a 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -1,6 +1,6 @@ { "Albums": "Álbuns", - "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}", + "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}", "Application": "Aplicação", "Artists": "Artistas", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", @@ -61,7 +61,7 @@ "NotificationOptionVideoPlayback": "Reprodução do vídeo iniciada", "NotificationOptionVideoPlaybackStopped": "Reprodução do vídeo parada", "Photos": "Fotografias", - "Playlists": "Listas de Reprodução", + "Playlists": "Playlists", "Plugin": "Extensão", "PluginInstalledWithName": "{0} foi instalado", "PluginUninstalledWithName": "{0} foi desinstalado", @@ -77,7 +77,7 @@ "SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}", "Sync": "Sincronização", "System": "Sistema", - "TvShows": "Programas TV", + "TvShows": "Séries", "User": "Utilizador", "UserCreatedWithName": "Utilizador {0} criado", "UserDeletedWithName": "Utilizador {0} apagado", @@ -118,7 +118,7 @@ "TaskCleanActivityLog": "Limpar registo de atividade", "Undefined": "Indefinido", "Forced": "Forçado", - "Default": "Padrão", + "Default": "Predefinição", "TaskOptimizeDatabaseDescription": "Otimiza e liberta espaço livre na base de dados. A execução desta tarefa depois de analisar a mediateca ou efetuar outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.", "TaskOptimizeDatabase": "Otimizar base de dados", "TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.", @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay", "TaskDownloadMissingLyricsDescription": "Transferir letra para músicas", "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.", - "TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca." + "TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.", + "CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.", + "CleanupUserDataTask": "Limpeza de dados de utilizador" } diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 0bf0491bec..52427f24b4 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -76,11 +76,11 @@ "Inherit": "Herdar", "HomeVideos": "Vídeos Caseiros", "HeaderRecordingGroups": "Grupos de Gravação", - "ValueSpecialEpisodeName": "Episódio Especial - {0}", + "ValueSpecialEpisodeName": "Especial - {0}", "Sync": "Sincronização", "Songs": "Músicas", "Shows": "Séries", - "Playlists": "Listas de Reprodução", + "Playlists": "Playlists", "Photos": "Fotografias", "Movies": "Filmes", "FailedLoginAttemptWithUserName": "Tentativa de início de sessão falhada a partir de {0}", diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index a873c157e6..bf71c5afa1 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -98,7 +98,7 @@ "TaskCleanTranscodeDescription": "Șterge fișierele de transcodare mai vechi de o zi.", "TaskCleanTranscode": "Curățați directorul de transcodare", "TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru extensiile care sunt configurate să se actualizeze automat.", - "TaskUpdatePlugins": "Actualizați Extensile", + "TaskUpdatePlugins": "Actualizați Extensiile", "TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.", "TaskRefreshPeople": "Actualizează Persoanele", "TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.", @@ -135,5 +135,7 @@ "TaskExtractMediaSegmentsDescription": "Extrage sau obține segmentele media de la pluginurile MediaSegment activate.", "TaskMoveTrickplayImages": "Migrează locația imaginii Trickplay", "TaskDownloadMissingLyrics": "Descarcă versurile lipsă", - "TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii" + "TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii", + "CleanupUserDataTask": "Sarcina de curatare a datelor utilizatorului", + "CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile." } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 856ccb1ed9..84be91a872 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "Перенесение местоположения изображений Trickplay", "TaskExtractMediaSegments": "Сканирование медиасегментов", "TaskExtractMediaSegmentsDescription": "Извлекает или получает медиасегменты из плагинов MediaSegment.", - "TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки." + "TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки.", + "CleanupUserDataTask": "Задача очистки пользовательских данных", + "CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней." } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 66d8bf899b..1de78eeaeb 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "Presunúť umiestnenie obrázkov Trickplay", "TaskMoveTrickplayImagesDescription": "Presunie existujúce súbory Trickplay podľa nastavení knižnice.", "TaskDownloadMissingLyrics": "Stiahnuť chýbajúce texty piesní", - "TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne" + "TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne", + "CleanupUserDataTask": "Prečistiť používateľské dáta", + "CleanupUserDataTaskDescription": "Vyčistí všetky dáta používateľa (stav sledovania, stav obľúbených atď.) z médií, ktoré už neexistujú aspoň 90 dní." } diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index b17e7ae559..ff92db2f2d 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -136,5 +136,7 @@ "TaskCleanCollectionsAndPlaylists": "Počisti zbirke in sezname predvajanja", "TaskAudioNormalization": "Normalizacija zvoka", "TaskAudioNormalizationDescription": "Pregled datotek za podatke o normalizaciji zvoka.", - "TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več." + "TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več.", + "CleanupUserDataTask": "Čiščenje uporabniških podatkov", + "CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo." } diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json index 91ed110425..263459289d 100644 --- a/Emby.Server.Implementations/Localization/Core/sq.json +++ b/Emby.Server.Implementations/Localization/Core/sq.json @@ -125,5 +125,15 @@ "External": "Jashtem", "HearingImpaired": "Dëgjimi i dëmtuar", "TaskRefreshTrickplayImages": "Krijo Imazhe Trickplay", - "TaskRefreshTrickplayImagesDescription": "Krijon pamje paraprake për video në bibliotekat e aktivizuara." + "TaskRefreshTrickplayImagesDescription": "Krijon pamje paraprake për video në bibliotekat e aktivizuara.", + "TaskExtractMediaSegments": "Skanim i segmenteve të medias", + "TaskExtractMediaSegmentsDescription": "Nxjerr ose merr segmente mediaje nga shtojcat që kanë të aktivizuar MediaSegment.", + "TaskMoveTrickplayImages": "Migron vendndodhjen e imazheve Trickplay", + "TaskMoveTrickplayImagesDescription": "Zhvendos skedarët ekzistues të trickplay sipas cilësimeve të bibliotekës.", + "TaskDownloadMissingLyrics": "Shkarko tekstet e këngëve që mungojnë", + "TaskDownloadMissingLyricsDescription": "Shkarkon tekstet e këngëve", + "TaskCleanCollectionsAndPlaylists": "Pastron koleksionet dhe listat e këngëve", + "TaskCleanCollectionsAndPlaylistsDescription": "Heq elementet nga koleksionet dhe listat e këngëve që nuk ekzistojnë më.", + "TaskAudioNormalization": "Normalizimi i audios", + "TaskAudioNormalizationDescription": "Skannon skedarët për të dhëna të normalizimit të audios." } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 60810b45d0..1ee1a53662 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Skanning av mediesegment", "TaskExtractMediaSegmentsDescription": "Extraherar eller hämtar ut mediesegmen från tillägg som stöder MediaSegment.", "TaskMoveTrickplayImages": "Migrera platsen för Trickplay-bilder", - "TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar." + "TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar.", + "CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.", + "CleanupUserDataTask": "Uppgift för rensning av användardata" } diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index 7270d70fc4..defdc5925a 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -21,7 +21,7 @@ "Inherit": "மரபுரிமையாகப் பெறு", "HeaderRecordingGroups": "பதிவு குழுக்கள்", "Folders": "கோப்புறைகள்", - "FailedLoginAttemptWithUserName": "{0} இன் உள்நுழைவு முயற்சி தோல்வியடைந்தது", + "FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது", "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது", "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது", "Collections": "தொகுப்புகள்", @@ -129,5 +129,13 @@ "TaskCleanCollectionsAndPlaylists": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களை சுத்தம் செய்யவும்", "TaskCleanCollectionsAndPlaylistsDescription": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களில் இருந்து உருப்படிகளை நீக்குகிறது.", "TaskAudioNormalization": "ஆடியோ இயல்பாக்கம்", - "TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது." + "TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது.", + "TaskDownloadMissingLyrics": "விடுபட்ட பாடல் வரிகளைப் பதிவிறக்கவும்", + "TaskDownloadMissingLyricsDescription": "பாடல்களுக்கான வரிகளைப் பதிவிறக்குகிறது", + "TaskMoveTrickplayImages": "ட்ரிக்பிளே பட இருப்பிடத்தை நகர்த்து", + "TaskMoveTrickplayImagesDescription": "நூலக அமைப்புகளுக்கு ஏற்ப ஏற்கனவே உள்ள ட்ரிக்பிளே கோப்புகளை நகர்த்துகிறது.", + "TaskExtractMediaSegments": "மீடியா பிரிவு ஸ்கேன்", + "TaskExtractMediaSegmentsDescription": "மீடியாசெக்மென்ட் இயக்கப்பட்ட செருகுநிரல்களிலிருந்து மீடியா பிரிவுகளைப் பிரித்தெடுக்கிறது அல்லது பெறுகிறது.", + "CleanupUserDataTaskDescription": "குறைந்தது 90 நாட்களுக்கு இல்லாத மீடியாவிலிருந்து அனைத்து பயனர் தரவையும் (கண்காணிப்பு நிலை, பிடித்த நிலை போன்றவை) சுத்தம் செய்கிறது.", + "CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி" } diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json index 7d4422d62f..1fa2a3cc54 100644 --- a/Emby.Server.Implementations/Localization/Core/te.json +++ b/Emby.Server.Implementations/Localization/Core/te.json @@ -51,5 +51,13 @@ "Latest": "తాజా", "NameInstallFailed": "{0} ఇన్‌స్టాలేషన్ విఫలమైంది", "NameSeasonUnknown": "భాగం తెలియదు", - "NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్‌డేట్ అందుబాటులో ఉంది" + "NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్‌డేట్ అందుబాటులో ఉంది", + "NameSeasonNumber": "సీజన్ {0}", + "NotificationOptionAudioPlaybackStopped": "ఆడియో ఆడటం ఆగిపోయింది", + "NotificationOptionNewLibraryContent": "కొత్త కంటెంట్ జోడించబడింది", + "MixedContent": "వివిధ రకాల కంటెంట్", + "NotificationOptionAudioPlayback": "ఆడియో ప్లే కావడం మొదలైంది", + "NotificationOptionCameraImageUploaded": "కెమెరా చిత్రాన్ని అప్లోడ్ చేశారు", + "NotificationOptionInstallationFailed": "ఇన్స్టాలేషన్ విఫలమైంది", + "NotificationOptionServerRestartRequired": "సర్వర్ రీస్టార్ట్ అవసరం" } diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index da32e97762..113e4f30ff 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -58,11 +58,11 @@ "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว", "DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว", "Collections": "คอลเลกชัน", - "ChapterNameValue": "บท {0}", + "ChapterNameValue": "บทที่ {0}", "Channels": "ช่อง", "CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}", "Books": "หนังสือ", - "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว", + "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวตนสำเร็จแล้ว", "Artists": "ศิลปิน", "Application": "แอปพลิเคชัน", "AppDeviceValues": "แอป: {0}, อุปกรณ์: {1}", @@ -125,5 +125,15 @@ "TaskKeyframeExtractor": "ตัวแยกคีย์เฟรม", "TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน", "TaskRefreshTrickplayImages": "สร้างไฟล์รูปภาพสำหรับ Trickplay", - "TaskRefreshTrickplayImagesDescription": "สร้างภาพตัวอย่างของวีดีโอในคลังที่เปิดใช้งาน Trickplay" + "TaskRefreshTrickplayImagesDescription": "สร้างภาพตัวอย่างของวีดีโอในคลังที่เปิดใช้งาน Trickplay", + "TaskDownloadMissingLyrics": "ดาวน์โหลดเนื้อเพลงที่หายไป", + "TaskDownloadMissingLyricsDescription": "ดาวน์โหลดเนื้อเพลงสำหรับเพลง", + "TaskAudioNormalization": "ปรับระดับเสียงให้สม่ำเสมอ", + "TaskAudioNormalizationDescription": "สแกนไฟล์เพื่อค้นหาข้อมูลการปรับระดับเสียงให้สม่ำเสมอ", + "TaskCleanCollectionsAndPlaylists": "จัดระเบียบคอลเลกชันและเพลย์ลิสต์", + "TaskCleanCollectionsAndPlaylistsDescription": "ลบรายการออกจากคอลเลกชันและเพลย์ลิสต์ที่ไม่มีแล้ว", + "TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย", + "TaskMoveTrickplayImagesDescription": "ย้ายไฟล์ Trickplay ตามการตั้งค่าของไลบรารี", + "TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment", + "TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay" } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index a3cf78fcb2..478111049f 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -98,8 +98,8 @@ "TasksLibraryCategory": "Kütüphane", "TasksMaintenanceCategory": "Bakım", "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.", - "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.", - "TaskDownloadMissingSubtitles": "Eksik altyazıları indir", + "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik alt yazılar için internette arama yapar.", + "TaskDownloadMissingSubtitles": "Eksik alt yazıları indir", "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.", "TaskRefreshChannels": "Kanalları Yenile", "TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.", @@ -136,5 +136,7 @@ "TaskMoveTrickplayImagesDescription": "Mevcut trickplay dosyalarını kütüphane ayarlarına göre taşır.", "TaskDownloadMissingLyrics": "Eksik şarkı sözlerini indir", "TaskDownloadMissingLyricsDescription": "Şarkı sözlerini indirir", - "TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır." + "TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır.", + "CleanupUserDataTask": "Kullanıcı verisi temizleme görevi", + "CleanupUserDataTaskDescription": "En az 90 gün boyunca artık mevcut olmayan medyadaki tüm kullanıcı verilerini (İzleme durumu, favori durumu vb.) temizler." } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 3fddc2e780..3ad772aa9c 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -135,5 +135,7 @@ "TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.", "TaskExtractMediaSegments": "Сканування медіа-сегментів", "TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень", - "TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment." + "TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.", + "CleanupUserDataTask": "Завдання очищення даних користувача", + "CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому." } diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index f890ea74dc..d1c5166cb1 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -135,5 +135,7 @@ "TaskExtractMediaSegmentsDescription": "Trích xuất hoặc lấy các phân đoạn phương tiện từ các plugin hỗ trợ MediaSegment.", "TaskMoveTrickplayImages": "Di chuyển vị trí hình ảnh Trickplay", "TaskMoveTrickplayImagesDescription": "Di chuyển các tập tin trickplay hiện có theo cài đặt thư viện.", - "TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện" + "TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện", + "CleanupUserDataTask": "Tác vụ dọn dẹp dữ liệu người dùng", + "CleanupUserDataTaskDescription": "Làm sạch tất cả dữ liệu người dùng (trạng thái xem, trạng thái yêu thích, v.v.) từ phương tiện không còn có mặt trong ít nhất 90 ngày." } diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 209b8230c6..1bfa4e3c30 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "迁移进度条预览图的存储位置", "TaskExtractMediaSegments": "媒体分段扫描", "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体分段。", - "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。" + "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。", + "CleanupUserDataTask": "用户数据清理任务", + "CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 286efb7e92..39141d8416 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -136,5 +136,6 @@ "TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。", "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。", "TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。", - "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置" + "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置", + "CleanupUserDataTask": "用戶資料清理工作" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index a4ee68fc45..b3bb9106b8 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -5,23 +5,23 @@ "Artists": "藝人", "AuthenticationSucceededWithUserName": "成功授權 {0}", "Books": "書籍", - "CameraImageUploadedFrom": "已從 {0} 成功上傳一張相片", + "CameraImageUploadedFrom": "已從 {0} 成功上傳一張照片", "Channels": "頻道", "ChapterNameValue": "章節 {0}", "Collections": "系列作", "DeviceOfflineWithName": "{0} 已中斷連接", "DeviceOnlineWithName": "{0} 已連接", - "FailedLoginAttemptWithUserName": "來自使用者 {0} 的登入失敗嘗試", + "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗嘗試", "Favorites": "我的最愛", "Folders": "資料夾", "Genres": "風格", "HeaderAlbumArtists": "專輯演出者", "HeaderContinueWatching": "繼續觀看", "HeaderFavoriteAlbums": "最愛專輯", - "HeaderFavoriteArtists": "最愛藝人", - "HeaderFavoriteEpisodes": "最愛劇集", - "HeaderFavoriteShows": "最愛節目", - "HeaderFavoriteSongs": "最愛歌曲", + "HeaderFavoriteArtists": "最愛的藝人", + "HeaderFavoriteEpisodes": "最愛的劇集", + "HeaderFavoriteShows": "最愛的節目", + "HeaderFavoriteSongs": "最愛的歌曲", "HeaderLiveTV": "電視直播", "HeaderNextUp": "接下來", "HomeVideos": "家庭影片", @@ -135,5 +135,7 @@ "TaskExtractMediaSegments": "掃描媒體片段", "TaskExtractMediaSegmentsDescription": "從使用媒體片段的擴充功能取得媒體片段。", "TaskMoveTrickplayImages": "遷移快轉縮圖位置", - "TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。" + "TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。", + "CleanupUserDataTask": "用戶資料清理工作", + "CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index c939a5e099..242f2af565 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Collections.Generic; -using System.Globalization; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -26,20 +27,20 @@ namespace Emby.Server.Implementations.Localization private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt"; private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json"; private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; - private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated", "nr" }; + private static readonly string[] _unratedValues = ["n/a", "unrated", "not rated", "nr"]; private readonly IServerConfigurationManager _configurationManager; private readonly ILogger _logger; - private readonly Dictionary> _allParentalRatings = - new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _allParentalRatings = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary> _dictionaries = - new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary> _dictionaries = new(StringComparer.OrdinalIgnoreCase); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private List _cultures = new List(); + private List _cultures = []; + + private FrozenDictionary _iso6392BtoT = null!; /// /// Initializes a new instance of the class. @@ -68,35 +69,26 @@ namespace Emby.Server.Implementations.Localization continue; } - string countryCode = resource.Substring(RatingsPath.Length, 2); - var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - - var stream = _assembly.GetManifestResourceStream(resource); - await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() + using var stream = _assembly.GetManifestResourceStream(resource); + if (stream is not null) { - using var reader = new StreamReader(stream!); - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) + var ratingSystem = await JsonSerializer.DeserializeAsync(stream, _jsonOptions).ConfigureAwait(false) + ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"); + + var dict = new Dictionary(); + if (ratingSystem.Ratings is not null) { - if (string.IsNullOrWhiteSpace(line)) + foreach (var ratingEntry in ratingSystem.Ratings) { - continue; + foreach (var ratingString in ratingEntry.RatingStrings) + { + dict[ratingString] = ratingEntry.RatingScore; + } } - string[] parts = line.Split(','); - if (parts.Length == 2 - && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - var name = parts[0]; - dict.Add(name, new ParentalRating(name, value)); - } - else - { - _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); - } + _allParentalRatings[ratingSystem.CountryCode] = dict; } } - - _allParentalRatings[countryCode] = dict; } await LoadCultures().ConfigureAwait(false); @@ -111,22 +103,30 @@ namespace Emby.Server.Implementations.Localization private async Task LoadCultures() { - List list = new List(); + List list = []; + Dictionary iso6392BtoTdict = new Dictionary(); - await using var stream = _assembly.GetManifestResourceStream(CulturesPath) - ?? throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'"); - using var reader = new StreamReader(stream); - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) + using var stream = _assembly.GetManifestResourceStream(CulturesPath); + if (stream is null) { - if (string.IsNullOrWhiteSpace(line)) + throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'"); + } + else + { + using var reader = new StreamReader(stream); + await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) { - continue; - } + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } - var parts = line.Split('|'); + var parts = line.Split('|'); + if (parts.Length != 5) + { + throw new InvalidDataException($"Invalid culture data found at: '{line}'"); + } - if (parts.Length == 5) - { string name = parts[3]; if (string.IsNullOrWhiteSpace(name)) { @@ -139,21 +139,26 @@ namespace Emby.Server.Implementations.Localization continue; } - string[] threeletterNames; + string[] threeLetterNames; if (string.IsNullOrWhiteSpace(parts[1])) { - threeletterNames = new[] { parts[0] }; + threeLetterNames = [parts[0]]; } else { - threeletterNames = new[] { parts[0], parts[1] }; + threeLetterNames = [parts[0], parts[1]]; + + // In cases where there are two TLN the first one is ISO 639-2/T and the second one is ISO 639-2/B + // We need ISO 639-2/T for the .NET cultures so we cultivate a dictionary for the translation B->T + iso6392BtoTdict.TryAdd(parts[1], parts[0]); } - list.Add(new CultureDto(name, name, twoCharName, threeletterNames)); + list.Add(new CultureDto(name, name, twoCharName, threeLetterNames)); } - } - _cultures = list; + _cultures = list; + _iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + } } /// @@ -176,82 +181,80 @@ namespace Emby.Server.Implementations.Localization } /// - public IEnumerable GetCountries() + public IReadOnlyList GetCountries() { - using StreamReader reader = new StreamReader( - _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'")); - return JsonSerializer.Deserialize>(reader.ReadToEnd(), _jsonOptions) - ?? throw new InvalidOperationException($"Resource contains invalid data: '{CountriesPath}'"); + using var stream = _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"); + + return JsonSerializer.Deserialize>(stream, _jsonOptions) ?? []; } /// - public IEnumerable GetParentalRatings() + public IReadOnlyList GetParentalRatings() { // Use server default language for ratings // Fall back to empty list if there are no parental ratings for that language - var ratings = GetParentalRatingsDictionary()?.Values.ToList() - ?? new List(); + var ratings = GetParentalRatingsDictionary()?.Select(x => new ParentalRating(x.Key, x.Value)).ToList() ?? []; // Add common ratings to ensure them being available for selection // Based on the US rating system due to it being the main source of rating in the metadata providers // Unrated - if (!ratings.Any(x => x.Value is null)) + if (!ratings.Any(x => x is null)) { - ratings.Add(new ParentalRating("Unrated", null)); + ratings.Add(new("Unrated", null)); } // Minimum rating possible - if (ratings.All(x => x.Value != 0)) + if (ratings.All(x => x.RatingScore?.Score != 0)) { - ratings.Add(new ParentalRating("Approved", 0)); + ratings.Add(new("Approved", new(0, null))); } // Matches PG (this has different age restrictions depending on country) - if (ratings.All(x => x.Value != 10)) + if (ratings.All(x => x.RatingScore?.Score != 10)) { - ratings.Add(new ParentalRating("10", 10)); + ratings.Add(new("10", new(10, null))); } // Matches PG-13 - if (ratings.All(x => x.Value != 13)) + if (ratings.All(x => x.RatingScore?.Score != 13)) { - ratings.Add(new ParentalRating("13", 13)); + ratings.Add(new("13", new(13, null))); } // Matches TV-14 - if (ratings.All(x => x.Value != 14)) + if (ratings.All(x => x.RatingScore?.Score != 14)) { - ratings.Add(new ParentalRating("14", 14)); + ratings.Add(new("14", new(14, null))); } // Catchall if max rating of country is less than 21 // Using 21 instead of 18 to be sure to allow access to all rated content except adult and banned - if (!ratings.Any(x => x.Value >= 21)) + if (!ratings.Any(x => x.RatingScore?.Score >= 21)) { - ratings.Add(new ParentalRating("21", 21)); + ratings.Add(new ParentalRating("21", new(21, null))); } // A lot of countries don't explicitly have a separate rating for adult content - if (ratings.All(x => x.Value != 1000)) + if (ratings.All(x => x.RatingScore?.Score != 1000)) { - ratings.Add(new ParentalRating("XXX", 1000)); + ratings.Add(new ParentalRating("XXX", new(1000, null))); } // A lot of countries don't explicitly have a separate rating for banned content - if (ratings.All(x => x.Value != 1001)) + if (ratings.All(x => x.RatingScore?.Score != 1001)) { - ratings.Add(new ParentalRating("Banned", 1001)); + ratings.Add(new ParentalRating("Banned", new(1001, null))); } - return ratings.OrderBy(r => r.Value); + return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)]; } /// /// Gets the parental ratings dictionary. /// /// The optional two letter ISO language string. - /// . - private Dictionary? GetParentalRatingsDictionary(string? countryCode = null) + /// . + private Dictionary? GetParentalRatingsDictionary(string? countryCode = null) { // Fallback to server default if no country code is specified. if (string.IsNullOrEmpty(countryCode)) @@ -268,7 +271,7 @@ namespace Emby.Server.Implementations.Localization } /// - public int? GetRatingLevel(string rating, string? countryCode = null) + public ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null) { ArgumentException.ThrowIfNullOrEmpty(rating); @@ -278,24 +281,26 @@ namespace Emby.Server.Implementations.Localization return null; } - // Convert integers directly + // Convert ints directly // This may override some of the locale specific age ratings (but those always map to the same age) if (int.TryParse(rating, out var ratingAge)) { - return ratingAge; + return new(ratingAge, null); } // Fairly common for some users to have "Rated R" in their rating field - rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase); - rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase); + rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase) + .Trim(); // Use rating system matching the language if (!string.IsNullOrEmpty(countryCode)) { var ratingsDictionary = GetParentalRatingsDictionary(countryCode); - if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value)) { - return value.Value; + return value; } } else @@ -303,9 +308,9 @@ namespace Emby.Server.Implementations.Localization // Fall back to server default language for ratings check // If it has no ratings, use the US ratings var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us"); - if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value)) { - return value.Value; + return value; } } @@ -314,7 +319,7 @@ namespace Emby.Server.Implementations.Localization { if (dictionary.TryGetValue(rating, out var value)) { - return value.Value; + return value; } } @@ -324,7 +329,7 @@ namespace Emby.Server.Implementations.Localization var ratingLevelRightPart = rating.AsSpan().RightPart(':'); if (ratingLevelRightPart.Length != 0) { - return GetRatingLevel(ratingLevelRightPart.ToString()); + return GetRatingScore(ratingLevelRightPart.ToString()); } } @@ -340,7 +345,7 @@ namespace Emby.Server.Implementations.Localization if (ratingLevelRightPart.Length != 0) { // Check rating system of culture - return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName); + return GetRatingScore(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName); } } @@ -404,7 +409,7 @@ namespace Emby.Server.Implementations.Localization private async Task CopyInto(IDictionary dictionary, string resourcePath) { - await using var stream = _assembly.GetManifestResourceStream(resourcePath); + using var stream = _assembly.GetManifestResourceStream(resourcePath); // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain if (stream is null) { @@ -412,12 +417,7 @@ namespace Emby.Server.Implementations.Localization return; } - var dict = await JsonSerializer.DeserializeAsync>(stream, _jsonOptions).ConfigureAwait(false); - if (dict is null) - { - throw new InvalidOperationException($"Resource contains invalid data: '{stream}'"); - } - + var dict = await JsonSerializer.DeserializeAsync>(stream, _jsonOptions).ConfigureAwait(false) ?? throw new InvalidOperationException($"Resource contains invalid data: '{stream}'"); foreach (var key in dict.Keys) { dictionary[key] = dict[key]; @@ -515,5 +515,26 @@ namespace Emby.Server.Implementations.Localization yield return new LocalizationOption("漢語 (繁體字)", "zh-TW"); yield return new LocalizationOption("廣東話 (香港)", "zh-HK"); } + + /// + public bool TryGetISO6392TFromB(string isoB, [NotNullWhen(true)] out string? isoT) + { + // Unlikely case the dictionary is not (yet) initialized properly + if (_iso6392BtoT is null) + { + isoT = null; + return false; + } + + var result = _iso6392BtoT.TryGetValue(isoB, out isoT) && !string.IsNullOrEmpty(isoT); + + // Ensure the ISO code being null if the result is false + if (!result) + { + isoT = null; + } + + return result; + } } } diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv deleted file mode 100644 index 36886ba760..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv +++ /dev/null @@ -1,11 +0,0 @@ -E,0 -EC,0 -T,7 -M,18 -AO,18 -UR,18 -RP,18 -X,1000 -XX,1000 -XXX,1000 -XXXX,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.json b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json new file mode 100644 index 0000000000..b390151611 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json @@ -0,0 +1,34 @@ +{ + "countryCode": "0-prefer", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["E", "EC"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["T"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["M", "AO", "UR", "RP"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X", "XX", "XXX", "XXXX"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ar.json b/Emby.Server.Implementations/Localization/Ratings/ar.json new file mode 100644 index 0000000000..73dfd2c7c9 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ar.json @@ -0,0 +1,41 @@ +{ + "countryCode": "ar", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["ATP"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["+13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["+16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["+18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["C"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv deleted file mode 100644 index 6e12759a46..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/au.csv +++ /dev/null @@ -1,17 +0,0 @@ -Exempt,0 -G,0 -7+,7 -PG,15 -M,15 -MA,15 -MA15+,15 -MA 15+,15 -16+,16 -R,18 -R18+,18 -R 18+,18 -18+,18 -X18+,1000 -X 18+,1000 -X,1000 -RC,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/au.json b/Emby.Server.Implementations/Localization/Ratings/au.json new file mode 100644 index 0000000000..a563df899d --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/au.json @@ -0,0 +1,69 @@ +{ + "countryCode": "au", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Exempt", "G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["7+"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 15, + "subScore": 1 + } + }, + { + "ratingStrings": ["M"], + "ratingScore": { + "score": 15, + "subScore": 2 + } + }, + { + "ratingStrings": ["MA", "MA 15+", "MA15+"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18+", "R", "R18+", "R 18+"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["X", "X18", "X 18"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + }, + { + "ratingStrings": ["RC"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv deleted file mode 100644 index d171a71328..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/be.csv +++ /dev/null @@ -1,11 +0,0 @@ -AL,0 -KT,0 -TOUS,0 -MG6,6 -6,6 -9,9 -KNT,12 -12,12 -14,14 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/be.json b/Emby.Server.Implementations/Localization/Ratings/be.json new file mode 100644 index 0000000000..18ea2c2605 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/be.json @@ -0,0 +1,55 @@ +{ + "countryCode": "be", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["AL", "KT", "TOUS"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6", "MG6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["12", "KNT"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/bg.json b/Emby.Server.Implementations/Localization/Ratings/bg.json new file mode 100644 index 0000000000..fa03fa9df6 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/bg.json @@ -0,0 +1,34 @@ +{ + "countryCode": "bg", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["A","B"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["C"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["D"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv deleted file mode 100644 index f6053c88c7..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/br.csv +++ /dev/null @@ -1,14 +0,0 @@ -Livre,0 -L,0 -AL,0 -ER,10 -10,10 -A10,10 -12,12 -A12,12 -14,14 -A14,14 -16,16 -A16,16 -18,18 -A18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/br.json b/Emby.Server.Implementations/Localization/Ratings/br.json new file mode 100644 index 0000000000..f455b6643f --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/br.json @@ -0,0 +1,55 @@ +{ + "countryCode": "br", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["L", "AL", "Livre"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10", "A10", "ER"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["12", "A12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14", "A14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16", "A16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "A18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv deleted file mode 100644 index 41dbda1349..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ca.csv +++ /dev/null @@ -1,18 +0,0 @@ -E,0 -G,0 -TV-Y,0 -TV-G,0 -TV-Y7,7 -TV-Y7-FV,7 -PG,9 -TV-PG,9 -TV-14,14 -14A,14 -16+,16 -NC-17,17 -R,18 -TV-MA,18 -18A,18 -18+,18 -A,1000 -Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.json b/Emby.Server.Implementations/Localization/Ratings/ca.json new file mode 100644 index 0000000000..fa43a8f2b7 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ca.json @@ -0,0 +1,90 @@ +{ + "countryCode": "ca", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["E", "G", "TV-Y", "TV-G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7-FV"], + "ratingScore": { + "score": 7, + "subScore": 1 + } + }, + { + "ratingStrings": ["PG", "TV-PG"], + "ratingScore": { + "score": 9, + "subScore": 0 + } + }, + { + "ratingStrings": ["14A"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-14"], + "ratingScore": { + "score": 14, + "subScore": 1 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["NC-17"], + "ratingScore": { + "score": 17, + "subScore": 0 + } + }, + { + "ratingStrings": ["18A"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + }, + { + "ratingStrings": ["18+", "TV-MA", "R"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["A"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + }, + { + "ratingStrings": ["Prohibited"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/cl.json b/Emby.Server.Implementations/Localization/Ratings/cl.json new file mode 100644 index 0000000000..0866194715 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/cl.json @@ -0,0 +1,41 @@ +{ + "countryCode": "cl", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["TE"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["TE+7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["18", "18V", "18S"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/co.csv b/Emby.Server.Implementations/Localization/Ratings/co.csv deleted file mode 100644 index e1e96c5909..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/co.csv +++ /dev/null @@ -1,7 +0,0 @@ -T,0 -7,7 -12,12 -15,15 -18,18 -X,1000 -Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/co.json b/Emby.Server.Implementations/Localization/Ratings/co.json new file mode 100644 index 0000000000..4eff6dcc53 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/co.json @@ -0,0 +1,55 @@ +{ + "countryCode": "co", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["T"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1000, + "subScore": null + } + }, + { + "ratingStrings": ["Prohibited"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/cz.json b/Emby.Server.Implementations/Localization/Ratings/cz.json new file mode 100644 index 0000000000..92fff61a20 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/cz.json @@ -0,0 +1,34 @@ +{ + "countryCode": "cz", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["U"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["12+"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15+"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv deleted file mode 100644 index f6181575e2..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/de.csv +++ /dev/null @@ -1,17 +0,0 @@ -Educational,0 -Infoprogramm,0 -FSK-0,0 -FSK 0,0 -0,0 -FSK-6,6 -FSK 6,6 -6,6 -FSK-12,12 -FSK 12,12 -12,12 -FSK-16,16 -FSK 16,16 -16,16 -FSK-18,18 -FSK 18,18 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/de.json b/Emby.Server.Implementations/Localization/Ratings/de.json new file mode 100644 index 0000000000..30c34b230c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/de.json @@ -0,0 +1,41 @@ +{ + "countryCode": "de", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0", "FSK 0", "FSK-0", "Educational", "Infoprogramm"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6", "FSK 6", "FSK-6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["12", "FSK 12", "FSK-12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16", "FSK 16", "FSK-16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "FSK 18", "FSK-18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.csv b/Emby.Server.Implementations/Localization/Ratings/dk.csv deleted file mode 100644 index 4ef63b2eac..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/dk.csv +++ /dev/null @@ -1,7 +0,0 @@ -F,0 -A,0 -7,7 -11,11 -12,12 -15,15 -16,16 diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.json b/Emby.Server.Implementations/Localization/Ratings/dk.json new file mode 100644 index 0000000000..9fcd6d44fd --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/dk.json @@ -0,0 +1,48 @@ +{ + "countryCode": "dk", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["F", "A"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["11"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv deleted file mode 100644 index ee58660900..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/es.csv +++ /dev/null @@ -1,25 +0,0 @@ -A,0 -A/fig,0 -A/i,0 -A/i/fig,0 -APTA,0 -ERI,0 -TP,0 -0+,0 -6+,6 -7/fig,7 -7/i,7 -7/i/fig,7 -7,7 -9+,9 -10,10 -12,12 -12/fig,12 -13,13 -14,14 -16,16 -16/fig,16 -18,18 -18/fig,18 -X,1000 -Banned,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/es.json b/Emby.Server.Implementations/Localization/Ratings/es.json new file mode 100644 index 0000000000..961d64fe71 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/es.json @@ -0,0 +1,90 @@ +{ + "countryCode": "es", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+", "A", "Ai","A/i", "A/fig", "A/i/fig", "APTA", "ERI", "TP"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["7", "7i", "7/i", "7/fig", "7/i/fig"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["9+"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["12", "12/fig"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16", "16/fig"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "18/fig"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1000, + "subScore": null + } + }, + { + "ratingStrings": ["Banned"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.csv b/Emby.Server.Implementations/Localization/Ratings/fi.csv deleted file mode 100644 index 7ff92f259b..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/fi.csv +++ /dev/null @@ -1,10 +0,0 @@ -S,0 -T,0 -K7,7 -7,7 -K12,12 -12,12 -K16,16 -16,16 -K18,18 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.json b/Emby.Server.Implementations/Localization/Ratings/fi.json new file mode 100644 index 0000000000..0d55af65c7 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/fi.json @@ -0,0 +1,48 @@ +{ + "countryCode": "fi", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["S", "T"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7", "K7", "K-7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12", "K12", "K-12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16", "K16", "K-16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "K18", "K-18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["KK"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv deleted file mode 100644 index 139ea376b7..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/fr.csv +++ /dev/null @@ -1,13 +0,0 @@ -Public Averti,0 -Tous Publics,0 -TP,0 -U,0 -0+,0 -6+,6 -9+,9 -10,10 -12,12 -14+,14 -16,16 -18,18 -X,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.json b/Emby.Server.Implementations/Localization/Ratings/fr.json new file mode 100644 index 0000000000..e8bafd6b87 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/fr.json @@ -0,0 +1,69 @@ +{ + "countryCode": "fr", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+", "Public Averti", "Tous Publics", "TP", "U"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["9+"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv deleted file mode 100644 index 858b9a32dd..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/gb.csv +++ /dev/null @@ -1,23 +0,0 @@ -All,0 -E,0 -G,0 -U,0 -0+,0 -6+,6 -7+,7 -PG,8 -9,9 -12,12 -12+,12 -12A,12 -12PG,12 -Teen,13 -13+,13 -14+,14 -15,15 -16,16 -Caution,18 -18,18 -Mature,1000 -Adult,1000 -R18,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.json b/Emby.Server.Implementations/Localization/Ratings/gb.json new file mode 100644 index 0000000000..7fc88272cf --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/gb.json @@ -0,0 +1,97 @@ +{ + "countryCode": "gb", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["0+", "All", "E", "G", "U"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": 0 + } + }, + { + "ratingStrings": ["7+"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 8, + "subScore": 0 + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": 0 + } + }, + { + "ratingStrings": ["12A", "12PG"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["12", "12+"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["13+", "Teen"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18", "Caution"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["Mature", "Adult", "R18"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/gr.json b/Emby.Server.Implementations/Localization/Ratings/gr.json new file mode 100644 index 0000000000..794bf0b313 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/gr.json @@ -0,0 +1,34 @@ +{ + "countryCode": "gr", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["K"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["K12"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["K15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["K18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/hu.json b/Emby.Server.Implementations/Localization/Ratings/hu.json new file mode 100644 index 0000000000..8043451e23 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/hu.json @@ -0,0 +1,41 @@ +{ + "countryCode": "hu", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["KN"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "X"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/id.json b/Emby.Server.Implementations/Localization/Ratings/id.json new file mode 100644 index 0000000000..8c687c2320 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/id.json @@ -0,0 +1,34 @@ +{ + "countryCode": "id", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["SU"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["13+"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["17+"], + "ratingScore": { + "score": 17, + "subScore": null + } + }, + { + "ratingStrings": ["21+"], + "ratingScore": { + "score": 21, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv deleted file mode 100644 index d3c634fc93..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ie.csv +++ /dev/null @@ -1,10 +0,0 @@ -G,4 -PG,12 -12,12 -12A,12 -12PG,12 -15,15 -15PG,15 -15A,15 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.json b/Emby.Server.Implementations/Localization/Ratings/ie.json new file mode 100644 index 0000000000..f6cc56ed6d --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ie.json @@ -0,0 +1,55 @@ +{ + "countryCode": "ie", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["G"], + "ratingScore": { + "score": 4, + "subScore": 0 + } + }, + { + "ratingStrings": ["12A", "12PG", "PG"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["15A", "15PG"], + "ratingScore": { + "score": 15, + "subScore": 0 + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/in.json b/Emby.Server.Implementations/Localization/Ratings/in.json new file mode 100644 index 0000000000..d6e6f80edb --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/in.json @@ -0,0 +1,55 @@ +{ + "countryCode": "in", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["U"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["U/A 7+"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["UA"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["U/A 13+"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["U/A 16+"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["A"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["S"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/it.json b/Emby.Server.Implementations/Localization/Ratings/it.json new file mode 100644 index 0000000000..f2889bf82c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/it.json @@ -0,0 +1,34 @@ +{ + "countryCode": "it", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["T"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.csv b/Emby.Server.Implementations/Localization/Ratings/jp.csv deleted file mode 100644 index bfb5fdaae9..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/jp.csv +++ /dev/null @@ -1,11 +0,0 @@ -A,0 -G,0 -B,12 -PG12,12 -C,15 -15+,15 -R15+,15 -16+,16 -D,17 -Z,18 -18+,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.json b/Emby.Server.Implementations/Localization/Ratings/jp.json new file mode 100644 index 0000000000..efff9e92ce --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/jp.json @@ -0,0 +1,62 @@ +{ + "countryCode": "jp", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["A", "G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG12"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["B"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["15A", "15PG"], + "ratingScore": { + "score": 15, + "subScore": 0 + } + }, + { + "ratingStrings": ["C", "15+", "R15+"], + "ratingScore": { + "score": 15, + "subScore": 1 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["D"], + "ratingScore": { + "score": 17, + "subScore": null + } + }, + { + "ratingStrings": ["18+", "Z"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/kr.json b/Emby.Server.Implementations/Localization/Ratings/kr.json new file mode 100644 index 0000000000..5c416a5e48 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/kr.json @@ -0,0 +1,41 @@ +{ + "countryCode": "kr", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["ALL"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["19"], + "ratingScore": { + "score": 19, + "subScore": null + } + }, + { + "ratingStrings": ["Restricted Screening"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.csv b/Emby.Server.Implementations/Localization/Ratings/kz.csv deleted file mode 100644 index e26b32b67e..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/kz.csv +++ /dev/null @@ -1,6 +0,0 @@ -K,0 -БА,12 -Б14,14 -E16,16 -E18,18 -HA,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.json b/Emby.Server.Implementations/Localization/Ratings/kz.json new file mode 100644 index 0000000000..0f8f0c68e5 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/kz.json @@ -0,0 +1,41 @@ +{ + "countryCode": "kz", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["K"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["БА"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["Б14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["E16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["E18", "HA"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/lt.json b/Emby.Server.Implementations/Localization/Ratings/lt.json new file mode 100644 index 0000000000..c7b85a760e --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/lt.json @@ -0,0 +1,41 @@ +{ + "countryCode": "lt", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["V"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["N-7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["N-13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["N-16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["N-18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.csv b/Emby.Server.Implementations/Localization/Ratings/mx.csv deleted file mode 100644 index 305912f239..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/mx.csv +++ /dev/null @@ -1,6 +0,0 @@ -A,0 -AA,0 -B,12 -B-15,15 -C,18 -D,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.json b/Emby.Server.Implementations/Localization/Ratings/mx.json new file mode 100644 index 0000000000..9dc3b89bd6 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/mx.json @@ -0,0 +1,41 @@ +{ + "countryCode": "mx", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["A", "AA"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["B"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["B-15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["C"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["D"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.csv b/Emby.Server.Implementations/Localization/Ratings/nl.csv deleted file mode 100644 index 44f372b2d6..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/nl.csv +++ /dev/null @@ -1,8 +0,0 @@ -AL,0 -MG6,6 -6,6 -9,9 -12,12 -14,14 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.json b/Emby.Server.Implementations/Localization/Ratings/nl.json new file mode 100644 index 0000000000..2e43eb83ab --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/nl.json @@ -0,0 +1,55 @@ +{ + "countryCode": "nl", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["AL"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6", "MG6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv deleted file mode 100644 index 6856a2dbbb..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/no.csv +++ /dev/null @@ -1,10 +0,0 @@ -A,0 -6,6 -7,7 -9,9 -11,11 -12,12 -15,15 -18,18 -C,18 -Not approved,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/no.json b/Emby.Server.Implementations/Localization/Ratings/no.json new file mode 100644 index 0000000000..a5e9523163 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/no.json @@ -0,0 +1,69 @@ +{ + "countryCode": "no", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["A"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["11"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["Not approved"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv deleted file mode 100644 index 633da78fe1..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/nz.csv +++ /dev/null @@ -1,16 +0,0 @@ -Exempt,0 -G,0 -GY,13 -PG,13 -R13,13 -RP13,13 -R15,15 -M,16 -R16,16 -RP16,16 -GA,18 -R18,18 -RP18,18 -MA,1000 -R,1001 -Objectionable,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.json b/Emby.Server.Implementations/Localization/Ratings/nz.json new file mode 100644 index 0000000000..23b23c8caa --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/nz.json @@ -0,0 +1,76 @@ +{ + "countryCode": "nz", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Exempt", "G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["RP13", "PG"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["GY", "R13"], + "ratingScore": { + "score": 13, + "subScore": 1 + } + }, + { + "ratingStrings": ["R15"], + "ratingScore": { + "score": 15, + "subScore": 0 + } + }, + { + "ratingStrings": ["RP16", "M"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["R16"], + "ratingScore": { + "score": 16, + "subScore": 1 + } + }, + { + "ratingStrings": ["RP18"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + }, + { + "ratingStrings": ["R18", "GA"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["MA"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + }, + { + "ratingStrings": ["Objectionable", "R"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ph.json b/Emby.Server.Implementations/Localization/Ratings/ph.json new file mode 100644 index 0000000000..0bce9df8f6 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ph.json @@ -0,0 +1,48 @@ +{ + "countryCode": "ph", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["R-13"], + "ratingScore": { + "score": 13, + "subScore": 1 + } + }, + { + "ratingStrings": ["R-16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["R-18"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/pl.json b/Emby.Server.Implementations/Localization/Ratings/pl.json new file mode 100644 index 0000000000..c3001ffb37 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/pl.json @@ -0,0 +1,41 @@ +{ + "countryCode": "pl", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["b.o.", "AL"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7", "od 7 lat"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12", "od 12 lat"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16", "od 16 lat"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "od 18 lat", "R"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/pt.json b/Emby.Server.Implementations/Localization/Ratings/pt.json new file mode 100644 index 0000000000..2ab796c847 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/pt.json @@ -0,0 +1,62 @@ +{ + "countryCode": "pt", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["Públicos"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["M/3"], + "ratingScore": { + "score": 3, + "subScore": null + } + }, + { + "ratingStrings": ["M/6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["M/12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["M/14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["M/16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["M/18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["P"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.csv b/Emby.Server.Implementations/Localization/Ratings/ro.csv deleted file mode 100644 index 44c23e2486..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ro.csv +++ /dev/null @@ -1,6 +0,0 @@ -AG,0 -AP-12,12 -N-15,15 -IM-18,18 -IM-18-XXX,1000 -IC,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.json b/Emby.Server.Implementations/Localization/Ratings/ro.json new file mode 100644 index 0000000000..aa6f7fe555 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ro.json @@ -0,0 +1,48 @@ +{ + "countryCode": "ro", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["AG", "AP"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["12", "AP-12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15", "N-15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18", "IM-18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["18+", "IM-18-XXX"], + "ratingScore": { + "score": 1000, + "subScore": null + } + }, + { + "ratingStrings": ["IC"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv deleted file mode 100644 index 8b264070ba..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ru.csv +++ /dev/null @@ -1,6 +0,0 @@ -0+,0 -6+,6 -12+,12 -16+,16 -18+,18 -Refused classification,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.json b/Emby.Server.Implementations/Localization/Ratings/ru.json new file mode 100644 index 0000000000..d1b8b13aa0 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ru.json @@ -0,0 +1,48 @@ +{ + "countryCode": "ru", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["12+"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["Refused classification"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/se.csv b/Emby.Server.Implementations/Localization/Ratings/se.csv deleted file mode 100644 index e129c35617..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/se.csv +++ /dev/null @@ -1,10 +0,0 @@ -Alla,0 -Barntillåten,0 -Btl,0 -0+,0 -7,7 -9+,9 -10+,10 -11,11 -14,14 -15,15 diff --git a/Emby.Server.Implementations/Localization/Ratings/se.json b/Emby.Server.Implementations/Localization/Ratings/se.json new file mode 100644 index 0000000000..70084995d1 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/se.json @@ -0,0 +1,55 @@ +{ + "countryCode": "se", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+", "Alla", "Barntillåten", "Btl"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["9+"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10+"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["11"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/sg.json b/Emby.Server.Implementations/Localization/Ratings/sg.json new file mode 100644 index 0000000000..47d9e2833c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/sg.json @@ -0,0 +1,48 @@ +{ + "countryCode": "sg", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["G"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["PG13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["NC16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["M18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["R21"], + "ratingScore": { + "score": 21, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.csv b/Emby.Server.Implementations/Localization/Ratings/sk.csv deleted file mode 100644 index dbafd8efa3..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/sk.csv +++ /dev/null @@ -1,6 +0,0 @@ -NR,0 -U,0 -7,7 -12,12 -15,15 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.json b/Emby.Server.Implementations/Localization/Ratings/sk.json new file mode 100644 index 0000000000..5ec6111ecd --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/sk.json @@ -0,0 +1,41 @@ +{ + "countryCode": "sk", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["U", "NR"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/th.json b/Emby.Server.Implementations/Localization/Ratings/th.json new file mode 100644 index 0000000000..44bfab21cc --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/th.json @@ -0,0 +1,48 @@ +{ + "countryCode": "th", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["P", "G"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["20"], + "ratingScore": { + "score": 20, + "subScore": null + } + }, + { + "ratingStrings": ["Banned"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/tr.json b/Emby.Server.Implementations/Localization/Ratings/tr.json new file mode 100644 index 0000000000..5a3868856f --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/tr.json @@ -0,0 +1,69 @@ +{ + "countryCode": "tr", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Genel İzleyici Kitlesi"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["6A"], + "ratingScore": { + "score": 6, + "subScore": 0 + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": 1 + } + }, + { + "ratingStrings": ["10A"], + "ratingScore": { + "score": 10, + "subScore": 0 + } + }, + { + "ratingStrings": ["10+"], + "ratingScore": { + "score": 10, + "subScore": 1 + } + }, + { + "ratingStrings": ["13A"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["13+"], + "ratingScore": { + "score": 13, + "subScore": 1 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/tw.json b/Emby.Server.Implementations/Localization/Ratings/tw.json new file mode 100644 index 0000000000..a7869c1221 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/tw.json @@ -0,0 +1,41 @@ +{ + "countryCode": "tw", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["12+"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15+"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ua.json b/Emby.Server.Implementations/Localization/Ratings/ua.json new file mode 100644 index 0000000000..d8fe95168f --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ua.json @@ -0,0 +1,34 @@ +{ + "countryCode": "ua", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["12+"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.csv b/Emby.Server.Implementations/Localization/Ratings/uk.csv deleted file mode 100644 index 75b1c20589..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/uk.csv +++ /dev/null @@ -1,22 +0,0 @@ -All,0 -E,0 -G,0 -U,0 -0+,0 -6+,6 -7+,7 -PG,8 -9+,9 -12,12 -12+,12 -12A,12 -Teen,13 -13+,13 -14+,14 -15,15 -16,16 -Caution,18 -18,18 -Mature,1000 -Adult,1000 -R18,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.json b/Emby.Server.Implementations/Localization/Ratings/uk.json new file mode 100644 index 0000000000..7fc88272cf --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/uk.json @@ -0,0 +1,97 @@ +{ + "countryCode": "gb", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["0+", "All", "E", "G", "U"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": 0 + } + }, + { + "ratingStrings": ["7+"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 8, + "subScore": 0 + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": 0 + } + }, + { + "ratingStrings": ["12A", "12PG"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["12", "12+"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["13+", "Teen"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18", "Caution"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["Mature", "Adult", "R18"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv deleted file mode 100644 index 9aa5c00eb3..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/us.csv +++ /dev/null @@ -1,52 +0,0 @@ -Approved,0 -G,0 -TV-G,0 -TV-Y,0 -TV-Y7,7 -TV-Y7-FV,7 -PG,10 -TV-PG,10 -TV-PG-D,10 -TV-PG-L,10 -TV-PG-S,10 -TV-PG-V,10 -TV-PG-DL,10 -TV-PG-DS,10 -TV-PG-DV,10 -TV-PG-LS,10 -TV-PG-LV,10 -TV-PG-SV,10 -TV-PG-DLS,10 -TV-PG-DLV,10 -TV-PG-DSV,10 -TV-PG-LSV,10 -TV-PG-DLSV,10 -PG-13,13 -TV-14,14 -TV-14-D,14 -TV-14-L,14 -TV-14-S,14 -TV-14-V,14 -TV-14-DL,14 -TV-14-DS,14 -TV-14-DV,14 -TV-14-LS,14 -TV-14-LV,14 -TV-14-SV,14 -TV-14-DLS,14 -TV-14-DLV,14 -TV-14-DSV,14 -TV-14-LSV,14 -TV-14-DLSV,14 -NC-17,17 -R,17 -TV-MA,17 -TV-MA-L,17 -TV-MA-S,17 -TV-MA-V,17 -TV-MA-LS,17 -TV-MA-LV,17 -TV-MA-SV,17 -TV-MA-LSV,17 -TV-X,18 -TV-AO,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/us.json b/Emby.Server.Implementations/Localization/Ratings/us.json new file mode 100644 index 0000000000..08a6373129 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/us.json @@ -0,0 +1,83 @@ +{ + "countryCode": "us", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Approved", "G", "TV-G", "TV-Y"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7-FV"], + "ratingScore": { + "score": 7, + "subScore": 1 + } + }, + { + "ratingStrings": ["PG", "TV-PG"], + "ratingScore": { + "score": 10, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-PG-D", "TV-PG-L", "TV-PG-S", "TV-PG-V", "TV-PG-DL", "TV-PG-DS", "TV-PG-DV", "TV-PG-LS", "TV-PG-LV", "TV-PG-SV", "TV-PG-DLS", "TV-PG-DLV", "TV-PG-DSV", "TV-PG-LSV", "TV-PG-DLSV"], + "ratingScore": { + "score": 10, + "subScore": 1 + } + }, + { + "ratingStrings": ["PG-13"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-14"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-14-D", "TV-14-L", "TV-14-S", "TV-14-V", "TV-14-DL", "TV-14-DS", "TV-14-DV", "TV-14-LS", "TV-14-LV", "TV-14-SV", "TV-14-DLS", "TV-14-DLV", "TV-14-DSV", "TV-14-LSV", "TV-14-DLSV"], + "ratingScore": { + "score": 14, + "subScore": 1 + } + }, + { + "ratingStrings": ["R"], + "ratingScore": { + "score": 17, + "subScore": 0 + } + }, + { + "ratingStrings": ["NC-17", "TV-MA", "TV-MA-L", "TV-MA-S", "TV-MA-V", "TV-MA-LS", "TV-MA-LV", "TV-MA-SV", "TV-MA-LSV"], + "ratingScore": { + "score": 17, + "subScore": 1 + } + }, + { + "ratingStrings": ["TV-X", "TV-AO"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/za.json b/Emby.Server.Implementations/Localization/Ratings/za.json new file mode 100644 index 0000000000..fe13af7978 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/za.json @@ -0,0 +1,55 @@ +{ + "countryCode": "za", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["A"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["PG", "7-9PG"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["10-12PG"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X18", "XX"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index 0a11b3e458..d92dc880b1 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -336,7 +336,7 @@ "TwoLetterISORegionName": "IE" }, { - "DisplayName": "Islamic Republic of Pakistan", + "DisplayName": "Pakistan", "Name": "PK", "ThreeLetterISORegionName": "PAK", "TwoLetterISORegionName": "PK" diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt index b55c0fa330..97da674819 100644 --- a/Emby.Server.Implementations/Localization/iso6392.txt +++ b/Emby.Server.Implementations/Localization/iso6392.txt @@ -10,7 +10,6 @@ afr||af|Afrikaans|afrikaans ain|||Ainu|aïnou aka||ak|Akan|akan akk|||Akkadian|akkadien -alb|sqi|sq|Albanian|albanais ale|||Aleut|aléoute alg|||Algonquian languages|algonquines, langues alt|||Southern Altai|altai du Sud @@ -21,7 +20,6 @@ apa|||Apache languages|apaches, langues ara||ar|Arabic|arabe arc|||Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)|araméen d'empire (700-300 BCE) arg||an|Aragonese|aragonais -arm|hye|hy|Armenian|arménien arn|||Mapudungun; Mapuche|mapudungun; mapuche; mapuce arp|||Arapaho|arapaho art|||Artificial languages|artificielles, langues @@ -41,7 +39,6 @@ bak||ba|Bashkir|bachkir bal|||Baluchi|baloutchi bam||bm|Bambara|bambara ban|||Balinese|balinais -baq|eus|eu|Basque|basque bas|||Basa|basa bat|||Baltic languages|baltes, langues bej|||Beja; Bedawiyet|bedja @@ -56,6 +53,7 @@ bin|||Bini; Edo|bini; edo bis||bi|Bislama|bichlamar bla|||Siksika|blackfoot bnt|||Bantu (Other)|bantoues, autres langues +bod|tib|bo|Tibetan|tibétain bos||bs|Bosnian|bosniaque bra|||Braj|braj bre||br|Breton|breton @@ -63,7 +61,6 @@ btk|||Batak languages|batak, langues bua|||Buriat|bouriate bug|||Buginese|bugi bul||bg|Bulgarian|bulgare -bur|mya|my|Burmese|birman byn|||Blin; Bilin|blin; bilen cad|||Caddo|caddo cai|||Central American Indian languages|amérindiennes de L'Amérique centrale, langues @@ -72,14 +69,11 @@ cat||ca|Catalan; Valencian|catalan; valencien cau|||Caucasian languages|caucasiennes, langues ceb|||Cebuano|cebuano cel|||Celtic languages|celtiques, langues; celtes, langues +ces|cze|cs|Czech|tchèque cha||ch|Chamorro|chamorro chb|||Chibcha|chibcha che||ce|Chechen|tchétchène chg|||Chagatai|djaghataï -chi|zho|zh|Chinese|chinois -chi|zho|ze|Chinese; Bilingual|chinois -chi|zho|zh-tw|Chinese; Traditional|chinois -chi|zho|zh-hk|Chinese; Hong Kong|chinois chk|||Chuukese|chuuk chm|||Mari|mari chn|||Chinook jargon|chinook, jargon @@ -101,13 +95,14 @@ crh|||Crimean Tatar; Crimean Turkish|tatar de Crimé crp|||Creoles and pidgins |créoles et pidgins csb|||Kashubian|kachoube cus|||Cushitic languages|couchitiques, langues -cze|ces|cs|Czech|tchèque +cym|wel|cy|Welsh|gallois dak|||Dakota|dakota dan||da|Danish|danois dar|||Dargwa|dargwa day|||Land Dayak languages|dayak, langues del|||Delaware|delaware den|||Slave (Athapascan)|esclave (athapascan) +deu|ger|de|German|allemand dgr|||Dogrib|dogrib din|||Dinka|dinka div||dv|Divehi; Dhivehi; Maldivian|maldivien @@ -116,28 +111,30 @@ dra|||Dravidian languages|dravidiennes, langues dsb|||Lower Sorbian|bas-sorabe dua|||Duala|douala dum|||Dutch, Middle (ca.1050-1350)|néerlandais moyen (ca. 1050-1350) -dut|nld|nl|Dutch; Flemish|néerlandais; flamand dyu|||Dyula|dioula dzo||dz|Dzongkha|dzongkha efi|||Efik|efik egy|||Egyptian (Ancient)|égyptien eka|||Ekajuk|ekajuk +ell|gre|el|Greek, Modern (1453-)|grec moderne (après 1453) elx|||Elamite|élamite eng||en|English|anglais enm|||English, Middle (1100-1500)|anglais moyen (1100-1500) epo||eo|Esperanto|espéranto est||et|Estonian|estonien +eus|baq|eu|Basque|basque ewe||ee|Ewe|éwé ewo|||Ewondo|éwondo fan|||Fang|fang fao||fo|Faroese|féroïen +fas|per|fa|Persian|persan fat|||Fanti|fanti fij||fj|Fijian|fidjien fil|||Filipino; Pilipino|filipino; pilipino fin||fi|Finnish|finnois fiu|||Finno-Ugrian languages|finno-ougriennes, langues fon|||Fon|fon -fre|fra|fr|French|français +fra|fre|fr|French|français frm|||French, Middle (ca.1400-1600)|français moyen (1400-1600) fro|||French, Old (842-ca.1400)|français ancien (842-ca.1400) frc||fr-ca|French (Canada)|french @@ -150,8 +147,6 @@ gaa|||Ga|ga gay|||Gayo|gayo gba|||Gbaya|gbaya gem|||Germanic languages|germaniques, langues -geo|kat|ka|Georgian|géorgien -ger|deu|de|German|allemand gez|||Geez|guèze gil|||Gilbertese|kiribati gla||gd|Gaelic; Scottish Gaelic|gaélique; gaélique écossais @@ -165,7 +160,6 @@ gor|||Gorontalo|gorontalo got|||Gothic|gothique grb|||Grebo|grebo grc|||Greek, Ancient (to 1453)|grec ancien (jusqu'à 1453) -gre|ell|el|Greek, Modern (1453-)|grec moderne (après 1453) grn||gn|Guarani|guarani gsw|||Swiss German; Alemannic; Alsatian|suisse alémanique; alémanique; alsacien guj||gu|Gujarati|goudjrati @@ -186,9 +180,10 @@ hrv||hr|Croatian|croate hsb|||Upper Sorbian|haut-sorabe hun||hu|Hungarian|hongrois hup|||Hupa|hupa +hye|arm|hy|Armenian|arménien iba|||Iban|iban ibo||ig|Igbo|igbo -ice|isl|is|Icelandic|islandais +isl|ice|is|Icelandic|islandais ido||io|Ido|ido iii||ii|Sichuan Yi; Nuosu|yi de Sichuan ijo|||Ijo languages|ijo, langues @@ -217,6 +212,7 @@ kam|||Kamba|kamba kan||kn|Kannada|kannada kar|||Karen languages|karen, langues kas||ks|Kashmiri|kashmiri +kat|geo|ka|Georgian|géorgien kau||kr|Kanuri|kanouri kaw|||Kawi|kawi kaz||kk|Kazakh|kazakh @@ -263,7 +259,6 @@ lui|||Luiseno|luiseno lun|||Lunda|lunda luo|||Luo (Kenya and Tanzania)|luo (Kenya et Tanzanie) lus|||Lushai|lushai -mac|mkd|mk|Macedonian|macédonien mad|||Madurese|madourais mag|||Magahi|magahi mah||mh|Marshallese|marshall @@ -271,11 +266,9 @@ mai|||Maithili|maithili mak|||Makasar|makassar mal||ml|Malayalam|malayalam man|||Mandingo|mandingue -mao|mri|mi|Maori|maori map|||Austronesian languages|austronésiennes, langues mar||mr|Marathi|marathe mas|||Masai|massaï -may|msa|ms|Malay|malais mdf|||Moksha|moksa mdr|||Mandar|mandar men|||Mende|mendé @@ -283,6 +276,7 @@ mga|||Irish, Middle (900-1200)|irlandais moyen (900-1200) mic|||Mi'kmaq; Micmac|mi'kmaq; micmac min|||Minangkabau|minangkabau mis|||Uncoded languages|langues non codées +mkd|mac|mk|Macedonian|macédonien mkh|||Mon-Khmer languages|môn-khmer, langues mlg||mg|Malagasy|malgache mlt||mt|Maltese|maltais @@ -292,11 +286,14 @@ mno|||Manobo languages|manobo, langues moh|||Mohawk|mohawk mon||mn|Mongolian|mongol mos|||Mossi|moré +mri|mao|mi|Maori|maori +msa|may|ms|Malay|malais mul|||Multiple languages|multilingue mun|||Munda languages|mounda, langues mus|||Creek|muskogee mwl|||Mirandese|mirandais mwr|||Marwari|marvari +mya|bur|my|Burmese|birman myn|||Mayan languages|maya, langues myv|||Erzya|erza nah|||Nahuatl languages|nahuatl, langues @@ -313,6 +310,7 @@ new|||Nepal Bhasa; Newari|nepal bhasa; newari nia|||Nias|nias nic|||Niger-Kordofanian languages|nigéro-kordofaniennes, langues niu|||Niuean|niué +nld|dut|nl|Dutch; Flemish|néerlandais; flamand nno||nn|Norwegian Nynorsk; Nynorsk, Norwegian|norvégien nynorsk; nynorsk, norvégien nob||nb|Bokmål, Norwegian; Norwegian Bokmål|norvégien bokmål nog|||Nogai|nogaï; nogay @@ -343,15 +341,14 @@ pan||pa|Panjabi; Punjabi|pendjabi pap|||Papiamento|papiamento pau|||Palauan|palau peo|||Persian, Old (ca.600-400 B.C.)|perse, vieux (ca. 600-400 av. J.-C.) -per|fas|fa|Persian|persan phi|||Philippine languages|philippines, langues phn|||Phoenician|phénicien pli||pi|Pali|pali pol||pl|Polish|polonais pon|||Pohnpeian|pohnpei por||pt|Portuguese|portugais -pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt) -pob||pt-br|Portuguese (Brazil)|portugais (pt-br) +por||pt-pt|Portuguese (Portugal)|portugais (pt-pt) +por||pt-br|Portuguese (Brazil)|portugais (pt-br) pra|||Prakrit languages|prâkrit, langues pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500) pus||ps|Pushto; Pashto|pachto @@ -363,7 +360,7 @@ rar|||Rarotongan; Cook Islands Maori|rarotonga; maori des îles Cook roa|||Romance languages|romanes, langues roh||rm|Romansh|romanche rom|||Romany|tsigane -rum|ron|ro|Romanian; Moldavian; Moldovan|roumain; moldave +ron|rum|ro|Romanian; Moldavian; Moldovan|roumain; moldave run||rn|Rundi|rundi rup|||Aromanian; Arumanian; Macedo-Romanian|aroumain; macédo-roumain rus||ru|Russian|russe @@ -376,6 +373,7 @@ sam|||Samaritan Aramaic|samaritain san||sa|Sanskrit|sanskrit sas|||Sasak|sasak sat|||Santali|santal +srp||sr|Serbian|serbe scn|||Sicilian|sicilien sco|||Scots|écossais sel|||Selkup|selkoupe @@ -388,7 +386,7 @@ sin||si|Sinhala; Sinhalese|singhalais sio|||Siouan languages|sioux, langues sit|||Sino-Tibetan languages|sino-tibétaines, langues sla|||Slavic languages|slaves, langues -slo|slk|sk|Slovak|slovaque +slk|slo|sk|Slovak|slovaque slv||sl|Slovenian|slovène sma|||Southern Sami|sami du Sud sme||se|Northern Sami|sami du Nord @@ -406,9 +404,9 @@ son|||Songhai languages|songhai, langues sot||st|Sotho, Southern|sotho du Sud spa||es-mx|Spanish; Latin|espagnol; Latin spa||es|Spanish; Castilian|espagnol; castillan +sqi|alb|sq|Albanian|albanais srd||sc|Sardinian|sarde srn|||Sranan Tongo|sranan tongo -srp|scc|sr|Serbian|serbe srr|||Serer|sérère ssa|||Nilo-Saharan languages|nilo-sahariennes, langues ssw||ss|Swati|swati @@ -431,7 +429,6 @@ tet|||Tetum|tetum tgk||tg|Tajik|tadjik tgl||tl|Tagalog|tagalog tha||th|Thai|thaï -tib|bod|bo|Tibetan|tibétain tig|||Tigre|tigré tir||ti|Tigrinya|tigrigna tiv|||Tiv|tiv @@ -470,7 +467,6 @@ wak|||Wakashan languages|wakashanes, langues wal|||Walamo|walamo war|||Waray|waray was|||Washo|washo -wel|cym|cy|Welsh|gallois wen|||Sorbian languages|sorabes, langues wln||wa|Walloon|wallon wol||wo|Wolof|wolof @@ -486,6 +482,10 @@ zbl|||Blissymbols; Blissymbolics; Bliss|symboles Bliss; Bliss zen|||Zenaga|zenaga zgh|||Standard Moroccan Tamazight|amazighe standard marocain zha||za|Zhuang; Chuang|zhuang; chuang +zho|chi|zh|Chinese|chinois +zho|chi|ze|Chinese; Bilingual|chinois +zho|chi|zh-tw|Chinese; Traditional|chinois +zho|chi|zh-hk|Chinese; Hong Kong|chinois znd|||Zande languages|zandé, langues zul||zu|Zulu|zoulou zun|||Zuni|zuni diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs deleted file mode 100644 index ea78968617..0000000000 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ /dev/null @@ -1,272 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Extensions; -using MediaBrowser.Controller.Chapters; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.MediaEncoder -{ - public class EncodingManager : IEncodingManager - { - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; - private readonly IMediaEncoder _encoder; - private readonly IChapterRepository _chapterManager; - private readonly ILibraryManager _libraryManager; - - /// - /// The first chapter ticks. - /// - private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks; - - public EncodingManager( - ILogger logger, - IFileSystem fileSystem, - IMediaEncoder encoder, - IChapterRepository chapterManager, - ILibraryManager libraryManager) - { - _logger = logger; - _fileSystem = fileSystem; - _encoder = encoder; - _chapterManager = chapterManager; - _libraryManager = libraryManager; - } - - /// - /// Gets the chapter images data path. - /// - /// The chapter images data path. - private static string GetChapterImagesPath(BaseItem item) - { - return Path.Combine(item.GetInternalMetadataPath(), "chapters"); - } - - /// - /// Determines whether [is eligible for chapter image extraction] [the specified video]. - /// - /// The video. - /// The library options for the video. - /// true if [is eligible for chapter image extraction] [the specified video]; otherwise, false. - private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions) - { - if (video.IsPlaceHolder) - { - return false; - } - - if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction) - { - return false; - } - - if (video.IsShortcut) - { - return false; - } - - if (!video.IsCompleteMedia) - { - return false; - } - - // Can't extract images if there are no video streams - return video.DefaultVideoStreamIndex.HasValue; - } - - private long GetAverageDurationBetweenChapters(IReadOnlyList chapters) - { - if (chapters.Count < 2) - { - return 0; - } - - long sum = 0; - for (int i = 1; i < chapters.Count; i++) - { - sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks; - } - - return sum / chapters.Count; - } - - public async Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) - { - if (chapters.Count == 0) - { - return true; - } - - var libraryOptions = _libraryManager.GetLibraryOptions(video); - - if (!IsEligibleForChapterImageExtraction(video, libraryOptions)) - { - extractImages = false; - } - - var averageChapterDuration = GetAverageDurationBetweenChapters(chapters); - var threshold = TimeSpan.FromSeconds(1).Ticks; - if (averageChapterDuration < threshold) - { - _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold); - extractImages = false; - } - - var success = true; - var changesMade = false; - - var runtimeTicks = video.RunTimeTicks ?? 0; - - var currentImages = GetSavedChapterImages(video, directoryService); - - foreach (var chapter in chapters) - { - if (chapter.StartPositionTicks >= runtimeTicks) - { - _logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name); - break; - } - - var path = GetChapterImagePath(video, chapter.StartPositionTicks); - - if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase)) - { - if (extractImages) - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - // Add some time for the first chapter to make sure we don't end up with a black image - var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); - - var inputPath = video.Path; - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - var container = video.Container; - var mediaSource = new MediaSourceInfo - { - VideoType = video.VideoType, - IsoType = video.IsoType, - Protocol = video.PathProtocol.Value, - }; - - var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); - File.Copy(tempFile, path, true); - - try - { - _fileSystem.DeleteFile(tempFile); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile); - } - - chapter.ImagePath = path; - chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); - changesMade = true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path)); - success = false; - break; - } - } - else if (!string.IsNullOrEmpty(chapter.ImagePath)) - { - chapter.ImagePath = null; - changesMade = true; - } - } - else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase)) - { - chapter.ImagePath = path; - chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); - changesMade = true; - } - else if (libraryOptions?.EnableChapterImageExtraction != true) - { - // We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image - chapter.ImagePath = null; - changesMade = true; - } - } - - if (saveChapters && changesMade) - { - _chapterManager.SaveChapters(video.Id, chapters); - } - - DeleteDeadImages(currentImages, chapters); - - return success; - } - - private string GetChapterImagePath(Video video, long chapterPositionTicks) - { - var filename = video.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg"; - - return Path.Combine(GetChapterImagesPath(video), filename); - } - - private static IReadOnlyList GetSavedChapterImages(Video video, IDirectoryService directoryService) - { - var path = GetChapterImagesPath(video); - if (!Directory.Exists(path)) - { - return Array.Empty(); - } - - try - { - return directoryService.GetFilePaths(path); - } - catch (IOException) - { - return Array.Empty(); - } - } - - private void DeleteDeadImages(IEnumerable images, IEnumerable chapters) - { - var deadImages = images - .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase) - .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase)) - .ToList(); - - foreach (var image in deadImages) - { - _logger.LogDebug("Deleting dead chapter image {Path}", image); - - try - { - _fileSystem.DeleteFile(image); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting {Path}.", image); - } - } - } - } -} diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index daeb7fed88..1ce363de5c 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -9,8 +9,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -134,14 +134,16 @@ namespace Emby.Server.Implementations.Playlists try { - Directory.CreateDirectory(path); + var info = Directory.CreateDirectory(path); var playlist = new Playlist { Name = name, Path = path, OwnerUserId = request.UserId, Shares = request.Users ?? [], - OpenAccess = request.Public ?? false + OpenAccess = request.Public ?? false, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc }; playlist.SetMediaType(request.MediaType); @@ -283,6 +285,16 @@ namespace Emby.Server.Implementations.Playlists RefreshPriority.High); } + internal static int DetermineAdjustedIndex(int newPriorIndexAllChildren, int newIndex) + { + if (newIndex == 0) + { + return newPriorIndexAllChildren > 0 ? newPriorIndexAllChildren - 1 : 0; + } + + return newPriorIndexAllChildren + 1; + } + public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId) { if (_libraryManager.GetItemById(playlistId) is not Playlist playlist) @@ -305,12 +317,12 @@ namespace Emby.Server.Implementations.Playlists var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1; var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId; var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId)); - var adjustedNewIndex = newPriorItemIndexOnAllChildren + 1; + var adjustedNewIndex = DetermineAdjustedIndex(newPriorItemIndexOnAllChildren, newIndex); var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); if (item is null) { - _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId); + _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", entryId, playlistId); return; } diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index db3aeaaf31..a5be2b616e 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Playlists; diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 8eeca3667e..91ccb16ef9 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.Plugins Overview = packageInfo.Overview, Owner = packageInfo.Owner, TargetAbi = versionInfo.TargetAbi ?? string.Empty, - Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture), + Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal), Version = versionInfo.Version, Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state. AutoUpdate = true, diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 985f0a8f85..24f554981a 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -16,663 +16,662 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks +namespace Emby.Server.Implementations.ScheduledTasks; + +/// +/// Class ScheduledTaskWorker. +/// +public class ScheduledTaskWorker : IScheduledTaskWorker { + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private readonly IApplicationPaths _applicationPaths; + private readonly ILogger _logger; + private readonly ITaskManager _taskManager; + private readonly Lock _lastExecutionResultSyncLock = new(); + private bool _readFromFile; + private TaskResult _lastExecutionResult; + private Task _currentTask; + private Tuple[] _triggers; + private string _id; + /// - /// Class ScheduledTaskWorker. + /// Initializes a new instance of the class. /// - public class ScheduledTaskWorker : IScheduledTaskWorker + /// The scheduled task. + /// The application paths. + /// The task manager. + /// The logger. + /// + /// scheduledTask + /// or + /// applicationPaths + /// or + /// taskManager + /// or + /// jsonSerializer + /// or + /// logger. + /// + public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger) { - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private readonly IApplicationPaths _applicationPaths; - private readonly ILogger _logger; - private readonly ITaskManager _taskManager; - private readonly Lock _lastExecutionResultSyncLock = new(); - private bool _readFromFile; - private TaskResult _lastExecutionResult; - private Task _currentTask; - private Tuple[] _triggers; - private string _id; + ArgumentNullException.ThrowIfNull(scheduledTask); + ArgumentNullException.ThrowIfNull(applicationPaths); + ArgumentNullException.ThrowIfNull(taskManager); + ArgumentNullException.ThrowIfNull(logger); - /// - /// Initializes a new instance of the class. - /// - /// The scheduled task. - /// The application paths. - /// The task manager. - /// The logger. - /// - /// scheduledTask - /// or - /// applicationPaths - /// or - /// taskManager - /// or - /// jsonSerializer - /// or - /// logger. - /// - public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger) + ScheduledTask = scheduledTask; + _applicationPaths = applicationPaths; + _taskManager = taskManager; + _logger = logger; + + InitTriggerEvents(); + } + + /// + public event EventHandler> TaskProgress; + + /// + public IScheduledTask ScheduledTask { get; private set; } + + /// + public TaskResult LastExecutionResult + { + get { - ArgumentNullException.ThrowIfNull(scheduledTask); - ArgumentNullException.ThrowIfNull(applicationPaths); - ArgumentNullException.ThrowIfNull(taskManager); - ArgumentNullException.ThrowIfNull(logger); + var path = GetHistoryFilePath(); - ScheduledTask = scheduledTask; - _applicationPaths = applicationPaths; - _taskManager = taskManager; - _logger = logger; - - InitTriggerEvents(); - } - - /// - public event EventHandler> TaskProgress; - - /// - public IScheduledTask ScheduledTask { get; private set; } - - /// - public TaskResult LastExecutionResult - { - get + lock (_lastExecutionResultSyncLock) { - var path = GetHistoryFilePath(); - - lock (_lastExecutionResultSyncLock) + if (_lastExecutionResult is null && !_readFromFile) { - if (_lastExecutionResult is null && !_readFromFile) + if (File.Exists(path)) { - if (File.Exists(path)) + var bytes = File.ReadAllBytes(path); + if (bytes.Length > 0) { - var bytes = File.ReadAllBytes(path); - if (bytes.Length > 0) + try { - try - { - _lastExecutionResult = JsonSerializer.Deserialize(bytes, _jsonOptions); - } - catch (JsonException ex) - { - _logger.LogError(ex, "Error deserializing {File}", path); - } + _lastExecutionResult = JsonSerializer.Deserialize(bytes, _jsonOptions); } - else + catch (JsonException ex) { - _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path); + _logger.LogError(ex, "Error deserializing {File}", path); } } - - _readFromFile = true; - } - } - - return _lastExecutionResult; - } - - private set - { - _lastExecutionResult = value; - - var path = GetHistoryFilePath(); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_lastExecutionResultSyncLock) - { - using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream); - JsonSerializer.Serialize(jsonStream, value, _jsonOptions); - } - } - } - - /// - public string Name => ScheduledTask.Name; - - /// - public string Description => ScheduledTask.Description; - - /// - public string Category => ScheduledTask.Category; - - /// - /// Gets or sets the current cancellation token. - /// - /// The current cancellation token source. - private CancellationTokenSource CurrentCancellationTokenSource { get; set; } - - /// - /// Gets or sets the current execution start time. - /// - /// The current execution start time. - private DateTime CurrentExecutionStartTime { get; set; } - - /// - public TaskState State - { - get - { - if (CurrentCancellationTokenSource is not null) - { - return CurrentCancellationTokenSource.IsCancellationRequested - ? TaskState.Cancelling - : TaskState.Running; - } - - return TaskState.Idle; - } - } - - /// - public double? CurrentProgress { get; private set; } - - /// - /// Gets or sets the triggers that define when the task will run. - /// - /// The triggers. - private Tuple[] InternalTriggers - { - get => _triggers; - set - { - ArgumentNullException.ThrowIfNull(value); - - // Cleanup current triggers - if (_triggers is not null) - { - DisposeTriggers(); - } - - _triggers = value.ToArray(); - - ReloadTriggerEvents(false); - } - } - - /// - public IReadOnlyList Triggers - { - get - { - return Array.ConvertAll(InternalTriggers, i => i.Item1); - } - - set - { - ArgumentNullException.ThrowIfNull(value); - - // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly - var triggerList = value.Where(i => i is not null).ToArray(); - - SaveTriggers(triggerList); - - InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple(i, GetTrigger(i))); - } - } - - /// - public string Id - { - get - { - return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - } - - private void InitTriggerEvents() - { - _triggers = LoadTriggers(); - ReloadTriggerEvents(true); - } - - /// - public void ReloadTriggerEvents() - { - ReloadTriggerEvents(false); - } - - /// - /// Reloads the trigger events. - /// - /// if set to true [is application startup]. - private void ReloadTriggerEvents(bool isApplicationStartup) - { - foreach (var triggerInfo in InternalTriggers) - { - var trigger = triggerInfo.Item2; - - trigger.Stop(); - - trigger.Triggered -= OnTriggerTriggered; - trigger.Triggered += OnTriggerTriggered; - trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup); - } - } - - /// - /// Handles the Triggered event of the trigger control. - /// - /// The source of the event. - /// The instance containing the event data. - private async void OnTriggerTriggered(object sender, EventArgs e) - { - var trigger = (ITaskTrigger)sender; - - if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled) - { - return; - } - - _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name); - - trigger.Stop(); - - _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); - - await Task.Delay(1000).ConfigureAwait(false); - - trigger.Start(LastExecutionResult, _logger, Name, false); - } - - /// - /// Executes the task. - /// - /// Task options. - /// Task. - /// Cannot execute a Task that is already running. - public async Task Execute(TaskOptions options) - { - var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false)); - - _currentTask = task; - - try - { - await task.ConfigureAwait(false); - } - finally - { - _currentTask = null; - GC.Collect(); - } - } - - private async Task ExecuteInternal(TaskOptions options) - { - // Cancel the current execution, if any - if (CurrentCancellationTokenSource is not null) - { - throw new InvalidOperationException("Cannot execute a Task that is already running"); - } - - var progress = new Progress(); - - CurrentCancellationTokenSource = new CancellationTokenSource(); - - _logger.LogDebug("Executing {0}", Name); - - ((TaskManager)_taskManager).OnTaskExecuting(this); - - progress.ProgressChanged += OnProgressChanged; - - TaskCompletionStatus status; - CurrentExecutionStartTime = DateTime.UtcNow; - - Exception failureException = null; - - try - { - if (options is not null && options.MaxRuntimeTicks.HasValue) - { - CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value)); - } - - await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false); - - status = TaskCompletionStatus.Completed; - } - catch (OperationCanceledException) - { - status = TaskCompletionStatus.Cancelled; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error executing Scheduled Task"); - - failureException = ex; - - status = TaskCompletionStatus.Failed; - } - - var startTime = CurrentExecutionStartTime; - var endTime = DateTime.UtcNow; - - progress.ProgressChanged -= OnProgressChanged; - CurrentCancellationTokenSource.Dispose(); - CurrentCancellationTokenSource = null; - CurrentProgress = null; - - OnTaskCompleted(startTime, endTime, status, failureException); - } - - /// - /// Progress_s the progress changed. - /// - /// The sender. - /// The e. - private void OnProgressChanged(object sender, double e) - { - e = Math.Min(e, 100); - - CurrentProgress = e; - - TaskProgress?.Invoke(this, new GenericEventArgs(e)); - } - - /// - /// Stops the task if it is currently executing. - /// - /// Cannot cancel a Task unless it is in the Running state. - public void Cancel() - { - if (State != TaskState.Running) - { - throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state."); - } - - CancelIfRunning(); - } - - /// - /// Cancels if running. - /// - public void CancelIfRunning() - { - if (State == TaskState.Running) - { - _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name); - CurrentCancellationTokenSource.Cancel(); - } - } - - /// - /// Gets the scheduled tasks configuration directory. - /// - /// System.String. - private string GetScheduledTasksConfigurationDirectory() - { - return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); - } - - /// - /// Gets the scheduled tasks data directory. - /// - /// System.String. - private string GetScheduledTasksDataDirectory() - { - return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"); - } - - /// - /// Gets the history file path. - /// - /// The history file path. - private string GetHistoryFilePath() - { - return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js"); - } - - /// - /// Gets the configuration file path. - /// - /// System.String. - private string GetConfigurationFilePath() - { - return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js"); - } - - /// - /// Loads the triggers. - /// - /// IEnumerable{BaseTaskTrigger}. - private Tuple[] LoadTriggers() - { - // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly - var settings = LoadTriggerSettings().Where(i => i is not null); - - return settings.Select(i => new Tuple(i, GetTrigger(i))).ToArray(); - } - - private TaskTriggerInfo[] LoadTriggerSettings() - { - string path = GetConfigurationFilePath(); - TaskTriggerInfo[] list = null; - if (File.Exists(path)) - { - var bytes = File.ReadAllBytes(path); - list = JsonSerializer.Deserialize(bytes, _jsonOptions); - } - - // Return defaults if file doesn't exist. - return list ?? GetDefaultTriggers(); - } - - private TaskTriggerInfo[] GetDefaultTriggers() - { - try - { - return ScheduledTask.GetDefaultTriggers().ToArray(); - } - catch - { - return - [ - new() - { - IntervalTicks = TimeSpan.FromDays(1).Ticks, - Type = TaskTriggerInfoType.IntervalTrigger - } - ]; - } - } - - /// - /// Saves the triggers. - /// - /// The triggers. - private void SaveTriggers(TaskTriggerInfo[] triggers) - { - var path = GetConfigurationFilePath(); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream); - JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions); - } - - /// - /// Called when [task completed]. - /// - /// The start time. - /// The end time. - /// The status. - /// The exception. - private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex) - { - var elapsedTime = endTime - startTime; - - _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); - - var result = new TaskResult - { - StartTimeUtc = startTime, - EndTimeUtc = endTime, - Status = status, - Name = Name, - Id = Id - }; - - result.Key = ScheduledTask.Key; - - if (ex is not null) - { - result.ErrorMessage = ex.Message; - result.LongErrorMessage = ex.StackTrace; - } - - LastExecutionResult = result; - - ((TaskManager)_taskManager).OnTaskCompleted(this, result); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - DisposeTriggers(); - - var wasRunning = State == TaskState.Running; - var startTime = CurrentExecutionStartTime; - - var token = CurrentCancellationTokenSource; - if (token is not null) - { - try - { - _logger.LogInformation("{Name}: Cancelling", Name); - token.Cancel(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error calling CancellationToken.Cancel();"); - } - } - - var task = _currentTask; - if (task is not null) - { - try - { - _logger.LogInformation("{Name}: Waiting on Task", Name); - var exited = task.Wait(2000); - - if (exited) - { - _logger.LogInformation("{Name}: Task exited", Name); - } else { - _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name); + _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path); } } - catch (Exception ex) - { - _logger.LogError(ex, "Error calling Task.WaitAll();"); - } - } - if (token is not null) - { - try - { - _logger.LogDebug("{Name}: Disposing CancellationToken", Name); - token.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error calling CancellationToken.Dispose();"); - } - } - - if (wasRunning) - { - OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null); + _readFromFile = true; } } + + return _lastExecutionResult; } - /// - /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger. - /// - /// The info. - /// BaseTaskTrigger. - /// Invalid trigger type: + info.Type. - private ITaskTrigger GetTrigger(TaskTriggerInfo info) + private set { - var options = new TaskOptions - { - MaxRuntimeTicks = info.MaxRuntimeTicks - }; + _lastExecutionResult = value; - if (info.Type == TaskTriggerInfoType.DailyTrigger) - { - if (!info.TimeOfDayTicks.HasValue) - { - throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); - } + var path = GetHistoryFilePath(); + Directory.CreateDirectory(Path.GetDirectoryName(path)); - return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options); + lock (_lastExecutionResultSyncLock) + { + using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream); + JsonSerializer.Serialize(jsonStream, value, _jsonOptions); + } + } + } + + /// + public string Name => ScheduledTask.Name; + + /// + public string Description => ScheduledTask.Description; + + /// + public string Category => ScheduledTask.Category; + + /// + /// Gets or sets the current cancellation token. + /// + /// The current cancellation token source. + private CancellationTokenSource CurrentCancellationTokenSource { get; set; } + + /// + /// Gets or sets the current execution start time. + /// + /// The current execution start time. + private DateTime CurrentExecutionStartTime { get; set; } + + /// + public TaskState State + { + get + { + if (CurrentCancellationTokenSource is not null) + { + return CurrentCancellationTokenSource.IsCancellationRequested + ? TaskState.Cancelling + : TaskState.Running; } - if (info.Type == TaskTriggerInfoType.WeeklyTrigger) + return TaskState.Idle; + } + } + + /// + public double? CurrentProgress { get; private set; } + + /// + /// Gets or sets the triggers that define when the task will run. + /// + /// The triggers. + private Tuple[] InternalTriggers + { + get => _triggers; + set + { + ArgumentNullException.ThrowIfNull(value); + + // Cleanup current triggers + if (_triggers is not null) { - if (!info.TimeOfDayTicks.HasValue) - { - throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); - } - - if (!info.DayOfWeek.HasValue) - { - throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info)); - } - - return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options); + DisposeTriggers(); } - if (info.Type == TaskTriggerInfoType.IntervalTrigger) - { - if (!info.IntervalTicks.HasValue) - { - throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info)); - } + _triggers = value.ToArray(); - return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options); - } + ReloadTriggerEvents(false); + } + } - if (info.Type == TaskTriggerInfoType.StartupTrigger) - { - return new StartupTrigger(options); - } - - throw new ArgumentException("Unrecognized trigger type: " + info.Type); + /// + public IReadOnlyList Triggers + { + get + { + return Array.ConvertAll(InternalTriggers, i => i.Item1); } - /// - /// Disposes each trigger. - /// - private void DisposeTriggers() + set { - foreach (var triggerInfo in InternalTriggers) + ArgumentNullException.ThrowIfNull(value); + + // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly + var triggerList = value.Where(i => i is not null).ToArray(); + + SaveTriggers(triggerList); + + InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple(i, GetTrigger(i))); + } + } + + /// + public string Id + { + get + { + return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture); + } + } + + private void InitTriggerEvents() + { + _triggers = LoadTriggers(); + ReloadTriggerEvents(true); + } + + /// + public void ReloadTriggerEvents() + { + ReloadTriggerEvents(false); + } + + /// + /// Reloads the trigger events. + /// + /// if set to true [is application startup]. + private void ReloadTriggerEvents(bool isApplicationStartup) + { + foreach (var triggerInfo in InternalTriggers) + { + var trigger = triggerInfo.Item2; + + trigger.Stop(); + + trigger.Triggered -= OnTriggerTriggered; + trigger.Triggered += OnTriggerTriggered; + trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup); + } + } + + /// + /// Handles the Triggered event of the trigger control. + /// + /// The source of the event. + /// The instance containing the event data. + private async void OnTriggerTriggered(object sender, EventArgs e) + { + var trigger = (ITaskTrigger)sender; + + if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled) + { + return; + } + + _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name); + + trigger.Stop(); + + _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); + + await Task.Delay(1000).ConfigureAwait(false); + + trigger.Start(LastExecutionResult, _logger, Name, false); + } + + /// + /// Executes the task. + /// + /// Task options. + /// Task. + /// Cannot execute a Task that is already running. + public async Task Execute(TaskOptions options) + { + var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false)); + + _currentTask = task; + + try + { + await task.ConfigureAwait(false); + } + finally + { + _currentTask = null; + GC.Collect(); + } + } + + private async Task ExecuteInternal(TaskOptions options) + { + // Cancel the current execution, if any + if (CurrentCancellationTokenSource is not null) + { + throw new InvalidOperationException("Cannot execute a Task that is already running"); + } + + var progress = new Progress(); + + CurrentCancellationTokenSource = new CancellationTokenSource(); + + _logger.LogDebug("Executing {0}", Name); + + ((TaskManager)_taskManager).OnTaskExecuting(this); + + progress.ProgressChanged += OnProgressChanged; + + TaskCompletionStatus status; + CurrentExecutionStartTime = DateTime.UtcNow; + + Exception failureException = null; + + try + { + if (options is not null && options.MaxRuntimeTicks.HasValue) { - var trigger = triggerInfo.Item2; - trigger.Triggered -= OnTriggerTriggered; - trigger.Stop(); - if (trigger is IDisposable disposable) + CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value)); + } + + await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false); + + status = TaskCompletionStatus.Completed; + } + catch (OperationCanceledException) + { + status = TaskCompletionStatus.Cancelled; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing Scheduled Task"); + + failureException = ex; + + status = TaskCompletionStatus.Failed; + } + + var startTime = CurrentExecutionStartTime; + var endTime = DateTime.UtcNow; + + progress.ProgressChanged -= OnProgressChanged; + CurrentCancellationTokenSource.Dispose(); + CurrentCancellationTokenSource = null; + CurrentProgress = null; + + OnTaskCompleted(startTime, endTime, status, failureException); + } + + /// + /// Progress_s the progress changed. + /// + /// The sender. + /// The e. + private void OnProgressChanged(object sender, double e) + { + e = Math.Min(e, 100); + + CurrentProgress = e; + + TaskProgress?.Invoke(this, new GenericEventArgs(e)); + } + + /// + /// Stops the task if it is currently executing. + /// + /// Cannot cancel a Task unless it is in the Running state. + public void Cancel() + { + if (State != TaskState.Running) + { + throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state."); + } + + CancelIfRunning(); + } + + /// + /// Cancels if running. + /// + public void CancelIfRunning() + { + if (State == TaskState.Running) + { + _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name); + CurrentCancellationTokenSource.Cancel(); + } + } + + /// + /// Gets the scheduled tasks configuration directory. + /// + /// System.String. + private string GetScheduledTasksConfigurationDirectory() + { + return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); + } + + /// + /// Gets the scheduled tasks data directory. + /// + /// System.String. + private string GetScheduledTasksDataDirectory() + { + return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"); + } + + /// + /// Gets the history file path. + /// + /// The history file path. + private string GetHistoryFilePath() + { + return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js"); + } + + /// + /// Gets the configuration file path. + /// + /// System.String. + private string GetConfigurationFilePath() + { + return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js"); + } + + /// + /// Loads the triggers. + /// + /// IEnumerable{BaseTaskTrigger}. + private Tuple[] LoadTriggers() + { + // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly + var settings = LoadTriggerSettings().Where(i => i is not null); + + return settings.Select(i => new Tuple(i, GetTrigger(i))).ToArray(); + } + + private TaskTriggerInfo[] LoadTriggerSettings() + { + string path = GetConfigurationFilePath(); + TaskTriggerInfo[] list = null; + if (File.Exists(path)) + { + var bytes = File.ReadAllBytes(path); + list = JsonSerializer.Deserialize(bytes, _jsonOptions); + } + + // Return defaults if file doesn't exist. + return list ?? GetDefaultTriggers(); + } + + private TaskTriggerInfo[] GetDefaultTriggers() + { + try + { + return ScheduledTask.GetDefaultTriggers().ToArray(); + } + catch + { + return + [ + new() { - disposable.Dispose(); + IntervalTicks = TimeSpan.FromDays(1).Ticks, + Type = TaskTriggerInfoType.IntervalTrigger } + ]; + } + } + + /// + /// Saves the triggers. + /// + /// The triggers. + private void SaveTriggers(TaskTriggerInfo[] triggers) + { + var path = GetConfigurationFilePath(); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream); + JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions); + } + + /// + /// Called when [task completed]. + /// + /// The start time. + /// The end time. + /// The status. + /// The exception. + private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex) + { + var elapsedTime = endTime - startTime; + + _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); + + var result = new TaskResult + { + StartTimeUtc = startTime, + EndTimeUtc = endTime, + Status = status, + Name = Name, + Id = Id + }; + + result.Key = ScheduledTask.Key; + + if (ex is not null) + { + result.ErrorMessage = ex.Message; + result.LongErrorMessage = ex.StackTrace; + } + + LastExecutionResult = result; + + ((TaskManager)_taskManager).OnTaskCompleted(this, result); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + DisposeTriggers(); + + var wasRunning = State == TaskState.Running; + var startTime = CurrentExecutionStartTime; + + var token = CurrentCancellationTokenSource; + if (token is not null) + { + try + { + _logger.LogInformation("{Name}: Cancelling", Name); + token.Cancel(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling CancellationToken.Cancel();"); + } + } + + var task = _currentTask; + if (task is not null) + { + try + { + _logger.LogInformation("{Name}: Waiting on Task", Name); + var exited = task.Wait(2000); + + if (exited) + { + _logger.LogInformation("{Name}: Task exited", Name); + } + else + { + _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling Task.WaitAll();"); + } + } + + if (token is not null) + { + try + { + _logger.LogDebug("{Name}: Disposing CancellationToken", Name); + token.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling CancellationToken.Dispose();"); + } + } + + if (wasRunning) + { + OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null); + } + } + } + + /// + /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger. + /// + /// The info. + /// BaseTaskTrigger. + /// Invalid trigger type: + info.Type. + private ITaskTrigger GetTrigger(TaskTriggerInfo info) + { + var options = new TaskOptions + { + MaxRuntimeTicks = info.MaxRuntimeTicks + }; + + if (info.Type == TaskTriggerInfoType.DailyTrigger) + { + if (!info.TimeOfDayTicks.HasValue) + { + throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); + } + + return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options); + } + + if (info.Type == TaskTriggerInfoType.WeeklyTrigger) + { + if (!info.TimeOfDayTicks.HasValue) + { + throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); + } + + if (!info.DayOfWeek.HasValue) + { + throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info)); + } + + return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options); + } + + if (info.Type == TaskTriggerInfoType.IntervalTrigger) + { + if (!info.IntervalTicks.HasValue) + { + throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info)); + } + + return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options); + } + + if (info.Type == TaskTriggerInfoType.StartupTrigger) + { + return new StartupTrigger(options); + } + + throw new ArgumentException("Unrecognized trigger type: " + info.Type); + } + + /// + /// Disposes each trigger. + /// + private void DisposeTriggers() + { + foreach (var triggerInfo in InternalTriggers) + { + var trigger = triggerInfo.Item2; + trigger.Triggered -= OnTriggerTriggered; + trigger.Stop(); + if (trigger is IDisposable disposable) + { + disposable.Dispose(); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index a5e4104ffe..4ec2c9c786 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -8,255 +8,254 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks +namespace Emby.Server.Implementations.ScheduledTasks; + +/// +/// Class TaskManager. +/// +public class TaskManager : ITaskManager { /// - /// Class TaskManager. + /// The _task queue. /// - public class TaskManager : ITaskManager + private readonly ConcurrentQueue> _taskQueue = + new ConcurrentQueue>(); + + private readonly IApplicationPaths _applicationPaths; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The application paths. + /// The logger. + public TaskManager( + IApplicationPaths applicationPaths, + ILogger logger) { - /// - /// The _task queue. - /// - private readonly ConcurrentQueue> _taskQueue = - new ConcurrentQueue>(); + _applicationPaths = applicationPaths; + _logger = logger; - private readonly IApplicationPaths _applicationPaths; - private readonly ILogger _logger; + ScheduledTasks = []; + } - /// - /// Initializes a new instance of the class. - /// - /// The application paths. - /// The logger. - public TaskManager( - IApplicationPaths applicationPaths, - ILogger logger) - { - _applicationPaths = applicationPaths; - _logger = logger; + /// + public event EventHandler>? TaskExecuting; - ScheduledTasks = Array.Empty(); - } + /// + public event EventHandler? TaskCompleted; - /// - public event EventHandler>? TaskExecuting; + /// + public IReadOnlyList ScheduledTasks { get; private set; } - /// - public event EventHandler? TaskCompleted; + /// + public void CancelIfRunningAndQueue(TaskOptions options) + where T : IScheduledTask + { + var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); + ((ScheduledTaskWorker)task).CancelIfRunning(); - /// - public IReadOnlyList ScheduledTasks { get; private set; } + QueueScheduledTask(options); + } - /// - public void CancelIfRunningAndQueue(TaskOptions options) + /// + public void CancelIfRunningAndQueue() where T : IScheduledTask + { + CancelIfRunningAndQueue(new TaskOptions()); + } + + /// + public void CancelIfRunning() + where T : IScheduledTask + { + var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); + ((ScheduledTaskWorker)task).CancelIfRunning(); + } + + /// + public void QueueScheduledTask(TaskOptions options) + where T : IScheduledTask + { + var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + + if (scheduledTask is null) { - var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - ((ScheduledTaskWorker)task).CancelIfRunning(); - - QueueScheduledTask(options); + _logger.LogError("Unable to find scheduled task of type {Type} in QueueScheduledTask.", typeof(T).Name); } - - /// - public void CancelIfRunningAndQueue() - where T : IScheduledTask + else { - CancelIfRunningAndQueue(new TaskOptions()); + QueueScheduledTask(scheduledTask, options); } + } - /// - public void CancelIfRunning() - where T : IScheduledTask - { - var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - ((ScheduledTaskWorker)task).CancelIfRunning(); - } + /// + public void QueueScheduledTask() + where T : IScheduledTask + { + QueueScheduledTask(new TaskOptions()); + } - /// - public void QueueScheduledTask(TaskOptions options) - where T : IScheduledTask - { - var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + /// + public void QueueIfNotRunning() + where T : IScheduledTask + { + var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - if (scheduledTask is null) - { - _logger.LogError("Unable to find scheduled task of type {0} in QueueScheduledTask.", typeof(T).Name); - } - else - { - QueueScheduledTask(scheduledTask, options); - } - } - - /// - public void QueueScheduledTask() - where T : IScheduledTask + if (task.State != TaskState.Running) { QueueScheduledTask(new TaskOptions()); } + } - /// - public void QueueIfNotRunning() - where T : IScheduledTask + /// + public void Execute() + where T : IScheduledTask + { + var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + + if (scheduledTask is null) { - var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - - if (task.State != TaskState.Running) - { - QueueScheduledTask(new TaskOptions()); - } + _logger.LogError("Unable to find scheduled task of type {Type} in Execute.", typeof(T).Name); } - - /// - public void Execute() - where T : IScheduledTask + else { - var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + var type = scheduledTask.ScheduledTask.GetType(); - if (scheduledTask is null) - { - _logger.LogError("Unable to find scheduled task of type {0} in Execute.", typeof(T).Name); - } - else - { - var type = scheduledTask.ScheduledTask.GetType(); - - _logger.LogDebug("Queuing task {0}", type.Name); - - lock (_taskQueue) - { - if (scheduledTask.State == TaskState.Idle) - { - Execute(scheduledTask, new TaskOptions()); - } - } - } - } - - /// - public void QueueScheduledTask(IScheduledTask task, TaskOptions options) - { - var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType()); - - if (scheduledTask is null) - { - _logger.LogError("Unable to find scheduled task of type {0} in QueueScheduledTask.", task.GetType().Name); - } - else - { - QueueScheduledTask(scheduledTask, options); - } - } - - /// - /// Queues the scheduled task. - /// - /// The task. - /// The task options. - private void QueueScheduledTask(IScheduledTaskWorker task, TaskOptions options) - { - var type = task.ScheduledTask.GetType(); - - _logger.LogDebug("Queuing task {0}", type.Name); + _logger.LogDebug("Queuing task {Name}", type.Name); lock (_taskQueue) { - if (task.State == TaskState.Idle) + if (scheduledTask.State == TaskState.Idle) { - Execute(task, options); - return; + Execute(scheduledTask, new TaskOptions()); } - - _taskQueue.Enqueue(new Tuple(type, options)); } } + } - /// - public void AddTasks(IEnumerable tasks) + /// + public void QueueScheduledTask(IScheduledTask task, TaskOptions options) + { + var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType()); + + if (scheduledTask is null) { - var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _logger)); - - ScheduledTasks = ScheduledTasks.Concat(list).ToArray(); + _logger.LogError("Unable to find scheduled task of type {Type} in QueueScheduledTask.", task.GetType().Name); } - - /// - public void Dispose() + else { - Dispose(true); - GC.SuppressFinalize(this); + QueueScheduledTask(scheduledTask, options); } + } - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) + /// + /// Queues the scheduled task. + /// + /// The task. + /// The task options. + private void QueueScheduledTask(IScheduledTaskWorker task, TaskOptions options) + { + var type = task.ScheduledTask.GetType(); + + _logger.LogDebug("Queuing task {Name}", type.Name); + + lock (_taskQueue) { - foreach (var task in ScheduledTasks) + if (task.State == TaskState.Idle) { - task.Dispose(); + Execute(task, options); + return; } + + _taskQueue.Enqueue(new Tuple(type, options)); } + } - /// - public void Cancel(IScheduledTaskWorker task) + /// + public void AddTasks(IEnumerable tasks) + { + var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _logger)); + + ScheduledTasks = ScheduledTasks.Concat(list).ToArray(); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + foreach (var task in ScheduledTasks) { - ((ScheduledTaskWorker)task).Cancel(); + task.Dispose(); } + } - /// - public Task Execute(IScheduledTaskWorker task, TaskOptions options) + /// + public void Cancel(IScheduledTaskWorker task) + { + ((ScheduledTaskWorker)task).Cancel(); + } + + /// + public Task Execute(IScheduledTaskWorker task, TaskOptions options) + { + return ((ScheduledTaskWorker)task).Execute(options); + } + + /// + /// Called when [task executing]. + /// + /// The task. + internal void OnTaskExecuting(IScheduledTaskWorker task) + { + TaskExecuting?.Invoke(this, new GenericEventArgs(task)); + } + + /// + /// Called when [task completed]. + /// + /// The task. + /// The result. + internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result) + { + TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result)); + + ExecuteQueuedTasks(); + } + + /// + /// Executes the queued tasks. + /// + private void ExecuteQueuedTasks() + { + lock (_taskQueue) { - return ((ScheduledTaskWorker)task).Execute(options); - } + var list = new List>(); - /// - /// Called when [task executing]. - /// - /// The task. - internal void OnTaskExecuting(IScheduledTaskWorker task) - { - TaskExecuting?.Invoke(this, new GenericEventArgs(task)); - } - - /// - /// Called when [task completed]. - /// - /// The task. - /// The result. - internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result) - { - TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result)); - - ExecuteQueuedTasks(); - } - - /// - /// Executes the queued tasks. - /// - private void ExecuteQueuedTasks() - { - lock (_taskQueue) + while (_taskQueue.TryDequeue(out var item)) { - var list = new List>(); - - while (_taskQueue.TryDequeue(out var item)) + if (list.All(i => i.Item1 != item.Item1)) { - if (list.All(i => i.Item1 != item.Item1)) - { - list.Add(item); - } + list.Add(item); } + } - foreach (var enqueuedType in list) + foreach (var enqueuedType in list) + { + var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1); + + if (scheduledTask.State == TaskState.Idle) { - var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1); - - if (scheduledTask.State == TaskState.Idle) - { - Execute(scheduledTask, enqueuedType.Item2); - } + Execute(scheduledTask, enqueuedType.Item2); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs index 031d147765..e912e9f014 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs @@ -76,93 +76,111 @@ public partial class AudioNormalizationTask : IScheduledTask /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { - foreach (var library in _libraryManager.RootFolder.Children) - { - var libraryOptions = _libraryManager.GetLibraryOptions(library); - if (!libraryOptions.EnableLUFSScan) - { - continue; - } + var numComplete = 0; + var libraries = _libraryManager.RootFolder.Children.Where(library => _libraryManager.GetLibraryOptions(library).EnableLUFSScan).ToArray(); + double percent = 0; - // Album gain - var albums = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = [BaseItemKind.MusicAlbum], - Parent = library, - Recursive = true - }); + foreach (var library in libraries) + { + var albums = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Parent = library, Recursive = true }); + + double nextPercent = numComplete + 1; + nextPercent /= libraries.Length; + nextPercent -= percent; + // Split the progress for this single library into two halves: album gain and track gain. + // The first half will be for album gain, the second half for track gain. + nextPercent /= 2; + var albumComplete = 0; foreach (var a in albums) { - if (a.NormalizationGain.HasValue || a.LUFS.HasValue) + if (!a.NormalizationGain.HasValue && !a.LUFS.HasValue) { - continue; + // Album gain + var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList(); + + // Skip albums that don't have multiple tracks, album gain is useless here + if (albumTracks.Count > 1) + { + _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id); + var tempDir = _applicationPaths.TempDirectory; + Directory.CreateDirectory(tempDir); + var tempFile = Path.Join(tempDir, a.Id + ".concat"); + var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal))); + await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false); + try + { + a.LUFS = await CalculateLUFSAsync( + string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile), + OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file + cancellationToken).ConfigureAwait(false); + } + finally + { + File.Delete(tempFile); + } + } } - // Skip albums that don't have multiple tracks, album gain is useless here - var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList(); - if (albumTracks.Count <= 1) - { - continue; - } + // Update sub-progress for album gain + albumComplete++; + double albumPercent = albumComplete; + albumPercent /= albums.Count; - _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id); - var tempDir = _applicationPaths.TempDirectory; - Directory.CreateDirectory(tempDir); - var tempFile = Path.Join(tempDir, a.Id + ".concat"); - var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal))); - await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false); - try - { - a.LUFS = await CalculateLUFSAsync( - string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile), - cancellationToken).ConfigureAwait(false); - } - finally - { - File.Delete(tempFile); - } + progress.Report(100 * (percent + (albumPercent * nextPercent))); } + // Update progress to start at the track gain percent calculation + percent += nextPercent; + _itemRepository.SaveItems(albums, cancellationToken); // Track gain - var tracks = _libraryManager.GetItemList(new InternalItemsQuery - { - MediaTypes = [MediaType.Audio], - IncludeItemTypes = [BaseItemKind.Audio], - Parent = library, - Recursive = true - }); + var tracks = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = [MediaType.Audio], IncludeItemTypes = [BaseItemKind.Audio], Parent = library, Recursive = true }); + var tracksComplete = 0; foreach (var t in tracks) { - if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol) + if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol) { - continue; + t.LUFS = await CalculateLUFSAsync( + string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), + false, + cancellationToken).ConfigureAwait(false); } - t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken).ConfigureAwait(false); + // Update sub-progress for track gain + tracksComplete++; + double trackPercent = tracksComplete; + trackPercent /= tracks.Count; + + progress.Report(100 * (percent + (trackPercent * nextPercent))); } _itemRepository.SaveItems(tracks, cancellationToken); + + // Update progress + numComplete++; + percent = numComplete; + percent /= libraries.Length; + + progress.Report(100 * percent); } + + progress.Report(100.0); } /// public IEnumerable GetDefaultTriggers() { - return - [ - new TaskTriggerInfo - { - Type = TaskTriggerInfoType.IntervalTrigger, - IntervalTicks = TimeSpan.FromHours(24).Ticks - } - ]; + yield return new TaskTriggerInfo + { + Type = TaskTriggerInfoType.IntervalTrigger, + IntervalTicks = TimeSpan.FromHours(24).Ticks + }; } - private async Task CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken) + private async Task CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken) { var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -"; @@ -189,18 +207,28 @@ public partial class AudioNormalizationTask : IScheduledTask } using var reader = process.StandardError; - await foreach (var line in reader.ReadAllLinesAsync(cancellationToken)) + float? lufs = null; + await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false)) { Match match = LUFSRegex().Match(line); - if (match.Success) { - return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat); + lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat); + break; } } - _logger.LogError("Failed to find LUFS value in output"); - return null; + if (lufs is null) + { + _logger.LogError("Failed to find LUFS value in output"); + } + + if (waitForExit) + { + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + } + + return lufs; } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 563e90fbea..f81309560e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -11,171 +11,157 @@ using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// +/// Class ChapterImagesTask. +/// +public class ChapterImagesTask : IScheduledTask { + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IApplicationPaths _appPaths; + private readonly IChapterManager _chapterManager; + private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; + /// - /// Class ChapterImagesTask. + /// Initializes a new instance of the class. /// - public class ChapterImagesTask : IScheduledTask + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ChapterImagesTask( + ILogger logger, + ILibraryManager libraryManager, + IApplicationPaths appPaths, + IChapterManager chapterManager, + IFileSystem fileSystem, + ILocalizationManager localization) { - private readonly ILogger _logger; - private readonly ILibraryManager _libraryManager; - private readonly IItemRepository _itemRepo; - private readonly IApplicationPaths _appPaths; - private readonly IEncodingManager _encodingManager; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; - private readonly IChapterRepository _chapterRepository; + _logger = logger; + _libraryManager = libraryManager; + _appPaths = appPaths; + _chapterManager = chapterManager; + _fileSystem = fileSystem; + _localization = localization; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public ChapterImagesTask( - ILogger logger, - ILibraryManager libraryManager, - IItemRepository itemRepo, - IApplicationPaths appPaths, - IEncodingManager encodingManager, - IFileSystem fileSystem, - ILocalizationManager localization, - IChapterRepository chapterRepository) + /// + public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages"); + + /// + public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription"); + + /// + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + + /// + public string Key => "RefreshChapterImages"; + + /// + public IEnumerable GetDefaultTriggers() + { + yield return new TaskTriggerInfo { - _logger = logger; - _libraryManager = libraryManager; - _itemRepo = itemRepo; - _appPaths = appPaths; - _encodingManager = encodingManager; - _fileSystem = fileSystem; - _localization = localization; - _chapterRepository = chapterRepository; + Type = TaskTriggerInfoType.DailyTrigger, + TimeOfDayTicks = TimeSpan.FromHours(2).Ticks, + MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks + }; + } + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + var videos = _libraryManager.GetItemList(new InternalItemsQuery + { + MediaTypes = [MediaType.Video], + IsFolder = false, + Recursive = true, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + }, + SourceTypes = [SourceType.Library], + IsVirtualItem = false + }) + .OfType