Merge branch 'master' into feature/DatabaseRefactor

This commit is contained in:
JPVenson 2025-03-25 15:12:48 +00:00
commit 850f1c79f1
105 changed files with 1478 additions and 802 deletions

View File

@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "9.0.2", "version": "9.0.3",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
] ]

View File

@ -22,16 +22,16 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12

View File

@ -17,7 +17,7 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
@ -26,7 +26,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: abi-head name: abi-head
retention-days: 14 retention-days: 14
@ -47,7 +47,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
@ -65,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out dotnet build Jellyfin.Server -o ./out
- name: Upload Head - name: Upload Head
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: abi-base name: abi-base
retention-days: 14 retention-days: 14
@ -85,13 +85,13 @@ jobs:
steps: steps:
- name: Download abi-head - name: Download abi-head
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with: with:
name: abi-head name: abi-head
path: abi-head path: abi-head
- name: Download abi-base - name: Download abi-base
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with: with:
name: abi-base name: abi-base
path: abi-base path: abi-base

View File

@ -21,13 +21,13 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
- name: Generate openapi.json - 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" 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 - name: Upload openapi.json
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: openapi-head name: openapi-head
retention-days: 14 retention-days: 14
@ -55,13 +55,13 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF) ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with: with:
dotnet-version: '9.0.x' dotnet-version: '9.0.x'
- name: Generate openapi.json - 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" 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 - name: Upload openapi.json
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: openapi-base name: openapi-base
retention-days: 14 retention-days: 14
@ -80,12 +80,12 @@ jobs:
- openapi-base - openapi-base
steps: steps:
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
- name: Download openapi-base - name: Download openapi-base
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with: with:
name: openapi-base name: openapi-base
path: openapi-base path: openapi-base
@ -158,7 +158,7 @@ jobs:
run: |- run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
@ -172,7 +172,7 @@ jobs:
strip_components: 1 strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place - name: Move openapi.json (unstable) into place
uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1 uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
with: with:
host: "${{ secrets.REPO_HOST }}" host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}" username: "${{ secrets.REPO_USER }}"
@ -220,7 +220,7 @@ jobs:
run: |- run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
@ -234,7 +234,7 @@ jobs:
strip_components: 1 strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place - name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1 uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
with: with:
host: "${{ secrets.REPO_HOST }}" host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}" username: "${{ secrets.REPO_USER }}"

View File

@ -22,7 +22,7 @@ jobs:
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with: with:
dotnet-version: ${{ env.SDK_VERSION }} dotnet-version: ${{ env.SDK_VERSION }}

View File

@ -46,7 +46,7 @@ jobs:
- name: install python - name: install python
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with: with:
python-version: '3.12' python-version: '3.13'
cache: 'pip' cache: 'pip'
- name: install python packages - name: install python packages
run: pip install -r rename/requirements.txt run: pip install -r rename/requirements.txt

View File

@ -16,7 +16,7 @@ jobs:
- name: install python - name: install python
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with: with:
python-version: '3.12' python-version: '3.13'
cache: 'pip' cache: 'pip'
- name: install python packages - name: install python packages
run: pip install -r main-repo-triage/requirements.txt run: pip install -r main-repo-triage/requirements.txt

View File

@ -22,31 +22,31 @@
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.10" /> <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="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.2" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.2" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="Moq" Version="4.18.4" />
@ -75,11 +75,11 @@
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.2" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.3" />
<PackageVersion Include="System.Text.Json" Version="9.0.2" /> <PackageVersion Include="System.Text.Json" Version="9.0.3" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.2" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.3" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" /> <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="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" />

View File

@ -1,43 +1,35 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text.RegularExpressions;
namespace Emby.Naming.TV namespace Emby.Naming.TV
{ {
/// <summary> /// <summary>
/// Class to parse season paths. /// Class to parse season paths.
/// </summary> /// </summary>
public static class SeasonPathParser public static partial class SeasonPathParser
{ {
/// <summary> [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>.*)$")]
/// A season folder must contain one of these somewhere in the name. private static partial Regex ProcessPre();
/// </summary>
private static readonly string[] _seasonFolderNames =
{
"season",
"sæson",
"temporada",
"saison",
"staffel",
"series",
"сезон",
"stagione"
};
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> /// <summary>
/// Attempts to parse season number from path. /// Attempts to parse season number from path.
/// </summary> /// </summary>
/// <param name="path">Path to season.</param> /// <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="supportSpecialAliases">Support special aliases when parsing.</param>
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param> /// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns> /// <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 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; result.SeasonNumber = seasonNumber;
@ -54,15 +46,24 @@ namespace Emby.Naming.TV
/// Gets the season number from path. /// Gets the season number from path.
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <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="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> /// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
/// <returns>System.Nullable{System.Int32}.</returns> /// <returns>System.Nullable{System.Int32}.</returns>
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath( private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
string path, string path,
string? parentFolderName,
bool supportSpecialAliases, bool supportSpecialAliases,
bool supportNumericSeasonFolders) bool supportNumericSeasonFolders)
{ {
string filename = Path.GetFileName(path); 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) 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); return (seasonNumber, true);
} }
// Look for one of the season folder names return (null, false);
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;
} }
/// <summary> /// <summary>

View File

@ -34,76 +34,46 @@ namespace Emby.Server.Implementations.AppBase
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
} }
/// <summary> /// <inheritdoc/>
/// Gets the path to the program data folder.
/// </summary>
/// <value>The program data path.</value>
public string ProgramDataPath { get; } public string ProgramDataPath { get; }
/// <inheritdoc/> /// <inheritdoc/>
public string WebPath { get; } public string WebPath { get; }
/// <summary> /// <inheritdoc/>
/// Gets the path to the system folder.
/// </summary>
/// <value>The path to the system folder.</value>
public string ProgramSystemPath { get; } = AppContext.BaseDirectory; public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
/// <summary> /// <inheritdoc/>
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
public string DataPath { get; } public string DataPath { get; }
/// <inheritdoc /> /// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%"; public string VirtualDataPath => "%AppDataPath%";
/// <summary> /// <inheritdoc/>
/// Gets the image cache path.
/// </summary>
/// <value>The image cache path.</value>
public string ImageCachePath => Path.Combine(CachePath, "images"); public string ImageCachePath => Path.Combine(CachePath, "images");
/// <summary> /// <inheritdoc/>
/// Gets the path to the plugin directory.
/// </summary>
/// <value>The plugins path.</value>
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins"); public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
/// <summary> /// <inheritdoc/>
/// Gets the path to the plugin configurations directory.
/// </summary>
/// <value>The plugin configurations path.</value>
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations"); public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
/// <summary> /// <inheritdoc/>
/// Gets the path to the log directory.
/// </summary>
/// <value>The log directory path.</value>
public string LogDirectoryPath { get; } public string LogDirectoryPath { get; }
/// <summary> /// <inheritdoc/>
/// Gets the path to the application configuration root directory.
/// </summary>
/// <value>The configuration directory path.</value>
public string ConfigurationDirectoryPath { get; } public string ConfigurationDirectoryPath { get; }
/// <summary> /// <inheritdoc/>
/// Gets the path to the system configuration file.
/// </summary>
/// <value>The system configuration file path.</value>
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml"); public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
/// <summary> /// <inheritdoc/>
/// Gets or sets the folder path to the cache directory.
/// </summary>
/// <value>The cache directory.</value>
public string CachePath { get; set; } public string CachePath { get; set; }
/// <summary> /// <inheritdoc/>
/// Gets the folder path to the temp directory within the cache folder.
/// </summary>
/// <value>The temp directory.</value>
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin"); public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
/// <inheritdoc />
public string TrickplayPath => Path.Combine(DataPath, "trickplay");
} }
} }

View File

@ -57,6 +57,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Lyrics;
@ -508,6 +509,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<EncodingHelper>(); serviceCollection.AddSingleton<EncodingHelper>();
serviceCollection.AddSingleton<IPathManager, PathManager>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));

View File

@ -5,15 +5,17 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Trickplay;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; 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 ILibraryManager _libraryManager;
private readonly ILogger<CleanDatabaseScheduledTask> _logger; private readonly ILogger<CleanDatabaseScheduledTask> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
@ -43,17 +45,16 @@ namespace Emby.Server.Implementations.Data
var numComplete = 0; var numComplete = 0;
var numItems = itemIds.Count + 1; var numItems = itemIds.Count + 1;
_logger.LogDebug("Cleaning {0} items with dead parent links", numItems); _logger.LogDebug("Cleaning {Number} items with dead parent links", numItems);
foreach (var itemId in itemIds) foreach (var itemId in itemIds)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
if (item is not null) if (item is not null)
{ {
_logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty); _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
_libraryManager.DeleteItem(item, new DeleteOptions _libraryManager.DeleteItem(item, new DeleteOptions
{ {
@ -80,5 +81,4 @@ namespace Emby.Server.Implementations.Data
progress.Report(100); progress.Report(100);
} }
}
} }

View File

