mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-18 21:50:25 +01:00
Merge branch 'jellyfin:master' into master
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.9",
|
||||
"version": "9.0.10",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
15
.github/CODEOWNERS
vendored
15
.github/CODEOWNERS
vendored
@@ -1,4 +1,11 @@
|
||||
# Joshua must review all changes to deployment and build.sh
|
||||
.ci/* @joshuaboniface
|
||||
deployment/* @joshuaboniface
|
||||
build.sh @joshuaboniface
|
||||
# Joshua must review all changes to bump_version and any files it touches
|
||||
bump_version @joshuaboniface
|
||||
.github/ISSUE_TEMPLATE @joshuaboniface
|
||||
MediaBrowser.Common/MediaBrowser.Common.csproj @joshuaboniface
|
||||
Jellyfin.Data/Jellyfin.Data.csproj @joshuaboniface
|
||||
MediaBrowser.Controller/MediaBrowser.Controller.csproj @joshuaboniface
|
||||
MediaBrowser.Model/MediaBrowser.Model.csproj @joshuaboniface
|
||||
Emby.Naming/Emby.Naming.csproj @joshuaboniface
|
||||
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @joshuaboniface
|
||||
# Core must approve all changes within the repo config
|
||||
.github/ @jellyfin/core
|
||||
|
||||
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -27,11 +27,11 @@ jobs:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||
uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
|
||||
8
.github/workflows/ci-compat.yml
vendored
8
.github/workflows/ci-compat.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: abi-head
|
||||
retention-days: 14
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: abi-base
|
||||
retention-days: 14
|
||||
@@ -85,13 +85,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download abi-head
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: abi-head
|
||||
path: abi-head
|
||||
|
||||
- name: Download abi-base
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: abi-base
|
||||
path: abi-base
|
||||
|
||||
12
.github/workflows/ci-openapi.yml
vendored
12
.github/workflows/ci-openapi.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
@@ -80,12 +80,12 @@ jobs:
|
||||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
@@ -220,7 +220,7 @@ jobs:
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
|
||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@9870ed167742d546b99962ff815fcc1098355ed8 # v5.4.17
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@f3c6b3f8a29686284ef7a7cf6dccb79a01d98444 # v5.4.18
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
@@ -26,32 +26,32 @@
|
||||
<PackageVersion Include="libse" Version="4.0.12" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||
@@ -84,11 +84,11 @@
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.9" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.10" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.5.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.6.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.3.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -10,10 +10,10 @@ namespace Emby.Naming.TV
|
||||
/// </summary>
|
||||
public static partial class SeasonPathParser
|
||||
{
|
||||
[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>.*)$")]
|
||||
[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>.*)$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex ProcessPre();
|
||||
|
||||
[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>.*)$")]
|
||||
[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>.*)$", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex ProcessPost();
|
||||
|
||||
/// <summary>
|
||||
@@ -86,7 +86,7 @@ namespace Emby.Naming.TV
|
||||
}
|
||||
}
|
||||
|
||||
if (filename.StartsWith('s'))
|
||||
if (filename.Length > 0 && (filename[0] == 'S' || filename[0] == 's'))
|
||||
{
|
||||
var testFilename = filename.AsSpan()[1..];
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -224,7 +223,7 @@ public class ChapterManager : IChapterManager
|
||||
|
||||
if (saveChapters && changesMade)
|
||||
{
|
||||
_chapterRepository.SaveChapters(video.Id, chapters);
|
||||
SaveChapters(video, chapters);
|
||||
}
|
||||
|
||||
DeleteDeadImages(currentImages, chapters);
|
||||
@@ -235,7 +234,9 @@ public class ChapterManager : IChapterManager
|
||||
/// <inheritdoc />
|
||||
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
|
||||
{
|
||||
_chapterRepository.SaveChapters(video.Id, chapters);
|
||||
// Remove any chapters that are outside of the runtime of the video
|
||||
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
|
||||
_chapterRepository.SaveChapters(video.Id, validChapters);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -251,23 +252,9 @@ public class ChapterManager : IChapterManager
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteChapterImages(Video video)
|
||||
public async Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = _pathManager.GetChapterImageFolderPath(video);
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
_logger.LogInformation("Removing chapter images for {Name} [{Id}]", video.Name, video.Id);
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Failed to remove chapter image folder for {Item}: {Exception}", video.Id, ex);
|
||||
}
|
||||
|
||||
_chapterRepository.DeleteChapters(video.Id);
|
||||
await _chapterRepository.DeleteChaptersAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
|
||||
|
||||
@@ -152,6 +152,10 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <inheritdoc />
|
||||
public void MoveDirectory(string source, string destination)
|
||||
{
|
||||
// Make sure parent directory of target exists
|
||||
var parent = Directory.GetParent(destination);
|
||||
parent?.Create();
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Move(source, destination);
|
||||
@@ -248,47 +252,40 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
|
||||
|
||||
// if (!result.IsDirectory)
|
||||
// {
|
||||
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
||||
// }
|
||||
|
||||
if (info is FileInfo fileInfo)
|
||||
{
|
||||
result.Length = fileInfo.Length;
|
||||
|
||||
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
|
||||
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
||||
result.CreationTimeUtc = GetCreationTimeUtc(info);
|
||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
|
||||
if (fileInfo.LinkTarget is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true);
|
||||
if (targetFileInfo is not null)
|
||||
{
|
||||
result.Length = RandomAccess.GetLength(fileHandle);
|
||||
result.Exists = targetFileInfo.Exists;
|
||||
if (result.Exists)
|
||||
{
|
||||
result.Length = targetFileInfo.Length;
|
||||
result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo);
|
||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Exists = false;
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
// Dangling symlinks cannot be detected before opening the file unfortunately...
|
||||
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
|
||||
result.Exists = false;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
// IOException generally means the file is not accessible due to filesystem issues
|
||||
// Catch this exception and mark the file as not exist to ignore it
|
||||
_logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
|
||||
result.Exists = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Length = fileInfo.Length;
|
||||
}
|
||||
}
|
||||
|
||||
result.CreationTimeUtc = GetCreationTimeUtc(info);
|
||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -51,8 +51,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
}
|
||||
|
||||
// Fast path in case the ignore files isn't a symlink and is empty
|
||||
if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0
|
||||
&& dirIgnoreFile.Length == 0)
|
||||
if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -93,6 +92,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
|
||||
private static string GetFileContent(FileInfo dirIgnoreFile)
|
||||
{
|
||||
dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile;
|
||||
if (!dirIgnoreFile.Exists)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
using (var reader = dirIgnoreFile.OpenText())
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.MediaSegments;
|
||||
@@ -20,6 +21,7 @@ public class ExternalDataManager : IExternalDataManager
|
||||
private readonly IMediaSegmentManager _mediaSegmentManager;
|
||||
private readonly IPathManager _pathManager;
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
private readonly IChapterManager _chapterManager;
|
||||
private readonly ILogger<ExternalDataManager> _logger;
|
||||
|
||||
/// <summary>
|
||||
@@ -29,18 +31,21 @@ public class ExternalDataManager : IExternalDataManager
|
||||
/// <param name="mediaSegmentManager">The media segment manager.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
/// <param name="trickplayManager">The trickplay manager.</param>
|
||||
/// <param name="chapterManager">The chapter manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ExternalDataManager(
|
||||
IKeyframeManager keyframeManager,
|
||||
IMediaSegmentManager mediaSegmentManager,
|
||||
IPathManager pathManager,
|
||||
ITrickplayManager trickplayManager,
|
||||
IChapterManager chapterManager,
|
||||
ILogger<ExternalDataManager> logger)
|
||||
{
|
||||
_keyframeManager = keyframeManager;
|
||||
_mediaSegmentManager = mediaSegmentManager;
|
||||
_pathManager = pathManager;
|
||||
_trickplayManager = trickplayManager;
|
||||
_chapterManager = chapterManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -67,5 +72,6 @@ public class ExternalDataManager : IExternalDataManager
|
||||
await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
await _chapterManager.DeleteChapterDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
|
||||
{
|
||||
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||
var instantMixItems = GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||
|
||||
return [item, .. instantMixItems.Where(i => !i.Id.Equals(item.Id))];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
|
||||
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
|
||||
"LabelIpAddressValue": "Dirección IP: {0}",
|
||||
"LabelRunningTimeValue": "Tiempo de reproducción: {0}",
|
||||
"LabelRunningTimeValue": "Tiempo corriendo: {0}",
|
||||
"Latest": "Recientes",
|
||||
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
|
||||
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"TaskCleanActivityLogDescription": "Kustutab määratud ajast vanemad tegevuslogi kirjed.",
|
||||
"UserDownloadingItemWithValues": "{0} laeb alla {1}",
|
||||
"UserDownloadingItemWithValues": "{0} laadib alla {1}",
|
||||
"HeaderRecordingGroups": "Salvestusrühmad",
|
||||
"TaskOptimizeDatabaseDescription": "Tihendab ja puhastab andmebaasi. Selle toimingu tegemine pärast meediakogu andmebaasiga seotud muudatuste skannimist võib jõudlust parandada.",
|
||||
"TaskOptimizeDatabase": "Optimeeri andmebaasi",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Otsib veebist puuduvaid subtiitreid vastavalt määratud metaandmete seadetele.",
|
||||
"TaskDownloadMissingSubtitles": "Laadi alla puuduvad subtiitrid",
|
||||
"TaskDownloadMissingSubtitles": "Hangi puuduvad subtiitrid",
|
||||
"TaskRefreshChannelsDescription": "Värskendab veebikanalite teavet.",
|
||||
"TaskRefreshChannels": "Värskenda kanaleid",
|
||||
"TaskCleanTranscodeDescription": "Kustutab üle ühe päeva vanused transkodeerimisfailid.",
|
||||
"TaskCleanTranscodeDescription": "Kustutab üle ühe päeva vanused transkoodimisfailid.",
|
||||
"TaskCleanTranscode": "Puhasta transkoodimise kataloog",
|
||||
"TaskUpdatePluginsDescription": "Laadib alla ja paigaldab nende pluginate uuendused, mis on seadistatud automaatselt uuenduma.",
|
||||
"TaskUpdatePlugins": "Uuenda pluginaid",
|
||||
@@ -41,10 +41,10 @@
|
||||
"StartupEmbyServerIsLoading": "Jellyfin server laadib. Proovi varsti uuesti.",
|
||||
"User": "Kasutaja",
|
||||
"Undefined": "Määratlemata",
|
||||
"TvShows": "Seriaalid",
|
||||
"TvShows": "Sarjad",
|
||||
"System": "Süsteem",
|
||||
"Sync": "Sünkrooni",
|
||||
"Songs": "Laulud",
|
||||
"Songs": "Lood",
|
||||
"Shows": "Sarjad",
|
||||
"ServerNameNeedsToBeRestarted": "{0} tuleb taaskäivitada",
|
||||
"ScheduledTaskFailedWithName": "{0} nurjus",
|
||||
@@ -92,7 +92,7 @@
|
||||
"HeaderNextUp": "Järgmisena",
|
||||
"HeaderLiveTV": "Otse TV",
|
||||
"HeaderFavoriteSongs": "Lemmiklood",
|
||||
"HeaderFavoriteShows": "Lemmikseriaalid",
|
||||
"HeaderFavoriteShows": "Lemmiksarjad",
|
||||
"HeaderFavoriteEpisodes": "Lemmikepisoodid",
|
||||
"HeaderFavoriteArtists": "Lemmikesitajad",
|
||||
"HeaderFavoriteAlbums": "Lemmikalbumid",
|
||||
@@ -122,20 +122,20 @@
|
||||
"UserOnlineFromDevice": "{0} on ühendatud seadmest {1}",
|
||||
"External": "Väline",
|
||||
"HearingImpaired": "Kuulmispuudega",
|
||||
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
|
||||
"TaskKeyframeExtractor": "Võtmekaadrite eraldamine",
|
||||
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadrid, et luua täpsemaid HLS-i esitusloendeid. See võib kesta pikka aega.",
|
||||
"TaskKeyframeExtractor": "Eralda võtmekaadrid",
|
||||
"TaskRefreshTrickplayImages": "Loo trickplay pildid",
|
||||
"TaskRefreshTrickplayImagesDescription": "Loob trickplay eelvaated videotele lubatud meediakogudes.",
|
||||
"TaskAudioNormalization": "Normaliseeri heli",
|
||||
"TaskAudioNormalization": "Normaliseeri helitugevus",
|
||||
"TaskAudioNormalizationDescription": "Otsib failidest helitugevuse normaliseerimise teavet.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest üksused, mida enam ei eksisteeri.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid",
|
||||
"TaskDownloadMissingLyrics": "Lae alla puuduolev lüürika",
|
||||
"TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika",
|
||||
"TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
|
||||
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
|
||||
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
|
||||
"TaskExtractMediaSegments": "Meediasegmentide skaneerimine",
|
||||
"TaskExtractMediaSegments": "Skaneeri meediasegmente",
|
||||
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
|
||||
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
|
||||
"CleanupUserDataTask": "Kasutajaandmete puhastamise ülesanne",
|
||||
"CleanupUserDataTask": "Puhasta kasutajaandmed",
|
||||
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mis pole enam vähemalt 90 päeva saadaval olnud."
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Collections": "Sammlungen",
|
||||
"DeviceOfflineWithName": "{0} wurde getrennt",
|
||||
"DeviceOnlineWithName": "{0} ist verbunden",
|
||||
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
|
||||
"FailedLoginAttemptWithUserName": "Fählgschlagene Ameldeversuech vo {0}",
|
||||
"Favorites": "Favorite",
|
||||
"Folders": "Ordner",
|
||||
"Genres": "Genre",
|
||||
|
||||
@@ -129,5 +129,12 @@
|
||||
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
|
||||
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
|
||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है"
|
||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
|
||||
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
|
||||
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
|
||||
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
|
||||
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
|
||||
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
|
||||
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
|
||||
"TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.",
|
||||
"TaskDownloadMissingLyrics": "누락된 가사 다운로드",
|
||||
"TaskDownloadMissingLyricsDescription": "가사 다운로드"
|
||||
"TaskDownloadMissingLyricsDescription": "가사 다운로드",
|
||||
"CleanupUserDataTask": "사용자 데이터 정리 작업",
|
||||
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다."
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"Books": "Номууд",
|
||||
"Books": "Номнууд",
|
||||
"HeaderNextUp": "Дараа нь",
|
||||
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
||||
"Songs": "Дуунууд",
|
||||
"Playlists": "Тоглуулах жагсаалт",
|
||||
"Movies": "Кино",
|
||||
"Playlists": "Тоглуулах жагсаалтууд",
|
||||
"Movies": "Кинонууд",
|
||||
"Latest": "Сүүлийн үеийн",
|
||||
"Genres": "Төрлүүд",
|
||||
"Favorites": "Дуртай",
|
||||
"Collections": "Багц",
|
||||
"Collections": "Цуглуулгууд",
|
||||
"Artists": "Уран бүтээлчид",
|
||||
"Albums": "Цомгууд",
|
||||
"Albums": "Дуут цомгууд",
|
||||
"TaskExtractMediaSegments": "Медиа сегмент шалга",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment идэвхжүүлсэн залгаасуудаас медиа сегментүүдийг задлах эсвэл олж авах.",
|
||||
"TaskMoveTrickplayImages": "Трикплэй зургуудын байршлыг шилжүүлэх",
|
||||
@@ -63,15 +63,15 @@
|
||||
"CameraImageUploadedFrom": "{0}-с шинэ зураг байршуулагдлаа",
|
||||
"Channels": "Сувгууд",
|
||||
"ChapterNameValue": "{0}-р бүлэг",
|
||||
"Default": "Өгөгдмөл",
|
||||
"Default": "Анхдагч",
|
||||
"DeviceOfflineWithName": "{0}-н холболт саллаа",
|
||||
"DeviceOnlineWithName": "{0} холбогдлоо",
|
||||
"FailedLoginAttemptWithUserName": "{0}-н нэвтрэх оролдлого амжилтгүй",
|
||||
"Folders": "Хавтаснууд",
|
||||
"Folders": "Хавтасууд",
|
||||
"Forced": "Хүчээр",
|
||||
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
|
||||
"HeaderFavoriteAlbums": "Дуртай цомгууд",
|
||||
"HeaderLiveTV": "Шууд",
|
||||
"HeaderLiveTV": "Шууд ТВ",
|
||||
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
|
||||
"HearingImpaired": "Сонсголын бэрхшээлтэй",
|
||||
"HomeVideos": "Үндсэн дүрсүүд",
|
||||
@@ -84,8 +84,8 @@
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server {0} болж шинэчлэгдлээ",
|
||||
"MessageServerConfigurationUpdated": "Server-н тохиргоо шинэчлэгдлээ",
|
||||
"MixedContent": "Холимог агуулга",
|
||||
"Music": "Дуу",
|
||||
"MusicVideos": "Дууны клип",
|
||||
"Music": "Хөгжим",
|
||||
"MusicVideos": "Дууны клипүүд",
|
||||
"NameInstallFailed": "{0} суулгахад алдаа гарлаа",
|
||||
"NameSeasonNumber": "{0}-р улирал",
|
||||
"NameSeasonUnknown": "Улирал олдсонгүй",
|
||||
@@ -101,15 +101,15 @@
|
||||
"NotificationOptionUserLockedOut": "Хэрэглэгчийг түгжив",
|
||||
"NotificationOptionVideoPlayback": "Бичлэгийг тоглуулж эхлэв",
|
||||
"Photos": "Зургууд",
|
||||
"Plugin": "Plugin",
|
||||
"Plugin": "Плагин",
|
||||
"PluginInstalledWithName": "{0}-г суулгалаа",
|
||||
"PluginUninstalledWithName": "{0}-г устгалаа",
|
||||
"PluginUpdatedWithName": "{0}-г шинэчиллээ",
|
||||
"ProviderValue": "Нийлүүлэгч: {0}",
|
||||
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
|
||||
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
|
||||
"Shows": "Нэвтрүүлгүүд",
|
||||
"Sync": "Дахин",
|
||||
"Shows": "Шоу",
|
||||
"Sync": "Синхрончлох",
|
||||
"System": "Систем",
|
||||
"TvShows": "ТВ нэвтрүүлгүүд",
|
||||
"Undefined": "Танисангүй",
|
||||
@@ -122,7 +122,7 @@
|
||||
"UserPolicyUpdatedWithName": "Хэрэглэгчийн журмыг {0}-д зориулан шинэчиллээ",
|
||||
"UserStartedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж байна",
|
||||
"UserStoppedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж дуусгалаа",
|
||||
"ValueSpecialEpisodeName": "Тусгай - {0}",
|
||||
"ValueSpecialEpisodeName": "Онцгой - {0}",
|
||||
"VersionNumber": "Хувилбар {0}",
|
||||
"TasksMaintenanceCategory": "Засвар",
|
||||
"TasksLibraryCategory": "Сан",
|
||||
|
||||
@@ -132,5 +132,10 @@
|
||||
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
|
||||
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो",
|
||||
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
|
||||
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
|
||||
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
|
||||
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
|
||||
}
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
|
||||
"TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
|
||||
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
|
||||
"TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।",
|
||||
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।"
|
||||
"TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।",
|
||||
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।",
|
||||
"CleanupUserDataTaskDescription": "ਘੱਟੋ-ਘੱਟ 90 ਦਿਨਾਂ ਤੋਂ ਮੌਜੂਦ ਨਾ ਹੋਣ ਵਾਲੇ ਮੀਡੀਆ ਤੋਂ ਸਾਰੇ ਉਪਭੋਗਤਾ ਡੇਟਾ (ਵਾਚ ਸਟੇਟ, ਮਨਪਸੰਦ ਸਟੇਟਸ ਆਦਿ) ਨੂੰ ਸਾਫ਼ ਕਰਦਾ ਹੈ।",
|
||||
"CleanupUserDataTask": "ਯੂਜ਼ਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨ ਦਾ ਕੰਮ"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"HeaderFavoriteShows": "最愛的節目",
|
||||
"HeaderFavoriteSongs": "最愛的歌曲",
|
||||
"HeaderLiveTV": "電視直播",
|
||||
"HeaderNextUp": "接著播放",
|
||||
"HeaderNextUp": "繼續觀看",
|
||||
"HeaderRecordingGroups": "錄製組",
|
||||
"HomeVideos": "家庭影片",
|
||||
"Inherit": "繼承",
|
||||
@@ -137,5 +137,6 @@
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
|
||||
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
|
||||
"CleanupUserDataTask": "用戶資料清理工作"
|
||||
"CleanupUserDataTask": "用戶資料清理工作",
|
||||
"CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。"
|
||||
}
|
||||
|
||||
@@ -261,14 +261,22 @@ public partial class AudioNormalizationTask : IScheduledTask
|
||||
|
||||
using var reader = process.StandardError;
|
||||
float? lufs = null;
|
||||
var foundLufs = false;
|
||||
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
Match match = LUFSRegex().Match(line);
|
||||
if (match.Success)
|
||||
if (foundLufs)
|
||||
{
|
||||
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
Match match = LUFSRegex().Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
foundLufs = true;
|
||||
}
|
||||
|
||||
if (lufs is null)
|
||||
|
||||
@@ -223,15 +223,14 @@ namespace Emby.Server.Implementations.Updates
|
||||
Guid id = default,
|
||||
Version? specificVersion = null)
|
||||
{
|
||||
if (name is not null)
|
||||
{
|
||||
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (!id.IsEmpty())
|
||||
{
|
||||
availablePackages = availablePackages.Where(x => x.Id.Equals(id));
|
||||
}
|
||||
else if (name is not null)
|
||||
{
|
||||
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (specificVersion is not null)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using System;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace Jellyfin.Api.Formatters;
|
||||
@@ -6,7 +10,7 @@ namespace Jellyfin.Api.Formatters;
|
||||
/// <summary>
|
||||
/// Xml output formatter.
|
||||
/// </summary>
|
||||
public sealed class XmlOutputFormatter : StringOutputFormatter
|
||||
public sealed class XmlOutputFormatter : TextOutputFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
|
||||
@@ -15,5 +19,24 @@ public sealed class XmlOutputFormatter : StringOutputFormatter
|
||||
{
|
||||
SupportedMediaTypes.Clear();
|
||||
SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
|
||||
|
||||
SupportedEncodings.Add(Encoding.UTF8);
|
||||
SupportedEncodings.Add(Encoding.Unicode);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(selectedEncoding);
|
||||
|
||||
var valueAsString = context.Object?.ToString();
|
||||
if (string.IsNullOrEmpty(valueAsString))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
await response.WriteAsync(valueAsString, selectedEncoding).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -128,7 +128,8 @@ public class BackupService : IBackupService
|
||||
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
|
||||
|
||||
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|
||||
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal))
|
||||
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|
||||
|| Path.EndsInDirectorySeparator(item.FullName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -199,7 +200,7 @@ public class BackupService : IBackupService
|
||||
var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json")));
|
||||
if (zipEntry is null)
|
||||
{
|
||||
_logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
|
||||
_logger.LogInformation("No backup of expected table {Table} is present in backup, continuing anyway", entityType.Type.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -223,7 +224,7 @@ public class BackupService : IBackupService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item);
|
||||
_logger.LogError(ex, "Could not store entity {Entity}, continuing anyway", item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,11 +234,11 @@ public class BackupService : IBackupService
|
||||
|
||||
_logger.LogInformation("Try restore Database");
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_logger.LogInformation("Restored database.");
|
||||
_logger.LogInformation("Restored database");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated);
|
||||
_logger.LogInformation("Restored Jellyfin system from {Date}", manifest.DateCreated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +264,8 @@ public class BackupService : IBackupService
|
||||
Options = Map(backupOptions)
|
||||
};
|
||||
|
||||
_logger.LogInformation("Running database optimization before backup");
|
||||
|
||||
await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var backupFolder = Path.Combine(_applicationPaths.BackupPath);
|
||||
@@ -281,130 +284,154 @@ public class BackupService : IBackupService
|
||||
}
|
||||
|
||||
var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss}.zip");
|
||||
_logger.LogInformation("Attempt to create a new backup at {BackupPath}", backupPath);
|
||||
var fileStream = File.OpenWrite(backupPath);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Start backup process.");
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
_logger.LogInformation("Attempting to create a new backup at {BackupPath}", backupPath);
|
||||
var fileStream = File.OpenWrite(backupPath);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
|
||||
{
|
||||
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
|
||||
_logger.LogInformation("Starting backup process");
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
|
||||
var enumerable = method.Invoke(dbSet, null)!;
|
||||
return (IAsyncEnumerable<object>)enumerable;
|
||||
}
|
||||
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
|
||||
// include the migration history as well
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
|
||||
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [
|
||||
.. typeof(JellyfinDbContext)
|
||||
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
|
||||
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
|
||||
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
|
||||
];
|
||||
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
|
||||
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
|
||||
|
||||
await using (transaction.ConfigureAwait(false))
|
||||
{
|
||||
_logger.LogInformation("Begin Database backup");
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
|
||||
{
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
||||
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
|
||||
var entities = 0;
|
||||
var zipEntryStream = zipEntry.Open();
|
||||
await using (zipEntryStream.ConfigureAwait(false))
|
||||
var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
|
||||
var enumerable = method.Invoke(dbSet, null)!;
|
||||
return (IAsyncEnumerable<object>)enumerable;
|
||||
}
|
||||
|
||||
// include the migration history as well
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
|
||||
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes =
|
||||
[
|
||||
.. typeof(JellyfinDbContext)
|
||||
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
|
||||
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
|
||||
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
|
||||
];
|
||||
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
|
||||
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
|
||||
|
||||
await using (transaction.ConfigureAwait(false))
|
||||
{
|
||||
_logger.LogInformation("Begin Database backup");
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
|
||||
await using (jsonSerializer.ConfigureAwait(false))
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
||||
var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
|
||||
var entities = 0;
|
||||
var zipEntryStream = zipEntry.Open();
|
||||
await using (zipEntryStream.ConfigureAwait(false))
|
||||
{
|
||||
jsonSerializer.WriteStartArray();
|
||||
|
||||
var set = entityType.ValueFactory().ConfigureAwait(false);
|
||||
await foreach (var item in set.ConfigureAwait(false))
|
||||
var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
|
||||
await using (jsonSerializer.ConfigureAwait(false))
|
||||
{
|
||||
entities++;
|
||||
try
|
||||
jsonSerializer.WriteStartArray();
|
||||
|
||||
var set = entityType.ValueFactory().ConfigureAwait(false);
|
||||
await foreach (var item in set.ConfigureAwait(false))
|
||||
{
|
||||
JsonSerializer.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not load entity {Entity}", item);
|
||||
throw;
|
||||
entities++;
|
||||
try
|
||||
{
|
||||
using var document = JsonSerializer.SerializeToDocument(item, _serializerSettings);
|
||||
document.WriteTo(jsonSerializer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not load entity {Entity}", item);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
jsonSerializer.WriteEndArray();
|
||||
}
|
||||
|
||||
jsonSerializer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("backup of entity {Table} with {Number} created", entityType.Type.Name, entities);
|
||||
_logger.LogInformation("Backup of entity {Table} with {Number} created", entityType.SourceName, entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
|
||||
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
|
||||
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
|
||||
}
|
||||
|
||||
void CopyDirectory(string source, string target, string filter = "*")
|
||||
{
|
||||
if (!Directory.Exists(source))
|
||||
_logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
|
||||
foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
|
||||
.Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
|
||||
{
|
||||
return;
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup of folder {Table}", source);
|
||||
|
||||
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
|
||||
void CopyDirectory(string source, string target, string filter = "*")
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
|
||||
if (!Directory.Exists(source))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup of folder {Table}", source);
|
||||
|
||||
foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
|
||||
{
|
||||
zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
|
||||
}
|
||||
}
|
||||
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
|
||||
if (backupOptions.Subtitles)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
|
||||
}
|
||||
|
||||
if (backupOptions.Trickplay)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
|
||||
}
|
||||
|
||||
if (backupOptions.Metadata)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
||||
}
|
||||
|
||||
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
|
||||
await using (manifestStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
|
||||
if (backupOptions.Subtitles)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
|
||||
}
|
||||
|
||||
if (backupOptions.Trickplay)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
|
||||
}
|
||||
|
||||
if (backupOptions.Metadata)
|
||||
{
|
||||
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
||||
}
|
||||
|
||||
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
|
||||
await using (manifestStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
|
||||
}
|
||||
_logger.LogInformation("Backup created");
|
||||
return Map(manifest, backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create backup, removing {BackupPath}", backupPath);
|
||||
try
|
||||
{
|
||||
if (File.Exists(backupPath))
|
||||
{
|
||||
File.Delete(backupPath);
|
||||
}
|
||||
}
|
||||
catch (Exception innerEx)
|
||||
{
|
||||
_logger.LogWarning(innerEx, "Unable to remove failed backup");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backup created");
|
||||
return Map(manifest, backupPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -422,7 +449,7 @@ public class BackupService : IBackupService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Tried to load archive from {Path} but failed.", archivePath);
|
||||
_logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", archivePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -459,7 +486,7 @@ public class BackupService : IBackupService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not load {BackupArchive} path.", item);
|
||||
_logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -614,6 +614,13 @@ public sealed class BaseItemRepository
|
||||
else
|
||||
{
|
||||
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
|
||||
if (entity.Images is { Count: > 0 })
|
||||
{
|
||||
context.BaseItemImageInfos.AddRange(entity.Images);
|
||||
}
|
||||
|
||||
context.BaseItems.Attach(entity).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
@@ -1232,8 +1239,20 @@ public sealed class BaseItemRepository
|
||||
ExcludeItemIds = filter.ExcludeItemIds
|
||||
};
|
||||
|
||||
var query = TranslateQuery(innerQuery, context, outerQueryFilter)
|
||||
.GroupBy(e => e.PresentationUniqueKey);
|
||||
var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
|
||||
.GroupBy(e => e.PresentationUniqueKey)
|
||||
.Select(e => e.FirstOrDefault())
|
||||
.Select(e => e!.Id);
|
||||
|
||||
var query = context.BaseItems
|
||||
.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.Images)
|
||||
.AsSingleQuery()
|
||||
.Where(e => masterQuery.Contains(e.Id));
|
||||
|
||||
query = ApplyOrder(query, filter);
|
||||
|
||||
var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
|
||||
if (filter.EnableTotalRecordCount)
|
||||
@@ -1288,12 +1307,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
var resultQuery = query.Select(e => new
|
||||
{
|
||||
item = e.AsQueryable()
|
||||
.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.Images)
|
||||
.AsSingleQuery().First(),
|
||||
item = e,
|
||||
// TODO: This is bad refactor!
|
||||
itemCount = new ItemCounts()
|
||||
{
|
||||
@@ -1325,13 +1339,6 @@ public sealed class BaseItemRepository
|
||||
result.Items =
|
||||
[
|
||||
.. query
|
||||
.Select(e => e.AsQueryable()
|
||||
.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.Images)
|
||||
.AsSingleQuery()
|
||||
.First())
|
||||
.AsEnumerable()
|
||||
.Where(e => e is not null)
|
||||
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
|
||||
@@ -1756,7 +1763,8 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Path))
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Path == filter.Path);
|
||||
var pathToQuery = GetPathToSave(filter.Path);
|
||||
baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
|
||||
@@ -2014,7 +2022,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ArtistIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ArtistIds);
|
||||
baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ArtistIds);
|
||||
}
|
||||
|
||||
if (filter.AlbumArtistIds.Length > 0)
|
||||
@@ -2024,7 +2032,18 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ContributingArtistIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ContributingArtistIds);
|
||||
var contributingNames = context.BaseItems
|
||||
.Where(b => filter.ContributingArtistIds.Contains(b.Id))
|
||||
.Select(b => b.CleanName);
|
||||
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(ivm =>
|
||||
ivm.ItemValue.Type == ItemValueType.Artist &&
|
||||
contributingNames.Contains(ivm.ItemValue.CleanValue))
|
||||
&&
|
||||
!e.ItemValues!.Any(ivm =>
|
||||
ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
|
||||
contributingNames.Contains(ivm.ItemValue.CleanValue)));
|
||||
}
|
||||
|
||||
if (filter.AlbumIds.Length > 0)
|
||||
@@ -2035,7 +2054,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ExcludeArtistIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true);
|
||||
baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ExcludeArtistIds, true);
|
||||
}
|
||||
|
||||
if (filter.GenreIds.Count > 0)
|
||||
@@ -2342,17 +2361,23 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.HasImdbId.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb"));
|
||||
baseQuery = filter.HasImdbId.Value
|
||||
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().ToLower()))
|
||||
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().ToLower()));
|
||||
}
|
||||
|
||||
if (filter.HasTmdbId.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb"));
|
||||
baseQuery = filter.HasTmdbId.Value
|
||||
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().ToLower()))
|
||||
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().ToLower()));
|
||||
}
|
||||
|
||||
if (filter.HasTvdbId.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb"));
|
||||
baseQuery = filter.HasTvdbId.Value
|
||||
? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().ToLower()))
|
||||
: baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().ToLower()));
|
||||
}
|
||||
|
||||
var queryTopParentIds = filter.TopParentIds;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
@@ -82,11 +84,14 @@ public class ChapterRepository : IChapterRepository
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteChapters(Guid itemId)
|
||||
public async Task DeleteChaptersAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
context.Chapters.Where(c => c.ItemId.Equals(itemId)).ExecuteDelete();
|
||||
context.SaveChanges();
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
await dbContext.Chapters.Where(c => c.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId)
|
||||
|
||||
@@ -95,6 +95,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
||||
.ToArray();
|
||||
|
||||
var toAdd = people
|
||||
.Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist)
|
||||
.Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString()))
|
||||
.Select(Map);
|
||||
context.Peoples.AddRange(toAdd);
|
||||
@@ -108,6 +109,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
if (person.Type == PersonKind.Artist || person.Type == PersonKind.AlbumArtist)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString());
|
||||
var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role);
|
||||
if (existingMap is null)
|
||||
|
||||
@@ -14,7 +14,7 @@ public static class StorageHelper
|
||||
{
|
||||
private const long TwoGigabyte = 2_147_483_647L;
|
||||
private const long FiveHundredAndTwelveMegaByte = 536_870_911L;
|
||||
private static readonly string[] _byteHumanizedSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
|
||||
private static readonly string[] _byteHumanizedSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
||||
|
||||
/// <summary>
|
||||
/// Tests the available storage capacity on the jellyfin paths with estimated minimum values.
|
||||
@@ -27,7 +27,7 @@ public static class StorageHelper
|
||||
TestDataDirectorySize(applicationPaths.LogDirectoryPath, logger, FiveHundredAndTwelveMegaByte);
|
||||
TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte);
|
||||
TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte);
|
||||
TestDataDirectorySize(applicationPaths.TempDirectory, logger, TwoGigabyte);
|
||||
TestDataDirectorySize(applicationPaths.TempDirectory, logger, FiveHundredAndTwelveMegaByte);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,7 +77,7 @@ public static class StorageHelper
|
||||
var drive = new DriveInfo(path);
|
||||
if (threshold != -1 && drive.AvailableFreeSpace < threshold)
|
||||
{
|
||||
throw new InvalidOperationException($"The path `{path}` has insufficient free space. Required: at least {HumanizeStorageSize(threshold)}.");
|
||||
throw new InvalidOperationException($"The path `{path}` has insufficient free space. Available: {HumanizeStorageSize(drive.AvailableFreeSpace)}, Required: {HumanizeStorageSize(threshold)}.");
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
|
||||
@@ -8,7 +8,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Responses.Add(
|
||||
operation.Responses.TryAdd(
|
||||
"503",
|
||||
new OpenApiResponse
|
||||
{
|
||||
|
||||
@@ -66,15 +66,8 @@ public class SecurityRequirementsOperationFilter : IOperationFilter
|
||||
return;
|
||||
}
|
||||
|
||||
if (!operation.Responses.ContainsKey("401"))
|
||||
{
|
||||
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
}
|
||||
|
||||
if (!operation.Responses.ContainsKey("403"))
|
||||
{
|
||||
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
}
|
||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||
|
||||
var scheme = new OpenApiSecurityScheme
|
||||
{
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) .NET Foundation and Contributors
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Jellyfin.Server.Infrastructure
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override FileMetadata GetFileInfo(string path)
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
var length = fileInfo.Length;
|
||||
// This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371
|
||||
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
||||
{
|
||||
using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
length = RandomAccess.GetLength(fileHandle);
|
||||
}
|
||||
|
||||
return new FileMetadata
|
||||
{
|
||||
Exists = fileInfo.Exists,
|
||||
Length = length,
|
||||
LastModified = fileInfo.LastWriteTimeUtc
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
if (range is not null && rangeLength == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
|
||||
if (!IsSymLink(result.FileName))
|
||||
{
|
||||
await base.WriteFileAsync(context, result, range, rangeLength).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = context.HttpContext.Response;
|
||||
|
||||
if (range is not null)
|
||||
{
|
||||
await SendFileAsync(
|
||||
result.FileName,
|
||||
response,
|
||||
offset: range.From ?? 0L,
|
||||
count: rangeLength).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendFileAsync(
|
||||
result.FileName,
|
||||
response,
|
||||
offset: 0,
|
||||
count: null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var fileInfo = GetFileInfo(filePath);
|
||||
if (offset < 0 || offset > fileInfo.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
|
||||
}
|
||||
|
||||
if (count.HasValue
|
||||
&& (count.Value < 0 || count.Value > fileInfo.Length - offset))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
|
||||
}
|
||||
|
||||
// Copied from SendFileFallback.SendFileAsync
|
||||
const int BufferSize = 1024 * 16;
|
||||
|
||||
var useRequestAborted = !cancellationToken.CanBeCanceled;
|
||||
var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
|
||||
|
||||
var fileStream = new FileStream(
|
||||
filePath,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite,
|
||||
bufferSize: BufferSize,
|
||||
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
localCancel.ThrowIfCancellationRequested();
|
||||
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||
await StreamCopyOperation
|
||||
.CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
catch (OperationCanceledException) when (useRequestAborted)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
|
||||
}
|
||||
}
|
||||
47
Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs
Normal file
47
Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up all Music artists that have been migrated in the 10.11 RC migrations.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-10-09T20:00:00", nameof(CleanMusicArtist))]
|
||||
[JellyfinMigrationBackup(JellyfinDb = true)]
|
||||
public class CleanMusicArtist : IAsyncMigrationRoutine
|
||||
{
|
||||
private readonly IStartupLogger<CleanMusicArtist> _startupLogger;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CleanMusicArtist"/> class.
|
||||
/// </summary>
|
||||
/// <param name="startupLogger">The startup logger.</param>
|
||||
/// <param name="dbContextFactory">The Db context factory.</param>
|
||||
public CleanMusicArtist(IStartupLogger<CleanMusicArtist> startupLogger, IDbContextFactory<JellyfinDbContext> dbContextFactory)
|
||||
{
|
||||
_startupLogger = startupLogger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (context.ConfigureAwait(false))
|
||||
{
|
||||
var peoples = context.Peoples.Where(e => e.PersonType == nameof(PersonKind.Artist) || e.PersonType == nameof(PersonKind.AlbumArtist));
|
||||
_startupLogger.LogInformation("Delete {Number} Artist and Album Artist person types from db", await peoples.CountAsync(cancellationToken).ConfigureAwait(false));
|
||||
|
||||
await peoples
|
||||
.ExecuteDeleteAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,9 +55,25 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
};
|
||||
|
||||
var dataPath = _paths.DataPath;
|
||||
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||
var activityLogPath = Path.Combine(dataPath, DbFilename);
|
||||
if (!File.Exists(activityLogPath))
|
||||
{
|
||||
_logger.LogWarning("{ActivityLogDb} doesn't exist, nothing to migrate", activityLogPath);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var connection = new SqliteConnection($"Filename={activityLogPath}"))
|
||||
{
|
||||
connection.Open();
|
||||
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='ActivityLog';");
|
||||
foreach (var row in tableQuery)
|
||||
{
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'ActivityLog' doesn't exist in {ActivityLogPath}, nothing to migrate", activityLogPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
|
||||
userDbConnection.Open();
|
||||
|
||||
@@ -122,6 +122,16 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
|
||||
{
|
||||
lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
|
||||
return null;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
|
||||
@@ -135,14 +145,21 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
|
||||
return Path.Join(keyframeCachePath, prefix, filename);
|
||||
}
|
||||
|
||||
private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
|
||||
private bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
|
||||
{
|
||||
if (File.Exists(cachePath))
|
||||
{
|
||||
var bytes = File.ReadAllBytes(cachePath);
|
||||
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(cachePath);
|
||||
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
|
||||
|
||||
return cachedResult is not null;
|
||||
return cachedResult is not null;
|
||||
}
|
||||
catch (JsonException jsonException)
|
||||
{
|
||||
_logger.LogWarning(jsonException, "Failed to read {Path}", cachePath);
|
||||
}
|
||||
}
|
||||
|
||||
cachedResult = null;
|
||||
|
||||
@@ -57,11 +57,28 @@ public class MigrateUserDb : IMigrationRoutine
|
||||
public void Perform()
|
||||
{
|
||||
var dataPath = _paths.DataPath;
|
||||
var userDbPath = Path.Combine(dataPath, DbFilename);
|
||||
if (!File.Exists(userDbPath))
|
||||
{
|
||||
_logger.LogWarning("{UserDbPath} doesn't exist, nothing to migrate", userDbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
|
||||
|
||||
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||
using (var connection = new SqliteConnection($"Filename={userDbPath}"))
|
||||
{
|
||||
connection.Open();
|
||||
var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='LocalUsersv2';");
|
||||
foreach (var row in tableQuery)
|
||||
{
|
||||
if (row.GetInt32(0) == 0)
|
||||
{
|
||||
_logger.LogWarning("Table 'LocalUsersv2' doesn't exist in {UserDbPath}, nothing to migrate", userDbPath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
using var dbContext = _provider.CreateDbContext();
|
||||
|
||||
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
|
||||
|
||||
@@ -224,6 +224,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping attachment at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -263,6 +275,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
|
||||
{
|
||||
date = File.GetLastWriteTimeUtc(path);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
|
||||
|
||||
@@ -184,6 +184,12 @@ namespace Jellyfin.Server
|
||||
.AddSingleton<IServiceCollection>(e))
|
||||
.Build();
|
||||
|
||||
/*
|
||||
* Initialize the transcode path marker so we avoid starting Jellyfin in a broken state.
|
||||
* This should really be a part of IApplicationPaths but this path is configured differently.
|
||||
*/
|
||||
_ = appHost.ConfigurationManager.GetTranscodePath();
|
||||
|
||||
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
|
||||
appHost.ServiceProvider = _jellyfinHost.Services;
|
||||
PrepareDatabaseProvider(appHost.ServiceProvider);
|
||||
|
||||
@@ -250,6 +250,7 @@ public sealed class SetupServer : IDisposable
|
||||
{ "isInReportingMode", _isUnhealthy },
|
||||
{ "retryValue", retryAfterValue },
|
||||
{ "logs", startupLogEntries },
|
||||
{ "networkManagerReady", networkManager is not null },
|
||||
{ "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
|
||||
},
|
||||
new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions))
|
||||
|
||||
@@ -213,7 +213,12 @@
|
||||
</ol>
|
||||
</div>
|
||||
{{#ELSE}}
|
||||
{{#IF networkManagerReady}}
|
||||
<p>Please visit this page from your local network to view detailed startup logs.</p>
|
||||
{{#ELSE}}
|
||||
<p>Initializing network settings. Please wait.</p>
|
||||
{{/ELSE}}
|
||||
{{/IF}}
|
||||
{{/ELSE}}
|
||||
{{/IF}}
|
||||
</div>
|
||||
|
||||
@@ -16,15 +16,12 @@ using Jellyfin.Networking.HappyEyeballs;
|
||||
using Jellyfin.Server.Extensions;
|
||||
using Jellyfin.Server.HealthChecks;
|
||||
using Jellyfin.Server.Implementations.Extensions;
|
||||
using Jellyfin.Server.Infrastructure;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.XbmcMetadata;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -69,8 +66,6 @@ namespace Jellyfin.Server
|
||||
options.HttpsPort = _serverApplicationHost.HttpsPort;
|
||||
});
|
||||
|
||||
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
|
||||
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
|
||||
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
|
||||
services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration);
|
||||
services.AddJellyfinApiSwagger();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -48,8 +48,10 @@ public interface IChapterManager
|
||||
Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the chapter images.
|
||||
/// Deletes the chapter data.
|
||||
/// </summary>
|
||||
/// <param name="video">Video to use.</param>
|
||||
void DeleteChapterImages(Video video);
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -457,6 +457,12 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
foreach (var item in itemsRemoved)
|
||||
{
|
||||
if (!item.CanDelete())
|
||||
{
|
||||
Logger.LogDebug("Item marked as non-removable, skipping: {Path}", item.Path ?? item.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
Logger.LogDebug("Removed item: {Path}", item.Path);
|
||||
@@ -709,9 +715,18 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
else
|
||||
{
|
||||
items = GetRecursiveChildren(user, query, out totalCount);
|
||||
// Save pagination params before clearing them to prevent pagination from happening
|
||||
// before sorting. PostFilterAndSort will apply pagination after sorting.
|
||||
var limit = query.Limit;
|
||||
var startIndex = query.StartIndex;
|
||||
query.Limit = null;
|
||||
query.StartIndex = null; // override these here as they have already been applied
|
||||
query.StartIndex = null;
|
||||
|
||||
items = GetRecursiveChildren(user, query, out totalCount);
|
||||
|
||||
// Restore pagination params so PostFilterAndSort can apply them after sorting
|
||||
query.Limit = limit;
|
||||
query.StartIndex = startIndex;
|
||||
}
|
||||
|
||||
var result = PostFilterAndSort(items, query);
|
||||
@@ -974,20 +989,16 @@ namespace MediaBrowser.Controller.Entities
|
||||
else
|
||||
{
|
||||
// need to pass this param to the children.
|
||||
// Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort
|
||||
var childQuery = new InternalItemsQuery
|
||||
{
|
||||
DisplayAlbumFolders = query.DisplayAlbumFolders,
|
||||
Limit = query.Limit,
|
||||
StartIndex = query.StartIndex,
|
||||
NameStartsWith = query.NameStartsWith,
|
||||
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
|
||||
NameLessThan = query.NameLessThan
|
||||
};
|
||||
|
||||
items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
|
||||
|
||||
query.Limit = null;
|
||||
query.StartIndex = null;
|
||||
}
|
||||
|
||||
var result = PostFilterAndSort(items, query);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -2390,8 +2390,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|
||||
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
|
||||
{
|
||||
// If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
|
||||
if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG)
|
||||
// If the video stream is in HDR10+ or a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
|
||||
if (videoStream.VideoRangeType is VideoRangeType.HDR10Plus or VideoRangeType.HDR10 or VideoRangeType.HLG)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence;
|
||||
@@ -13,7 +15,9 @@ public interface IChapterRepository
|
||||
/// Deletes the chapters.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item.</param>
|
||||
void DeleteChapters(Guid itemId);
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteChaptersAsync(Guid itemId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the chapters.
|
||||
|
||||
@@ -18,10 +18,16 @@ namespace MediaBrowser.MediaEncoding.Configuration
|
||||
|
||||
public void Validate(object oldConfig, object newConfig)
|
||||
{
|
||||
var newPath = ((EncodingOptions)newConfig).TranscodingTempPath;
|
||||
var oldEncodingOptions = (EncodingOptions)oldConfig;
|
||||
var newEncodingOptions = (EncodingOptions)newConfig;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(oldEncodingOptions, nameof(oldConfig));
|
||||
ArgumentNullException.ThrowIfNull(newEncodingOptions, nameof(newConfig));
|
||||
|
||||
var newPath = newEncodingOptions.TranscodingTempPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newPath)
|
||||
&& !string.Equals(((EncodingOptions)oldConfig).TranscodingTempPath, newPath, StringComparison.Ordinal))
|
||||
&& !string.Equals(oldEncodingOptions.TranscodingTempPath, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
// Validate
|
||||
if (!Directory.Exists(newPath))
|
||||
@@ -33,6 +39,12 @@ namespace MediaBrowser.MediaEncoding.Configuration
|
||||
newPath));
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newEncodingOptions.EncoderAppPath)
|
||||
&& !string.Equals(oldEncodingOptions.EncoderAppPath, newEncodingOptions.EncoderAppPath, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("Unable to update encoder app path.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1122,7 +1122,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
private void StartProcess(ProcessWrapper process)
|
||||
{
|
||||
process.Process.Start();
|
||||
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
|
||||
try
|
||||
{
|
||||
process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Process.StartInfo.FileName);
|
||||
}
|
||||
|
||||
lock (_runningProcessesLock)
|
||||
{
|
||||
|
||||
@@ -930,6 +930,15 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
stream.Rotation = data.Rotation;
|
||||
}
|
||||
|
||||
// Parse video frame cropping metadata from side_data
|
||||
// TODO: save them and make HW filters to apply them in HWA pipelines
|
||||
else if (string.Equals(data.SideDataType, "Frame Cropping", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Streams containing artificially added frame cropping
|
||||
// metadata should not be marked as anamorphic.
|
||||
stream.IsAnamorphic = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -138,6 +138,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||
}
|
||||
|
||||
var item = itemResult.Item;
|
||||
item.IndexNumber = episodeNumber;
|
||||
item.ParentIndexNumber = seasonNumber;
|
||||
|
||||
var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
|
||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, null, null, cancellationToken).ConfigureAwait(false);
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, null, null, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (collection?.Images is null)
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
|
||||
if (tmdbId > 0)
|
||||
{
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language, searchInfo.MetadataCountryCode), searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (collection is null)
|
||||
{
|
||||
@@ -70,7 +70,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
return new[] { result };
|
||||
}
|
||||
|
||||
var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false);
|
||||
var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var collections = new RemoteSearchResult[collectionSearchResults.Count];
|
||||
for (var i = 0; i < collectionSearchResults.Count; i++)
|
||||
@@ -95,6 +95,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
{
|
||||
var tmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
|
||||
var language = info.MetadataLanguage;
|
||||
|
||||
// We don't already have an Id, need to fetch it
|
||||
if (tmdbId <= 0)
|
||||
{
|
||||
@@ -102,7 +103,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
// Caller provides the filename with extension stripped and NOT the parsed filename
|
||||
var parsedName = _libraryManager.ParseName(info.Name);
|
||||
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
|
||||
var searchResults = await _tmdbClientManager.SearchCollectionAsync(cleanedName, language, cancellationToken).ConfigureAwait(false);
|
||||
var searchResults = await _tmdbClientManager.SearchCollectionAsync(cleanedName, language, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (searchResults is not null && searchResults.Count > 0)
|
||||
{
|
||||
@@ -114,7 +115,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
|
||||
|
||||
if (tmdbId > 0)
|
||||
{
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
|
||||
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (collection is not null)
|
||||
{
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
var countryCode = item.GetPreferredMetadataCountryCode();
|
||||
|
||||
var movieTmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
|
||||
if (movieTmdbId <= 0)
|
||||
@@ -69,7 +70,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
}
|
||||
|
||||
var movieResult = await _tmdbClientManager.FindByExternalIdAsync(movieImdbId, FindExternalSource.Imdb, language, cancellationToken).ConfigureAwait(false);
|
||||
var movieResult = await _tmdbClientManager.FindByExternalIdAsync(movieImdbId, FindExternalSource.Imdb, language, countryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (movieResult?.MovieResults is not null && movieResult.MovieResults.Count > 0)
|
||||
{
|
||||
movieTmdbId = movieResult.MovieResults[0].Id;
|
||||
@@ -83,7 +84,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
|
||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||
var movie = await _tmdbClientManager
|
||||
.GetMovieAsync(movieTmdbId, null, null, cancellationToken)
|
||||
.GetMovieAsync(movieTmdbId, null, null, null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (movie?.Images is null)
|
||||
|
||||
@@ -59,7 +59,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
.GetMovieAsync(
|
||||
int.Parse(id, CultureInfo.InvariantCulture),
|
||||
searchInfo.MetadataLanguage,
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode),
|
||||
searchInfo.MetadataCountryCode,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -93,7 +94,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
var result = await _tmdbClientManager.FindByExternalIdAsync(
|
||||
id,
|
||||
FindExternalSource.Imdb,
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode),
|
||||
searchInfo.MetadataCountryCode,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
movieResults = result?.MovieResults;
|
||||
}
|
||||
@@ -103,7 +105,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
var result = await _tmdbClientManager.FindByExternalIdAsync(
|
||||
id,
|
||||
FindExternalSource.TvDb,
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage),
|
||||
TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode),
|
||||
searchInfo.MetadataCountryCode,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
movieResults = result?.MovieResults;
|
||||
}
|
||||
@@ -111,7 +114,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
if (movieResults is null)
|
||||
{
|
||||
movieResults = await _tmdbClientManager
|
||||
.SearchMovieAsync(searchInfo.Name, searchInfo.Year ?? 0, searchInfo.MetadataLanguage, cancellationToken)
|
||||
.SearchMovieAsync(searchInfo.Name, searchInfo.Year ?? 0, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -152,7 +155,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
// Caller provides the filename with extension stripped and NOT the parsed filename
|
||||
var parsedName = _libraryManager.ParseName(info.Name);
|
||||
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
|
||||
var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (searchResults.Count > 0)
|
||||
{
|
||||
@@ -162,7 +166,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
|
||||
if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (movieResultFromImdbId?.MovieResults.Count > 0)
|
||||
{
|
||||
tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture);
|
||||
@@ -175,7 +179,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
}
|
||||
|
||||
var movieResult = await _tmdbClientManager
|
||||
.GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
||||
.GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (movieResult is null)
|
||||
|
||||
@@ -60,7 +60,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
}
|
||||
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), language, cancellationToken).ConfigureAwait(false);
|
||||
var countryCode = item.GetPreferredMetadataCountryCode();
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), language, countryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (personResult?.Images?.Profiles is null)
|
||||
{
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
{
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var personTmdbId))
|
||||
{
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
var personResult = await _tmdbClientManager.GetPersonAsync(int.Parse(personTmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (personResult is not null)
|
||||
{
|
||||
@@ -101,7 +101,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
||||
|
||||
if (personTmdbId > 0)
|
||||
{
|
||||
var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (person is null)
|
||||
{
|
||||
return result;
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||
var episodeResult = await _tmdbClientManager
|
||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
|
||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, series.DisplayOrder, null, null, null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var stills = episodeResult?.Images?.Stills;
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
List<TvEpisode>? result = null;
|
||||
for (int? episode = startindex; episode <= endindex; episode++)
|
||||
{
|
||||
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
|
||||
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (episodeInfo is not null)
|
||||
{
|
||||
(result ??= new List<TvEpisode>()).Add(episodeInfo);
|
||||
@@ -157,7 +157,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
else
|
||||
{
|
||||
episodeResult = await _tmdbClientManager
|
||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
||||
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -177,8 +177,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
var item = new Episode
|
||||
{
|
||||
IndexNumber = info.IndexNumber,
|
||||
ParentIndexNumber = info.ParentIndexNumber,
|
||||
IndexNumber = episodeNumber,
|
||||
ParentIndexNumber = seasonNumber,
|
||||
IndexNumberEnd = info.IndexNumberEnd,
|
||||
Name = episodeResult.Name,
|
||||
PremiereDate = episodeResult.AirDate,
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||
var seasonResult = await _tmdbClientManager
|
||||
.GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, null, null, cancellationToken)
|
||||
.GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, null, null, null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var posters = seasonResult?.Images?.Posters;
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
}
|
||||
|
||||
var seasonResult = await _tmdbClientManager
|
||||
.GetSeasonAsync(Convert.ToInt32(seriesTmdbId, CultureInfo.InvariantCulture), seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
||||
.GetSeasonAsync(Convert.ToInt32(seriesTmdbId, CultureInfo.InvariantCulture), seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (seasonResult is null)
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
|
||||
var series = await _tmdbClientManager
|
||||
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), null, null, cancellationToken)
|
||||
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), null, null, null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (series?.Images is null)
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId))
|
||||
{
|
||||
var series = await _tmdbClientManager
|
||||
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, cancellationToken)
|
||||
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (series is not null)
|
||||
@@ -71,7 +71,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
|
||||
{
|
||||
var findResult = await _tmdbClientManager
|
||||
.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, cancellationToken)
|
||||
.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var tvResults = findResult?.TvResults;
|
||||
@@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
|
||||
{
|
||||
var findResult = await _tmdbClientManager
|
||||
.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, cancellationToken)
|
||||
.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var tvResults = findResult?.TvResults;
|
||||
@@ -110,7 +110,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
}
|
||||
}
|
||||
|
||||
var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken: cancellationToken)
|
||||
var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
|
||||
@@ -173,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
|
||||
{
|
||||
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (searchResult?.TvResults.Count > 0)
|
||||
{
|
||||
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
|
||||
@@ -182,7 +182,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
|
||||
{
|
||||
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (searchResult?.TvResults.Count > 0)
|
||||
{
|
||||
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
|
||||
@@ -196,7 +196,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
// Caller provides the filename with extension stripped and NOT the parsed filename
|
||||
var parsedName = _libraryManager.ParseName(info.Name);
|
||||
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
|
||||
var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false);
|
||||
var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.MetadataCountryCode, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (searchResults.Count > 0)
|
||||
{
|
||||
@@ -212,7 +212,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var tvShow = await _tmdbClientManager
|
||||
.GetSeriesAsync(tmdbIdInt, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
|
||||
.GetSeriesAsync(tmdbIdInt, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage, info.MetadataCountryCode), info.MetadataCountryCode, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (tvShow is null)
|
||||
|
||||
@@ -51,9 +51,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="tmdbId">The movie's TMDb id.</param>
|
||||
/// <param name="language">The movie's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb movie or null if not found.</returns>
|
||||
public async Task<Movie?> GetMovieAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
public async Task<Movie?> GetMovieAsync(int tmdbId, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out Movie? movie))
|
||||
@@ -71,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
movie = await _tmDbClient.GetMovieAsync(
|
||||
tmdbId,
|
||||
TmdbUtils.NormalizeLanguage(language),
|
||||
TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
imageLanguages,
|
||||
extraMethods,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
@@ -90,9 +91,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="tmdbId">The collection's TMDb id.</param>
|
||||
/// <param name="language">The collection's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb collection or null if not found.</returns>
|
||||
public async Task<Collection?> GetCollectionAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
public async Task<Collection?> GetCollectionAsync(int tmdbId, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out Collection? collection))
|
||||
@@ -104,7 +106,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
collection = await _tmDbClient.GetCollectionAsync(
|
||||
tmdbId,
|
||||
TmdbUtils.NormalizeLanguage(language),
|
||||
TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
imageLanguages,
|
||||
CollectionMethods.Images,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
@@ -123,9 +125,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="tmdbId">The tv show's TMDb id.</param>
|
||||
/// <param name="language">The tv show's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb tv show information or null if not found.</returns>
|
||||
public async Task<TvShow?> GetSeriesAsync(int tmdbId, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
public async Task<TvShow?> GetSeriesAsync(int tmdbId, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out TvShow? series))
|
||||
@@ -143,7 +146,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
series = await _tmDbClient.GetTvShowAsync(
|
||||
tmdbId,
|
||||
language: TmdbUtils.NormalizeLanguage(language),
|
||||
language: TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
includeImageLanguage: imageLanguages,
|
||||
extraMethods: extraMethods,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
@@ -163,9 +166,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="displayOrder">The display order.</param>
|
||||
/// <param name="language">The tv show's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb tv show episode group information or null if not found.</returns>
|
||||
private async Task<TvGroupCollection?> GetSeriesGroupAsync(int tvShowId, string displayOrder, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
private async Task<TvGroupCollection?> GetSeriesGroupAsync(int tvShowId, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
TvGroupType? groupType =
|
||||
string.Equals(displayOrder, "originalAirDate", StringComparison.Ordinal) ? TvGroupType.OriginalAirDate :
|
||||
@@ -190,7 +194,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
await EnsureClientConfigAsync().ConfigureAwait(false);
|
||||
|
||||
var series = await GetSeriesAsync(tvShowId, language, imageLanguages, cancellationToken).ConfigureAwait(false);
|
||||
var series = await GetSeriesAsync(tvShowId, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
|
||||
var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id;
|
||||
|
||||
if (episodeGroupId is null)
|
||||
@@ -200,7 +204,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
group = await _tmDbClient.GetTvEpisodeGroupsAsync(
|
||||
episodeGroupId,
|
||||
language: TmdbUtils.NormalizeLanguage(language),
|
||||
language: TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (group is not null)
|
||||
@@ -218,9 +222,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="seasonNumber">The season number.</param>
|
||||
/// <param name="language">The tv season's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb tv season information or null if not found.</returns>
|
||||
public async Task<TvSeason?> GetSeasonAsync(int tvShowId, int seasonNumber, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
public async Task<TvSeason?> GetSeasonAsync(int tvShowId, int seasonNumber, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out TvSeason? season))
|
||||
@@ -233,7 +238,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
season = await _tmDbClient.GetTvSeasonAsync(
|
||||
tvShowId,
|
||||
seasonNumber,
|
||||
language: TmdbUtils.NormalizeLanguage(language),
|
||||
language: TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
includeImageLanguage: imageLanguages,
|
||||
extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonMethods.Videos,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
@@ -255,9 +260,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="displayOrder">The display order.</param>
|
||||
/// <param name="language">The episode's language.</param>
|
||||
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb tv episode information or null if not found.</returns>
|
||||
public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string? language, string? imageLanguages, CancellationToken cancellationToken)
|
||||
public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out TvEpisode? episode))
|
||||
@@ -267,7 +273,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
await EnsureClientConfigAsync().ConfigureAwait(false);
|
||||
|
||||
var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken).ConfigureAwait(false);
|
||||
var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
|
||||
if (group is not null)
|
||||
{
|
||||
var season = group.Groups.Find(s => s.Order == seasonNumber);
|
||||
@@ -284,7 +290,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
tvShowId,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
language: TmdbUtils.NormalizeLanguage(language),
|
||||
language: TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
includeImageLanguage: imageLanguages,
|
||||
extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpisodeMethods.Videos,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
@@ -301,10 +307,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id.
|
||||
/// </summary>
|
||||
/// <param name="personTmdbId">The person's TMDb id.</param>
|
||||
/// <param name="language">The episode's language.</param>
|
||||
/// <param name="language">The person's language.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb person information or null if not found.</returns>
|
||||
public async Task<Person?> GetPersonAsync(int personTmdbId, string language, CancellationToken cancellationToken)
|
||||
public async Task<Person?> GetPersonAsync(int personTmdbId, string language, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out Person? person))
|
||||
@@ -316,7 +323,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
|
||||
person = await _tmDbClient.GetPersonAsync(
|
||||
personTmdbId,
|
||||
TmdbUtils.NormalizeLanguage(language),
|
||||
TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -334,12 +341,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="externalId">The item's external id.</param>
|
||||
/// <param name="source">The source of the id eg. IMDb.</param>
|
||||
/// <param name="language">The item's language.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb item or null if not found.</returns>
|
||||
public async Task<FindContainer?> FindByExternalIdAsync(
|
||||
string externalId,
|
||||
FindExternalSource source,
|
||||
string language,
|
||||
string? countryCode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
@@ -353,7 +362,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
result = await _tmDbClient.FindAsync(
|
||||
source,
|
||||
externalId,
|
||||
TmdbUtils.NormalizeLanguage(language),
|
||||
TmdbUtils.NormalizeLanguage(language, countryCode),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result is not null)
|
||||
@@ -369,10 +378,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the tv show.</param>
|
||||
/// <param name="language">The tv show's language.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="year">The year the tv show first aired.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb tv show information.</returns>
|
||||
public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, string? countryCode, int year = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = $"searchseries-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null)
|
||||
@@ -383,7 +393,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
await EnsureClientConfigAsync().ConfigureAwait(false);
|
||||
|
||||
var searchResults = await _tmDbClient
|
||||
.SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
|
||||
.SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (searchResults.Results.Count > 0)
|
||||
@@ -431,7 +441,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <returns>The TMDb movie information.</returns>
|
||||
public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
|
||||
{
|
||||
return SearchMovieAsync(name, 0, language, cancellationToken);
|
||||
return SearchMovieAsync(name, 0, language, null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -440,9 +450,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// <param name="name">The name of the movie.</param>
|
||||
/// <param name="year">The release year of the movie.</param>
|
||||
/// <param name="language">The movie's language.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb movie information.</returns>
|
||||
public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken)
|
||||
public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null)
|
||||
@@ -453,7 +464,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
await EnsureClientConfigAsync().ConfigureAwait(false);
|
||||
|
||||
var searchResults = await _tmDbClient
|
||||
.SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
|
||||
.SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (searchResults.Results.Count > 0)
|
||||
@@ -469,9 +480,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the collection.</param>
|
||||
/// <param name="language">The collection's language.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The TMDb collection information.</returns>
|
||||
public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken)
|
||||
public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, string? countryCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var key = $"collectionsearch-{name}-{language}";
|
||||
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not null)
|
||||
@@ -482,7 +494,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
await EnsureClientConfigAsync().ConfigureAwait(false);
|
||||
|
||||
var searchResults = await _tmDbClient
|
||||
.SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken)
|
||||
.SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (searchResults.Results.Count > 0)
|
||||
|
||||
@@ -105,14 +105,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// Normalizes a language string for use with TMDb's include image language parameter.
|
||||
/// </summary>
|
||||
/// <param name="preferredLanguage">The preferred language as either a 2 letter code with or without country code.</param>
|
||||
/// <param name="countryCode">The country code, ISO 3166-1.</param>
|
||||
/// <returns>The comma separated language string.</returns>
|
||||
public static string GetImageLanguagesParam(string preferredLanguage)
|
||||
public static string GetImageLanguagesParam(string preferredLanguage, string? countryCode = null)
|
||||
{
|
||||
var languages = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(preferredLanguage))
|
||||
{
|
||||
preferredLanguage = NormalizeLanguage(preferredLanguage);
|
||||
preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode);
|
||||
|
||||
languages.Add(preferredLanguage);
|
||||
|
||||
@@ -140,15 +141,24 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// Normalizes a language string for use with TMDb's language parameter.
|
||||
/// </summary>
|
||||
/// <param name="language">The language code.</param>
|
||||
/// <param name="countryCode">The country code.</param>
|
||||
/// <returns>The normalized language code.</returns>
|
||||
[return: NotNullIfNotNull(nameof(language))]
|
||||
public static string? NormalizeLanguage(string? language)
|
||||
public static string? NormalizeLanguage(string? language, string? countryCode = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(language))
|
||||
{
|
||||
return language;
|
||||
}
|
||||
|
||||
// Handle es-419 (Latin American Spanish) by converting to regional variant
|
||||
if (string.Equals(language, "es-419", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(countryCode))
|
||||
{
|
||||
language = string.Equals(countryCode, "AR", StringComparison.OrdinalIgnoreCase)
|
||||
? "es-AR"
|
||||
: "es-MX";
|
||||
}
|
||||
|
||||
// TMDb requires this to be uppercase
|
||||
// Everything after the hyphen must be written in uppercase due to a way TMDb wrote their API.
|
||||
// See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab
|
||||
|
||||
@@ -107,6 +107,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
// Additional Mappings
|
||||
_validProviderIds.Add("collectionnumber", "TmdbCollection");
|
||||
_validProviderIds.Add("tmdbcolid", "TmdbCollection");
|
||||
_validProviderIds.Add("tmdbcol", "TmdbCollection");
|
||||
_validProviderIds.Add("imdb_id", "Imdb");
|
||||
|
||||
Fetch(item, metadataFile, GetXmlReaderSettings(), cancellationToken);
|
||||
@@ -315,7 +316,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
if (userData is not null)
|
||||
{
|
||||
userData.Played = played;
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
|
||||
if (!item.Id.IsEmpty())
|
||||
{
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,7 +337,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
if (userData is not null)
|
||||
{
|
||||
userData.PlayCount = count;
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
|
||||
if (!item.Id.IsEmpty())
|
||||
{
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,7 +358,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
if (userData is not null)
|
||||
{
|
||||
userData.LastPlayedDate = lastPlayed;
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
|
||||
if (!item.Id.IsEmpty())
|
||||
{
|
||||
_userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,7 +603,18 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
|
||||
var provider = reader.GetAttribute("type");
|
||||
var providerId = reader.ReadElementContentAsString();
|
||||
item.TrySetProviderId(provider, providerId);
|
||||
|
||||
if (!string.IsNullOrEmpty(provider))
|
||||
{
|
||||
if (_validProviderIds.TryGetValue(provider, out string? normalizedProvider))
|
||||
{
|
||||
item.TrySetProviderId(normalizedProvider, providerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.TrySetProviderId(provider, providerId);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case "thumb":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.11.0")]
|
||||
[assembly: AssemblyFileVersion("10.11.0")]
|
||||
[assembly: AssemblyVersion("10.12.0")]
|
||||
[assembly: AssemblyFileVersion("10.12.0")]
|
||||
|
||||
@@ -58,7 +58,7 @@ for subproject in ${jellyfin_subprojects[@]}; do
|
||||
done
|
||||
|
||||
# Set the version in the GitHub issue template file
|
||||
sed -i "s|${old_version}|${new_version_sed}|g" ${issue_template_file}
|
||||
sed -i "s|${old_version}|${new_version_sed}|g" "${issue_template_file}"
|
||||
|
||||
# Stage the changed files for commit
|
||||
git add .
|
||||
|
||||
@@ -53,6 +53,34 @@ public static class JellyfinQueryHelperExtensions
|
||||
return baseQuery.Where(ReferencedItemFilterExpressionBuilder(context, itemValueType, referenceIds, invert));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a query that checks referenced ItemValues for a cross BaseItem lookup.
|
||||
/// </summary>
|
||||
/// <param name="baseQuery">The source query.</param>
|
||||
/// <param name="context">The database context.</param>
|
||||
/// <param name="itemValueTypes">The type of item value to reference.</param>
|
||||
/// <param name="referenceIds">The list of BaseItem ids to check matches.</param>
|
||||
/// <param name="invert">If set an exclusion check is performed instead.</param>
|
||||
/// <returns>A Query.</returns>
|
||||
public static IQueryable<BaseItemEntity> WhereReferencedItemMultipleTypes(
|
||||
this IQueryable<BaseItemEntity> baseQuery,
|
||||
JellyfinDbContext context,
|
||||
IList<ItemValueType> itemValueTypes,
|
||||
IList<Guid> referenceIds,
|
||||
bool invert = false)
|
||||
{
|
||||
var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id);
|
||||
var typeFilter = OneOrManyExpressionBuilder<ItemValue, ItemValueType>(itemValueTypes, iv => iv.Type);
|
||||
|
||||
return baseQuery.Where(item =>
|
||||
context.ItemValues
|
||||
.Where(typeFilter)
|
||||
.Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (itemVal, map) => new { itemVal, map })
|
||||
.Any(val =>
|
||||
context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue)
|
||||
&& val.map.ItemId == item.Id) == EF.Constant(!invert));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a query expression that checks referenced ItemValues for a cross BaseItem lookup.
|
||||
/// </summary>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Extensions</PackageId>
|
||||
<VersionPrefix>10.11.0</VersionPrefix>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -42,7 +42,15 @@ public static class FfProbeKeyframeExtractor
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
try
|
||||
{
|
||||
process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We do not care if process priority setting fails
|
||||
// Ideally log a warning but this does not have a logger available
|
||||
}
|
||||
|
||||
return ParseStream(process.StandardOutput);
|
||||
}
|
||||
|
||||
@@ -7,23 +7,38 @@ public class SeasonPathParserTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("/Drive/Season 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEASON 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Staffel 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/STAFFEL 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Stagione 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/STAGIONE 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/sæson 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SÆSON 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Temporada 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/TEMPORADA 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/series 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SERIES 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Kausi 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/KAUSI 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Säsong 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SÄSONG 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Seizoen 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEIZOEN 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Seasong 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEASONG 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Sezon 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEZON 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/sezona 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEZONA 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/sezóna 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEZÓNA 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Sezonul 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/SEZONUL 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/시즌 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/シーズン 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/сезон 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Сезон 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/СЕЗОН 1", "/Drive", 1, true)]
|
||||
[InlineData("/Drive/Season 10", "/Drive", 10, true)]
|
||||
[InlineData("/Drive/Season 100", "/Drive", 100, true)]
|
||||
[InlineData("/Drive/s1", "/Drive", 1, true)]
|
||||
@@ -46,8 +61,11 @@ public class SeasonPathParserTests
|
||||
[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/EXTRAS", "/Drive", 0, true)]
|
||||
[InlineData("/Drive/specials", "/Drive", 0, true)]
|
||||
[InlineData("/Drive/SPECIALS", "/Drive", 0, true)]
|
||||
[InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)]
|
||||
[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, parentPath, true, true);
|
||||
|
||||
@@ -275,5 +275,24 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
||||
Assert.StartsWith(">>", item.Overview, StringComparison.InvariantCulture);
|
||||
Assert.EndsWith("<<", item.Overview, StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_TmdbcolUniqueId_NormalizedToTmdbCollection()
|
||||
{
|
||||
var result = new MetadataResult<Video>()
|
||||
{
|
||||
Item = new Movie()
|
||||
};
|
||||
|
||||
_parser.Fetch(result, "Test Data/Lilo & Stitch.nfo", CancellationToken.None);
|
||||
var item = (Movie)result.Item;
|
||||
|
||||
// Verify that <uniqueid type="tmdbcol"> is normalized to TmdbCollection
|
||||
Assert.True(item.ProviderIds.ContainsKey(MetadataProvider.TmdbCollection.ToString()));
|
||||
Assert.Equal("97020", item.ProviderIds[MetadataProvider.TmdbCollection.ToString()]);
|
||||
|
||||
// Verify that the lowercase "tmdbcol" is NOT in the provider IDs
|
||||
Assert.False(item.ProviderIds.ContainsKey("tmdbcol"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<movie>
|
||||
<title>Lilo & Stitch</title>
|
||||
<originaltitle>Lilo & Stitch</originaltitle>
|
||||
<uniqueid type="tmdbcol" default="false">97020</uniqueid>
|
||||
<set>Lilo & Stitch Collection</set>
|
||||
<plot>>>As Stitch, a runaway genetic experiment from a faraway planet, wreaks havoc on the Hawaiian Islands, he becomes the mischievous adopted alien "puppy" of an independent little girl named Lilo and learns about loyalty, friendship, and ʻohana, the Hawaiian tradition of family.<<</plot>
|
||||
</movie>
|
||||
|
||||
Reference in New Issue
Block a user