mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
Merge branch 'master' into feature/DatabaseRefactor
This commit is contained in:
commit
850f1c79f1
@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.2",
|
||||
"version": "9.0.3",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
8
.github/workflows/ci-codeql-analysis.yml
vendored
8
.github/workflows/ci-codeql-analysis.yml
vendored
@ -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@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||
uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||
uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||
uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
|
||||
|
12
.github/workflows/ci-compat.yml
vendored
12
.github/workflows/ci-compat.yml
vendored
@ -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@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
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@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
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@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
name: abi-head
|
||||
path: abi-head
|
||||
|
||||
- name: Download abi-base
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
name: abi-base
|
||||
path: abi-base
|
||||
|
20
.github/workflows/ci-openapi.yml
vendored
20
.github/workflows/ci-openapi.yml
vendored
@ -21,13 +21,13 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
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'
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
@ -55,13 +55,13 @@ jobs:
|
||||
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
|
||||
git checkout --progress --force $ANCESTOR_REF
|
||||
- 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: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
@ -80,12 +80,12 @@ jobs:
|
||||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
@ -158,7 +158,7 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
@ -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,7 +220,7 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
@ -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 }}"
|
||||
|
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@ -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 }}
|
||||
|
||||
|
2
.github/workflows/commands.yml
vendored
2
.github/workflows/commands.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
||||
- name: install python
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.13'
|
||||
cache: 'pip'
|
||||
- name: install python packages
|
||||
run: pip install -r rename/requirements.txt
|
||||
|
2
.github/workflows/issue-template-check.yml
vendored
2
.github/workflows/issue-template-check.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
- name: install python
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.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
|
||||
|
@ -22,31 +22,31 @@
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="4.0.10" />
|
||||
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.228.1" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.3" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
@ -75,11 +75,11 @@
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.3" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.3" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.3" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.17.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.19.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
|
@ -1,43 +1,35 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to parse season paths.
|
||||
/// </summary>
|
||||
public static class SeasonPathParser
|
||||
public static partial class SeasonPathParser
|
||||
{
|
||||
/// <summary>
|
||||
/// A season folder must contain one of these somewhere in the name.
|
||||
/// </summary>
|
||||
private static readonly string[] _seasonFolderNames =
|
||||
{
|
||||
"season",
|
||||
"sæson",
|
||||
"temporada",
|
||||
"saison",
|
||||
"staffel",
|
||||
"series",
|
||||
"сезон",
|
||||
"stagione"
|
||||
};
|
||||
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\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*(?<rightpart>.*)$")]
|
||||
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*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$")]
|
||||
private static partial Regex ProcessPost();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse season number from path.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to season.</param>
|
||||
/// <param name="parentPath">Folder name of the parent.</param>
|
||||
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
|
||||
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
|
||||
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="parentFolderName">The parent folder name.</param>
|
||||
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
|
||||
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
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<char> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -34,76 +34,46 @@ namespace Emby.Server.Implementations.AppBase
|
||||
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the program data folder.
|
||||
/// </summary>
|
||||
/// <value>The program data path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ProgramDataPath { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string WebPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the system folder.
|
||||
/// </summary>
|
||||
/// <value>The path to the system folder.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder path to the data directory.
|
||||
/// </summary>
|
||||
/// <value>The data directory.</value>
|
||||
/// <inheritdoc/>
|
||||
public string DataPath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string VirtualDataPath => "%AppDataPath%";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache path.
|
||||
/// </summary>
|
||||
/// <value>The image cache path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ImageCachePath => Path.Combine(CachePath, "images");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the plugin directory.
|
||||
/// </summary>
|
||||
/// <value>The plugins path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the plugin configurations directory.
|
||||
/// </summary>
|
||||
/// <value>The plugin configurations path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the log directory.
|
||||
/// </summary>
|
||||
/// <value>The log directory path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string LogDirectoryPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the application configuration root directory.
|
||||
/// </summary>
|
||||
/// <value>The configuration directory path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string ConfigurationDirectoryPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the system configuration file.
|
||||
/// </summary>
|
||||
/// <value>The system configuration file path.</value>
|
||||
/// <inheritdoc/>
|
||||
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the folder path to the cache directory.
|
||||
/// </summary>
|
||||
/// <value>The cache directory.</value>
|
||||
/// <inheritdoc/>
|
||||
public string CachePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder path to the temp directory within the cache folder.
|
||||
/// </summary>
|
||||
/// <value>The temp directory.</value>
|
||||
/// <inheritdoc/>
|
||||
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string TrickplayPath => Path.Combine(DataPath, "trickplay");
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Lyrics;
|
||||
@ -508,6 +509,7 @@ namespace Emby.Server.Implementations
|
||||
|
||||
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||
serviceCollection.AddSingleton<EncodingHelper>();
|
||||
serviceCollection.AddSingleton<IPathManager, PathManager>();
|
||||
|
||||
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
||||
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
||||
|
@ -5,80 +5,80 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Trickplay;
|
||||
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<CleanDatabaseScheduledTask> _logger;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
|
||||
public CleanDatabaseScheduledTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<CleanDatabaseScheduledTask> logger,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider)
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_dbProvider = dbProvider;
|
||||
}
|
||||
|
||||
public CleanDatabaseScheduledTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<CleanDatabaseScheduledTask> logger,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_dbProvider = dbProvider;
|
||||
}
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
|
||||
}
|
||||
HasDeadParentId = true
|
||||
});
|
||||
|
||||
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
var numComplete = 0;
|
||||
var numItems = itemIds.Count + 1;
|
||||
|
||||
_logger.LogDebug("Cleaning {Number} items with dead parent links", 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)
|
||||
_libraryManager.DeleteItem(item, new DeleteOptions
|
||||
{
|
||||
_logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
|
||||
|
||||
_libraryManager.DeleteItem(item, new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
});
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= numItems;
|
||||
progress.Report(percent * 100);
|
||||
DeleteFileLocation = false
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +79,7 @@ namespace Emby.Server.Implementations.Library
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IPeopleRepository _peopleRepository;
|
||||
private readonly ExtraResolver _extraResolver;
|
||||
private readonly IPathManager _pathManager;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder sync lock.
|
||||
@ -114,7 +115,8 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
/// <param name="peopleRepository">The People Repository.</param>
|
||||
/// <param name="peopleRepository">The people repository.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
public LibraryManager(
|
||||
IServerApplicationHost appHost,
|
||||
ILoggerFactory loggerFactory,
|
||||
@ -131,7 +133,8 @@ namespace Emby.Server.Implementations.Library
|
||||
IImageProcessor imageProcessor,
|
||||
NamingOptions namingOptions,
|
||||
IDirectoryService directoryService,
|
||||
IPeopleRepository peopleRepository)
|
||||
IPeopleRepository peopleRepository,
|
||||
IPathManager pathManager)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_logger = loggerFactory.CreateLogger<LibraryManager>();
|
||||
@ -149,6 +152,7 @@ namespace Emby.Server.Implementations.Library
|
||||
_cache = new ConcurrentDictionary<Guid, BaseItem>();
|
||||
_namingOptions = namingOptions;
|
||||
_peopleRepository = peopleRepository;
|
||||
_pathManager = pathManager;
|
||||
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
|
||||
|
||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||
@ -201,33 +205,33 @@ namespace Emby.Server.Implementations.Library
|
||||
/// Gets or sets the postscan tasks.
|
||||
/// </summary>
|
||||
/// <value>The postscan tasks.</value>
|
||||
private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>();
|
||||
private ILibraryPostScanTask[] PostscanTasks { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the intro providers.
|
||||
/// </summary>
|
||||
/// <value>The intro providers.</value>
|
||||
private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>();
|
||||
private IIntroProvider[] IntroProviders { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of entity resolution ignore rules.
|
||||
/// </summary>
|
||||
/// <value>The entity resolution ignore rules.</value>
|
||||
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>();
|
||||
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of currently registered entity resolvers.
|
||||
/// </summary>
|
||||
/// <value>The entity resolvers enumerable.</value>
|
||||
private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>();
|
||||
private IItemResolver[] EntityResolvers { get; set; } = [];
|
||||
|
||||
private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>();
|
||||
private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the comparers.
|
||||
/// </summary>
|
||||
/// <value>The comparers.</value>
|
||||
private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>();
|
||||
private IBaseItemComparer[] Comparers { get; set; } = [];
|
||||
|
||||
public bool IsScanRunning { get; private set; }
|
||||
|
||||
@ -360,7 +364,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var children = item.IsFolder
|
||||
? ((Folder)item).GetRecursiveChildren(false)
|
||||
: Array.Empty<BaseItem>();
|
||||
: [];
|
||||
|
||||
foreach (var metadataPath in GetMetadataPaths(item, children))
|
||||
{
|
||||
@ -466,14 +470,28 @@ namespace Emby.Server.Implementations.Library
|
||||
ReportItemRemoved(item, parent);
|
||||
}
|
||||
|
||||
private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
|
||||
private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
|
||||
{
|
||||
var list = GetInternalMetadataPaths(item);
|
||||
foreach (var child in children)
|
||||
{
|
||||
list.AddRange(GetInternalMetadataPaths(child));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<string> GetInternalMetadataPaths(BaseItem item)
|
||||
{
|
||||
var list = new List<string>
|
||||
{
|
||||
item.GetInternalMetadataPath()
|
||||
};
|
||||
|
||||
list.AddRange(children.Select(i => i.GetInternalMetadataPath()));
|
||||
if (item is Video video)
|
||||
{
|
||||
list.Add(_pathManager.GetTrickplayDirectory(video));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
@ -594,7 +612,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
_logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf);
|
||||
|
||||
files = Array.Empty<FileSystemMetadata>();
|
||||
files = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1345,6 +1363,21 @@ namespace Emby.Server.Implementations.Library
|
||||
return _itemRepository.GetItemList(query);
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> 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<BaseItem> QueryItems(InternalItemsQuery query)
|
||||
{
|
||||
if (query.User is not null)
|
||||
@ -1449,7 +1482,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<Guid>();
|
||||
query.AncestorIds = [];
|
||||
|
||||
// Prevent searching in all libraries due to empty filter
|
||||
if (query.TopParentIds.Length == 0)
|
||||
@ -1569,7 +1602,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return GetTopParentIdsForQuery(displayParent, user);
|
||||
}
|
||||
|
||||
return Array.Empty<Guid>();
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!view.ParentId.IsEmpty())
|
||||
@ -1580,7 +1613,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return GetTopParentIdsForQuery(displayParent, user);
|
||||
}
|
||||
|
||||
return Array.Empty<Guid>();
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handle grouping
|
||||
@ -1595,7 +1628,7 @@ namespace Emby.Server.Implementations.Library
|
||||
.SelectMany(i => GetTopParentIdsForQuery(i, user));
|
||||
}
|
||||
|
||||
return Array.Empty<Guid>();
|
||||
return [];
|
||||
}
|
||||
|
||||
if (item is CollectionFolder collectionFolder)
|
||||
@ -1609,7 +1642,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return new[] { topParent.Id };
|
||||
}
|
||||
|
||||
return Array.Empty<Guid>();
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1653,7 +1686,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
_logger.LogError(ex, "Error getting intros");
|
||||
|
||||
return Enumerable.Empty<IntroInfo>();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -2480,8 +2513,11 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
|
||||
@ -2880,7 +2916,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<byte>()).ConfigureAwait(false);
|
||||
await File.WriteAllBytesAsync(path, []).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
|
||||
|
@ -783,9 +783,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<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
|
||||
var info = GetLiveStreamInfo(id);
|
||||
if (info is null)
|
||||
{
|
||||
return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(null, null));
|
||||
}
|
||||
|
||||
return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
|
||||
}
|
||||
|
||||
public ILiveStream GetLiveStreamInfo(string id)
|
||||
|
@ -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<MediaStream>();
|
||||
|
||||
foreach (var stream in iterStreams)
|
||||
foreach (var stream in filteredStreams)
|
||||
{
|
||||
stream.Score = GetStreamScore(stream, preferredLanguages);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> 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<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> 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<string> languagePreferences)
|
||||
{
|
||||
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
|
||||
|
36
Emby.Server.Implementations/Library/PathManager.cs
Normal file
36
Emby.Server.Implementations/Library/PathManager.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
|
||||
namespace Emby.Server.Implementations.Library;
|
||||
|
||||
/// <summary>
|
||||
/// IPathManager implementation.
|
||||
/// </summary>
|
||||
public class PathManager : IPathManager
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The server configuration manager.</param>
|
||||
public PathManager(
|
||||
IServerConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
|
||||
{
|
||||
var basePath = _config.ApplicationPaths.TrickplayPath;
|
||||
var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
return saveWithMedia
|
||||
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
|
||||
: Path.Combine(basePath, idString);
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
@ -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].
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="parentPath">The parentpath.</param>
|
||||
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
|
||||
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
|
||||
private static bool IsSeasonFolder(string path, bool isTvContentType)
|
||||
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;
|
||||
}
|
||||
|
@ -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": "Жанры",
|
||||
|
@ -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": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization
|
||||
}
|
||||
|
||||
// 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))
|
||||
|
@ -336,7 +336,7 @@
|
||||
"TwoLetterISORegionName": "IE"
|
||||
},
|
||||
{
|
||||
"DisplayName": "Islamic Republic of Pakistan",
|
||||
"DisplayName": "Pakistan",
|
||||
"Name": "PK",
|
||||
"ThreeLetterISORegionName": "PAK",
|
||||
"TwoLetterISORegionName": "PK"
|
||||
|
@ -310,7 +310,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
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;
|
||||
}
|
||||
|
@ -344,6 +344,11 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <returns>Task.</returns>
|
||||
private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime)
|
||||
{
|
||||
if (session is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(info.MediaSourceId))
|
||||
{
|
||||
info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
|
||||
@ -676,6 +681,11 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
|
||||
{
|
||||
if (session is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var item = session.FullNowPlayingItem;
|
||||
if (item is not null && item.Id.Equals(itemId))
|
||||
{
|
||||
@ -795,7 +805,11 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
ArgumentNullException.ThrowIfNull(info);
|
||||
|
||||
var session = GetSession(info.SessionId);
|
||||
var session = GetSession(info.SessionId, false);
|
||||
if (session is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var libraryItem = info.ItemId.IsEmpty()
|
||||
? null
|
||||
|
@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.TV
|
||||
|
||||
if (!string.IsNullOrEmpty(presentationUniqueKey))
|
||||
{
|
||||
return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request);
|
||||
return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request);
|
||||
}
|
||||
|
||||
if (limit.HasValue)
|
||||
@ -100,25 +100,9 @@ namespace Emby.Server.Implementations.TV
|
||||
limit = limit.Value + 10;
|
||||
}
|
||||
|
||||
var items = _libraryManager
|
||||
.GetItemList(
|
||||
new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||
OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
|
||||
SeriesPresentationUniqueKey = presentationUniqueKey,
|
||||
Limit = limit,
|
||||
DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
|
||||
GroupBySeriesPresentationUniqueKey = true
|
||||
},
|
||||
parentsFolders.ToList())
|
||||
.Cast<Episode>()
|
||||
.Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
|
||||
.Select(GetUniqueSeriesKey)
|
||||
.ToList();
|
||||
var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff);
|
||||
|
||||
// Avoid implicitly captured closure
|
||||
var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options);
|
||||
var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options);
|
||||
|
||||
return GetResult(episodes, request);
|
||||
}
|
||||
@ -134,36 +118,11 @@ namespace Emby.Server.Implementations.TV
|
||||
.OrderByDescending(i => i.LastWatchedDate);
|
||||
}
|
||||
|
||||
// If viewing all next up for all series, remove first episodes
|
||||
// But if that returns empty, keep those first episodes (avoid completely empty view)
|
||||
var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty();
|
||||
var anyFound = false;
|
||||
|
||||
return allNextUp
|
||||
.Where(i =>
|
||||
{
|
||||
if (request.DisableFirstEpisode)
|
||||
{
|
||||
return i.LastWatchedDate != DateTime.MinValue;
|
||||
}
|
||||
|
||||
if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff))
|
||||
{
|
||||
anyFound = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return !anyFound && i.LastWatchedDate == DateTime.MinValue;
|
||||
})
|
||||
.Select(i => i.GetEpisodeFunction())
|
||||
.Where(i => i is not null)!;
|
||||
}
|
||||
|
||||
private static string GetUniqueSeriesKey(Episode episode)
|
||||
{
|
||||
return episode.SeriesPresentationUniqueKey;
|
||||
}
|
||||
|
||||
private static string GetUniqueSeriesKey(Series series)
|
||||
{
|
||||
return series.GetPresentationUniqueKey();
|
||||
@ -179,13 +138,13 @@ namespace Emby.Server.Implementations.TV
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||
IncludeItemTypes = [BaseItemKind.Episode],
|
||||
IsPlayed = true,
|
||||
Limit = 1,
|
||||
ParentIndexNumberNotEquals = 0,
|
||||
DtoOptions = new DtoOptions
|
||||
{
|
||||
Fields = new[] { ItemFields.SortName },
|
||||
Fields = [ItemFields.SortName],
|
||||
EnableImages = false
|
||||
}
|
||||
};
|
||||
@ -203,8 +162,8 @@ namespace Emby.Server.Implementations.TV
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||
OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
|
||||
IncludeItemTypes = [BaseItemKind.Episode],
|
||||
OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)],
|
||||
Limit = 1,
|
||||
IsPlayed = includePlayed,
|
||||
IsVirtualItem = false,
|
||||
@ -229,7 +188,7 @@ namespace Emby.Server.Implementations.TV
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
ParentIndexNumber = 0,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||
IncludeItemTypes = [BaseItemKind.Episode],
|
||||
IsPlayed = includePlayed,
|
||||
IsVirtualItem = false,
|
||||
DtoOptions = dtoOptions
|
||||
@ -249,7 +208,7 @@ namespace Emby.Server.Implementations.TV
|
||||
consideredEpisodes.Add(nextEpisode);
|
||||
}
|
||||
|
||||
var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
|
||||
var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
|
||||
.Cast<Episode>();
|
||||
if (lastWatchedEpisode is not null)
|
||||
{
|
||||
|
@ -698,6 +698,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// Gets recommended live tv epgs.
|
||||
/// </summary>
|
||||
/// <param name="userId">Optional. filter by user id.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param>
|
||||
/// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param>
|
||||
@ -720,6 +721,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? isAiring,
|
||||
[FromQuery] bool? hasAired,
|
||||
@ -744,6 +746,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
IsAiring = isAiring,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
HasAired = hasAired,
|
||||
IsSeries = isSeries,
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
@ -86,7 +87,7 @@ public class TvShowsController : BaseJellyfinApiController
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] DateTime? nextUpDateCutoff,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool disableFirstEpisode = false,
|
||||
[FromQuery][ParameterObsolete] bool disableFirstEpisode = false,
|
||||
[FromQuery] bool enableResumable = true,
|
||||
[FromQuery] bool enableRewatching = false)
|
||||
{
|
||||
@ -109,7 +110,6 @@ public class TvShowsController : BaseJellyfinApiController
|
||||
StartIndex = startIndex,
|
||||
User = user,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
DisableFirstEpisode = disableFirstEpisode,
|
||||
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
|
||||
EnableResumable = enableResumable,
|
||||
EnableRewatching = enableRewatching
|
||||
|
@ -101,16 +101,23 @@ public sealed class BaseItemRepository
|
||||
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
using var transaction = context.Database.BeginTransaction();
|
||||
context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
|
||||
context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
|
||||
context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
|
||||
context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
|
||||
context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
|
||||
context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
|
||||
context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
|
||||
context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.SaveChanges();
|
||||
transaction.Commit();
|
||||
}
|
||||
@ -255,6 +262,37 @@ public sealed class BaseItemRepository
|
||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(filter);
|
||||
ArgumentNullException.ThrowIfNull(filter.User);
|
||||
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
|
||||
var query = context.BaseItems
|
||||
.AsNoTracking()
|
||||
.Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
|
||||
.Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
|
||||
.Join(
|
||||
context.UserData.AsNoTracking(),
|
||||
i => new { UserId = filter.User.Id, ItemId = i.Id },
|
||||
u => new { UserId = u.UserId, ItemId = u.ItemId },
|
||||
(entity, data) => new { Item = entity, UserData = data })
|
||||
.GroupBy(g => g.Item.SeriesPresentationUniqueKey)
|
||||
.Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
|
||||
.Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
|
||||
.OrderByDescending(g => g.LastPlayedDate)
|
||||
.Select(g => g.Key!);
|
||||
|
||||
if (filter.Limit.HasValue)
|
||||
{
|
||||
query = query.Take(filter.Limit.Value);
|
||||
}
|
||||
|
||||
return query.ToArray();
|
||||
}
|
||||
|
||||
private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
|
||||
{
|
||||
// This whole block is needed to filter duplicate entries on request
|
||||
@ -925,25 +963,11 @@ public sealed class BaseItemRepository
|
||||
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
|
||||
var innerQuery = new InternalItemsQuery(filter.User)
|
||||
{
|
||||
ExcludeItemTypes = filter.ExcludeItemTypes,
|
||||
IncludeItemTypes = filter.IncludeItemTypes,
|
||||
MediaTypes = filter.MediaTypes,
|
||||
AncestorIds = filter.AncestorIds,
|
||||
ItemIds = filter.ItemIds,
|
||||
TopParentIds = filter.TopParentIds,
|
||||
ParentId = filter.ParentId,
|
||||
IsAiring = filter.IsAiring,
|
||||
IsMovie = filter.IsMovie,
|
||||
IsSports = filter.IsSports,
|
||||
IsKids = filter.IsKids,
|
||||
IsNews = filter.IsNews,
|
||||
IsSeries = filter.IsSeries
|
||||
};
|
||||
var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, innerQuery);
|
||||
var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
|
||||
|
||||
query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type)));
|
||||
query = query.Where(e => e.Type == returnType);
|
||||
// this does not seem to be nesseary but it does not make any sense why this isn't working.
|
||||
// && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type)));
|
||||
|
||||
if (filter.OrderBy.Count != 0
|
||||
|| !string.IsNullOrEmpty(filter.SearchTerm))
|
||||
|
@ -12,6 +12,7 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Trickplay;
|
||||
@ -37,9 +38,10 @@ public class TrickplayManager : ITrickplayManager
|
||||
private readonly IImageEncoder _imageEncoder;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IPathManager _pathManager;
|
||||
|
||||
private static readonly AsyncNonKeyedLocker _resourcePool = new(1);
|
||||
private static readonly string[] _trickplayImgExtensions = { ".jpg" };
|
||||
private static readonly string[] _trickplayImgExtensions = [".jpg"];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TrickplayManager"/> class.
|
||||
@ -53,6 +55,7 @@ public class TrickplayManager : ITrickplayManager
|
||||
/// <param name="imageEncoder">The image encoder.</param>
|
||||
/// <param name="dbProvider">The database provider.</param>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
public TrickplayManager(
|
||||
ILogger<TrickplayManager> logger,
|
||||
IMediaEncoder mediaEncoder,
|
||||
@ -62,7 +65,8 @@ public class TrickplayManager : ITrickplayManager
|
||||
IServerConfigurationManager config,
|
||||
IImageEncoder imageEncoder,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider,
|
||||
IApplicationPaths appPaths)
|
||||
IApplicationPaths appPaths,
|
||||
IPathManager pathManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
@ -73,6 +77,7 @@ public class TrickplayManager : ITrickplayManager
|
||||
_imageEncoder = imageEncoder;
|
||||
_dbProvider = dbProvider;
|
||||
_appPaths = appPaths;
|
||||
_pathManager = pathManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -610,10 +615,7 @@ public class TrickplayManager : ITrickplayManager
|
||||
/// <inheritdoc />
|
||||
public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
|
||||
{
|
||||
var path = saveWithMedia
|
||||
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
|
||||
: Path.Combine(item.GetInternalMetadataPath(), "trickplay");
|
||||
|
||||
var path = _pathManager.GetTrickplayDirectory(item, saveWithMedia);
|
||||
var subdirectory = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} - {1}x{2}",
|
||||
|
@ -94,7 +94,7 @@ public class MigrateLibraryDb : IMigrationRoutine
|
||||
Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
|
||||
DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
|
||||
PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
|
||||
ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType FROM TypedBaseItems
|
||||
ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName FROM TypedBaseItems
|
||||
""";
|
||||
dbContext.BaseItems.ExecuteDelete();
|
||||
|
||||
@ -168,7 +168,6 @@ public class MigrateLibraryDb : IMigrationRoutine
|
||||
dbContext.UserData.ExecuteDelete();
|
||||
|
||||
var users = dbContext.Users.AsNoTracking().ToImmutableArray();
|
||||
var oldUserdata = new Dictionary<string, UserData>();
|
||||
|
||||
foreach (var entity in queryResult)
|
||||
{
|
||||
@ -189,6 +188,8 @@ public class MigrateLibraryDb : IMigrationRoutine
|
||||
dbContext.UserData.Add(userData);
|
||||
}
|
||||
|
||||
users.Clear();
|
||||
legacyBaseItemWithUserKeys.Clear();
|
||||
_logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count);
|
||||
dbContext.SaveChanges();
|
||||
|
||||
@ -225,11 +226,12 @@ public class MigrateLibraryDb : IMigrationRoutine
|
||||
dbContext.PeopleBaseItemMap.ExecuteDelete();
|
||||
|
||||
var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
|
||||
var baseItemIds = dbContext.BaseItems.Select(b => b.Id).ToHashSet();
|
||||
|
||||
foreach (SqliteDataReader reader in connection.Query(personsQuery))
|
||||
{
|
||||
var itemId = reader.GetGuid(0);
|
||||
if (!dbContext.BaseItems.Any(f => f.Id == itemId))
|
||||
if (!baseItemIds.Contains(itemId))
|
||||
{
|
||||
_logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
|
||||
continue;
|
||||
@ -261,12 +263,16 @@ public class MigrateLibraryDb : IMigrationRoutine
|
||||
});
|
||||
}
|
||||
|
||||
baseItemIds.Clear();
|
||||
|
||||
foreach (var item in peopleCache)
|
||||
{
|
||||
dbContext.Peoples.Add(item.Value.Person);
|
||||
dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
|
||||
}
|
||||
|
||||
peopleCache.Clear();
|
||||
|
||||
_logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count);
|
||||
dbContext.SaveChanges();
|
||||
migrationTotalTime += stopwatch.Elapsed;
|
||||
@ -1029,6 +1035,16 @@ public class MigrateLibraryDb : IMigrationRoutine
|
||||
entity.MediaType = mediaType;
|
||||
}
|
||||
|
||||
if (reader.TryGetString(index++, out var sortName))
|
||||
{
|
||||
entity.SortName = sortName;
|
||||
}
|
||||
|
||||
if (reader.TryGetString(index++, out var cleanName))
|
||||
{
|
||||
entity.CleanName = cleanName;
|
||||
}
|
||||
|
||||
var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
|
||||
var dataKeys = baseItem.GetUserDataKeys();
|
||||
userDataKeys.AddRange(dataKeys);
|
||||
|
@ -39,7 +39,7 @@ public class MoveTrickplayFiles : IMigrationRoutine
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B");
|
||||
public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "MoveTrickplayFiles";
|
||||
@ -89,6 +89,12 @@ public class MoveTrickplayFiles : IMigrationRoutine
|
||||
{
|
||||
_fileSystem.MoveDirectory(oldPath, newPath);
|
||||
}
|
||||
|
||||
oldPath = GetNewOldTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, trickplayInfo.Width, false);
|
||||
if (_fileSystem.DirectoryExists(oldPath))
|
||||
{
|
||||
_fileSystem.MoveDirectory(oldPath, newPath);
|
||||
}
|
||||
}
|
||||
} while (previousCount == Limit);
|
||||
|
||||
@ -101,4 +107,20 @@ public class MoveTrickplayFiles : IMigrationRoutine
|
||||
|
||||
return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
|
||||
}
|
||||
|
||||
private string GetNewOldTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
|
||||
{
|
||||
var path = saveWithMedia
|
||||
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
|
||||
: Path.Combine(item.GetInternalMetadataPath(), "trickplay");
|
||||
|
||||
var subdirectory = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} - {1}x{2}",
|
||||
width.ToString(CultureInfo.InvariantCulture),
|
||||
tileWidth.ToString(CultureInfo.InvariantCulture),
|
||||
tileHeight.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
return Path.Combine(path, subdirectory);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,9 @@ using Emby.Server.Implementations;
|
||||
using Jellyfin.Server.Extensions;
|
||||
using Jellyfin.Server.Helpers;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@ -44,6 +46,9 @@ namespace Jellyfin.Server
|
||||
public const string LoggingConfigFileSystem = "logging.json";
|
||||
|
||||
private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory();
|
||||
private static SetupServer _setupServer = new();
|
||||
private static CoreAppHost? _appHost;
|
||||
private static IHost? _jellyfinHost = null;
|
||||
private static long _startTimestamp;
|
||||
private static ILogger _logger = NullLogger.Instance;
|
||||
private static bool _restartOnShutdown;
|
||||
@ -70,6 +75,7 @@ namespace Jellyfin.Server
|
||||
{
|
||||
_startTimestamp = Stopwatch.GetTimestamp();
|
||||
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
|
||||
await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false);
|
||||
|
||||
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
|
||||
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
|
||||
@ -124,22 +130,23 @@ namespace Jellyfin.Server
|
||||
if (_restartOnShutdown)
|
||||
{
|
||||
_startTimestamp = Stopwatch.GetTimestamp();
|
||||
_setupServer = new SetupServer();
|
||||
await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false);
|
||||
}
|
||||
} while (_restartOnShutdown);
|
||||
}
|
||||
|
||||
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
|
||||
{
|
||||
using var appHost = new CoreAppHost(
|
||||
appPaths,
|
||||
_loggerFactory,
|
||||
options,
|
||||
startupConfig);
|
||||
|
||||
IHost? host = null;
|
||||
using CoreAppHost appHost = new CoreAppHost(
|
||||
appPaths,
|
||||
_loggerFactory,
|
||||
options,
|
||||
startupConfig);
|
||||
_appHost = appHost;
|
||||
try
|
||||
{
|
||||
host = Host.CreateDefaultBuilder()
|
||||
_jellyfinHost = Host.CreateDefaultBuilder()
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureServices(services => appHost.Init(services))
|
||||
.ConfigureWebHostDefaults(webHostBuilder =>
|
||||
@ -156,14 +163,17 @@ namespace Jellyfin.Server
|
||||
.Build();
|
||||
|
||||
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
|
||||
appHost.ServiceProvider = host.Services;
|
||||
appHost.ServiceProvider = _jellyfinHost.Services;
|
||||
|
||||
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
|
||||
Migrations.MigrationRunner.Run(appHost, _loggerFactory);
|
||||
|
||||
try
|
||||
{
|
||||
await host.StartAsync().ConfigureAwait(false);
|
||||
await _setupServer.StopAsync().ConfigureAwait(false);
|
||||
_setupServer.Dispose();
|
||||
_setupServer = null!;
|
||||
await _jellyfinHost.StartAsync().ConfigureAwait(false);
|
||||
|
||||
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
|
||||
{
|
||||
@ -182,7 +192,7 @@ namespace Jellyfin.Server
|
||||
|
||||
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
|
||||
|
||||
await host.WaitForShutdownAsync().ConfigureAwait(false);
|
||||
await _jellyfinHost.WaitForShutdownAsync().ConfigureAwait(false);
|
||||
_restartOnShutdown = appHost.ShouldRestart;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -203,7 +213,8 @@ namespace Jellyfin.Server
|
||||
await databaseProvider.RunShutdownTask(shutdownSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
host?.Dispose();
|
||||
_appHost = null;
|
||||
_jellyfinHost?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
172
Jellyfin.Server/ServerSetupApp/SetupServer.cs
Normal file
172
Jellyfin.Server/ServerSetupApp/SetupServer.cs
Normal file
@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fake application pipeline that will only exist for as long as the main app is not started.
|
||||
/// </summary>
|
||||
public sealed class SetupServer : IDisposable
|
||||
{
|
||||
private IHost? _startupServer;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup.
|
||||
/// </summary>
|
||||
/// <param name="networkManagerFactory">The networkmanager.</param>
|
||||
/// <param name="applicationPaths">The application paths.</param>
|
||||
/// <param name="serverApplicationHost">The servers application host.</param>
|
||||
/// <returns>A Task.</returns>
|
||||
public async Task RunAsync(
|
||||
Func<INetworkManager?> networkManagerFactory,
|
||||
IApplicationPaths applicationPaths,
|
||||
Func<IServerApplicationHost?> serverApplicationHost)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
_startupServer = Host.CreateDefaultBuilder()
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureServices(serv =>
|
||||
{
|
||||
serv.AddHealthChecks()
|
||||
.AddCheck<SetupHealthcheck>("StartupCheck");
|
||||
})
|
||||
.ConfigureWebHostDefaults(webHostBuilder =>
|
||||
{
|
||||
webHostBuilder
|
||||
.UseKestrel()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
|
||||
app.Map("/startup/logger", loggerRoute =>
|
||||
{
|
||||
loggerRoute.Run(async context =>
|
||||
{
|
||||
var networkManager = networkManagerFactory();
|
||||
if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress))
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
|
||||
return;
|
||||
}
|
||||
|
||||
var logFilePath = new DirectoryInfo(applicationPaths.LogDirectoryPath)
|
||||
.EnumerateFiles()
|
||||
.OrderBy(f => f.CreationTimeUtc)
|
||||
.FirstOrDefault()
|
||||
?.FullName;
|
||||
if (logFilePath is not null)
|
||||
{
|
||||
await context.Response.SendFileAsync(logFilePath, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.Map("/System/Info/Public", systemRoute =>
|
||||
{
|
||||
systemRoute.Run(async context =>
|
||||
{
|
||||
var jfApplicationHost = serverApplicationHost();
|
||||
|
||||
var retryCounter = 0;
|
||||
while (jfApplicationHost is null && retryCounter < 5)
|
||||
{
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
jfApplicationHost = serverApplicationHost();
|
||||
retryCounter++;
|
||||
}
|
||||
|
||||
if (jfApplicationHost is null)
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
|
||||
context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60");
|
||||
return;
|
||||
}
|
||||
|
||||
var sysInfo = new PublicSystemInfo
|
||||
{
|
||||
Version = jfApplicationHost.ApplicationVersionString,
|
||||
ProductName = jfApplicationHost.Name,
|
||||
Id = jfApplicationHost.SystemId,
|
||||
ServerName = jfApplicationHost.FriendlyName,
|
||||
LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request),
|
||||
StartupWizardCompleted = false
|
||||
};
|
||||
|
||||
await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false);
|
||||
});
|
||||
});
|
||||
|
||||
app.Run((context) =>
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
|
||||
context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60");
|
||||
context.Response.WriteAsync("<p>Jellyfin Server still starting. Please wait.</p>");
|
||||
var networkManager = networkManagerFactory();
|
||||
if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress))
|
||||
{
|
||||
context.Response.WriteAsync("<p>You can download the current logfiles <a href='/startup/logger'>here</a>.</p>");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
await _startupServer.StartAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the Setup server.
|
||||
/// </summary>
|
||||
/// <returns>A task. Duh.</returns>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (_startupServer is null)
|
||||
{
|
||||
throw new InvalidOperationException("Tried to stop a non existing startup server");
|
||||
}
|
||||
|
||||
await _startupServer.StopAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_startupServer?.Dispose();
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
||||
private class SetupHealthcheck : IHealthCheck
|
||||
{
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up."));
|
||||
}
|
||||
}
|
||||
}
|
@ -84,5 +84,11 @@ namespace MediaBrowser.Common.Configuration
|
||||
/// </summary>
|
||||
/// <value>The magic string used for virtual path manipulation.</value>
|
||||
string VirtualDataPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path used for storing trickplay files.
|
||||
/// </summary>
|
||||
/// <value>The trickplay path.</value>
|
||||
string TrickplayPath { get; }
|
||||
}
|
||||
}
|
||||
|
@ -326,4 +326,23 @@ public static partial class NetworkUtils
|
||||
|
||||
return new IPAddress(BitConverter.GetBytes(broadCastIPAddress));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a subnet contains an address. This method also handles IPv4 mapped to IPv6 addresses.
|
||||
/// </summary>
|
||||
/// <param name="network">The <see cref="IPNetwork"/>.</param>
|
||||
/// <param name="address">The <see cref="IPAddress"/>.</param>
|
||||
/// <returns>Whether the supplied IP is in the supplied network.</returns>
|
||||
public static bool SubnetContainsAddress(IPNetwork network, IPAddress address)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(address);
|
||||
ArgumentNullException.ThrowIfNull(network);
|
||||
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
return network.Contains(address);
|
||||
}
|
||||
}
|
||||
|
@ -139,11 +139,9 @@ namespace MediaBrowser.Controller.Entities.Audio
|
||||
private static List<string> GetUserDataKeys(MusicArtist item)
|
||||
{
|
||||
var list = new List<string>();
|
||||
var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist);
|
||||
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId))
|
||||
{
|
||||
list.Add("Artist-Musicbrainz-" + id);
|
||||
list.Add("Artist-Musicbrainz-" + externalId);
|
||||
}
|
||||
|
||||
list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics());
|
||||
|
@ -920,7 +920,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
// Remove from middle if surrounded by spaces
|
||||
sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
|
||||
|
||||
// Remove from end if followed by a space
|
||||
// Remove from end if preceeded by a space
|
||||
if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
|
||||
{
|
||||
sortable = sortable.Remove(sortable.Length - (search.Length + 1));
|
||||
@ -1776,7 +1776,6 @@ namespace MediaBrowser.Controller.Entities
|
||||
public void AddStudio(string name)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(name);
|
||||
|
||||
var current = Studios;
|
||||
|
||||
if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
|
||||
@ -1795,7 +1794,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public void SetStudios(IEnumerable<string> names)
|
||||
{
|
||||
Studios = names.Distinct().ToArray();
|
||||
Studios = names.Trimmed().Distinct().ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -15,6 +15,8 @@ namespace MediaBrowser.Controller.Entities
|
||||
ArgumentNullException.ThrowIfNull(person);
|
||||
ArgumentException.ThrowIfNullOrEmpty(person.Name);
|
||||
|
||||
person.Name = person.Name.Trim();
|
||||
|
||||
// Normalize
|
||||
if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
@ -257,7 +257,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
|
||||
if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
|
||||
{
|
||||
IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path);
|
||||
IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path, ParentId);
|
||||
|
||||
// If a change was made record it
|
||||
if (IndexNumber.HasValue)
|
||||
|
17
MediaBrowser.Controller/IO/IPathManager.cs
Normal file
17
MediaBrowser.Controller/IO/IPathManager.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Interface ITrickplayManager.
|
||||
/// </summary>
|
||||
public interface IPathManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the path to the trickplay image base folder.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param>
|
||||
/// <returns>The absolute path.</returns>
|
||||
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false);
|
||||
}
|
@ -426,8 +426,9 @@ namespace MediaBrowser.Controller.Library
|
||||
/// Gets the season number from path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="parentId">The parent id.</param>
|
||||
/// <returns>System.Nullable<System.Int32>.</returns>
|
||||
int? GetSeasonNumberFromPath(string path);
|
||||
int? GetSeasonNumberFromPath(string path, Guid? parentId);
|
||||
|
||||
/// <summary>
|
||||
/// Fills the missing episode numbers from path.
|
||||
@ -565,6 +566,15 @@ namespace MediaBrowser.Controller.Library
|
||||
/// <returns>List of items.</returns>
|
||||
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of series presentation keys for next up.
|
||||
/// </summary>
|
||||
/// <param name="query">The query to use.</param>
|
||||
/// <param name="parents">Items to use for query.</param>
|
||||
/// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param>
|
||||
/// <returns>List of series presentation keys.</returns>
|
||||
IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the items result.
|
||||
/// </summary>
|
||||
|
@ -59,6 +59,14 @@ public interface IItemRepository
|
||||
/// <returns>List<BaseItem>.</returns>
|
||||
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery filter);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of series presentation keys for next up.
|
||||
/// </summary>
|
||||
/// <param name="filter">The query.</param>
|
||||
/// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param>
|
||||
/// <returns>The list of keys.</returns>
|
||||
IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the inherited values.
|
||||
/// </summary>
|
||||
|
@ -31,12 +31,6 @@ namespace MediaBrowser.Controller.Providers
|
||||
/// </remarks>
|
||||
ExternalIdMediaType? Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL format string for this id.
|
||||
/// </summary>
|
||||
[Obsolete("Obsolete in 10.10, to be removed in 10.11")]
|
||||
string? UrlFormatString { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this id supports a given item type.
|
||||
/// </summary>
|
||||
|
@ -234,8 +234,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
item.CustomRating = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "RunningTime":
|
||||
var runtimeText = reader.ReadElementContentAsString();
|
||||
if (!string.IsNullOrWhiteSpace(runtimeText))
|
||||
var runtimeText = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(runtimeText))
|
||||
{
|
||||
if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
|
||||
{
|
||||
@ -253,7 +253,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
|
||||
break;
|
||||
case "LockData":
|
||||
item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||
item.IsLocked = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
case "Network":
|
||||
foreach (var name in reader.GetStringArray())
|
||||
@ -331,9 +331,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
case "Rating":
|
||||
case "IMDBrating":
|
||||
{
|
||||
var rating = reader.ReadElementContentAsString();
|
||||
var rating = reader.ReadNormalizedString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(rating))
|
||||
if (!string.IsNullOrEmpty(rating))
|
||||
{
|
||||
// All external meta is saving this as '.' for decimal I believe...but just to be sure
|
||||
if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
|
||||
@ -449,7 +449,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
|
||||
case "OwnerUserId":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
var val = reader.ReadNormalizedString();
|
||||
|
||||
if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty))
|
||||
{
|
||||
@ -464,7 +464,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
|
||||
case "Format3D":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
var val = reader.ReadNormalizedString();
|
||||
|
||||
if (item is Video video)
|
||||
{
|
||||
@ -498,7 +498,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
string readerName = reader.Name;
|
||||
if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
|
||||
{
|
||||
var id = reader.ReadElementContentAsString();
|
||||
var id = reader.ReadNormalizedString();
|
||||
item.TrySetProviderId(providerIdValue, id);
|
||||
}
|
||||
else
|
||||
@ -580,7 +580,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Tagline":
|
||||
item.Tagline = reader.ReadNormalizedString();
|
||||
var val = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
item.Tagline = val;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
@ -842,7 +847,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
userId = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "CanEdit":
|
||||
canEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||
canEdit = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
@ -856,7 +861,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
}
|
||||
|
||||
// This is valid
|
||||
if (!string.IsNullOrWhiteSpace(userId) && Guid.TryParse(userId, out var guid))
|
||||
if (!string.IsNullOrEmpty(userId) && Guid.TryParse(userId, out var guid))
|
||||
{
|
||||
return new PlaylistUserPermissions(guid, canEdit);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@ -531,42 +532,44 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
private void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info)
|
||||
{
|
||||
List<BaseItemPerson> peoples = new List<BaseItemPerson>();
|
||||
var distinctPairs = pairs.Select(p => p.Value)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Trimmed()
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (string.Equals(key, "studio", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
info.Studios = pairs.Select(p => p.Value)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
info.Studios = distinctPairs.ToArray();
|
||||
}
|
||||
else if (string.Equals(key, "screenwriters", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var pair in pairs)
|
||||
foreach (var pair in distinctPairs)
|
||||
{
|
||||
peoples.Add(new BaseItemPerson
|
||||
{
|
||||
Name = pair.Value,
|
||||
Name = pair,
|
||||
Type = PersonKind.Writer
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (string.Equals(key, "producers", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var pair in pairs)
|
||||
foreach (var pair in distinctPairs)
|
||||
{
|
||||
peoples.Add(new BaseItemPerson
|
||||
{
|
||||
Name = pair.Value,
|
||||
Name = pair,
|
||||
Type = PersonKind.Producer
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (string.Equals(key, "directors", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var pair in pairs)
|
||||
foreach (var pair in distinctPairs)
|
||||
{
|
||||
peoples.Add(new BaseItemPerson
|
||||
{
|
||||
Name = pair.Value,
|
||||
Name = pair,
|
||||
Type = PersonKind.Director
|
||||
});
|
||||
}
|
||||
@ -591,10 +594,10 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "key":
|
||||
name = reader.ReadElementContentAsString();
|
||||
name = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "string":
|
||||
value = reader.ReadElementContentAsString();
|
||||
value = reader.ReadNormalizedString();
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
@ -607,8 +610,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name)
|
||||
|| string.IsNullOrWhiteSpace(value))
|
||||
if (string.IsNullOrEmpty(name)
|
||||
|| string.IsNullOrEmpty(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -1453,7 +1456,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
var genres = new List<string>(info.Genres);
|
||||
foreach (var genre in Split(genreVal, true))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(genre))
|
||||
if (string.IsNullOrEmpty(genre))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Providers
|
||||
{
|
||||
/// <summary>
|
||||
@ -13,15 +11,11 @@ namespace MediaBrowser.Model.Providers
|
||||
/// <param name="name">Name of the external id provider (IE: IMDB, MusicBrainz, etc).</param>
|
||||
/// <param name="key">Key for this id. This key should be unique across all providers.</param>
|
||||
/// <param name="type">Specific media type for this id.</param>
|
||||
/// <param name="urlFormatString">URL format string.</param>
|
||||
public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string? urlFormatString)
|
||||
public ExternalIdInfo(string name, string key, ExternalIdMediaType? type)
|
||||
{
|
||||
Name = name;
|
||||
Key = key;
|
||||
Type = type;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
|
||||
UrlFormatString = urlFormatString;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -46,11 +40,5 @@ namespace MediaBrowser.Model.Providers
|
||||
/// This can be used along with the <see cref="Name"/> to localize the external id on the client.
|
||||
/// </remarks>
|
||||
public ExternalIdMediaType? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL format string.
|
||||
/// </summary>
|
||||
[Obsolete("Obsolete in 10.10, to be removed in 10.11")]
|
||||
public string? UrlFormatString { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -4,76 +4,69 @@ using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Model.Querying
|
||||
namespace MediaBrowser.Model.Querying;
|
||||
|
||||
public class NextUpQuery
|
||||
{
|
||||
public class NextUpQuery
|
||||
public NextUpQuery()
|
||||
{
|
||||
public NextUpQuery()
|
||||
{
|
||||
EnableImageTypes = Array.Empty<ImageType>();
|
||||
EnableTotalRecordCount = true;
|
||||
DisableFirstEpisode = false;
|
||||
NextUpDateCutoff = DateTime.MinValue;
|
||||
EnableResumable = false;
|
||||
EnableRewatching = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user.
|
||||
/// </summary>
|
||||
/// <value>The user.</value>
|
||||
public required User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent identifier.
|
||||
/// </summary>
|
||||
/// <value>The parent identifier.</value>
|
||||
public Guid? ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the series id.
|
||||
/// </summary>
|
||||
/// <value>The series id.</value>
|
||||
public Guid? SeriesId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start index. Use for paging.
|
||||
/// </summary>
|
||||
/// <value>The start index.</value>
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of items to return.
|
||||
/// </summary>
|
||||
/// <value>The limit.</value>
|
||||
public int? Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the enable image types.
|
||||
/// </summary>
|
||||
/// <value>The enable image types.</value>
|
||||
public ImageType[] EnableImageTypes { get; set; }
|
||||
|
||||
public bool EnableTotalRecordCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether do disable sending first episode as next up.
|
||||
/// </summary>
|
||||
public bool DisableFirstEpisode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the oldest date for a show to appear in Next Up.
|
||||
/// </summary>
|
||||
public DateTime NextUpDateCutoff { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to include resumable episodes as next up.
|
||||
/// </summary>
|
||||
public bool EnableResumable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether getting rewatching next up list.
|
||||
/// </summary>
|
||||
public bool EnableRewatching { get; set; }
|
||||
EnableImageTypes = Array.Empty<ImageType>();
|
||||
EnableTotalRecordCount = true;
|
||||
NextUpDateCutoff = DateTime.MinValue;
|
||||
EnableResumable = false;
|
||||
EnableRewatching = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user.
|
||||
/// </summary>
|
||||
/// <value>The user.</value>
|
||||
public required User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent identifier.
|
||||
/// </summary>
|
||||
/// <value>The parent identifier.</value>
|
||||
public Guid? ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the series id.
|
||||
/// </summary>
|
||||
/// <value>The series id.</value>
|
||||
public Guid? SeriesId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start index. Use for paging.
|
||||
/// </summary>
|
||||
/// <value>The start index.</value>
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of items to return.
|
||||
/// </summary>
|
||||
/// <value>The limit.</value>
|
||||
public int? Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the enable image types.
|
||||
/// </summary>
|
||||
/// <value>The enable image types.</value>
|
||||
public ImageType[] EnableImageTypes { get; set; }
|
||||
|
||||
public bool EnableTotalRecordCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the oldest date for a show to appear in Next Up.
|
||||
/// </summary>
|
||||
public DateTime NextUpDateCutoff { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to include resumable episodes as next up.
|
||||
/// </summary>
|
||||
public bool EnableResumable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether getting rewatching next up list.
|
||||
/// </summary>
|
||||
public bool EnableRewatching { get; set; }
|
||||
}
|
||||
|
@ -1146,13 +1146,24 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target)
|
||||
{
|
||||
foreach (var person in target)
|
||||
{
|
||||
var normalizedName = person.Name.RemoveDiacritics();
|
||||
var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.OrdinalIgnoreCase));
|
||||
var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
|
||||
var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (personInSource is not null)
|
||||
foreach (var name in targetByName.Select(g => g.Key))
|
||||
{
|
||||
var targetPeople = targetByName[name].ToArray();
|
||||
var sourcePeople = sourceByName[name].ToArray();
|
||||
|
||||
if (sourcePeople.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < targetPeople.Length; i++)
|
||||
{
|
||||
var person = targetPeople[i];
|
||||
var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0];
|
||||
|
||||
foreach (var providerId in personInSource.ProviderIds)
|
||||
{
|
||||
person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
|
||||
@ -1162,6 +1173,16 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
person.ImageUrl = personInSource.ImageUrl;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
|
||||
{
|
||||
person.Role = personInSource.Role;
|
||||
}
|
||||
|
||||
if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
|
||||
{
|
||||
person.SortOrder = personInSource.SortOrder;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -899,35 +899,10 @@ namespace MediaBrowser.Providers.Manager
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
|
||||
var legacyExternalIdUrls = GetExternalIds(item)
|
||||
.Select(i =>
|
||||
{
|
||||
var urlFormatString = i.UrlFormatString;
|
||||
if (string.IsNullOrEmpty(urlFormatString)
|
||||
|| !item.TryGetProviderId(i.Key, out var providerId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ExternalUrl
|
||||
{
|
||||
Name = i.ProviderName,
|
||||
Url = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
urlFormatString,
|
||||
providerId)
|
||||
};
|
||||
})
|
||||
.OfType<ExternalUrl>();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
var externalUrls = _externalUrlProviders
|
||||
return _externalUrlProviders
|
||||
.SelectMany(p => p
|
||||
.GetExternalUrls(item)
|
||||
.Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl }));
|
||||
|
||||
return legacyExternalIdUrls.Concat(externalUrls).OrderBy(u => u.Name);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@ -937,10 +912,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
.Select(i => new ExternalIdInfo(
|
||||
name: i.ProviderName,
|
||||
key: i.Key,
|
||||
type: i.Type,
|
||||
#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
|
||||
urlFormatString: i.UrlFormatString));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
type: i.Type));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -6,6 +6,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ATL;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@ -175,11 +176,15 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
_logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
|
||||
}
|
||||
|
||||
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
|
||||
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
|
||||
track.Year = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
|
||||
track.TrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
|
||||
track.DiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
|
||||
// We should never use the property setter of the ATL.Track class.
|
||||
// That setter is meant for its own tag parser and external editor usage and will have unwanted side effects
|
||||
// For example, setting the Year property will also set the Date property, which is not what we want here.
|
||||
// To properly handle fallback values, we make a clone of those fields when valid.
|
||||
var trackTitle = (string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title).Trim();
|
||||
var trackAlbum = (string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album).Trim();
|
||||
var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
|
||||
var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
|
||||
var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
|
||||
|
||||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||
{
|
||||
@ -193,11 +198,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
foreach (var albumArtist in albumArtists)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(albumArtist))
|
||||
if (!string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = albumArtist,
|
||||
Name = albumArtist.Trim(),
|
||||
Type = PersonKind.AlbumArtist
|
||||
});
|
||||
}
|
||||
@ -225,11 +230,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
foreach (var performer in performers)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(performer))
|
||||
if (!string.IsNullOrWhiteSpace(performer))
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = performer,
|
||||
Name = performer.Trim(),
|
||||
Type = PersonKind.Artist
|
||||
});
|
||||
}
|
||||
@ -237,11 +242,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
foreach (var composer in track.Composer.Split(InternalValueSeparator))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(composer))
|
||||
if (!string.IsNullOrWhiteSpace(composer))
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = composer,
|
||||
Name = composer.Trim(),
|
||||
Type = PersonKind.Composer
|
||||
});
|
||||
}
|
||||
@ -276,22 +281,22 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
}
|
||||
|
||||
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title))
|
||||
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(trackTitle))
|
||||
{
|
||||
audio.Name = track.Title;
|
||||
audio.Name = trackTitle;
|
||||
}
|
||||
|
||||
if (options.ReplaceAllMetadata)
|
||||
{
|
||||
audio.Album = track.Album;
|
||||
audio.IndexNumber = track.TrackNumber;
|
||||
audio.ParentIndexNumber = track.DiscNumber;
|
||||
audio.Album = trackAlbum;
|
||||
audio.IndexNumber = trackTrackNumber;
|
||||
audio.ParentIndexNumber = trackDiscNumber;
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.Album ??= track.Album;
|
||||
audio.IndexNumber ??= track.TrackNumber;
|
||||
audio.ParentIndexNumber ??= track.DiscNumber;
|
||||
audio.Album ??= trackAlbum;
|
||||
audio.IndexNumber ??= trackTrackNumber;
|
||||
audio.ParentIndexNumber ??= trackDiscNumber;
|
||||
}
|
||||
|
||||
if (track.Date.HasValue)
|
||||
@ -299,11 +304,12 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
audio.PremiereDate = track.Date;
|
||||
}
|
||||
|
||||
if (track.Year.HasValue)
|
||||
if (trackYear.HasValue)
|
||||
{
|
||||
var year = track.Year.Value;
|
||||
var year = trackYear.Value;
|
||||
audio.ProductionYear = year;
|
||||
|
||||
// ATL library handles such fallback this with its own internal logic, but we also need to handle it here for the ffprobe fallbacks.
|
||||
if (!audio.PremiereDate.HasValue)
|
||||
{
|
||||
try
|
||||
@ -312,7 +318,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year);
|
||||
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, trackYear);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -326,6 +332,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray();
|
||||
}
|
||||
|
||||
genres = genres.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
|
||||
audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
|
||||
? genres
|
||||
: audio.Genres;
|
||||
|
@ -6,6 +6,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@ -407,7 +408,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
{
|
||||
video.Genres = Array.Empty<string>();
|
||||
|
||||
foreach (var genre in data.Genres)
|
||||
foreach (var genre in data.Genres.Trimmed())
|
||||
{
|
||||
video.AddGenre(genre);
|
||||
}
|
||||
@ -516,9 +517,9 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = person.Name,
|
||||
Name = person.Name.Trim(),
|
||||
Type = person.Type,
|
||||
Role = person.Role
|
||||
Role = person.Role.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,6 @@ namespace MediaBrowser.Providers.Movies
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://www.imdb.com/title/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item)
|
||||
{
|
||||
|
32
MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs
Normal file
32
MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Movies;
|
||||
|
||||
/// <summary>
|
||||
/// External URLs for IMDb.
|
||||
/// </summary>
|
||||
public class ImdbExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "IMDb";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
var baseUrl = "https://www.imdb.com/";
|
||||
if (item.TryGetProviderId(MetadataProvider.Imdb, out var externalId))
|
||||
{
|
||||
if (item is Person)
|
||||
{
|
||||
yield return baseUrl + $"name/{externalId}";
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return baseUrl + $"title/{externalId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Movies
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://www.imdb.com/name/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Person;
|
||||
}
|
||||
|
@ -187,7 +187,7 @@ namespace MediaBrowser.Providers.Music
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = albumArtist,
|
||||
Name = albumArtist.Trim(),
|
||||
Type = PersonKind.AlbumArtist
|
||||
});
|
||||
}
|
||||
@ -196,7 +196,7 @@ namespace MediaBrowser.Providers.Music
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = artist,
|
||||
Name = artist.Trim(),
|
||||
Type = PersonKind.Artist
|
||||
});
|
||||
}
|
||||
|
@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Music
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? UrlFormatString => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item)
|
||||
=> item is MusicVideo;
|
||||
|
@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is MusicAlbum;
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.AudioDb;
|
||||
|
||||
/// <summary>
|
||||
/// External artist URLs for AudioDb.
|
||||
/// </summary>
|
||||
public class AudioDbAlbumExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "TheAudioDb Album";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out var externalId))
|
||||
{
|
||||
var baseUrl = "https://www.theaudiodb.com/";
|
||||
switch (item)
|
||||
{
|
||||
case MusicAlbum:
|
||||
yield return baseUrl + $"album/{externalId}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@ -50,9 +49,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var id = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(id))
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var id))
|
||||
{
|
||||
await AudioDbAlbumProvider.Current.EnsureInfo(id, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@ -70,7 +67,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
}
|
||||
}
|
||||
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
return [];
|
||||
}
|
||||
|
||||
private List<RemoteImageInfo> GetImages(AudioDbAlbumProvider.Album item)
|
||||
|
@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is MusicArtist;
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.AudioDb;
|
||||
|
||||
/// <summary>
|
||||
/// External artist URLs for AudioDb.
|
||||
/// </summary>
|
||||
public class AudioDbArtistExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "TheAudioDb Artist";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId))
|
||||
{
|
||||
var baseUrl = "https://www.theaudiodb.com/";
|
||||
switch (item)
|
||||
{
|
||||
case MusicAlbum:
|
||||
case Person:
|
||||
yield return baseUrl + $"artist/{externalId}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@ -43,21 +42,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
|
||||
{
|
||||
return new ImageType[]
|
||||
{
|
||||
return
|
||||
[
|
||||
ImageType.Primary,
|
||||
ImageType.Logo,
|
||||
ImageType.Banner,
|
||||
ImageType.Backdrop
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(id))
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var id))
|
||||
{
|
||||
await AudioDbArtistProvider.Current.EnsureArtistInfo(id, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@ -75,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
}
|
||||
}
|
||||
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
return [];
|
||||
}
|
||||
|
||||
private List<RemoteImageInfo> GetImages(AudioDbArtistProvider.Artist item)
|
||||
|
@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Audio;
|
||||
}
|
||||
|
@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
|
||||
}
|
||||
|
@ -19,9 +19,6 @@ public class MusicBrainzAlbumArtistExternalId : IExternalId
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Audio;
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
|
||||
/// <summary>
|
||||
/// External album artist URLs for MusicBrainz.
|
||||
/// </summary>
|
||||
public class MusicBrainzAlbumArtistExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "MusicBrainz Album Artist";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item is MusicAlbum)
|
||||
{
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out var externalId))
|
||||
{
|
||||
yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,9 +19,6 @@ public class MusicBrainzAlbumExternalId : IExternalId
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
|
||||
/// <summary>
|
||||
/// External album URLs for MusicBrainz.
|
||||
/// </summary>
|
||||
public class MusicBrainzAlbumExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "MusicBrainz Album";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item is MusicAlbum)
|
||||
{
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out var externalId))
|
||||
{
|
||||
yield return Plugin.Instance!.Configuration.Server + $"/release/{externalId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,9 +19,6 @@ public class MusicBrainzArtistExternalId : IExternalId
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is MusicArtist;
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
|
||||
/// <summary>
|
||||
/// External artist URLs for MusicBrainz.
|
||||
/// </summary>
|
||||
public class MusicBrainzArtistExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "MusicBrainz Artist";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId))
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case MusicAlbum:
|
||||
case Person:
|
||||
yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}";
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,9 +19,6 @@ public class MusicBrainzOtherArtistExternalId : IExternalId
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
|
||||
}
|
||||
|
@ -19,9 +19,6 @@ public class MusicBrainzRecordingId : IExternalId
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Recording;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/recording/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Audio;
|
||||
}
|
||||
|
@ -19,9 +19,6 @@ public class MusicBrainzReleaseGroupExternalId : IExternalId
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
|
||||
/// <summary>
|
||||
/// External release group URLs for MusicBrainz.
|
||||
/// </summary>
|
||||
public class MusicBrainzReleaseGroupExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "MusicBrainz Release Group";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item is MusicAlbum)
|
||||
{
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var externalId))
|
||||
{
|
||||
yield return Plugin.Instance!.Configuration.Server + $"/release-group/{externalId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
|
||||
/// <summary>
|
||||
/// External track URLs for MusicBrainz.
|
||||
/// </summary>
|
||||
public class MusicBrainzTrackExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "MusicBrainz Track";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item is Audio)
|
||||
{
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out var externalId))
|
||||
{
|
||||
yield return Plugin.Instance!.Configuration.Server + $"/track/{externalId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,9 +19,6 @@ public class MusicBrainzTrackId : IExternalId
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Audio;
|
||||
}
|
||||
|
@ -421,7 +421,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||
{
|
||||
var person = new PersonInfo
|
||||
{
|
||||
Name = result.Director,
|
||||
Name = result.Director.Trim(),
|
||||
Type = PersonKind.Director
|
||||
};
|
||||
|
||||
@ -432,7 +432,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||
{
|
||||
var person = new PersonInfo
|
||||
{
|
||||
Name = result.Writer,
|
||||
Name = result.Writer.Trim(),
|
||||
Type = PersonKind.Writer
|
||||
};
|
||||
|
||||
|
@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item)
|
||||
{
|
||||
|
@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item)
|
||||
{
|
||||
|
@ -234,7 +234,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
|
||||
var genres = movieResult.Genres;
|
||||
|
||||
foreach (var genre in genres.Select(g => g.Name))
|
||||
foreach (var genre in genres.Select(g => g.Name).Trimmed())
|
||||
{
|
||||
movie.AddGenre(genre);
|
||||
}
|
||||
@ -254,7 +254,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = actor.Name.Trim(),
|
||||
Role = actor.Character,
|
||||
Role = actor.Character.Trim(),
|
||||
Type = PersonKind.Actor,
|
||||
SortOrder = actor.Order
|
||||
};
|
||||
@ -289,7 +289,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = person.Name.Trim(),
|
||||
Role = person.Job,
|
||||
Role = person.Job?.Trim(),
|
||||
Type = type
|
||||
};
|
||||
|
||||
|
@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item)
|
||||
{
|
||||
|
@ -211,7 +211,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
metadataResult.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = actor.Name.Trim(),
|
||||
Role = actor.Character,
|
||||
Role = actor.Character.Trim(),
|
||||
Type = PersonKind.Actor,
|
||||
SortOrder = actor.Order
|
||||
});
|
||||
@ -225,7 +225,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
metadataResult.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = guest.Name.Trim(),
|
||||
Role = guest.Character,
|
||||
Role = guest.Character.Trim(),
|
||||
Type = PersonKind.GuestStar,
|
||||
SortOrder = guest.Order
|
||||
});
|
||||
@ -249,7 +249,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
metadataResult.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = person.Name.Trim(),
|
||||
Role = person.Job,
|
||||
Role = person.Job?.Trim(),
|
||||
Type = type
|
||||
});
|
||||
}
|
||||
|
@ -82,12 +82,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList();
|
||||
for (var i = 0; i < cast.Count; i++)
|
||||
{
|
||||
var member = cast[i];
|
||||
result.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = cast[i].Name.Trim(),
|
||||
Role = cast[i].Character,
|
||||
Name = member.Name.Trim(),
|
||||
Role = member.Character.Trim(),
|
||||
Type = PersonKind.Actor,
|
||||
SortOrder = cast[i].Order
|
||||
SortOrder = member.Order
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -108,7 +109,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
result.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = person.Name.Trim(),
|
||||
Role = person.Job,
|
||||
Role = person.Job?.Trim(),
|
||||
Type = type
|
||||
});
|
||||
}
|
||||
|
@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Series;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item)
|
||||
{
|
||||
|
@ -330,7 +330,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = actor.Name.Trim(),
|
||||
Role = actor.Character,
|
||||
Role = actor.Character.Trim(),
|
||||
Type = PersonKind.Actor,
|
||||
SortOrder = actor.Order,
|
||||
ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath)
|
||||
@ -368,7 +368,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
yield return new PersonInfo
|
||||
{
|
||||
Name = person.Name.Trim(),
|
||||
Role = person.Job,
|
||||
Role = person.Job?.Trim(),
|
||||
Type = type
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using TMDbLib.Objects.TvShows;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.Tmdb;
|
||||
|
||||
/// <summary>
|
||||
/// External URLs for TMDb.
|
||||
/// </summary>
|
||||
public class TmdbExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "TMDB";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case Series:
|
||||
if (item.TryGetProviderId(MetadataProvider.Tmdb, out var externalId))
|
||||
{
|
||||
yield return TmdbUtils.BaseTmdbUrl + $"tv/{externalId}";
|
||||
}
|
||||
|
||||
break;
|
||||
case Season season:
|
||||
if (season.Series.TryGetProviderId(MetadataProvider.Tmdb, out var seriesExternalId))
|
||||
{
|
||||
var orderString = season.Series.DisplayOrder;
|
||||
if (string.IsNullOrEmpty(orderString))
|
||||
{
|
||||
// Default order is airdate
|
||||
yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}";
|
||||
}
|
||||
|
||||
if (Enum.TryParse<TvGroupType>(season.Series.DisplayOrder, out var order))
|
||||
{
|
||||
if (order.Equals(TvGroupType.OriginalAirDate))
|
||||
{
|
||||
yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case Episode episode:
|
||||
if (episode.Series.TryGetProviderId(MetadataProvider.Imdb, out seriesExternalId))
|
||||
{
|
||||
var orderString = episode.Series.DisplayOrder;
|
||||
if (string.IsNullOrEmpty(orderString))
|
||||
{
|
||||
// Default order is airdate
|
||||
yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}";
|
||||
}
|
||||
|
||||
if (Enum.TryParse<TvGroupType>(orderString, out var order))
|
||||
{
|
||||
if (order.Equals(TvGroupType.OriginalAirDate))
|
||||
{
|
||||
yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case Movie:
|
||||
if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId))
|
||||
{
|
||||
yield return TmdbUtils.BaseTmdbUrl + $"movie/{externalId}";
|
||||
}
|
||||
|
||||
break;
|
||||
case Person:
|
||||
if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId))
|
||||
{
|
||||
yield return TmdbUtils.BaseTmdbUrl + $"person/{externalId}";
|
||||
}
|
||||
|
||||
break;
|
||||
case BoxSet:
|
||||
if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId))
|
||||
{
|
||||
yield return TmdbUtils.BaseTmdbUrl + $"collection/{externalId}";
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.TV
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Series;
|
||||
}
|
||||
|
24
MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs
Normal file
24
MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.TV;
|
||||
|
||||
/// <summary>
|
||||
/// External URLs for TMDb.
|
||||
/// </summary>
|
||||
public class Zap2ItExternalUrlProvider : IExternalUrlProvider
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Zap2It";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> GetExternalUrls(BaseItem item)
|
||||
{
|
||||
if (item.TryGetProviderId(MetadataProvider.Zap2It, out var externalId))
|
||||
{
|
||||
yield return $"http://tvlistings.zap2it.com/overview.html?programSeriesId={externalId}";
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
@ -55,12 +56,12 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
{
|
||||
var album = (MusicAlbum)item;
|
||||
|
||||
foreach (var artist in album.Artists)
|
||||
foreach (var artist in album.Artists.Trimmed().OrderBy(artist => artist))
|
||||
{
|
||||
writer.WriteElementString("artist", artist);
|
||||
}
|
||||
|
||||
foreach (var artist in album.AlbumArtists)
|
||||
foreach (var artist in album.AlbumArtists.Trimmed().OrderBy(artist => artist))
|
||||
{
|
||||
writer.WriteElementString("albumartist", artist);
|
||||
}
|
||||
@ -70,11 +71,20 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
|
||||
private void AddTracks(IEnumerable<BaseItem> tracks, XmlWriter writer)
|
||||
{
|
||||
foreach (var track in tracks.OrderBy(i => i.ParentIndexNumber ?? 0).ThenBy(i => i.IndexNumber ?? 0))
|
||||
foreach (var track in tracks
|
||||
.OrderBy(i => i.ParentIndexNumber ?? 0)
|
||||
.ThenBy(i => i.IndexNumber ?? 0)
|
||||
.ThenBy(i => SortNameOrName(i))
|
||||
.ThenBy(i => i.Name?.Trim()))
|
||||
{
|
||||
writer.WriteStartElement("track");
|
||||
|
||||
if (track.IndexNumber.HasValue)
|
||||
if (track.ParentIndexNumber.HasValue && track.ParentIndexNumber.Value != 0)
|
||||
{
|
||||
writer.WriteElementString("disc", track.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (track.IndexNumber.HasValue && track.IndexNumber.Value != 0)
|
||||
{
|
||||
writer.WriteElementString("position", track.IndexNumber.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -69,7 +70,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
|
||||
private void AddAlbums(IReadOnlyList<BaseItem> albums, XmlWriter writer)
|
||||
{
|
||||
foreach (var album in albums)
|
||||
foreach (var album in albums
|
||||
.OrderBy(album => album.ProductionYear ?? 0)
|
||||
.ThenBy(album => SortNameOrName(album))
|
||||
.ThenBy(album => album.Name?.Trim()))
|
||||
{
|
||||
writer.WriteStartElement("album");
|
||||
|
||||
|
@ -488,7 +488,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
|
||||
var directors = people
|
||||
.Where(i => i.IsType(PersonKind.Director))
|
||||
.Select(i => i.Name)
|
||||
.Select(i => i.Name?.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(i => i)
|
||||
.ToList();
|
||||
|
||||
foreach (var person in directors)
|
||||
@ -498,8 +500,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
|
||||
var writers = people
|
||||
.Where(i => i.IsType(PersonKind.Writer))
|
||||
.Select(i => i.Name)
|
||||
.Select(i => i.Name?.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(i => i)
|
||||
.ToList();
|
||||
|
||||
foreach (var person in writers)
|
||||
@ -512,7 +515,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
writer.WriteElementString("credits", person);
|
||||
}
|
||||
|
||||
foreach (var trailer in item.RemoteTrailers)
|
||||
foreach (var trailer in item.RemoteTrailers.OrderBy(t => t.Url?.Trim()))
|
||||
{
|
||||
writer.WriteElementString("trailer", GetOutputTrailerUrl(trailer.Url));
|
||||
}
|
||||
@ -544,16 +547,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
|
||||
}
|
||||
|
||||
var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection);
|
||||
|
||||
if (!string.IsNullOrEmpty(tmdbCollection))
|
||||
if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection))
|
||||
{
|
||||
writer.WriteElementString("collectionnumber", tmdbCollection);
|
||||
writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());
|
||||
}
|
||||
|
||||
var imdb = item.GetProviderId(MetadataProvider.Imdb);
|
||||
if (!string.IsNullOrEmpty(imdb))
|
||||
if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
|
||||
{
|
||||
if (item is Series)
|
||||
{
|
||||
@ -570,16 +570,14 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
// Series xml saver already saves this
|
||||
if (item is not Series)
|
||||
{
|
||||
var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
|
||||
if (!string.IsNullOrEmpty(tvdb))
|
||||
if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
|
||||
{
|
||||
writer.WriteElementString("tvdbid", tvdb);
|
||||
writtenProviderIds.Add(MetadataProvider.Tvdb.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
var tmdb = item.GetProviderId(MetadataProvider.Tmdb);
|
||||
if (!string.IsNullOrEmpty(tmdb))
|
||||
if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb))
|
||||
{
|
||||
writer.WriteElementString("tmdbid", tmdb);
|
||||
writtenProviderIds.Add(MetadataProvider.Tmdb.ToString());
|
||||
@ -660,22 +658,22 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
writer.WriteElementString("tagline", item.Tagline);
|
||||
}
|
||||
|
||||
foreach (var country in item.ProductionLocations)
|
||||
foreach (var country in item.ProductionLocations.Trimmed().OrderBy(country => country))
|
||||
{
|
||||
writer.WriteElementString("country", country);
|
||||
}
|
||||
|
||||
foreach (var genre in item.Genres)
|
||||
foreach (var genre in item.Genres.Trimmed().OrderBy(genre => genre))
|
||||
{
|
||||
writer.WriteElementString("genre", genre);
|
||||
}
|
||||
|
||||
foreach (var studio in item.Studios)
|
||||
foreach (var studio in item.Studios.Trimmed().OrderBy(studio => studio))
|
||||
{
|
||||
writer.WriteElementString("studio", studio);
|
||||
}
|
||||
|
||||
foreach (var tag in item.Tags)
|
||||
foreach (var tag in item.Tags.Trimmed().OrderBy(tag => tag))
|
||||
{
|
||||
if (item is MusicAlbum || item is MusicArtist)
|
||||
{
|
||||
@ -687,64 +685,49 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
}
|
||||
}
|
||||
|
||||
var externalId = item.GetProviderId(MetadataProvider.AudioDbArtist);
|
||||
|
||||
if (!string.IsNullOrEmpty(externalId))
|
||||
if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId))
|
||||
{
|
||||
writer.WriteElementString("audiodbartistid", externalId);
|
||||
writtenProviderIds.Add(MetadataProvider.AudioDbArtist.ToString());
|
||||
}
|
||||
|
||||
externalId = item.GetProviderId(MetadataProvider.AudioDbAlbum);
|
||||
|
||||
if (!string.IsNullOrEmpty(externalId))
|
||||
if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out externalId))
|
||||
{
|
||||
writer.WriteElementString("audiodbalbumid", externalId);
|
||||
writtenProviderIds.Add(MetadataProvider.AudioDbAlbum.ToString());
|
||||
}
|
||||
|
||||
externalId = item.GetProviderId(MetadataProvider.Zap2It);
|
||||
|
||||
if (!string.IsNullOrEmpty(externalId))
|
||||
if (item.TryGetProviderId(MetadataProvider.Zap2It, out externalId))
|
||||
{
|
||||
writer.WriteElementString("zap2itid", externalId);
|
||||
writtenProviderIds.Add(MetadataProvider.Zap2It.ToString());
|
||||
}
|
||||
|
||||
externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbum);
|
||||
|
||||
if (!string.IsNullOrEmpty(externalId))
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out externalId))
|
||||
{
|
||||
writer.WriteElementString("musicbrainzalbumid", externalId);
|
||||
writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbum.ToString());
|
||||
}
|
||||
|
||||
externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist);
|
||||
|
||||
if (!string.IsNullOrEmpty(externalId))
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out externalId))
|
||||
{
|
||||
writer.WriteElementString("musicbrainzalbumartistid", externalId);
|
||||
writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbumArtist.ToString());
|
||||
}
|
||||
|
||||
externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist);
|
||||
|
||||
if (!string.IsNullOrEmpty(externalId))
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out externalId))
|
||||
{
|
||||
writer.WriteElementString("musicbrainzartistid", externalId);
|
||||
writtenProviderIds.Add(MetadataProvider.MusicBrainzArtist.ToString());
|
||||
}
|
||||
|
||||
externalId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup);
|
||||
|
||||
if (!string.IsNullOrEmpty(externalId))
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out externalId))
|
||||
{
|
||||
writer.WriteElementString("musicbrainzreleasegroupid", externalId);
|
||||
writtenProviderIds.Add(MetadataProvider.MusicBrainzReleaseGroup.ToString());
|
||||
}
|
||||
|
||||
externalId = item.GetProviderId(MetadataProvider.TvRage);
|
||||
if (!string.IsNullOrEmpty(externalId))
|
||||
if (item.TryGetProviderId(MetadataProvider.TvRage, out externalId))
|
||||
{
|
||||
writer.WriteElementString("tvrageid", externalId);
|
||||
writtenProviderIds.Add(MetadataProvider.TvRage.ToString());
|
||||
@ -752,7 +735,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
|
||||
if (item.ProviderIds is not null)
|
||||
{
|
||||
foreach (var providerKey in item.ProviderIds.Keys)
|
||||
foreach (var providerKey in item.ProviderIds.Keys.OrderBy(providerKey => providerKey))
|
||||
{
|
||||
var providerId = item.ProviderIds[providerKey];
|
||||
if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey))
|
||||
@ -764,7 +747,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
XmlConvert.VerifyName(tagName);
|
||||
Logger.LogDebug("Saving custom provider tagname {0}", tagName);
|
||||
|
||||
writer.WriteElementString(GetTagForProviderKey(providerKey), providerId);
|
||||
writer.WriteElementString(tagName, providerId);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
@ -785,7 +768,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
|
||||
AddUserData(item, writer, userManager, userDataRepo, options);
|
||||
|
||||
AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo);
|
||||
if (item is not MusicAlbum && item is not MusicArtist)
|
||||
{
|
||||
AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo);
|
||||
}
|
||||
|
||||
if (item is BoxSet folder)
|
||||
{
|
||||
@ -797,6 +783,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
{
|
||||
var items = item.LinkedChildren
|
||||
.Where(i => i.Type == LinkedChildType.Manual)
|
||||
.OrderBy(i => i.Path?.Trim())
|
||||
.ThenBy(i => i.LibraryItemId?.Trim())
|
||||
.ToList();
|
||||
|
||||
foreach (var link in items)
|
||||
@ -839,7 +827,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager));
|
||||
}
|
||||
|
||||
foreach (var backdrop in item.GetImages(ImageType.Backdrop))
|
||||
foreach (var backdrop in item.GetImages(ImageType.Backdrop).OrderBy(b => b.Path?.Trim()))
|
||||
{
|
||||
writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager));
|
||||
}
|
||||
@ -916,7 +904,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
|
||||
private void AddActors(IReadOnlyList<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, bool saveImagePath)
|
||||
{
|
||||
foreach (var person in people)
|
||||
foreach (var person in people
|
||||
.OrderBy(person => person.SortOrder ?? 0)
|
||||
.ThenBy(person => person.Name?.Trim()))
|
||||
{
|
||||
if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer))
|
||||
{
|
||||
@ -1027,5 +1017,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
|
||||
private string GetTagForProviderKey(string providerKey)
|
||||
=> providerKey.ToLowerInvariant() + "id";
|
||||
|
||||
protected static string SortNameOrName(BaseItem item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (item.SortName != null)
|
||||
{
|
||||
string trimmed = item.SortName.Trim();
|
||||
if (trimmed.Length > 0)
|
||||
{
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
return (item.Name ?? string.Empty).Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
@ -91,16 +92,14 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
/// <inheritdoc />
|
||||
protected override void WriteCustomElements(BaseItem item, XmlWriter writer)
|
||||
{
|
||||
var imdb = item.GetProviderId(MetadataProvider.Imdb);
|
||||
|
||||
if (!string.IsNullOrEmpty(imdb))
|
||||
if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
|
||||
{
|
||||
writer.WriteElementString("id", imdb);
|
||||
}
|
||||
|
||||
if (item is MusicVideo musicVideo)
|
||||
{
|
||||
foreach (var artist in musicVideo.Artists)
|
||||
foreach (var artist in musicVideo.Artists.Trimmed().OrderBy(artist => artist))
|
||||
{
|
||||
writer.WriteElementString("artist", artist);
|
||||
}
|
||||
|
@ -54,9 +54,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
{
|
||||
var series = (Series)item;
|
||||
|
||||
var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
|
||||
|
||||
if (!string.IsNullOrEmpty(tvdb))
|
||||
if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
|
||||
{
|
||||
writer.WriteElementString("id", tvdb);
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using ICU4N.Text;
|
||||
|
||||
@ -123,5 +125,15 @@ namespace Jellyfin.Extensions
|
||||
{
|
||||
return (_transliterator.Value is null) ? text : _transliterator.Value.Transliterate(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures all strings are non-null and trimmed of leading an trailing blanks.
|
||||
/// </summary>
|
||||
/// <param name="values">The enumerable of strings to trim.</param>
|
||||
/// <returns>The enumeration of trimmed strings.</returns>
|
||||
public static IEnumerable<string> Trimmed(this IEnumerable<string> values)
|
||||
{
|
||||
return values.Select(i => (i ?? string.Empty).Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -344,15 +344,12 @@ public class RecordingsMetadataManager
|
||||
await writer.WriteElementStringAsync(null, "credits", null, person).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection);
|
||||
|
||||
if (!string.IsNullOrEmpty(tmdbCollection))
|
||||
if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "collectionnumber", null, tmdbCollection).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var imdb = item.GetProviderId(MetadataProvider.Imdb);
|
||||
if (!string.IsNullOrEmpty(imdb))
|
||||
if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
|
||||
{
|
||||
if (!isSeriesEpisode)
|
||||
{
|
||||
@ -365,8 +362,7 @@ public class RecordingsMetadataManager
|
||||
lockData = false;
|
||||
}
|
||||
|
||||
var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
|
||||
if (!string.IsNullOrEmpty(tvdb))
|
||||
if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "tvdbid", null, tvdb).ConfigureAwait(false);
|
||||
|
||||
@ -374,8 +370,7 @@ public class RecordingsMetadataManager
|
||||
lockData = false;
|
||||
}
|
||||
|
||||
var tmdb = item.GetProviderId(MetadataProvider.Tmdb);
|
||||
if (!string.IsNullOrEmpty(tmdb))
|
||||
if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "tmdbid", null, tmdb).ConfigureAwait(false);
|
||||
|
||||
|
@ -673,10 +673,10 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
{
|
||||
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
|
||||
// If left blank, all remote addresses will be allowed.
|
||||
if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP)))
|
||||
if (_remoteAddressFilter.Any() && !IsInLocalNetwork(remoteIP))
|
||||
{
|
||||
// remoteAddressFilter is a whitelist or blacklist.
|
||||
var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
|
||||
var matches = _remoteAddressFilter.Count(remoteNetwork => NetworkUtils.SubnetContainsAddress(remoteNetwork, remoteIP));
|
||||
if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
|
||||
|| (config.IsRemoteIPFilterBlacklist && matches == 0))
|
||||
{
|
||||
@ -793,7 +793,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
_logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
|
||||
}
|
||||
|
||||
bool isExternal = !_lanSubnets.Any(network => network.Contains(source));
|
||||
bool isExternal = !IsInLocalNetwork(source);
|
||||
_logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
|
||||
|
||||
if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
|
||||
@ -840,7 +840,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
// (For systems with multiple internal network cards, and multiple subnets)
|
||||
foreach (var intf in availableInterfaces)
|
||||
{
|
||||
if (intf.Subnet.Contains(source))
|
||||
if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source))
|
||||
{
|
||||
result = NetworkUtils.FormatIPString(intf.Address);
|
||||
_logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
|
||||
@ -868,21 +868,11 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
{
|
||||
if (NetworkUtils.TryParseToSubnet(address, out var subnet))
|
||||
{
|
||||
return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix)));
|
||||
return IsInLocalNetwork(subnet.Prefix);
|
||||
}
|
||||
|
||||
if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
|
||||
{
|
||||
foreach (var ept in addresses)
|
||||
{
|
||||
if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)
|
||||
&& addresses.Any(IsInLocalNetwork);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -917,6 +907,11 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
return CheckIfLanAndNotExcluded(address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the address is in the LAN and not excluded.
|
||||
/// </summary>
|
||||
/// <param name="address">The IP address to check. The caller should make sure this is not an IPv4MappedToIPv6 address.</param>
|
||||
/// <returns>Boolean indicates whether the address is in LAN.</returns>
|
||||
private bool CheckIfLanAndNotExcluded(IPAddress address)
|
||||
{
|
||||
foreach (var lanSubnet in _lanSubnets)
|
||||
@ -956,7 +951,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
{
|
||||
// Only use matching internal subnets
|
||||
// Prefer more specific (bigger subnet prefix) overrides
|
||||
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
|
||||
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && NetworkUtils.SubnetContainsAddress(x.Data.Subnet, source))
|
||||
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
||||
.ToList();
|
||||
}
|
||||
@ -964,7 +959,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
{
|
||||
// Only use matching external subnets
|
||||
// Prefer more specific (bigger subnet prefix) overrides
|
||||
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
|
||||
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && NetworkUtils.SubnetContainsAddress(x.Data.Subnet, source))
|
||||
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
||||
.ToList();
|
||||
}
|
||||
@ -972,7 +967,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
foreach (var data in validPublishedServerUrls)
|
||||
{
|
||||
// Get interface matching override subnet
|
||||
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
|
||||
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => NetworkUtils.SubnetContainsAddress(data.Data.Subnet, x.Address));
|
||||
|
||||
if (intf?.Address is not null
|
||||
|| (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
|
||||
@ -1035,6 +1030,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
if (isInExternalSubnet)
|
||||
{
|
||||
var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
|
||||
.Where(x => !IsLinkLocalAddress(x.Address))
|
||||
.OrderBy(x => x.Index)
|
||||
.ToList();
|
||||
if (externalInterfaces.Count > 0)
|
||||
@ -1042,7 +1038,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
// Check to see if any of the external bind interfaces are in the same subnet as the source.
|
||||
// If none exists, this will select the first external interface if there is one.
|
||||
bindAddress = externalInterfaces
|
||||
.OrderByDescending(x => x.Subnet.Contains(source))
|
||||
.OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source))
|
||||
.ThenByDescending(x => x.Subnet.PrefixLength)
|
||||
.ThenBy(x => x.Index)
|
||||
.Select(x => x.Address)
|
||||
@ -1060,7 +1056,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
// Check to see if any of the internal bind interfaces are in the same subnet as the source.
|
||||
// If none exists, this will select the first internal interface if there is one.
|
||||
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
|
||||
.OrderByDescending(x => x.Subnet.Contains(source))
|
||||
.OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source))
|
||||
.ThenByDescending(x => x.Subnet.PrefixLength)
|
||||
.ThenBy(x => x.Index)
|
||||
.Select(x => x.Address)
|
||||
@ -1104,7 +1100,7 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
// (For systems with multiple network cards and/or multiple subnets)
|
||||
foreach (var intf in extResult)
|
||||
{
|
||||
if (intf.Subnet.Contains(source))
|
||||
if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source))
|
||||
{
|
||||
result = NetworkUtils.FormatIPString(intf.Address);
|
||||
_logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
|
||||
|
@ -6,32 +6,54 @@ namespace Jellyfin.Naming.Tests.TV;
|
||||
public class SeasonPathParserTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("/Drive/Season 1", 1, true)]
|
||||
[InlineData("/Drive/s1", 1, true)]
|
||||
[InlineData("/Drive/S1", 1, true)]
|
||||
[InlineData("/Drive/Season 2", 2, true)]
|
||||
[InlineData("/Drive/Season 02", 2, true)]
|
||||
[InlineData("/Drive/Seinfeld/S02", 2, true)]
|
||||
[InlineData("/Drive/Seinfeld/2", 2, true)]
|
||||
[InlineData("/Drive/Seinfeld - S02", 2, true)]
|
||||
[InlineData("/Drive/Season 2009", 2009, true)]
|
||||
[InlineData("/Drive/Season1", 1, true)]
|
||||
[InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
|
||||
[InlineData("/Drive/Season 7 (2016)", 7, false)]
|
||||
[InlineData("/Drive/Staffel 7 (2016)", 7, false)]
|
||||
[InlineData("/Drive/Stagione 7 (2016)", 7, false)]
|
||||
[InlineData("/Drive/Season (8)", null, false)]
|
||||
[InlineData("/Drive/3.Staffel", 3, false)]
|
||||
[InlineData("/Drive/s06e05", null, false)]
|
||||
[InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
|
||||
[InlineData("/Drive/extras", 0, true)]
|
||||
[InlineData("/Drive/specials", 0, true)]
|
||||
public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
|
||||
[InlineData("/Drive/Season 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Staffel 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Stagione 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/sæson 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Temporada 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/series 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Kausi 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Säsong 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Seizoen 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Seasong 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Sezon 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/sezona 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/sezóna 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Sezonul 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/시즌 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/シーズン 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/сезон 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Сезон 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Season 10", "/Drive", 10, true)]
|
||||
[InlineData("/Drive/Season 100", "/Drive", 100, true)]
|
||||
[InlineData("/Drive/s1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/S1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Season 2", "/Drive", 2, true)]
|
||||
[InlineData("/Drive/Season 02", "/Drive", 2, true)]
|
||||
[InlineData("/Drive/Seinfeld/S02", "/Seinfeld", 2, true)]
|
||||
[InlineData("/Drive/Seinfeld/2", "/Seinfeld", 2, true)]
|
||||
[InlineData("/Drive/Seinfeld Season 2", "/Drive", null, false)]
|
||||
[InlineData("/Drive/Season 2009", "/Drive", 2009, true)]
|
||||
[InlineData("/Drive/Season1", "/Drive", 1, true)]
|
||||
[InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", "/The Wonder Years", 4, true)]
|
||||
[InlineData("/Drive/Season 7 (2016)", "/Drive", 7, true)]
|
||||
[InlineData("/Drive/Staffel 7 (2016)", "/Drive", 7, true)]
|
||||
[InlineData("/Drive/Stagione 7 (2016)", "/Drive", 7, true)]
|
||||
[InlineData("/Drive/Stargate SG-1/Season 1", "/Drive/Stargate SG-1", 1, true)]
|
||||
[InlineData("/Drive/Stargate SG-1/Stargate SG-1 Season 1", "/Drive/Stargate SG-1", 1, true)]
|
||||
[InlineData("/Drive/Season (8)", "/Drive", null, false)]
|
||||
[InlineData("/Drive/3.Staffel", "/Drive", 3, true)]
|
||||
[InlineData("/Drive/s06e05", "/Drive", null, false)]
|
||||
[InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", "/Drive", null, false)]
|
||||
[InlineData("/Drive/extras", "/Drive", 0, true)]
|
||||
[InlineData("/Drive/specials", "/Drive", 0, true)]
|
||||
[InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)]
|
||||
public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory)
|
||||
{
|
||||
var result = SeasonPathParser.Parse(path, true, true);
|
||||
var result = SeasonPathParser.Parse(path, parentPath, true, true);
|
||||
|
||||
Assert.Equal(result.SeasonNumber is not null, result.Success);
|
||||
Assert.Equal(result.SeasonNumber, seasonNumber);
|
||||
Assert.Equal(seasonNumber, result.SeasonNumber);
|
||||
Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
|
||||
}
|
||||
}
|
||||
|
@ -217,68 +217,58 @@ public class MediaInfoResolverTests
|
||||
string file = "My.Video.srt";
|
||||
data.Add(
|
||||
file,
|
||||
new[]
|
||||
{
|
||||
[
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
|
||||
},
|
||||
new[]
|
||||
{
|
||||
],
|
||||
[
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
|
||||
});
|
||||
]);
|
||||
|
||||
// filename has metadata
|
||||
file = "My.Video.Title1.default.forced.sdh.en.srt";
|
||||
data.Add(
|
||||
file,
|
||||
new[]
|
||||
{
|
||||
[
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
|
||||
},
|
||||
new[]
|
||||
{
|
||||
],
|
||||
[
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true, true)
|
||||
});
|
||||
]);
|
||||
|
||||
// single stream with metadata
|
||||
file = "My.Video.mks";
|
||||
data.Add(
|
||||
file,
|
||||
new[]
|
||||
{
|
||||
[
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true)
|
||||
},
|
||||
new[]
|
||||
{
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true)
|
||||
});
|
||||
],
|
||||
[
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, false, true)
|
||||
]);
|
||||
|
||||
// stream wins for title/language, filename wins for flags when conflicting
|
||||
file = "My.Video.Title2.default.forced.sdh.en.srt";
|
||||
data.Add(
|
||||
file,
|
||||
new[]
|
||||
{
|
||||
[
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0)
|
||||
},
|
||||
new[]
|
||||
{
|
||||
],
|
||||
[
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0, true, true, true)
|
||||
});
|
||||
]);
|
||||
|
||||
// multiple stream with metadata - filename flags ignored but other data filled in when missing from stream
|
||||
file = "My.Video.Title3.default.forced.en.srt";
|
||||
data.Add(
|
||||
file,
|
||||
new[]
|
||||
{
|
||||
[
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0, true, true),
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1)
|
||||
},
|
||||
new[]
|
||||
{
|
||||
],
|
||||
[
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title3", 0, true, true),
|
||||
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1)
|
||||
});
|
||||
]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user