@ -79,6 +79,7 @@ namespace Emby.Server.Implementations.Library
private readonly NamingOptions _namingOptions; private readonly NamingOptions _namingOptions;
private readonly IPeopleRepository _peopleRepository; private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver; private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager;
/// <summary> /// <summary>
/// The _root folder sync lock. /// The _root folder sync lock.
@ -114,7 +115,8 @@ namespace Emby.Server.Implementations.Library
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</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( public LibraryManager(
IServerApplicationHost appHost, IServerApplicationHost appHost,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
@ -131,7 +133,8 @@ namespace Emby.Server.Implementations.Library
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
NamingOptions namingOptions, NamingOptions namingOptions,
IDirectoryService directoryService, IDirectoryService directoryService,
IPeopleRepository peopleRepository) IPeopleRepository peopleRepository,
IPathManager pathManager)
{ {
_appHost = appHost; _appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>(); _logger = loggerFactory.CreateLogger<LibraryManager>();
@ -149,6 +152,7 @@ namespace Emby.Server.Implementations.Library
_cache = new ConcurrentDictionary<Guid, BaseItem>(); _cache = new ConcurrentDictionary<Guid, BaseItem>();
_namingOptions = namingOptions; _namingOptions = namingOptions;
_peopleRepository = peopleRepository; _peopleRepository = peopleRepository;
_pathManager = pathManager;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService); _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated; _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@ -201,33 +205,33 @@ namespace Emby.Server.Implementations.Library
/// Gets or sets the postscan tasks. /// Gets or sets the postscan tasks.
/// </summary> /// </summary>
/// <value>The postscan tasks.</value> /// <value>The postscan tasks.</value>
private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>(); private ILibraryPostScanTask[] PostscanTasks { get; set; } = [];
/// <summary> /// <summary>
/// Gets or sets the intro providers. /// Gets or sets the intro providers.
/// </summary> /// </summary>
/// <value>The intro providers.</value> /// <value>The intro providers.</value>
private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>(); private IIntroProvider[] IntroProviders { get; set; } = [];
/// <summary> /// <summary>
/// Gets or sets the list of entity resolution ignore rules. /// Gets or sets the list of entity resolution ignore rules.
/// </summary> /// </summary>
/// <value>The entity resolution ignore rules.</value> /// <value>The entity resolution ignore rules.</value>
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>(); private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
/// <summary> /// <summary>
/// Gets or sets the list of currently registered entity resolvers. /// Gets or sets the list of currently registered entity resolvers.
/// </summary> /// </summary>
/// <value>The entity resolvers enumerable.</value> /// <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> /// <summary>
/// Gets or sets the comparers. /// Gets or sets the comparers.
/// </summary> /// </summary>
/// <value>The comparers.</value> /// <value>The comparers.</value>
private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>(); private IBaseItemComparer[] Comparers { get; set; } = [];
public bool IsScanRunning { get; private set; } public bool IsScanRunning { get; private set; }
@ -360,7 +364,7 @@ namespace Emby.Server.Implementations.Library
var children = item.IsFolder var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false) ? ((Folder)item).GetRecursiveChildren(false)
: Array.Empty<BaseItem>(); : [];
foreach (var metadataPath in GetMetadataPaths(item, children)) foreach (var metadataPath in GetMetadataPaths(item, children))
{ {
@ -466,14 +470,28 @@ namespace Emby.Server.Implementations.Library
ReportItemRemoved(item, parent); 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> var list = new List<string>
{ {
item.GetInternalMetadataPath() item.GetInternalMetadataPath()
}; };
list.AddRange(children.Select(i => i.GetInternalMetadataPath())); if (item is Video video)
{
list.Add(_pathManager.GetTrickplayDirectory(video));
}
return list; return list;
} }
@ -594,7 +612,7 @@ namespace Emby.Server.Implementations.Library
{ {
_logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf); _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf);
files = Array.Empty<FileSystemMetadata>(); files = [];
} }
else else
{ {
@ -1345,6 +1363,21 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItemList(query); 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) public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
{ {
if (query.User is not null) if (query.User is not null)
@ -1449,7 +1482,7 @@ namespace Emby.Server.Implementations.Library
// Optimize by querying against top level views // Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); 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 // Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0) if (query.TopParentIds.Length == 0)
@ -1569,7 +1602,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user); return GetTopParentIdsForQuery(displayParent, user);
} }
return Array.Empty<Guid>(); return [];
} }
if (!view.ParentId.IsEmpty()) if (!view.ParentId.IsEmpty())
@ -1580,7 +1613,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user); return GetTopParentIdsForQuery(displayParent, user);
} }
return Array.Empty<Guid>(); return [];
} }
// Handle grouping // Handle grouping
@ -1595,7 +1628,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => GetTopParentIdsForQuery(i, user)); .SelectMany(i => GetTopParentIdsForQuery(i, user));
} }
return Array.Empty<Guid>(); return [];
} }
if (item is CollectionFolder collectionFolder) if (item is CollectionFolder collectionFolder)
@ -1609,7 +1642,7 @@ namespace Emby.Server.Implementations.Library
return new[] { topParent.Id }; return new[] { topParent.Id };
} }
return Array.Empty<Guid>(); return [];
} }
/// <summary> /// <summary>
@ -1653,7 +1686,7 @@ namespace Emby.Server.Implementations.Library
{ {
_logger.LogError(ex, "Error getting intros"); _logger.LogError(ex, "Error getting intros");
return Enumerable.Empty<IntroInfo>(); return [];
} }
} }
@ -2480,8 +2513,11 @@ namespace Emby.Server.Implementations.Library
} }
/// <inheritdoc /> /// <inheritdoc />
public int? GetSeasonNumberFromPath(string path) public int? GetSeasonNumberFromPath(string path, Guid? parentId)
=> SeasonPathParser.Parse(path, true, true).SeasonNumber; {
var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
}
/// <inheritdoc /> /// <inheritdoc />
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) 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? 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); CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);

View File

@ -783,9 +783,13 @@ namespace Emby.Server.Implementations.Library
{ {
ArgumentException.ThrowIfNullOrEmpty(id); ArgumentException.ThrowIfNullOrEmpty(id);
// TODO probably shouldn't throw here but it is kept for "backwards compatibility" var info = GetLiveStreamInfo(id);
var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException(); if (info is null)
return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider)); {
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) public ILiveStream GetLiveStreamInfo(string id)

View File

@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library
return null; return null;
} }
// Sort in the following order: Default > No tag > Forced
var sortedStreams = streams var sortedStreams = streams
.Where(i => i.Type == MediaStreamType.Subtitle) .Where(i => i.Type == MediaStreamType.Subtitle)
.OrderByDescending(x => x.IsExternal) .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 => 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(); .ToList();
MediaStream? stream = null; MediaStream? stream = null;
if (mode == SubtitlePlaybackMode.Default) if (mode == SubtitlePlaybackMode.Default)
{ {
// Load subtitles according to external, forced and default flags. // Load subtitles according to external, default and forced flags.
stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced);
} }
else if (mode == SubtitlePlaybackMode.Smart) else if (mode == SubtitlePlaybackMode.Smart)
{ {
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages. // 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)) if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{ {
stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ?? stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages));
sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
} }
else else
{ {
// Respect forced flag. stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
stream = sortedStreams.FirstOrDefault(x => x.IsForced);
} }
} }
else if (mode == SubtitlePlaybackMode.Always) else if (mode == SubtitlePlaybackMode.Always)
{ {
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour. // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour.
stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ?? stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ??
sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
} }
else if (mode == SubtitlePlaybackMode.OnlyForced) else if (mode == SubtitlePlaybackMode.OnlyForced)
{ {
// Only load subtitles that are flagged forced. // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
stream = sortedStreams.FirstOrDefault(x => x.IsForced); stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
} }
return stream?.Index; return stream?.Index;
@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library
if (mode == SubtitlePlaybackMode.Default) if (mode == SubtitlePlaybackMode.Default)
{ {
// Prefer embedded metadata over smart logic // 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(); .ToList();
} }
else if (mode == SubtitlePlaybackMode.Smart) else if (mode == SubtitlePlaybackMode.Smart)
{ {
// Prefer smart logic over embedded metadata // 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)) 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(); .ToList();
} }
else
{
filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
}
} }
else if (mode == SubtitlePlaybackMode.Always) else if (mode == SubtitlePlaybackMode.Always)
{ {
// Always load the most suitable full subtitles // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior.
filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList(); filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages))
.ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages);
} }
else if (mode == SubtitlePlaybackMode.OnlyForced) else if (mode == SubtitlePlaybackMode.OnlyForced)
{ {
// Always load the most suitable full subtitles // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
filteredStreams = sortedStreams.Where(s => s.IsForced).ToList(); filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
} }
// Load forced subs if we have found no suitable full subtitles // If filteredStreams is null, initialize it as an empty list to avoid null reference errors
var iterStreams = filteredStreams is null || filteredStreams.Count == 0 filteredStreams ??= new List<MediaStream>();
? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
: filteredStreams;
foreach (var stream in iterStreams) foreach (var stream in filteredStreams)
{ {
stream.Score = GetStreamScore(stream, preferredLanguages); 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) internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
{ {
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase)); var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));

View 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);
}
}

View File

@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var path = args.Path; var path = args.Path;
var seasonParserResult = SeasonPathParser.Parse(path, true, true); var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true);
var season = new Season var season = new Season
{ {

View File

@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{ {
if (child.IsDirectory) 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); _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true; return true;
@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// Determines whether [is season folder] [the specified path]. /// Determines whether [is season folder] [the specified path].
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <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> /// <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> /// <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; return seasonNumber.HasValue;
} }

View File

@ -1,6 +1,6 @@
{ {
"Sync": "Сінхранізаваць", "Sync": "Сінхранізаваць",
"Playlists": "Плэйлісты", "Playlists": "Спісы прайгравання",
"Latest": "Апошні", "Latest": "Апошні",
"LabelIpAddressValue": "IP-адрас: {0}", "LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} быў дададзены ў бібліятэку", "ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
@ -16,7 +16,7 @@
"Collections": "Калекцыі", "Collections": "Калекцыі",
"Default": "Па змаўчанні", "Default": "Па змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі", "Folders": "Тэчкі",
"Favorites": "Абранае", "Favorites": "Абранае",
"External": "Знешні", "External": "Знешні",
"Genres": "Жанры", "Genres": "Жанры",

View File

@ -11,7 +11,7 @@
"Collections": "Συλλογές", "Collections": "Συλλογές",
"DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε", "DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
"DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε", "DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
"FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}", "FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}",
"Favorites": "Αγαπημένα", "Favorites": "Αγαπημένα",
"Folders": "Φάκελοι", "Folders": "Φάκελοι",
"Genres": "Είδη", "Genres": "Είδη",
@ -27,8 +27,8 @@
"HeaderRecordingGroups": "Ομάδες Ηχογράφησης", "HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
"HomeVideos": "Προσωπικά Βίντεο", "HomeVideos": "Προσωπικά Βίντεο",
"Inherit": "Κληρονόμηση", "Inherit": "Κληρονόμηση",
"ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη", "ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη",
"ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη", "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη",
"LabelIpAddressValue": "Διεύθυνση IP: {0}", "LabelIpAddressValue": "Διεύθυνση IP: {0}",
"LabelRunningTimeValue": "Διάρκεια: {0}", "LabelRunningTimeValue": "Διάρκεια: {0}",
"Latest": "Πρόσφατα", "Latest": "Πρόσφατα",
@ -40,7 +40,7 @@
"Movies": "Ταινίες", "Movies": "Ταινίες",
"Music": "Μουσική", "Music": "Μουσική",
"MusicVideos": "Μουσικά Βίντεο", "MusicVideos": "Μουσικά Βίντεο",
"NameInstallFailed": "{0} η εγκατάσταση απέτυχε", "NameInstallFailed": "H εγκατάσταση του {0} απέτυχε",
"NameSeasonNumber": "Κύκλος {0}", "NameSeasonNumber": "Κύκλος {0}",
"NameSeasonUnknown": "Άγνωστος Κύκλος", "NameSeasonUnknown": "Άγνωστος Κύκλος",
"NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.", "NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.",
@ -54,7 +54,7 @@
"NotificationOptionPluginError": "Αποτυχία του πρόσθετου", "NotificationOptionPluginError": "Αποτυχία του πρόσθετου",
"NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε", "NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε",
"NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε", "NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε",
"NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε", "NotificationOptionPluginUpdateInstalled": "Η ενημέρωση του πρόσθετου εγκαταστάθηκε",
"NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση", "NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση",
"NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας", "NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας",
"NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε", "NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε",
@ -63,9 +63,9 @@
"Photos": "Φωτογραφίες", "Photos": "Φωτογραφίες",
"Playlists": "Λίστες αναπαραγωγής", "Playlists": "Λίστες αναπαραγωγής",
"Plugin": "Πρόσθετο", "Plugin": "Πρόσθετο",
"PluginInstalledWithName": "{0} εγκαταστήθηκε", "PluginInstalledWithName": "Το {0} εγκαταστάθηκε",
"PluginUninstalledWithName": "{0} έχει απεγκατασταθεί", "PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί",
"PluginUpdatedWithName": "{0} έχει αναβαθμιστεί", "PluginUpdatedWithName": "Το {0} ενημερώθηκε",
"ProviderValue": "Πάροχος: {0}", "ProviderValue": "Πάροχος: {0}",
"ScheduledTaskFailedWithName": "{0} αποτυχία", "ScheduledTaskFailedWithName": "{0} αποτυχία",
"ScheduledTaskStartedWithName": "{0} ξεκίνησε", "ScheduledTaskStartedWithName": "{0} ξεκίνησε",
@ -96,7 +96,7 @@
"TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.", "TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
"TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής", "TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής",
"TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.", "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.",
"TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων", "TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων",
"TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.", "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.",
"TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου", "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου",
"TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.", "TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.",
@ -125,7 +125,7 @@
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο", "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
"External": "Εξωτερικό", "External": "Εξωτερικό",
"HearingImpaired": "Με προβλήματα ακοής", "HearingImpaired": "Με προβλήματα ακοής",
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay", "TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay",
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.", "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
"TaskAudioNormalization": "Ομοιομορφία ήχου", "TaskAudioNormalization": "Ομοιομορφία ήχου",
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.", "TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",

View File

@ -122,5 +122,9 @@
"AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis", "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis",
"TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.", "TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.",
"TaskKeyframeExtractor": "Eltiri Ĉefkadrojn", "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"
} }

View File

@ -1,6 +1,6 @@
{ {
"Albums": "Álbuns", "Albums": "Álbuns",
"AppDeviceValues": "Aplicação {0}, Dispositivo: {1}", "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"Application": "Aplicação", "Application": "Aplicação",
"Artists": "Artistas", "Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",

View File

@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization
} }
// Fairly common for some users to have "Rated R" in their rating field // 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 // Use rating system matching the language
if (!string.IsNullOrEmpty(countryCode)) if (!string.IsNullOrEmpty(countryCode))

View File

@ -336,7 +336,7 @@
"TwoLetterISORegionName": "IE" "TwoLetterISORegionName": "IE"
}, },
{ {
"DisplayName": "Islamic Republic of Pakistan", "DisplayName": "Pakistan",
"Name": "PK", "Name": "PK",
"ThreeLetterISORegionName": "PAK", "ThreeLetterISORegionName": "PAK",
"TwoLetterISORegionName": "PK" "TwoLetterISORegionName": "PK"

View File

@ -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)); var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
if (item is null) 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; return;
} }

View File

@ -344,6 +344,11 @@ namespace Emby.Server.Implementations.Session
/// <returns>Task.</returns> /// <returns>Task.</returns>
private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime) private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime)
{ {
if (session is null)
{
return;
}
if (string.IsNullOrEmpty(info.MediaSourceId)) if (string.IsNullOrEmpty(info.MediaSourceId))
{ {
info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
@ -676,6 +681,11 @@ namespace Emby.Server.Implementations.Session
private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId) private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
{ {
if (session is null)
{
return null;
}
var item = session.FullNowPlayingItem; var item = session.FullNowPlayingItem;
if (item is not null && item.Id.Equals(itemId)) if (item is not null && item.Id.Equals(itemId))
{ {
@ -795,7 +805,11 @@ namespace Emby.Server.Implementations.Session
ArgumentNullException.ThrowIfNull(info); ArgumentNullException.ThrowIfNull(info);
var session = GetSession(info.SessionId); var session = GetSession(info.SessionId, false);
if (session is null)
{
return;
}
var libraryItem = info.ItemId.IsEmpty() var libraryItem = info.ItemId.IsEmpty()
? null ? null

View File

@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.TV
if (!string.IsNullOrEmpty(presentationUniqueKey)) if (!string.IsNullOrEmpty(presentationUniqueKey))
{ {
return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request); return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request);
} }
if (limit.HasValue) if (limit.HasValue)
@ -100,25 +100,9 @@ namespace Emby.Server.Implementations.TV
limit = limit.Value + 10; limit = limit.Value + 10;
} }
var items = _libraryManager var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff);
.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();
// Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options);
var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options);
return GetResult(episodes, request); return GetResult(episodes, request);
} }
@ -134,36 +118,11 @@ namespace Emby.Server.Implementations.TV
.OrderByDescending(i => i.LastWatchedDate); .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 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()) .Select(i => i.GetEpisodeFunction())
.Where(i => i is not null)!; .Where(i => i is not null)!;
} }
private static string GetUniqueSeriesKey(Episode episode)
{
return episode.SeriesPresentationUniqueKey;
}
private static string GetUniqueSeriesKey(Series series) private static string GetUniqueSeriesKey(Series series)
{ {
return series.GetPresentationUniqueKey(); return series.GetPresentationUniqueKey();
@ -179,13 +138,13 @@ namespace Emby.Server.Implementations.TV
{ {
AncestorWithPresentationUniqueKey = null, AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey, SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode }, IncludeItemTypes = [BaseItemKind.Episode],
IsPlayed = true, IsPlayed = true,
Limit = 1, Limit = 1,
ParentIndexNumberNotEquals = 0, ParentIndexNumberNotEquals = 0,
DtoOptions = new DtoOptions DtoOptions = new DtoOptions
{ {
Fields = new[] { ItemFields.SortName }, Fields = [ItemFields.SortName],
EnableImages = false EnableImages = false
} }
}; };
@ -203,8 +162,8 @@ namespace Emby.Server.Implementations.TV
{ {
AncestorWithPresentationUniqueKey = null, AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey, SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode }, IncludeItemTypes = [BaseItemKind.Episode],
OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) }, OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)],
Limit = 1, Limit = 1,
IsPlayed = includePlayed, IsPlayed = includePlayed,
IsVirtualItem = false, IsVirtualItem = false,
@ -229,7 +188,7 @@ namespace Emby.Server.Implementations.TV
AncestorWithPresentationUniqueKey = null, AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey, SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0, ParentIndexNumber = 0,
IncludeItemTypes = new[] { BaseItemKind.Episode }, IncludeItemTypes = [BaseItemKind.Episode],
IsPlayed = includePlayed, IsPlayed = includePlayed,
IsVirtualItem = false, IsVirtualItem = false,
DtoOptions = dtoOptions DtoOptions = dtoOptions
@ -249,7 +208,7 @@ namespace Emby.Server.Implementations.TV
consideredEpisodes.Add(nextEpisode); 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>(); .Cast<Episode>();
if (lastWatchedEpisode is not null) if (lastWatchedEpisode is not null)
{ {

View File

@ -698,6 +698,7 @@ public class LiveTvController : BaseJellyfinApiController
/// Gets recommended live tv epgs. /// Gets recommended live tv epgs.
/// </summary> /// </summary>
/// <param name="userId">Optional. filter by user id.</param> /// <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="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="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> /// <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)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms( public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? isAiring, [FromQuery] bool? isAiring,
[FromQuery] bool? hasAired, [FromQuery] bool? hasAired,
@ -744,6 +746,7 @@ public class LiveTvController : BaseJellyfinApiController
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
IsAiring = isAiring, IsAiring = isAiring,
StartIndex = startIndex,
Limit = limit, Limit = limit,
HasAired = hasAired, HasAired = hasAired,
IsSeries = isSeries, IsSeries = isSeries,

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
@ -86,7 +87,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] DateTime? nextUpDateCutoff, [FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool disableFirstEpisode = false, [FromQuery][ParameterObsolete] bool disableFirstEpisode = false,
[FromQuery] bool enableResumable = true, [FromQuery] bool enableResumable = true,
[FromQuery] bool enableRewatching = false) [FromQuery] bool enableRewatching = false)
{ {
@ -109,7 +110,6 @@ public class TvShowsController : BaseJellyfinApiController
StartIndex = startIndex, StartIndex = startIndex,
User = user, User = user,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
DisableFirstEpisode = disableFirstEpisode,
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
EnableResumable = enableResumable, EnableResumable = enableResumable,
EnableRewatching = enableRewatching EnableRewatching = enableRewatching

View File

@ -101,16 +101,23 @@ public sealed class BaseItemRepository
using var context = _dbProvider.CreateDbContext(); using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction(); 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.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete(); context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); context.TrickplayInfos.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.SaveChanges(); context.SaveChanges();
transaction.Commit(); 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(); 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) private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{ {
// This whole block is needed to filter duplicate entries on request // This whole block is needed to filter duplicate entries on request
@ -925,25 +963,11 @@ public sealed class BaseItemRepository
using var context = _dbProvider.CreateDbContext(); using var context = _dbProvider.CreateDbContext();
var innerQuery = new InternalItemsQuery(filter.User) var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
{
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);
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 if (filter.OrderBy.Count != 0
|| !string.IsNullOrEmpty(filter.SearchTerm)) || !string.IsNullOrEmpty(filter.SearchTerm))

View File

@ -12,6 +12,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Trickplay; using MediaBrowser.Controller.Trickplay;
@ -37,9 +38,10 @@ public class TrickplayManager : ITrickplayManager
private readonly IImageEncoder _imageEncoder; private readonly IImageEncoder _imageEncoder;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IPathManager _pathManager;
private static readonly AsyncNonKeyedLocker _resourcePool = new(1); private static readonly AsyncNonKeyedLocker _resourcePool = new(1);
private static readonly string[] _trickplayImgExtensions = { ".jpg" }; private static readonly string[] _trickplayImgExtensions = [".jpg"];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TrickplayManager"/> class. /// 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="imageEncoder">The image encoder.</param>
/// <param name="dbProvider">The database provider.</param> /// <param name="dbProvider">The database provider.</param>
/// <param name="appPaths">The application paths.</param> /// <param name="appPaths">The application paths.</param>
/// <param name="pathManager">The path manager.</param>
public TrickplayManager( public TrickplayManager(
ILogger<TrickplayManager> logger, ILogger<TrickplayManager> logger,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
@ -62,7 +65,8 @@ public class TrickplayManager : ITrickplayManager
IServerConfigurationManager config, IServerConfigurationManager config,
IImageEncoder imageEncoder, IImageEncoder imageEncoder,
IDbContextFactory<JellyfinDbContext> dbProvider, IDbContextFactory<JellyfinDbContext> dbProvider,
IApplicationPaths appPaths) IApplicationPaths appPaths,
IPathManager pathManager)
{ {
_logger = logger; _logger = logger;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
@ -73,6 +77,7 @@ public class TrickplayManager : ITrickplayManager
_imageEncoder = imageEncoder; _imageEncoder = imageEncoder;
_dbProvider = dbProvider; _dbProvider = dbProvider;
_appPaths = appPaths; _appPaths = appPaths;
_pathManager = pathManager;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -610,10 +615,7 @@ public class TrickplayManager : ITrickplayManager
/// <inheritdoc /> /// <inheritdoc />
public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
{ {
var path = saveWithMedia var path = _pathManager.GetTrickplayDirectory(item, saveWithMedia);
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
: Path.Combine(item.GetInternalMetadataPath(), "trickplay");
var subdirectory = string.Format( var subdirectory = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"{0} - {1}x{2}", "{0} - {1}x{2}",

View File

@ -94,7 +94,7 @@ public class MigrateLibraryDb : IMigrationRoutine
Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, 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(); dbContext.BaseItems.ExecuteDelete();
@ -168,7 +168,6 @@ public class MigrateLibraryDb : IMigrationRoutine
dbContext.UserData.ExecuteDelete(); dbContext.UserData.ExecuteDelete();
var users = dbContext.Users.AsNoTracking().ToImmutableArray(); var users = dbContext.Users.AsNoTracking().ToImmutableArray();
var oldUserdata = new Dictionary<string, UserData>();
foreach (var entity in queryResult) foreach (var entity in queryResult)
{ {
@ -189,6 +188,8 @@ public class MigrateLibraryDb : IMigrationRoutine
dbContext.UserData.Add(userData); dbContext.UserData.Add(userData);
} }
users.Clear();
legacyBaseItemWithUserKeys.Clear();
_logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count); _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count);
dbContext.SaveChanges(); dbContext.SaveChanges();
@ -225,11 +226,12 @@ public class MigrateLibraryDb : IMigrationRoutine
dbContext.PeopleBaseItemMap.ExecuteDelete(); dbContext.PeopleBaseItemMap.ExecuteDelete();
var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>(); 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)) foreach (SqliteDataReader reader in connection.Query(personsQuery))
{ {
var itemId = reader.GetGuid(0); 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)); _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
continue; continue;
@ -261,12 +263,16 @@ public class MigrateLibraryDb : IMigrationRoutine
}); });
} }
baseItemIds.Clear();
foreach (var item in peopleCache) foreach (var item in peopleCache)
{ {
dbContext.Peoples.Add(item.Value.Person); dbContext.Peoples.Add(item.Value.Person);
dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId))); 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); _logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count);
dbContext.SaveChanges(); dbContext.SaveChanges();
migrationTotalTime += stopwatch.Elapsed; migrationTotalTime += stopwatch.Elapsed;
@ -1029,6 +1035,16 @@ public class MigrateLibraryDb : IMigrationRoutine
entity.MediaType = mediaType; 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 baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
var dataKeys = baseItem.GetUserDataKeys(); var dataKeys = baseItem.GetUserDataKeys();
userDataKeys.AddRange(dataKeys); userDataKeys.AddRange(dataKeys);

View File

@ -39,7 +39,7 @@ public class MoveTrickplayFiles : IMigrationRoutine
} }
/// <inheritdoc /> /// <inheritdoc />
public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B"); public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52");
/// <inheritdoc /> /// <inheritdoc />
public string Name => "MoveTrickplayFiles"; public string Name => "MoveTrickplayFiles";
@ -89,6 +89,12 @@ public class MoveTrickplayFiles : IMigrationRoutine
{ {
_fileSystem.MoveDirectory(oldPath, newPath); _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); } while (previousCount == Limit);
@ -101,4 +107,20 @@ public class MoveTrickplayFiles : IMigrationRoutine
return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; 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);
}
} }

View File

@ -11,7 +11,9 @@ using Emby.Server.Implementations;
using Jellyfin.Server.Extensions; using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers; using Jellyfin.Server.Helpers;
using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations;
using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
@ -44,6 +46,9 @@ namespace Jellyfin.Server
public const string LoggingConfigFileSystem = "logging.json"; public const string LoggingConfigFileSystem = "logging.json";
private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); 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 long _startTimestamp;
private static ILogger _logger = NullLogger.Instance; private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown; private static bool _restartOnShutdown;
@ -70,6 +75,7 @@ namespace Jellyfin.Server
{ {
_startTimestamp = Stopwatch.GetTimestamp(); _startTimestamp = Stopwatch.GetTimestamp();
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); 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 // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
@ -124,22 +130,23 @@ namespace Jellyfin.Server
if (_restartOnShutdown) if (_restartOnShutdown)
{ {
_startTimestamp = Stopwatch.GetTimestamp(); _startTimestamp = Stopwatch.GetTimestamp();
_setupServer = new SetupServer();
await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false);
} }
} while (_restartOnShutdown); } while (_restartOnShutdown);
} }
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
{ {
using var appHost = new CoreAppHost( using CoreAppHost appHost = new CoreAppHost(
appPaths, appPaths,
_loggerFactory, _loggerFactory,
options, options,
startupConfig); startupConfig);
_appHost = appHost;
IHost? host = null;
try try
{ {
host = Host.CreateDefaultBuilder() _jellyfinHost = Host.CreateDefaultBuilder()
.UseConsoleLifetime() .UseConsoleLifetime()
.ConfigureServices(services => appHost.Init(services)) .ConfigureServices(services => appHost.Init(services))
.ConfigureWebHostDefaults(webHostBuilder => .ConfigureWebHostDefaults(webHostBuilder =>
@ -156,14 +163,17 @@ namespace Jellyfin.Server
.Build(); .Build();
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. // 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); await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
Migrations.MigrationRunner.Run(appHost, _loggerFactory); Migrations.MigrationRunner.Run(appHost, _loggerFactory);
try 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()) if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{ {
@ -182,7 +192,7 @@ namespace Jellyfin.Server
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
await host.WaitForShutdownAsync().ConfigureAwait(false); await _jellyfinHost.WaitForShutdownAsync().ConfigureAwait(false);
_restartOnShutdown = appHost.ShouldRestart; _restartOnShutdown = appHost.ShouldRestart;
} }
catch (Exception ex) catch (Exception ex)
@ -203,7 +213,8 @@ namespace Jellyfin.Server
await databaseProvider.RunShutdownTask(shutdownSource.Token).ConfigureAwait(false); await databaseProvider.RunShutdownTask(shutdownSource.Token).ConfigureAwait(false);
} }
host?.Dispose(); _appHost = null;
_jellyfinHost?.Dispose();
} }
} }

View 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."));
}
}
}

View File

@ -84,5 +84,11 @@ namespace MediaBrowser.Common.Configuration
/// </summary> /// </summary>
/// <value>The magic string used for virtual path manipulation.</value> /// <value>The magic string used for virtual path manipulation.</value>
string VirtualDataPath { get; } string VirtualDataPath { get; }
/// <summary>
/// Gets the path used for storing trickplay files.
/// </summary>
/// <value>The trickplay path.</value>
string TrickplayPath { get; }
} }
} }

View File

@ -326,4 +326,23 @@ public static partial class NetworkUtils
return new IPAddress(BitConverter.GetBytes(broadCastIPAddress)); 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);
}
} }

View File

@ -139,11 +139,9 @@ namespace MediaBrowser.Controller.Entities.Audio
private static List<string> GetUserDataKeys(MusicArtist item) private static List<string> GetUserDataKeys(MusicArtist item)
{ {
var list = new List<string>(); var list = new List<string>();
var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId))
if (!string.IsNullOrEmpty(id))
{ {
list.Add("Artist-Musicbrainz-" + id); list.Add("Artist-Musicbrainz-" + externalId);
} }
list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics()); list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics());

View File

@ -920,7 +920,7 @@ namespace MediaBrowser.Controller.Entities
// Remove from middle if surrounded by spaces // Remove from middle if surrounded by spaces
sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal); 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)) if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
{ {
sortable = sortable.Remove(sortable.Length - (search.Length + 1)); sortable = sortable.Remove(sortable.Length - (search.Length + 1));
@ -1776,7 +1776,6 @@ namespace MediaBrowser.Controller.Entities
public void AddStudio(string name) public void AddStudio(string name)
{ {
ArgumentException.ThrowIfNullOrEmpty(name); ArgumentException.ThrowIfNullOrEmpty(name);
var current = Studios; var current = Studios;
if (!current.Contains(name, StringComparison.OrdinalIgnoreCase)) if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
@ -1795,7 +1794,7 @@ namespace MediaBrowser.Controller.Entities
public void SetStudios(IEnumerable<string> names) public void SetStudios(IEnumerable<string> names)
{ {
Studios = names.Distinct().ToArray(); Studios = names.Trimmed().Distinct().ToArray();
} }
/// <summary> /// <summary>

View File

@ -15,6 +15,8 @@ namespace MediaBrowser.Controller.Entities
ArgumentNullException.ThrowIfNull(person); ArgumentNullException.ThrowIfNull(person);
ArgumentException.ThrowIfNullOrEmpty(person.Name); ArgumentException.ThrowIfNullOrEmpty(person.Name);
person.Name = person.Name.Trim();
// Normalize // Normalize
if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
{ {

View File

@ -257,7 +257,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
{ {
IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path); IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path, ParentId);
// If a change was made record it // If a change was made record it
if (IndexNumber.HasValue) if (IndexNumber.HasValue)

View 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);
}

View File

@ -426,8 +426,9 @@ namespace MediaBrowser.Controller.Library
/// Gets the season number from path. /// Gets the season number from path.
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <param name="parentId">The parent id.</param>
/// <returns>System.Nullable&lt;System.Int32&gt;.</returns> /// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
int? GetSeasonNumberFromPath(string path); int? GetSeasonNumberFromPath(string path, Guid? parentId);
/// <summary> /// <summary>
/// Fills the missing episode numbers from path. /// Fills the missing episode numbers from path.
@ -565,6 +566,15 @@ namespace MediaBrowser.Controller.Library
/// <returns>List of items.</returns> /// <returns>List of items.</returns>
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents); 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> /// <summary>
/// Gets the items result. /// Gets the items result.
/// </summary> /// </summary>

View File

@ -59,6 +59,14 @@ public interface IItemRepository
/// <returns>List&lt;BaseItem&gt;.</returns> /// <returns>List&lt;BaseItem&gt;.</returns>
IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery filter); 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> /// <summary>
/// Updates the inherited values. /// Updates the inherited values.
/// </summary> /// </summary>

View File

@ -31,12 +31,6 @@ namespace MediaBrowser.Controller.Providers
/// </remarks> /// </remarks>
ExternalIdMediaType? Type { get; } 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> /// <summary>
/// Determines whether this id supports a given item type. /// Determines whether this id supports a given item type.
/// </summary> /// </summary>

View File

@ -234,8 +234,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
item.CustomRating = reader.ReadNormalizedString(); item.CustomRating = reader.ReadNormalizedString();
break; break;
case "RunningTime": case "RunningTime":
var runtimeText = reader.ReadElementContentAsString(); var runtimeText = reader.ReadNormalizedString();
if (!string.IsNullOrWhiteSpace(runtimeText)) if (!string.IsNullOrEmpty(runtimeText))
{ {
if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{ {
@ -253,7 +253,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
break; break;
case "LockData": case "LockData":
item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); item.IsLocked = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase);
break; break;
case "Network": case "Network":
foreach (var name in reader.GetStringArray()) foreach (var name in reader.GetStringArray())
@ -331,9 +331,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "Rating": case "Rating":
case "IMDBrating": 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 // 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)) if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
@ -449,7 +449,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "OwnerUserId": case "OwnerUserId":
{ {
var val = reader.ReadElementContentAsString(); var val = reader.ReadNormalizedString();
if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty)) if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty))
{ {
@ -464,7 +464,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "Format3D": case "Format3D":
{ {
var val = reader.ReadElementContentAsString(); var val = reader.ReadNormalizedString();
if (item is Video video) if (item is Video video)
{ {
@ -498,7 +498,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
string readerName = reader.Name; string readerName = reader.Name;
if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue)) if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
{ {
var id = reader.ReadElementContentAsString(); var id = reader.ReadNormalizedString();
item.TrySetProviderId(providerIdValue, id); item.TrySetProviderId(providerIdValue, id);
} }
else else
@ -580,7 +580,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "Tagline": case "Tagline":
item.Tagline = reader.ReadNormalizedString(); var val = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(val))
{
item.Tagline = val;
}
break; break;
default: default:
reader.Skip(); reader.Skip();
@ -842,7 +847,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
userId = reader.ReadNormalizedString(); userId = reader.ReadNormalizedString();
break; break;
case "CanEdit": case "CanEdit":
canEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); canEdit = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase);
break; break;
default: default:
reader.Skip(); reader.Skip();
@ -856,7 +861,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
} }
// This is valid // 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); return new PlaylistUserPermissions(guid, canEdit);
} }

View File

@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using System.Xml; using System.Xml;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -531,42 +532,44 @@ namespace MediaBrowser.MediaEncoding.Probing
private void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info) private void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info)
{ {
List<BaseItemPerson> peoples = new List<BaseItemPerson>(); 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)) if (string.Equals(key, "studio", StringComparison.OrdinalIgnoreCase))
{ {
info.Studios = pairs.Select(p => p.Value) info.Studios = distinctPairs.ToArray();
.Where(i => !string.IsNullOrWhiteSpace(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
} }
else if (string.Equals(key, "screenwriters", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(key, "screenwriters", StringComparison.OrdinalIgnoreCase))
{ {
foreach (var pair in pairs) foreach (var pair in distinctPairs)
{ {
peoples.Add(new BaseItemPerson peoples.Add(new BaseItemPerson
{ {
Name = pair.Value, Name = pair,
Type = PersonKind.Writer Type = PersonKind.Writer
}); });
} }
} }
else if (string.Equals(key, "producers", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(key, "producers", StringComparison.OrdinalIgnoreCase))
{ {
foreach (var pair in pairs) foreach (var pair in distinctPairs)
{ {
peoples.Add(new BaseItemPerson peoples.Add(new BaseItemPerson
{ {
Name = pair.Value, Name = pair,
Type = PersonKind.Producer Type = PersonKind.Producer
}); });
} }
} }
else if (string.Equals(key, "directors", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(key, "directors", StringComparison.OrdinalIgnoreCase))
{ {
foreach (var pair in pairs) foreach (var pair in distinctPairs)
{ {
peoples.Add(new BaseItemPerson peoples.Add(new BaseItemPerson
{ {
Name = pair.Value, Name = pair,
Type = PersonKind.Director Type = PersonKind.Director
}); });
} }
@ -591,10 +594,10 @@ namespace MediaBrowser.MediaEncoding.Probing
switch (reader.Name) switch (reader.Name)
{ {
case "key": case "key":
name = reader.ReadElementContentAsString(); name = reader.ReadNormalizedString();
break; break;
case "string": case "string":
value = reader.ReadElementContentAsString(); value = reader.ReadNormalizedString();
break; break;
default: default:
reader.Skip(); reader.Skip();
@ -607,8 +610,8 @@ namespace MediaBrowser.MediaEncoding.Probing
} }
} }
if (string.IsNullOrWhiteSpace(name) if (string.IsNullOrEmpty(name)
|| string.IsNullOrWhiteSpace(value)) || string.IsNullOrEmpty(value))
{ {
return null; return null;
} }
@ -1453,7 +1456,7 @@ namespace MediaBrowser.MediaEncoding.Probing
var genres = new List<string>(info.Genres); var genres = new List<string>(info.Genres);
foreach (var genre in Split(genreVal, true)) foreach (var genre in Split(genreVal, true))
{ {
if (string.IsNullOrWhiteSpace(genre)) if (string.IsNullOrEmpty(genre))
{ {
continue; continue;
} }

View File

@ -1,5 +1,3 @@
using System;
namespace MediaBrowser.Model.Providers namespace MediaBrowser.Model.Providers
{ {
/// <summary> /// <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="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="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="type">Specific media type for this id.</param>
/// <param name="urlFormatString">URL format string.</param> public ExternalIdInfo(string name, string key, ExternalIdMediaType? type)
public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string? urlFormatString)
{ {
Name = name; Name = name;
Key = key; Key = key;
Type = type; 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> /// <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. /// This can be used along with the <see cref="Name"/> to localize the external id on the client.
/// </remarks> /// </remarks>
public ExternalIdMediaType? Type { get; set; } 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; }
} }
} }

View File

@ -4,15 +4,14 @@ using System;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Model.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>(); EnableImageTypes = Array.Empty<ImageType>();
EnableTotalRecordCount = true; EnableTotalRecordCount = true;
DisableFirstEpisode = false;
NextUpDateCutoff = DateTime.MinValue; NextUpDateCutoff = DateTime.MinValue;
EnableResumable = false; EnableResumable = false;
EnableRewatching = false; EnableRewatching = false;
@ -56,11 +55,6 @@ namespace MediaBrowser.Model.Querying
public bool EnableTotalRecordCount { 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> /// <summary>
/// Gets or sets a value indicating the oldest date for a show to appear in Next Up. /// Gets or sets a value indicating the oldest date for a show to appear in Next Up.
/// </summary> /// </summary>
@ -75,5 +69,4 @@ namespace MediaBrowser.Model.Querying
/// Gets or sets a value indicating whether getting rewatching next up list. /// Gets or sets a value indicating whether getting rewatching next up list.
/// </summary> /// </summary>
public bool EnableRewatching { get; set; } public bool EnableRewatching { get; set; }
}
} }

View File

@ -1146,13 +1146,24 @@ namespace MediaBrowser.Providers.Manager
private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target) private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target)
{ {
foreach (var person in target) var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
{ var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
var normalizedName = person.Name.RemoveDiacritics();
var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.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) foreach (var providerId in personInSource.ProviderIds)
{ {
person.ProviderIds.TryAdd(providerId.Key, providerId.Value); person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
@ -1162,6 +1173,16 @@ namespace MediaBrowser.Providers.Manager
{ {
person.ImageUrl = personInSource.ImageUrl; 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;
}
} }
} }
} }

View File

@ -899,35 +899,10 @@ namespace MediaBrowser.Providers.Manager
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item) public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
{ {
#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 return _externalUrlProviders
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
.SelectMany(p => p .SelectMany(p => p
.GetExternalUrls(item) .GetExternalUrls(item)
.Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl })); .Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl }));
return legacyExternalIdUrls.Concat(externalUrls).OrderBy(u => u.Name);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -937,10 +912,7 @@ namespace MediaBrowser.Providers.Manager
.Select(i => new ExternalIdInfo( .Select(i => new ExternalIdInfo(
name: i.ProviderName, name: i.ProviderName,
key: i.Key, key: i.Key,
type: i.Type, 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
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ATL; using ATL;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; 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); _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; // We should never use the property setter of the ATL.Track class.
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album; // That setter is meant for its own tag parser and external editor usage and will have unwanted side effects
track.Year = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year; // For example, setting the Year property will also set the Date property, which is not what we want here.
track.TrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber; // To properly handle fallback values, we make a clone of those fields when valid.
track.DiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber; 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)) if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{ {
@ -193,11 +198,11 @@ namespace MediaBrowser.Providers.MediaInfo
foreach (var albumArtist in albumArtists) foreach (var albumArtist in albumArtists)
{ {
if (!string.IsNullOrEmpty(albumArtist)) if (!string.IsNullOrWhiteSpace(albumArtist))
{ {
PeopleHelper.AddPerson(people, new PersonInfo PeopleHelper.AddPerson(people, new PersonInfo
{ {
Name = albumArtist, Name = albumArtist.Trim(),
Type = PersonKind.AlbumArtist Type = PersonKind.AlbumArtist
}); });
} }
@ -225,11 +230,11 @@ namespace MediaBrowser.Providers.MediaInfo
foreach (var performer in performers) foreach (var performer in performers)
{ {
if (!string.IsNullOrEmpty(performer)) if (!string.IsNullOrWhiteSpace(performer))
{ {
PeopleHelper.AddPerson(people, new PersonInfo PeopleHelper.AddPerson(people, new PersonInfo
{ {
Name = performer, Name = performer.Trim(),
Type = PersonKind.Artist Type = PersonKind.Artist
}); });
} }
@ -237,11 +242,11 @@ namespace MediaBrowser.Providers.MediaInfo
foreach (var composer in track.Composer.Split(InternalValueSeparator)) foreach (var composer in track.Composer.Split(InternalValueSeparator))
{ {
if (!string.IsNullOrEmpty(composer)) if (!string.IsNullOrWhiteSpace(composer))
{ {
PeopleHelper.AddPerson(people, new PersonInfo PeopleHelper.AddPerson(people, new PersonInfo
{ {
Name = composer, Name = composer.Trim(),
Type = PersonKind.Composer 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) if (options.ReplaceAllMetadata)
{ {
audio.Album = track.Album; audio.Album = trackAlbum;
audio.IndexNumber = track.TrackNumber; audio.IndexNumber = trackTrackNumber;
audio.ParentIndexNumber = track.DiscNumber; audio.ParentIndexNumber = trackDiscNumber;
} }
else else
{ {
audio.Album ??= track.Album; audio.Album ??= trackAlbum;
audio.IndexNumber ??= track.TrackNumber; audio.IndexNumber ??= trackTrackNumber;
audio.ParentIndexNumber ??= track.DiscNumber; audio.ParentIndexNumber ??= trackDiscNumber;
} }
if (track.Date.HasValue) if (track.Date.HasValue)
@ -299,11 +304,12 @@ namespace MediaBrowser.Providers.MediaInfo
audio.PremiereDate = track.Date; audio.PremiereDate = track.Date;
} }
if (track.Year.HasValue) if (trackYear.HasValue)
{ {
var year = track.Year.Value; var year = trackYear.Value;
audio.ProductionYear = year; 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) if (!audio.PremiereDate.HasValue)
{ {
try try
@ -312,7 +318,7 @@ namespace MediaBrowser.Providers.MediaInfo
} }
catch (ArgumentOutOfRangeException ex) 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.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 audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
? genres ? genres
: audio.Genres; : audio.Genres;

View File

@ -6,6 +6,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -407,7 +408,7 @@ namespace MediaBrowser.Providers.MediaInfo
{ {
video.Genres = Array.Empty<string>(); video.Genres = Array.Empty<string>();
foreach (var genre in data.Genres) foreach (var genre in data.Genres.Trimmed())
{ {
video.AddGenre(genre); video.AddGenre(genre);
} }
@ -516,9 +517,9 @@ namespace MediaBrowser.Providers.MediaInfo
{ {
PeopleHelper.AddPerson(people, new PersonInfo PeopleHelper.AddPerson(people, new PersonInfo
{ {
Name = person.Name, Name = person.Name.Trim(),
Type = person.Type, Type = person.Type,
Role = person.Role Role = person.Role.Trim()
}); });
} }

View File

@ -21,9 +21,6 @@ namespace MediaBrowser.Providers.Movies
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => null; public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://www.imdb.com/title/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) public bool Supports(IHasProviderIds item)
{ {

View 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}";
}
}
}
}

View File

@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Movies
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Person; public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
public string UrlFormatString => "https://www.imdb.com/name/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Person; public bool Supports(IHasProviderIds item) => item is Person;
} }

View File

@ -187,7 +187,7 @@ namespace MediaBrowser.Providers.Music
{ {
PeopleHelper.AddPerson(people, new PersonInfo PeopleHelper.AddPerson(people, new PersonInfo
{ {
Name = albumArtist, Name = albumArtist.Trim(),
Type = PersonKind.AlbumArtist Type = PersonKind.AlbumArtist
}); });
} }
@ -196,7 +196,7 @@ namespace MediaBrowser.Providers.Music
{ {
PeopleHelper.AddPerson(people, new PersonInfo PeopleHelper.AddPerson(people, new PersonInfo
{ {
Name = artist, Name = artist.Trim(),
Type = PersonKind.Artist Type = PersonKind.Artist
}); });
} }

View File

@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Music
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => null; public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string? UrlFormatString => null;
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) public bool Supports(IHasProviderIds item)
=> item is MusicVideo; => item is MusicVideo;

View File

@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => null; public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicAlbum; public bool Supports(IHasProviderIds item) => item is MusicAlbum;
} }

View File

@ -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;
}
}
}
}

View File

@ -4,7 +4,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
@ -50,9 +49,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
/// <inheritdoc /> /// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{ {
var id = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var id))
if (!string.IsNullOrWhiteSpace(id))
{ {
await AudioDbAlbumProvider.Current.EnsureInfo(id, cancellationToken).ConfigureAwait(false); 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) private List<RemoteImageInfo> GetImages(AudioDbAlbumProvider.Album item)

View File

@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
/// <inheritdoc />
public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicArtist; public bool Supports(IHasProviderIds item) => item is MusicArtist;
} }

View File

@ -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;
}
}
}
}

View File

@ -4,7 +4,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
@ -43,21 +42,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{ {
return new ImageType[] return
{ [
ImageType.Primary, ImageType.Primary,
ImageType.Logo, ImageType.Logo,
ImageType.Banner, ImageType.Banner,
ImageType.Backdrop ImageType.Backdrop
}; ];
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{ {
var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var id))
if (!string.IsNullOrWhiteSpace(id))
{ {
await AudioDbArtistProvider.Current.EnsureArtistInfo(id, cancellationToken).ConfigureAwait(false); 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) private List<RemoteImageInfo> GetImages(AudioDbArtistProvider.Artist item)

View File

@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Album; public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
/// <inheritdoc />
public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio; public bool Supports(IHasProviderIds item) => item is Audio;
} }

View File

@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
/// <inheritdoc />
public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
} }

View File

@ -19,9 +19,6 @@ public class MusicBrainzAlbumArtistExternalId : IExternalId
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
/// <inheritdoc />
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio; public bool Supports(IHasProviderIds item) => item is Audio;
} }

View File

@ -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}";
}
}
}
}

View File

@ -19,9 +19,6 @@ public class MusicBrainzAlbumExternalId : IExternalId
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Album; public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
/// <inheritdoc />
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
} }

View File

@ -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}";
}
}
}
}

View File

@ -19,9 +19,6 @@ public class MusicBrainzArtistExternalId : IExternalId
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
/// <inheritdoc />
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicArtist; public bool Supports(IHasProviderIds item) => item is MusicArtist;
} }

View File

@ -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;
}
}
}
}

View File

@ -19,9 +19,6 @@ public class MusicBrainzOtherArtistExternalId : IExternalId
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
/// <inheritdoc />
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum; public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
} }

View File

@ -19,9 +19,6 @@ public class MusicBrainzRecordingId : IExternalId
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Recording; public ExternalIdMediaType? Type => ExternalIdMediaType.Recording;
/// <inheritdoc />
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/recording/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio; public bool Supports(IHasProviderIds item) => item is Audio;
} }

View File

@ -19,9 +19,6 @@ public class MusicBrainzReleaseGroupExternalId : IExternalId
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
/// <inheritdoc />
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum; public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
} }

View File

@ -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}";
}
}
}
}

View File

@ -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}";
}
}
}
}

View File

@ -19,9 +19,6 @@ public class MusicBrainzTrackId : IExternalId
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Track; public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
/// <inheritdoc />
public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio; public bool Supports(IHasProviderIds item) => item is Audio;
} }

View File

@ -421,7 +421,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{ {
var person = new PersonInfo var person = new PersonInfo
{ {
Name = result.Director, Name = result.Director.Trim(),
Type = PersonKind.Director Type = PersonKind.Director
}; };
@ -432,7 +432,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{ {
var person = new PersonInfo var person = new PersonInfo
{ {
Name = result.Writer, Name = result.Writer.Trim(),
Type = PersonKind.Writer Type = PersonKind.Writer
}; };

View File

@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet; public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;
/// <inheritdoc />
public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) public bool Supports(IHasProviderIds item)
{ {

View File

@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Movie; public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
/// <inheritdoc />
public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) public bool Supports(IHasProviderIds item)
{ {

View File

@ -234,7 +234,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var genres = movieResult.Genres; 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); movie.AddGenre(genre);
} }
@ -254,7 +254,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var personInfo = new PersonInfo var personInfo = new PersonInfo
{ {
Name = actor.Name.Trim(), Name = actor.Name.Trim(),
Role = actor.Character, Role = actor.Character.Trim(),
Type = PersonKind.Actor, Type = PersonKind.Actor,
SortOrder = actor.Order SortOrder = actor.Order
}; };
@ -289,7 +289,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var personInfo = new PersonInfo var personInfo = new PersonInfo
{ {
Name = person.Name.Trim(), Name = person.Name.Trim(),
Role = person.Job, Role = person.Job?.Trim(),
Type = type Type = type
}; };

View File

@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Person; public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) public bool Supports(IHasProviderIds item)
{ {

View File

@ -211,7 +211,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
metadataResult.AddPerson(new PersonInfo metadataResult.AddPerson(new PersonInfo
{ {
Name = actor.Name.Trim(), Name = actor.Name.Trim(),
Role = actor.Character, Role = actor.Character.Trim(),
Type = PersonKind.Actor, Type = PersonKind.Actor,
SortOrder = actor.Order SortOrder = actor.Order
}); });
@ -225,7 +225,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
metadataResult.AddPerson(new PersonInfo metadataResult.AddPerson(new PersonInfo
{ {
Name = guest.Name.Trim(), Name = guest.Name.Trim(),
Role = guest.Character, Role = guest.Character.Trim(),
Type = PersonKind.GuestStar, Type = PersonKind.GuestStar,
SortOrder = guest.Order SortOrder = guest.Order
}); });
@ -249,7 +249,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
metadataResult.AddPerson(new PersonInfo metadataResult.AddPerson(new PersonInfo
{ {
Name = person.Name.Trim(), Name = person.Name.Trim(),
Role = person.Job, Role = person.Job?.Trim(),
Type = type Type = type
}); });
} }

View File

@ -82,12 +82,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList(); var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList();
for (var i = 0; i < cast.Count; i++) for (var i = 0; i < cast.Count; i++)
{ {
var member = cast[i];
result.AddPerson(new PersonInfo result.AddPerson(new PersonInfo
{ {
Name = cast[i].Name.Trim(), Name = member.Name.Trim(),
Role = cast[i].Character, Role = member.Character.Trim(),
Type = PersonKind.Actor, Type = PersonKind.Actor,
SortOrder = cast[i].Order SortOrder = member.Order
}); });
} }
} }
@ -108,7 +109,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.AddPerson(new PersonInfo result.AddPerson(new PersonInfo
{ {
Name = person.Name.Trim(), Name = person.Name.Trim(),
Role = person.Job, Role = person.Job?.Trim(),
Type = type Type = type
}); });
} }

View File

@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Series; public ExternalIdMediaType? Type => ExternalIdMediaType.Series;
/// <inheritdoc />
public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) public bool Supports(IHasProviderIds item)
{ {

View File

@ -330,7 +330,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var personInfo = new PersonInfo var personInfo = new PersonInfo
{ {
Name = actor.Name.Trim(), Name = actor.Name.Trim(),
Role = actor.Character, Role = actor.Character.Trim(),
Type = PersonKind.Actor, Type = PersonKind.Actor,
SortOrder = actor.Order, SortOrder = actor.Order,
ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath) ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath)
@ -368,7 +368,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
yield return new PersonInfo yield return new PersonInfo
{ {
Name = person.Name.Trim(), Name = person.Name.Trim(),
Role = person.Job, Role = person.Job?.Trim(),
Type = type Type = type
}; };
} }

View File

@ -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;
}
}
}

View File

@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.TV
/// <inheritdoc /> /// <inheritdoc />
public ExternalIdMediaType? Type => null; public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Series; public bool Supports(IHasProviderIds item) => item is Series;
} }

View 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}";
}
}
}

View File

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
@ -55,12 +56,12 @@ namespace MediaBrowser.XbmcMetadata.Savers
{ {
var album = (MusicAlbum)item; var album = (MusicAlbum)item;
foreach (var artist in album.Artists) foreach (var artist in album.Artists.Trimmed().OrderBy(artist => artist))
{ {
writer.WriteElementString("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); writer.WriteElementString("albumartist", artist);
} }
@ -70,11 +71,20 @@ namespace MediaBrowser.XbmcMetadata.Savers
private void AddTracks(IEnumerable<BaseItem> tracks, XmlWriter writer) 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"); 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)); writer.WriteElementString("position", track.IndexNumber.Value.ToString(CultureInfo.InvariantCulture));
} }

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Xml; using System.Xml;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -69,7 +70,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
private void AddAlbums(IReadOnlyList<BaseItem> albums, XmlWriter writer) 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"); writer.WriteStartElement("album");

View File

@ -488,7 +488,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
var directors = people var directors = people
.Where(i => i.IsType(PersonKind.Director)) .Where(i => i.IsType(PersonKind.Director))
.Select(i => i.Name) .Select(i => i.Name?.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(i => i)
.ToList(); .ToList();
foreach (var person in directors) foreach (var person in directors)
@ -498,8 +500,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
var writers = people var writers = people
.Where(i => i.IsType(PersonKind.Writer)) .Where(i => i.IsType(PersonKind.Writer))
.Select(i => i.Name) .Select(i => i.Name?.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(i => i)
.ToList(); .ToList();
foreach (var person in writers) foreach (var person in writers)
@ -512,7 +515,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("credits", person); 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)); writer.WriteElementString("trailer", GetOutputTrailerUrl(trailer.Url));
} }
@ -544,16 +547,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio); writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
} }
var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection); if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection))
if (!string.IsNullOrEmpty(tmdbCollection))
{ {
writer.WriteElementString("collectionnumber", tmdbCollection); writer.WriteElementString("collectionnumber", tmdbCollection);
writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString()); writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());
} }
var imdb = item.GetProviderId(MetadataProvider.Imdb); if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
if (!string.IsNullOrEmpty(imdb))
{ {
if (item is Series) if (item is Series)
{ {
@ -570,16 +570,14 @@ namespace MediaBrowser.XbmcMetadata.Savers
// Series xml saver already saves this // Series xml saver already saves this
if (item is not Series) if (item is not Series)
{ {
var tvdb = item.GetProviderId(MetadataProvider.Tvdb); if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
if (!string.IsNullOrEmpty(tvdb))
{ {
writer.WriteElementString("tvdbid", tvdb); writer.WriteElementString("tvdbid", tvdb);
writtenProviderIds.Add(MetadataProvider.Tvdb.ToString()); writtenProviderIds.Add(MetadataProvider.Tvdb.ToString());
} }
} }
var tmdb = item.GetProviderId(MetadataProvider.Tmdb); if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb))
if (!string.IsNullOrEmpty(tmdb))
{ {
writer.WriteElementString("tmdbid", tmdb); writer.WriteElementString("tmdbid", tmdb);
writtenProviderIds.Add(MetadataProvider.Tmdb.ToString()); writtenProviderIds.Add(MetadataProvider.Tmdb.ToString());
@ -660,22 +658,22 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("tagline", item.Tagline); 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); writer.WriteElementString("country", country);
} }
foreach (var genre in item.Genres) foreach (var genre in item.Genres.Trimmed().OrderBy(genre => genre))
{ {
writer.WriteElementString("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); 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) if (item is MusicAlbum || item is MusicArtist)
{ {
@ -687,64 +685,49 @@ namespace MediaBrowser.XbmcMetadata.Savers
} }
} }
var externalId = item.GetProviderId(MetadataProvider.AudioDbArtist); if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId))
if (!string.IsNullOrEmpty(externalId))
{ {
writer.WriteElementString("audiodbartistid", externalId); writer.WriteElementString("audiodbartistid", externalId);
writtenProviderIds.Add(MetadataProvider.AudioDbArtist.ToString()); writtenProviderIds.Add(MetadataProvider.AudioDbArtist.ToString());
} }
externalId = item.GetProviderId(MetadataProvider.AudioDbAlbum); if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out externalId))
if (!string.IsNullOrEmpty(externalId))
{ {
writer.WriteElementString("audiodbalbumid", externalId); writer.WriteElementString("audiodbalbumid", externalId);
writtenProviderIds.Add(MetadataProvider.AudioDbAlbum.ToString()); writtenProviderIds.Add(MetadataProvider.AudioDbAlbum.ToString());
} }
externalId = item.GetProviderId(MetadataProvider.Zap2It); if (item.TryGetProviderId(MetadataProvider.Zap2It, out externalId))
if (!string.IsNullOrEmpty(externalId))
{ {
writer.WriteElementString("zap2itid", externalId); writer.WriteElementString("zap2itid", externalId);
writtenProviderIds.Add(MetadataProvider.Zap2It.ToString()); writtenProviderIds.Add(MetadataProvider.Zap2It.ToString());
} }
externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbum); if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out externalId))
if (!string.IsNullOrEmpty(externalId))
{ {
writer.WriteElementString("musicbrainzalbumid", externalId); writer.WriteElementString("musicbrainzalbumid", externalId);
writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbum.ToString()); writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbum.ToString());
} }
externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist); if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out externalId))
if (!string.IsNullOrEmpty(externalId))
{ {
writer.WriteElementString("musicbrainzalbumartistid", externalId); writer.WriteElementString("musicbrainzalbumartistid", externalId);
writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbumArtist.ToString()); writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbumArtist.ToString());
} }
externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out externalId))
if (!string.IsNullOrEmpty(externalId))
{ {
writer.WriteElementString("musicbrainzartistid", externalId); writer.WriteElementString("musicbrainzartistid", externalId);
writtenProviderIds.Add(MetadataProvider.MusicBrainzArtist.ToString()); writtenProviderIds.Add(MetadataProvider.MusicBrainzArtist.ToString());
} }
externalId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out externalId))
if (!string.IsNullOrEmpty(externalId))
{ {
writer.WriteElementString("musicbrainzreleasegroupid", externalId); writer.WriteElementString("musicbrainzreleasegroupid", externalId);
writtenProviderIds.Add(MetadataProvider.MusicBrainzReleaseGroup.ToString()); writtenProviderIds.Add(MetadataProvider.MusicBrainzReleaseGroup.ToString());
} }
externalId = item.GetProviderId(MetadataProvider.TvRage); if (item.TryGetProviderId(MetadataProvider.TvRage, out externalId))
if (!string.IsNullOrEmpty(externalId))
{ {
writer.WriteElementString("tvrageid", externalId); writer.WriteElementString("tvrageid", externalId);
writtenProviderIds.Add(MetadataProvider.TvRage.ToString()); writtenProviderIds.Add(MetadataProvider.TvRage.ToString());
@ -752,7 +735,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (item.ProviderIds is not null) 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]; var providerId = item.ProviderIds[providerKey];
if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey)) if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey))
@ -764,7 +747,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
XmlConvert.VerifyName(tagName); XmlConvert.VerifyName(tagName);
Logger.LogDebug("Saving custom provider tagname {0}", tagName); Logger.LogDebug("Saving custom provider tagname {0}", tagName);
writer.WriteElementString(GetTagForProviderKey(providerKey), providerId); writer.WriteElementString(tagName, providerId);
} }
catch (ArgumentException) catch (ArgumentException)
{ {
@ -785,7 +768,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
AddUserData(item, writer, userManager, userDataRepo, options); AddUserData(item, writer, userManager, userDataRepo, options);
if (item is not MusicAlbum && item is not MusicArtist)
{
AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo); AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo);
}
if (item is BoxSet folder) if (item is BoxSet folder)
{ {
@ -797,6 +783,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
{ {
var items = item.LinkedChildren var items = item.LinkedChildren
.Where(i => i.Type == LinkedChildType.Manual) .Where(i => i.Type == LinkedChildType.Manual)
.OrderBy(i => i.Path?.Trim())
.ThenBy(i => i.LibraryItemId?.Trim())
.ToList(); .ToList();
foreach (var link in items) foreach (var link in items)
@ -839,7 +827,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager)); 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)); 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) 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)) if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer))
{ {
@ -1027,5 +1017,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
private string GetTagForProviderKey(string providerKey) private string GetTagForProviderKey(string providerKey)
=> providerKey.ToLowerInvariant() + "id"; => 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();
}
} }
} }

View File

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
@ -91,16 +92,14 @@ namespace MediaBrowser.XbmcMetadata.Savers
/// <inheritdoc /> /// <inheritdoc />
protected override void WriteCustomElements(BaseItem item, XmlWriter writer) protected override void WriteCustomElements(BaseItem item, XmlWriter writer)
{ {
var imdb = item.GetProviderId(MetadataProvider.Imdb); if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
if (!string.IsNullOrEmpty(imdb))
{ {
writer.WriteElementString("id", imdb); writer.WriteElementString("id", imdb);
} }
if (item is MusicVideo musicVideo) 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); writer.WriteElementString("artist", artist);
} }

View File

@ -54,9 +54,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
{ {
var series = (Series)item; var series = (Series)item;
var tvdb = item.GetProviderId(MetadataProvider.Tvdb); if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
if (!string.IsNullOrEmpty(tvdb))
{ {
writer.WriteElementString("id", tvdb); writer.WriteElementString("id", tvdb);

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ICU4N.Text; using ICU4N.Text;
@ -123,5 +125,15 @@ namespace Jellyfin.Extensions
{ {
return (_transliterator.Value is null) ? text : _transliterator.Value.Transliterate(text); 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());
}
} }
} }

View File

@ -344,15 +344,12 @@ public class RecordingsMetadataManager
await writer.WriteElementStringAsync(null, "credits", null, person).ConfigureAwait(false); await writer.WriteElementStringAsync(null, "credits", null, person).ConfigureAwait(false);
} }
var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection); if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection))
if (!string.IsNullOrEmpty(tmdbCollection))
{ {
await writer.WriteElementStringAsync(null, "collectionnumber", null, tmdbCollection).ConfigureAwait(false); await writer.WriteElementStringAsync(null, "collectionnumber", null, tmdbCollection).ConfigureAwait(false);
} }
var imdb = item.GetProviderId(MetadataProvider.Imdb); if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
if (!string.IsNullOrEmpty(imdb))
{ {
if (!isSeriesEpisode) if (!isSeriesEpisode)
{ {
@ -365,8 +362,7 @@ public class RecordingsMetadataManager
lockData = false; lockData = false;
} }
var tvdb = item.GetProviderId(MetadataProvider.Tvdb); if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
if (!string.IsNullOrEmpty(tvdb))
{ {
await writer.WriteElementStringAsync(null, "tvdbid", null, tvdb).ConfigureAwait(false); await writer.WriteElementStringAsync(null, "tvdbid", null, tvdb).ConfigureAwait(false);
@ -374,8 +370,7 @@ public class RecordingsMetadataManager
lockData = false; lockData = false;
} }
var tmdb = item.GetProviderId(MetadataProvider.Tmdb); if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb))
if (!string.IsNullOrEmpty(tmdb))
{ {
await writer.WriteElementStringAsync(null, "tmdbid", null, tmdb).ConfigureAwait(false); await writer.WriteElementStringAsync(null, "tmdbid", null, tmdb).ConfigureAwait(false);

View File

@ -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. // 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 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. // 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) if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
|| (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."); _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); _logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result)) 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) // (For systems with multiple internal network cards, and multiple subnets)
foreach (var intf in availableInterfaces) foreach (var intf in availableInterfaces)
{ {
if (intf.Subnet.Contains(source)) if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source))
{ {
result = NetworkUtils.FormatIPString(intf.Address); result = NetworkUtils.FormatIPString(intf.Address);
_logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); _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)) 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)) return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)
{ && addresses.Any(IsInLocalNetwork);
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;
} }
/// <summary> /// <summary>
@ -917,6 +907,11 @@ public class NetworkManager : INetworkManager, IDisposable
return CheckIfLanAndNotExcluded(address); 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) private bool CheckIfLanAndNotExcluded(IPAddress address)
{ {
foreach (var lanSubnet in _lanSubnets) foreach (var lanSubnet in _lanSubnets)
@ -956,7 +951,7 @@ public class NetworkManager : INetworkManager, IDisposable
{ {
// Only use matching internal subnets // Only use matching internal subnets
// Prefer more specific (bigger subnet prefix) overrides // 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) .OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList(); .ToList();
} }
@ -964,7 +959,7 @@ public class NetworkManager : INetworkManager, IDisposable
{ {
// Only use matching external subnets // Only use matching external subnets
// Prefer more specific (bigger subnet prefix) overrides // 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) .OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList(); .ToList();
} }
@ -972,7 +967,7 @@ public class NetworkManager : INetworkManager, IDisposable
foreach (var data in validPublishedServerUrls) foreach (var data in validPublishedServerUrls)
{ {
// Get interface matching override subnet // 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 if (intf?.Address is not null
|| (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any)) || (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
@ -1035,6 +1030,7 @@ public class NetworkManager : INetworkManager, IDisposable
if (isInExternalSubnet) if (isInExternalSubnet)
{ {
var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address)) var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
.Where(x => !IsLinkLocalAddress(x.Address))
.OrderBy(x => x.Index) .OrderBy(x => x.Index)
.ToList(); .ToList();
if (externalInterfaces.Count > 0) 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. // 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. // If none exists, this will select the first external interface if there is one.
bindAddress = externalInterfaces bindAddress = externalInterfaces
.OrderByDescending(x => x.Subnet.Contains(source)) .OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source))
.ThenByDescending(x => x.Subnet.PrefixLength) .ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index) .ThenBy(x => x.Index)
.Select(x => x.Address) .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. // 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. // If none exists, this will select the first internal interface if there is one.
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address)) 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) .ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index) .ThenBy(x => x.Index)
.Select(x => x.Address) .Select(x => x.Address)
@ -1104,7 +1100,7 @@ public class NetworkManager : INetworkManager, IDisposable
// (For systems with multiple network cards and/or multiple subnets) // (For systems with multiple network cards and/or multiple subnets)
foreach (var intf in extResult) foreach (var intf in extResult)
{ {
if (intf.Subnet.Contains(source)) if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source))
{ {
result = NetworkUtils.FormatIPString(intf.Address); result = NetworkUtils.FormatIPString(intf.Address);
_logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result); _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);

View File

@ -6,32 +6,54 @@ namespace Jellyfin.Naming.Tests.TV;
public class SeasonPathParserTests public class SeasonPathParserTests
{ {
[Theory] [Theory]
[InlineData("/Drive/Season 1", 1, true)] [InlineData("/Drive/Season 1", "/Drive", 1, true)]
[InlineData("/Drive/s1", 1, true)] [InlineData("/Drive/Staffel 1", "/Drive", 1, true)]
[InlineData("/Drive/S1", 1, true)] [InlineData("/Drive/Stagione 1", "/Drive", 1, true)]
[InlineData("/Drive/Season 2", 2, true)] [InlineData("/Drive/sæson 1", "/Drive", 1, true)]
[InlineData("/Drive/Season 02", 2, true)] [InlineData("/Drive/Temporada 1", "/Drive", 1, true)]
[InlineData("/Drive/Seinfeld/S02", 2, true)] [InlineData("/Drive/series 1", "/Drive", 1, true)]
[InlineData("/Drive/Seinfeld/2", 2, true)] [InlineData("/Drive/Kausi 1", "/Drive", 1, true)]
[InlineData("/Drive/Seinfeld - S02", 2, true)] [InlineData("/Drive/Säsong 1", "/Drive", 1, true)]
[InlineData("/Drive/Season 2009", 2009, true)] [InlineData("/Drive/Seizoen 1", "/Drive", 1, true)]
[InlineData("/Drive/Season1", 1, true)] [InlineData("/Drive/Seasong 1", "/Drive", 1, true)]
[InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)] [InlineData("/Drive/Sezon 1", "/Drive", 1, true)]
[InlineData("/Drive/Season 7 (2016)", 7, false)] [InlineData("/Drive/sezona 1", "/Drive", 1, true)]
[InlineData("/Drive/Staffel 7 (2016)", 7, false)] [InlineData("/Drive/sezóna 1", "/Drive", 1, true)]
[InlineData("/Drive/Stagione 7 (2016)", 7, false)] [InlineData("/Drive/Sezonul 1", "/Drive", 1, true)]
[InlineData("/Drive/Season (8)", null, false)] [InlineData("/Drive/시즌 1", "/Drive", 1, true)]
[InlineData("/Drive/3.Staffel", 3, false)] [InlineData("/Drive/シーズン 1", "/Drive", 1, true)]
[InlineData("/Drive/s06e05", null, false)] [InlineData("/Drive/сезон 1", "/Drive", 1, true)]
[InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)] [InlineData("/Drive/Сезон 1", "/Drive", 1, true)]
[InlineData("/Drive/extras", 0, true)] [InlineData("/Drive/Season 10", "/Drive", 10, true)]
[InlineData("/Drive/specials", 0, true)] [InlineData("/Drive/Season 100", "/Drive", 100, true)]
public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory) [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 is not null, result.Success);
Assert.Equal(result.SeasonNumber, seasonNumber); Assert.Equal(seasonNumber, result.SeasonNumber);
Assert.Equal(isSeasonDirectory, result.IsSeasonFolder); Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
} }
} }

View File

@ -217,68 +217,58 @@ public class MediaInfoResolverTests
string file = "My.Video.srt"; string file = "My.Video.srt";
data.Add( data.Add(
file, file,
new[] [
{
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
}, ],
new[] [
{
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
}); ]);
// filename has metadata // filename has metadata
file = "My.Video.Title1.default.forced.sdh.en.srt"; file = "My.Video.Title1.default.forced.sdh.en.srt";
data.Add( data.Add(
file, file,
new[] [
{
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0)
}, ],
new[] [
{
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true, true) CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true, true)
}); ]);
// single stream with metadata // single stream with metadata
file = "My.Video.mks"; file = "My.Video.mks";
data.Add( data.Add(
file, file,
new[] [
{
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true) CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true)
}, ],
new[] [
{ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, false, true)
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true) ]);
});
// stream wins for title/language, filename wins for flags when conflicting // stream wins for title/language, filename wins for flags when conflicting
file = "My.Video.Title2.default.forced.sdh.en.srt"; file = "My.Video.Title2.default.forced.sdh.en.srt";
data.Add( data.Add(
file, file,
new[] [
{
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0) CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0)
}, ],
new[] [
{
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0, true, true, true) 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 // multiple stream with metadata - filename flags ignored but other data filled in when missing from stream
file = "My.Video.Title3.default.forced.en.srt"; file = "My.Video.Title3.default.forced.en.srt";
data.Add( data.Add(
file, file,
new[] [
{
CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0, true, true), CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0, true, true),
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1) CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1)
}, ],
new[] [
{
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title3", 0, true, true), CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title3", 0, true, true),
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1) CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1)
}); ]);
return data; return data;
} }

Some files were not shown because too many files have changed in this diff Show More