mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-21 08:15:28 +03:00
Compare commits
104 Commits
openapi-ca
...
explicit-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6709a69e7 | ||
|
|
4cdd8c8233 | ||
|
|
6e60634c9f | ||
|
|
12c5d6b636 | ||
|
|
b617c62f8e | ||
|
|
035b5895b0 | ||
|
|
22da5187c8 | ||
|
|
5804d6840c | ||
|
|
b50ce1ad6b | ||
|
|
481ee03f35 | ||
|
|
d91adb5d54 | ||
|
|
ef7f138a4e | ||
|
|
2e8d9a311b | ||
|
|
4c5a3fbff3 | ||
|
|
636908fc4d | ||
|
|
997362fc97 | ||
|
|
c5147341e3 | ||
|
|
ca33bcebf0 | ||
|
|
d32f487e8e | ||
|
|
fb65f8f853 | ||
|
|
2a0b90e385 | ||
|
|
dde70fd8a2 | ||
|
|
98d1d0cb35 | ||
|
|
ba76a8f3ad | ||
|
|
8cd5652157 | ||
|
|
8aff4227d9 | ||
|
|
026f7472cb | ||
|
|
daca285568 | ||
|
|
fbb9a0b2c7 | ||
|
|
29b3aa8543 | ||
|
|
94f3725208 | ||
|
|
0ee81e87be | ||
|
|
c491a918c2 | ||
|
|
1e7e46cb82 | ||
|
|
5ae444d96d | ||
|
|
ee7ad83427 | ||
|
|
921d7d3364 | ||
|
|
f8e012582a | ||
|
|
def5956cd1 | ||
|
|
abfbaca336 | ||
|
|
6566188e45 | ||
|
|
078f9584ed | ||
|
|
ee34c75386 | ||
|
|
e8150428b6 | ||
|
|
4b38e35bbb | ||
|
|
435bb14bb2 | ||
|
|
2e5ced5098 | ||
|
|
f4a846aa4d | ||
|
|
7c1063177f | ||
|
|
5878b1ffc5 | ||
|
|
3c3c2aee0d | ||
|
|
511223aac4 | ||
|
|
3b2d64995a | ||
|
|
13c4517a66 | ||
|
|
177b6464ca | ||
|
|
5a9a8363f4 | ||
|
|
49efd68fc7 | ||
|
|
90a8a26c6e | ||
|
|
002c83e6f5 | ||
|
|
7222910b05 | ||
|
|
097cb87f6f | ||
|
|
91c3b1617e | ||
|
|
8f71922734 | ||
|
|
d140630208 | ||
|
|
63a3e55297 | ||
|
|
c2e5081d64 | ||
|
|
4187c6f620 | ||
|
|
e7dbb3afec | ||
|
|
f994dd6211 | ||
|
|
da254ee968 | ||
|
|
4ad3141875 | ||
|
|
b5f0199a25 | ||
|
|
6bf88c049e | ||
|
|
40a33da2a5 | ||
|
|
3596fc0693 | ||
|
|
93824dad97 | ||
|
|
e5656af1f2 | ||
|
|
c127c10458 | ||
|
|
7d1824ea27 | ||
|
|
2966d27c97 | ||
|
|
618ec4543e | ||
|
|
0e4031ae52 | ||
|
|
442af96ed9 | ||
|
|
a305204cfa | ||
|
|
75f472e6a7 | ||
|
|
cc32e8f7cb | ||
|
|
14b3085ff1 | ||
|
|
5691eee4f1 | ||
|
|
1520a697ad | ||
|
|
81b8b0ca4a | ||
|
|
ac3fa3c376 | ||
|
|
7a1c1cd342 | ||
|
|
70c32a26fa | ||
|
|
2b94bb54aa | ||
|
|
0a6e8146be | ||
|
|
305b0fdca3 | ||
|
|
d738386fe2 | ||
|
|
ca830d5be7 | ||
|
|
a5bc4524d8 | ||
|
|
175ee12bbc | ||
|
|
a725220c21 | ||
|
|
a245605152 | ||
|
|
f4a53209f4 | ||
|
|
877251bcae |
15
.github/CODEOWNERS
vendored
15
.github/CODEOWNERS
vendored
@@ -1,11 +1,4 @@
|
||||
# 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
|
||||
# Joshua must review all changes to deployment and build.sh
|
||||
.ci/* @joshuaboniface
|
||||
deployment/* @joshuaboniface
|
||||
build.sh @joshuaboniface
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/issue report.yml
vendored
5
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -87,10 +87,7 @@ body:
|
||||
label: Jellyfin Server version
|
||||
description: What version of Jellyfin are you using?
|
||||
options:
|
||||
- 10.11.3
|
||||
- 10.11.2
|
||||
- 10.11.1
|
||||
- 10.11.0
|
||||
- 10.10.0+
|
||||
- Master
|
||||
- Unstable
|
||||
- Older*
|
||||
|
||||
8
.github/workflows/ci-codeql-analysis.yml
vendored
8
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -20,18 +20,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||
|
||||
4
.github/workflows/ci-compat.yml
vendored
4
.github/workflows/ci-compat.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
4
.github/workflows/ci-openapi.yml
vendored
4
.github/workflows/ci-openapi.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
with:
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@dcdfb6e704e87df6b2ed0cf123a6c9f69e364869 # v5.5.0
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # v5.5.1
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
4
.github/workflows/commands.yml
vendored
4
.github/workflows/commands.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
|
||||
2
.github/workflows/issue-stale.yml
vendored
2
.github/workflows/issue-stale.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
ascending: true
|
||||
|
||||
2
.github/workflows/issue-template-check.yml
vendored
2
.github/workflows/issue-template-check.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
|
||||
2
.github/workflows/pull-request-stale.yaml
vendored
2
.github/workflows/pull-request-stale.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
ascending: true
|
||||
|
||||
4
.github/workflows/release-bump-version.yaml
vendored
4
.github/workflows/release-bump-version.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
yq-version: v4.9.8
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
- [sachk](https://github.com/sachk)
|
||||
- [sammyrc34](https://github.com/sammyrc34)
|
||||
- [samuel9554](https://github.com/samuel9554)
|
||||
- [SapientGuardian](https://github.com/SapientGuardian)
|
||||
- [scheidleon](https://github.com/scheidleon)
|
||||
- [sebPomme](https://github.com/sebPomme)
|
||||
- [SegiH](https://github.com/SegiH)
|
||||
@@ -205,6 +206,7 @@
|
||||
- [theshoeshiner](https://github.com/theshoeshiner)
|
||||
- [TokerX](https://github.com/TokerX)
|
||||
- [GeneMarks](https://github.com/GeneMarks)
|
||||
- [martenumberto](https://github.com/martenumberto)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</PropertyGroup>
|
||||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||
<ItemGroup Label="Package Dependencies">
|
||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.7" />
|
||||
<PackageVersion Include="AsyncKeyedLock" Version="7.1.8" />
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
@@ -96,4 +96,4 @@
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -17,13 +17,6 @@ namespace Emby.Naming.TV
|
||||
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
|
||||
private static partial Regex SeriesNameRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Regex that matches titles with year in parentheses. Captures the title (which may be
|
||||
/// numeric) before the year, i.e. turns "1923 (2022)" into "1923".
|
||||
/// </summary>
|
||||
[GeneratedRegex(@"(?<title>.+?)\s*\(\d{4}\)")]
|
||||
private static partial Regex TitleWithYearRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Resolve information about series from path.
|
||||
/// </summary>
|
||||
@@ -34,20 +27,6 @@ namespace Emby.Naming.TV
|
||||
{
|
||||
string seriesName = Path.GetFileName(path);
|
||||
|
||||
// First check if the filename matches a title with year pattern (handles numeric titles)
|
||||
if (!string.IsNullOrEmpty(seriesName))
|
||||
{
|
||||
var titleWithYearMatch = TitleWithYearRegex().Match(seriesName);
|
||||
if (titleWithYearMatch.Success)
|
||||
{
|
||||
seriesName = titleWithYearMatch.Groups["title"].Value.Trim();
|
||||
return new SeriesInfo(path)
|
||||
{
|
||||
Name = seriesName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
|
||||
if (result.Success)
|
||||
{
|
||||
|
||||
@@ -497,8 +497,17 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <inheritdoc />
|
||||
public virtual bool AreEqual(string path1, string path2)
|
||||
{
|
||||
return Path.TrimEndingDirectorySeparator(path1).Equals(
|
||||
Path.TrimEndingDirectorySeparator(path2),
|
||||
if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized1 = Path.TrimEndingDirectorySeparator(path1);
|
||||
var normalized2 = Path.TrimEndingDirectorySeparator(path2);
|
||||
|
||||
return string.Equals(
|
||||
normalized1,
|
||||
normalized2,
|
||||
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
@@ -70,12 +71,55 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||
{
|
||||
// If file has content, base ignoring off the content .gitignore-style rules
|
||||
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return CheckIgnoreRules(path, rules, isDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a path should be ignored based on an array of ignore rules.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to check.</param>
|
||||
/// <param name="rules">The array of ignore rules.</param>
|
||||
/// <param name="isDirectory">Whether the path is a directory.</param>
|
||||
/// <returns>True if the path should be ignored.</returns>
|
||||
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory)
|
||||
=> CheckIgnoreRules(path, rules, isDirectory, IsWindows);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a path should be ignored based on an array of ignore rules.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to check.</param>
|
||||
/// <param name="rules">The array of ignore rules.</param>
|
||||
/// <param name="isDirectory">Whether the path is a directory.</param>
|
||||
/// <param name="normalizePath">Whether to normalize backslashes to forward slashes (for Windows paths).</param>
|
||||
/// <returns>True if the path should be ignored.</returns>
|
||||
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath)
|
||||
{
|
||||
var ignore = new Ignore.Ignore();
|
||||
ignore.Add(rules);
|
||||
|
||||
// Add each rule individually to catch and skip invalid patterns
|
||||
var validRulesAdded = 0;
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
try
|
||||
{
|
||||
ignore.Add(rule);
|
||||
validRulesAdded++;
|
||||
}
|
||||
catch (RegexParseException)
|
||||
{
|
||||
// Ignore invalid patterns
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid rules were added, fall back to ignoring everything (like an empty .ignore file)
|
||||
if (validRulesAdded == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
|
||||
// See https://github.com/jellyfin/jellyfin/issues/15484
|
||||
var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
|
||||
var pathToCheck = normalizePath ? path.NormalizePath('/') : path;
|
||||
|
||||
// Add trailing slash for directories to match "folder/"
|
||||
if (isDirectory)
|
||||
|
||||
@@ -1058,6 +1058,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.MusicArtist],
|
||||
Name = name,
|
||||
UseRawName = true,
|
||||
DtoOptions = options
|
||||
}).Cast<MusicArtist>()
|
||||
.OrderBy(i => i.IsAccessedByName ? 1 : 0)
|
||||
|
||||
@@ -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 corriendo: {0}",
|
||||
"LabelRunningTimeValue": "Tiempo de reproducción: {0}",
|
||||
"Latest": "Recientes",
|
||||
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
|
||||
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
|
||||
|
||||
@@ -137,5 +137,5 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
|
||||
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
|
||||
"CleanupUserDataTask": "Puhasta kasutajaandmed",
|
||||
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."
|
||||
"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": "Fählgschlagene Ameldeversuech vo {0}",
|
||||
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
|
||||
"Favorites": "Favorite",
|
||||
"Folders": "Ordner",
|
||||
"Genres": "Genre",
|
||||
|
||||
@@ -129,12 +129,5 @@
|
||||
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
|
||||
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
|
||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
|
||||
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
|
||||
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
|
||||
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
|
||||
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
|
||||
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
|
||||
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
|
||||
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है"
|
||||
}
|
||||
|
||||
@@ -136,7 +136,5 @@
|
||||
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
|
||||
"TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.",
|
||||
"TaskDownloadMissingLyrics": "누락된 가사 다운로드",
|
||||
"TaskDownloadMissingLyricsDescription": "가사 다운로드",
|
||||
"CleanupUserDataTask": "사용자 데이터 정리 작업",
|
||||
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다."
|
||||
"TaskDownloadMissingLyricsDescription": "가사 다운로드"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"HeaderNextUp": "Дараа нь",
|
||||
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
||||
"Songs": "Дуунууд",
|
||||
"Playlists": "Тоглуулах жагсаалтууд",
|
||||
"Playlists": "Playlist-ууд",
|
||||
"Movies": "Кинонууд",
|
||||
"Latest": "Сүүлийн үеийн",
|
||||
"Genres": "Төрлүүд",
|
||||
@@ -71,7 +71,7 @@
|
||||
"Forced": "Хүчээр",
|
||||
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
|
||||
"HeaderFavoriteAlbums": "Дуртай цомгууд",
|
||||
"HeaderLiveTV": "Шууд ТВ",
|
||||
"HeaderLiveTV": "Шууд",
|
||||
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
|
||||
"HearingImpaired": "Сонсголын бэрхшээлтэй",
|
||||
"HomeVideos": "Үндсэн дүрсүүд",
|
||||
@@ -109,7 +109,7 @@
|
||||
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
|
||||
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
|
||||
"Shows": "Шоу",
|
||||
"Sync": "Синхрончлох",
|
||||
"Sync": "Дахин",
|
||||
"System": "Систем",
|
||||
"TvShows": "ТВ нэвтрүүлгүүд",
|
||||
"Undefined": "Танисангүй",
|
||||
|
||||
@@ -132,10 +132,5 @@
|
||||
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
|
||||
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो",
|
||||
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
|
||||
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
|
||||
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
|
||||
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -134,8 +134,6 @@
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
|
||||
"TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
|
||||
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
|
||||
"TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।",
|
||||
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।",
|
||||
"CleanupUserDataTaskDescription": "ਘੱਟੋ-ਘੱਟ 90 ਦਿਨਾਂ ਤੋਂ ਮੌਜੂਦ ਨਾ ਹੋਣ ਵਾਲੇ ਮੀਡੀਆ ਤੋਂ ਸਾਰੇ ਉਪਭੋਗਤਾ ਡੇਟਾ (ਵਾਚ ਸਟੇਟ, ਮਨਪਸੰਦ ਸਟੇਟਸ ਆਦਿ) ਨੂੰ ਸਾਫ਼ ਕਰਦਾ ਹੈ।",
|
||||
"CleanupUserDataTask": "ਯੂਜ਼ਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨ ਦਾ ਕੰਮ"
|
||||
"TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।",
|
||||
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"Collections": "Barrels",
|
||||
"ItemAddedWithName": "{0} is now with yer treasure",
|
||||
"Default": "Normal-like",
|
||||
"FailedLoginAttemptWithUserName": "Ye failed to enter from {0}",
|
||||
"FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}",
|
||||
"Favorites": "Finest Loot",
|
||||
"ItemRemovedWithName": "{0} was taken from yer treasure",
|
||||
"LabelIpAddressValue": "Ship's coordinates: {0}",
|
||||
@@ -113,10 +113,5 @@
|
||||
"TaskCleanCache": "Sweep the Cache Chest",
|
||||
"TaskRefreshChapterImages": "Claim chapter portraits",
|
||||
"TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.",
|
||||
"TaskRefreshLibrary": "Scan the Treasure Trove",
|
||||
"TasksChannelsCategory": "Channels o' thy Internet",
|
||||
"TaskRefreshTrickplayImages": "Summon the picture tricks",
|
||||
"TaskRefreshTrickplayImagesDescription": "Summons picture trick previews for videos in ye enabled book roost",
|
||||
"TaskUpdatePlugins": "Resummon yer Plugins",
|
||||
"TaskCleanTranscode": "Swab Ye Transcode Directory"
|
||||
"TaskRefreshLibrary": "Scan the Treasure Trove"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"Artists": "Artistas",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
|
||||
"Books": "Livros",
|
||||
"CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
|
||||
"CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}",
|
||||
"Channels": "Canais",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"Collections": "Coleções",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"TasksMaintenanceCategory": "Bảo Trì",
|
||||
"VersionNumber": "Phiên Bản {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
|
||||
"UserStoppedPlayingItemWithValues": "{0} đã kết thúc phát {1} trên {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
|
||||
"UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
|
||||
"UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"HeaderFavoriteShows": "最愛的節目",
|
||||
"HeaderFavoriteSongs": "最愛的歌曲",
|
||||
"HeaderLiveTV": "電視直播",
|
||||
"HeaderNextUp": "繼續觀看",
|
||||
"HeaderNextUp": "接著播放",
|
||||
"HeaderRecordingGroups": "錄製組",
|
||||
"HomeVideos": "家庭影片",
|
||||
"Inherit": "繼承",
|
||||
@@ -127,8 +127,8 @@
|
||||
"HearingImpaired": "聽力障礙",
|
||||
"TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
|
||||
"TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。",
|
||||
"TaskExtractMediaSegments": "掃描媒體分段資訊",
|
||||
"TaskExtractMediaSegmentsDescription": "從允許MediaSegment 功能的插件中獲取媒體片段。",
|
||||
"TaskExtractMediaSegments": "掃描媒體段落",
|
||||
"TaskExtractMediaSegmentsDescription": "從MediaSegment中被允許的插件獲取媒體段落。",
|
||||
"TaskDownloadMissingLyrics": "下載欠缺歌詞",
|
||||
"TaskDownloadMissingLyricsDescription": "下載歌詞",
|
||||
"TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單",
|
||||
@@ -137,6 +137,5 @@
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
|
||||
"TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
|
||||
"TaskMoveTrickplayImages": "轉移 Trickplay 影像位置",
|
||||
"CleanupUserDataTask": "用戶資料清理工作",
|
||||
"CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。"
|
||||
"CleanupUserDataTask": "用戶資料清理工作"
|
||||
}
|
||||
|
||||
@@ -347,8 +347,8 @@ pli||pi|Pali|pali
|
||||
pol||pl|Polish|polonais
|
||||
pon|||Pohnpeian|pohnpei
|
||||
por||pt|Portuguese|portugais
|
||||
por||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
|
||||
por||pt-br|Portuguese (Brazil)|portugais (pt-br)
|
||||
pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
|
||||
pob||pt-br|Portuguese (Brazil)|portugais (pt-br)
|
||||
pra|||Prakrit languages|prâkrit, langues
|
||||
pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500)
|
||||
pus||ps|Pushto; Pashto|pachto
|
||||
|
||||
@@ -122,6 +122,7 @@ public class ArtistsController : BaseJellyfinApiController
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
@@ -325,6 +326,7 @@ public class ArtistsController : BaseJellyfinApiController
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
@@ -465,7 +467,7 @@ public class ArtistsController : BaseJellyfinApiController
|
||||
public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
|
||||
var item = _libraryManager.GetArtist(name, dtoOptions);
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public class CollectionController : BaseJellyfinApiController
|
||||
UserIds = new[] { userId }
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
|
||||
var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ public class GenresController : BaseJellyfinApiController
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = userId.IsNullOrEmpty()
|
||||
@@ -158,7 +159,8 @@ public class GenresController : BaseJellyfinApiController
|
||||
public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddClientFields(User);
|
||||
|
||||
Genre? item;
|
||||
if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -90,6 +90,7 @@ public class InstantMixController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
@@ -133,6 +134,7 @@ public class InstantMixController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
@@ -176,6 +178,7 @@ public class InstantMixController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
@@ -211,6 +214,7 @@ public class InstantMixController : BaseJellyfinApiController
|
||||
? null
|
||||
: _userManager.GetUserById(userId.Value);
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
@@ -254,6 +258,7 @@ public class InstantMixController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
@@ -297,6 +302,7 @@ public class InstantMixController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
@@ -379,6 +385,7 @@ public class InstantMixController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
|
||||
@@ -268,6 +268,7 @@ public class ItemsController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
if (includeItemTypes.Length == 1
|
||||
@@ -848,6 +849,7 @@ public class ItemsController : BaseJellyfinApiController
|
||||
|
||||
var parentIdGuid = parentId ?? Guid.Empty;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var ancestorIds = Array.Empty<Guid>();
|
||||
|
||||
@@ -23,6 +23,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Activity;
|
||||
@@ -187,7 +188,7 @@ public class LibraryController : BaseJellyfinApiController
|
||||
item = parent;
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
var items = themeItems
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
|
||||
.ToArray();
|
||||
@@ -260,7 +261,7 @@ public class LibraryController : BaseJellyfinApiController
|
||||
item = parent;
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
var items = themeItems
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
|
||||
.ToArray();
|
||||
@@ -496,7 +497,7 @@ public class LibraryController : BaseJellyfinApiController
|
||||
|
||||
var baseItemDtos = new List<BaseItemDto>();
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
BaseItem? parent = item.GetParent();
|
||||
|
||||
while (parent is not null)
|
||||
@@ -556,7 +557,7 @@ public class LibraryController : BaseJellyfinApiController
|
||||
items = items.Where(i => i.IsHidden == val).ToList();
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions);
|
||||
return new QueryResult<BaseItemDto>(resultArray);
|
||||
}
|
||||
@@ -700,7 +701,18 @@ public class LibraryController : BaseJellyfinApiController
|
||||
// Quotes are valid in linux. They'll possibly cause issues here.
|
||||
var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal);
|
||||
|
||||
return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true);
|
||||
var filePath = item.Path;
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
// PhysicalFile does not work well with symlinks at the moment.
|
||||
var resolved = FileSystemHelper.ResolveLinkTarget(filePath, returnFinalTarget: true);
|
||||
if (resolved is not null && resolved.Exists)
|
||||
{
|
||||
filePath = resolved.FullName;
|
||||
}
|
||||
}
|
||||
|
||||
return PhysicalFile(filePath, MimeTypes.GetMimeType(filePath), filename, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -747,7 +759,8 @@ public class LibraryController : BaseJellyfinApiController
|
||||
return new QueryResult<BaseItemDto>();
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields };
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User);
|
||||
|
||||
var program = item as IHasProgramAttributes;
|
||||
bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;
|
||||
|
||||
@@ -170,6 +170,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var channelResult = _liveTvManager.GetInternalChannels(
|
||||
@@ -241,7 +242,8 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddClientFields(User);
|
||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||
}
|
||||
|
||||
@@ -295,6 +297,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
return await _liveTvManager.GetRecordingsAsync(
|
||||
@@ -441,7 +444,8 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddClientFields(User);
|
||||
|
||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||
}
|
||||
@@ -631,6 +635,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
@@ -685,6 +690,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = body.Fields ?? [] }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes ?? []);
|
||||
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
@@ -754,6 +760,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
};
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ public class MoviesController : BaseJellyfinApiController
|
||||
var user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
: _userManager.GetUserById(userId.Value);
|
||||
var dtoOptions = new DtoOptions { Fields = fields };
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User);
|
||||
|
||||
var categories = new List<RecommendationDto>();
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ public class MusicGenresController : BaseJellyfinApiController
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = userId.IsNullOrEmpty()
|
||||
@@ -147,7 +148,7 @@ public class MusicGenresController : BaseJellyfinApiController
|
||||
public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
|
||||
MusicGenre? item;
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ public class PersonsController : BaseJellyfinApiController
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = userId.IsNullOrEmpty()
|
||||
@@ -120,7 +121,8 @@ public class PersonsController : BaseJellyfinApiController
|
||||
public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddClientFields(User);
|
||||
|
||||
var item = _libraryManager.GetPerson(name);
|
||||
if (item is null)
|
||||
|
||||
@@ -548,6 +548,7 @@ public class PlaylistsController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
|
||||
|
||||
@@ -89,6 +89,7 @@ public class StudiosController : BaseJellyfinApiController
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = userId.IsNullOrEmpty()
|
||||
@@ -141,7 +142,7 @@ public class StudiosController : BaseJellyfinApiController
|
||||
public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
|
||||
var item = _libraryManager.GetStudio(name);
|
||||
if (!userId.IsNullOrEmpty())
|
||||
|
||||
@@ -77,7 +77,7 @@ public class SuggestionsController : BaseJellyfinApiController
|
||||
user = _userManager.GetUserById(requestUserId);
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
|
||||
{
|
||||
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
|
||||
|
||||
@@ -86,7 +86,7 @@ public class TrickplayController : BaseJellyfinApiController
|
||||
[FromRoute, Required] int index,
|
||||
[FromQuery] Guid? mediaSourceId)
|
||||
{
|
||||
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
|
||||
var item = _libraryManager.GetItemById<BaseItem>(mediaSourceId ?? itemId, User.GetUserId());
|
||||
if (item is null)
|
||||
{
|
||||
return NotFound();
|
||||
|
||||
@@ -99,6 +99,7 @@ public class TvShowsController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var options = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var result = _tvSeriesManager.GetNextUp(
|
||||
@@ -160,6 +161,7 @@ public class TvShowsController : BaseJellyfinApiController
|
||||
var parentIdGuid = parentId ?? Guid.Empty;
|
||||
|
||||
var options = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
@@ -229,6 +231,7 @@ public class TvShowsController : BaseJellyfinApiController
|
||||
List<BaseItem> episodes;
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey();
|
||||
|
||||
@@ -357,6 +360,7 @@ public class TvShowsController : BaseJellyfinApiController
|
||||
});
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
|
||||
|
||||
@@ -94,7 +94,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
||||
|
||||
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
|
||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||
}
|
||||
@@ -133,7 +133,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var item = _libraryManager.GetUserRootFolder();
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
|
||||
|
||||
return new QueryResult<BaseItemDto>(dtos);
|
||||
@@ -422,7 +422,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
if (item is IHasTrailers hasTrailers)
|
||||
{
|
||||
var trailers = hasTrailers.LocalTrailers;
|
||||
@@ -478,7 +478,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
|
||||
return Ok(item
|
||||
.GetExtras()
|
||||
@@ -549,6 +549,7 @@ public class UserLibraryController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var list = _userViewManager.GetLatestItems(
|
||||
|
||||
@@ -86,7 +86,7 @@ public class UserViewsController : BaseJellyfinApiController
|
||||
|
||||
var folders = _userViewManager.GetUserViews(query);
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId];
|
||||
|
||||
var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user));
|
||||
|
||||
@@ -111,6 +111,7 @@ public class VideosController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
dtoOptions = dtoOptions.AddClientFields(User);
|
||||
|
||||
BaseItemDto[] items;
|
||||
if (item is Video video)
|
||||
|
||||
@@ -89,6 +89,7 @@ public class YearsController : BaseJellyfinApiController
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = userId.IsNullOrEmpty()
|
||||
@@ -181,7 +182,8 @@ public class YearsController : BaseJellyfinApiController
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions();
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddClientFields(User);
|
||||
|
||||
if (!userId.IsNullOrEmpty())
|
||||
{
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace Jellyfin.Api.Extensions;
|
||||
|
||||
@@ -9,6 +13,55 @@ namespace Jellyfin.Api.Extensions;
|
||||
/// </summary>
|
||||
public static class DtoExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add additional fields depending on client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use in place of GetDtoOptions.
|
||||
/// Legacy order: 2.
|
||||
/// </remarks>
|
||||
/// <param name="dtoOptions">DtoOptions object.</param>
|
||||
/// <param name="user">Current claims principal.</param>
|
||||
/// <returns>Modified DtoOptions object.</returns>
|
||||
internal static DtoOptions AddClientFields(
|
||||
this DtoOptions dtoOptions, ClaimsPrincipal user)
|
||||
{
|
||||
string? client = user.GetClient();
|
||||
|
||||
// No client in claim
|
||||
if (string.IsNullOrEmpty(client))
|
||||
{
|
||||
return dtoOptions;
|
||||
}
|
||||
|
||||
if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount))
|
||||
{
|
||||
if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("wmc", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("classic", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.RecursiveItemCount];
|
||||
}
|
||||
}
|
||||
|
||||
if (!dtoOptions.ContainsField(ItemFields.ChildCount))
|
||||
{
|
||||
if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("wmc", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("classic", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("roku", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("samsung", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("androidtv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.ChildCount];
|
||||
}
|
||||
}
|
||||
|
||||
return dtoOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add additional DtoOptions.
|
||||
/// </summary>
|
||||
|
||||
@@ -159,6 +159,13 @@ public static class StreamingHelpers
|
||||
|
||||
string? containerInternal = Path.GetExtension(state.RequestedUrl);
|
||||
|
||||
if (string.IsNullOrEmpty(containerInternal)
|
||||
&& (!string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId)
|
||||
|| (mediaSource != null && mediaSource.IsInfiniteStream)))
|
||||
{
|
||||
containerInternal = ".ts";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(streamingRequest.Container))
|
||||
{
|
||||
containerInternal = streamingRequest.Container;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -275,9 +275,8 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
|
||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
result.Items = GetEntities(dbQuery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
result.StartIndex = filter.StartIndex ?? 0;
|
||||
return result;
|
||||
}
|
||||
@@ -295,9 +294,8 @@ public sealed class BaseItemRepository
|
||||
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
|
||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
return GetEntities(dbQuery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -339,9 +337,7 @@ public sealed class BaseItemRepository
|
||||
mainquery = ApplyGroupingFilter(context, mainquery, filter);
|
||||
mainquery = ApplyQueryPaging(mainquery, filter);
|
||||
|
||||
mainquery = ApplyNavigations(mainquery, filter);
|
||||
|
||||
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
return GetEntities(mainquery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -408,21 +404,6 @@ public sealed class BaseItemRepository
|
||||
return dbQuery;
|
||||
}
|
||||
|
||||
private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
|
||||
{
|
||||
dbQuery = dbQuery.Include(e => e.TrailerTypes)
|
||||
.Include(e => e.Provider)
|
||||
.Include(e => e.LockedFields)
|
||||
.Include(e => e.UserData);
|
||||
|
||||
if (filter.DtoOptions.EnableImages)
|
||||
{
|
||||
dbQuery = dbQuery.Include(e => e.Images);
|
||||
}
|
||||
|
||||
return dbQuery;
|
||||
}
|
||||
|
||||
private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
|
||||
{
|
||||
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
|
||||
@@ -448,18 +429,50 @@ public sealed class BaseItemRepository
|
||||
dbQuery = TranslateQuery(dbQuery, context, filter);
|
||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
return dbQuery;
|
||||
}
|
||||
|
||||
private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
|
||||
{
|
||||
IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
|
||||
IQueryable<BaseItemEntity> dbQuery = context.BaseItems;
|
||||
dbQuery = dbQuery.AsSingleQuery();
|
||||
|
||||
return dbQuery;
|
||||
}
|
||||
|
||||
private IReadOnlyList<BaseItemEntity> GetEntities(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter)
|
||||
{
|
||||
var items = dbQuery.AsEnumerable().Where(e => e is not null).ToArray();
|
||||
var itemIds = items.Select(e => e.Id).ToArray();
|
||||
|
||||
if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
|
||||
{
|
||||
context.BaseItemTrailerTypes.WhereOneOrMany(itemIds, e => e.ItemId).Load();
|
||||
}
|
||||
|
||||
if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
|
||||
{
|
||||
context.BaseItemProviders.WhereOneOrMany(itemIds, e => e.ItemId).Load();
|
||||
}
|
||||
|
||||
if (filter.DtoOptions.ContainsField(ItemFields.Settings))
|
||||
{
|
||||
context.BaseItemMetadataFields.WhereOneOrMany(itemIds, e => e.ItemId).Load();
|
||||
}
|
||||
|
||||
if (filter.DtoOptions.EnableImages)
|
||||
{
|
||||
context.BaseItemImageInfos.WhereOneOrMany(itemIds, e => e.ItemId).Load();
|
||||
}
|
||||
|
||||
if (filter.DtoOptions.EnableUserData)
|
||||
{
|
||||
context.UserData.WhereOneOrMany(itemIds, e => e.ItemId).Load();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetCount(InternalItemsQuery filter)
|
||||
{
|
||||
@@ -618,12 +631,18 @@ public sealed class BaseItemRepository
|
||||
{
|
||||
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete();
|
||||
|
||||
if (entity.Images is { Count: > 0 })
|
||||
{
|
||||
context.BaseItemImageInfos.AddRange(entity.Images);
|
||||
}
|
||||
|
||||
if (entity.LockedFields is { Count: > 0 })
|
||||
{
|
||||
context.BaseItemMetadataFields.AddRange(entity.LockedFields);
|
||||
}
|
||||
|
||||
context.BaseItems.Attach(entity).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
@@ -1523,14 +1542,14 @@ public sealed class BaseItemRepository
|
||||
|
||||
private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDbContext context)
|
||||
{
|
||||
var orderBy = filter.OrderBy;
|
||||
var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray();
|
||||
var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
|
||||
|
||||
if (hasSearch)
|
||||
{
|
||||
orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
|
||||
orderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
|
||||
}
|
||||
else if (orderBy.Count == 0)
|
||||
else if (orderBy.Length == 0)
|
||||
{
|
||||
return query.OrderBy(e => e.SortName);
|
||||
}
|
||||
@@ -1647,19 +1666,18 @@ public sealed class BaseItemRepository
|
||||
var tags = filter.Tags.ToList();
|
||||
var excludeTags = filter.ExcludeTags.ToList();
|
||||
|
||||
if (filter.IsMovie == true)
|
||||
if (filter.IsMovie.HasValue)
|
||||
{
|
||||
if (filter.IncludeItemTypes.Length == 0
|
||||
|| filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
|
||||
|| filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
|
||||
var shouldIncludeAllMovieTypes = filter.IsMovie.Value
|
||||
&& (filter.IncludeItemTypes.Length == 0
|
||||
|| filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
|
||||
|| filter.IncludeItemTypes.Contains(BaseItemKind.Trailer));
|
||||
|
||||
if (!shouldIncludeAllMovieTypes)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.IsMovie);
|
||||
baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value);
|
||||
}
|
||||
}
|
||||
else if (filter.IsMovie.HasValue)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
|
||||
}
|
||||
|
||||
if (filter.IsSeries.HasValue)
|
||||
{
|
||||
@@ -1925,8 +1943,15 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.Name))
|
||||
{
|
||||
var cleanName = GetCleanValue(filter.Name);
|
||||
baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
|
||||
if (filter.UseRawName == true)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Name == filter.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
var cleanName = GetCleanValue(filter.Name);
|
||||
baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
|
||||
}
|
||||
}
|
||||
|
||||
// These are the same, for now
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
}
|
||||
|
||||
// As long as jellyfin supports password-less users, we need this little block here to accommodate
|
||||
if (string.IsNullOrEmpty(resolvedUser.Password) && string.IsNullOrEmpty(password))
|
||||
if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password))
|
||||
{
|
||||
return Task.FromResult(new ProviderAuthenticationResult
|
||||
{
|
||||
@@ -93,6 +93,10 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasPassword(User user)
|
||||
=> !string.IsNullOrEmpty(user?.Password);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ChangePassword(User user, string newPassword)
|
||||
{
|
||||
|
||||
@@ -21,6 +21,12 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasPassword(User user)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ChangePassword(User user, string newPassword)
|
||||
{
|
||||
|
||||
@@ -149,7 +149,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
|
||||
ThrowIfInvalidUsername(newName);
|
||||
|
||||
if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))
|
||||
if (user.Username.Equals(newName, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("The new and old names must be different.");
|
||||
}
|
||||
@@ -306,12 +306,15 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
/// <inheritdoc/>
|
||||
public UserDto GetUserDto(User user, string? remoteEndPoint = null)
|
||||
{
|
||||
var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
|
||||
var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
|
||||
return new UserDto
|
||||
{
|
||||
Name = user.Username,
|
||||
Id = user.Id,
|
||||
ServerId = _appHost.SystemId,
|
||||
HasPassword = hasPassword,
|
||||
HasConfiguredPassword = hasPassword,
|
||||
EnableAutoLogin = user.EnableAutoLogin,
|
||||
LastLoginDate = user.LastLoginDate,
|
||||
LastActivityDate = user.LastActivityDate,
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Migration to disable legacy authorization in the system config.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-11-18T16:00:00", nameof(DisableLegacyAuthorization))]
|
||||
public class DisableLegacyAuthorization : IAsyncMigrationRoutine
|
||||
{
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DisableLegacyAuthorization"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public DisableLegacyAuthorization(IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_serverConfigurationManager.Configuration.EnableLegacyAuthorization = false;
|
||||
_serverConfigurationManager.SaveConfiguration();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -249,7 +249,6 @@ public sealed class SetupServer : IDisposable
|
||||
{
|
||||
{ "isInReportingMode", _isUnhealthy },
|
||||
{ "retryValue", retryAfterValue },
|
||||
{ "version", typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName().Version! },
|
||||
{ "logs", startupLogEntries },
|
||||
{ "networkManagerReady", networkManager is not null },
|
||||
{ "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
<header class="flex-row">
|
||||
|
||||
{{^IF isInReportingMode}}
|
||||
<p>Jellyfin Server {{version}} still starting. Please wait.</p>
|
||||
<p>Jellyfin Server still starting. Please wait.</p>
|
||||
{{#ELSE}}
|
||||
<p>Jellyfin Server has encountered an error and was not able to start.</p>
|
||||
{{/ELSE}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace MediaBrowser.Controller.Authentication
|
||||
|
||||
Task<ProviderAuthenticationResult> Authenticate(string username, string password);
|
||||
|
||||
bool HasPassword(User user);
|
||||
|
||||
Task ChangePassword(User user, string newPassword);
|
||||
}
|
||||
|
||||
|
||||
@@ -1620,12 +1620,17 @@ namespace MediaBrowser.Controller.Entities
|
||||
return isAllowed;
|
||||
}
|
||||
|
||||
if (maxAllowedSubRating is not null)
|
||||
if (!maxAllowedRating.HasValue)
|
||||
{
|
||||
return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value;
|
||||
if (ratingScore.Score != maxAllowedRating.Value)
|
||||
{
|
||||
return ratingScore.Score < maxAllowedRating.Value;
|
||||
}
|
||||
|
||||
return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value;
|
||||
}
|
||||
|
||||
public ParentalRatingScore GetParentalRatingScore()
|
||||
|
||||
@@ -1406,13 +1406,6 @@ namespace MediaBrowser.Controller.Entities
|
||||
.Where(e => query is null || UserViewBuilder.FilterItem(e, query))
|
||||
.ToArray();
|
||||
|
||||
if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0))
|
||||
{
|
||||
realChildren = realChildren
|
||||
.OrderBy(e => e.ProductionYear ?? int.MaxValue)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var childCount = realChildren.Length;
|
||||
if (result.Count < limit)
|
||||
{
|
||||
|
||||
@@ -125,6 +125,8 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public bool? UseRawName { get; set; }
|
||||
|
||||
public string? Person { get; set; }
|
||||
|
||||
public Guid[] PersonIds { get; set; }
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace MediaBrowser.Controller.Entities.Movies
|
||||
|
||||
if (sortBy == ItemSortBy.Default)
|
||||
{
|
||||
return items;
|
||||
return items;
|
||||
}
|
||||
|
||||
return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending);
|
||||
@@ -136,6 +136,12 @@ namespace MediaBrowser.Controller.Entities.Movies
|
||||
return Sort(children, user).ToArray();
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null)
|
||||
{
|
||||
var children = base.GetChildren(user, includeLinkedChildren, out totalItemCount, query);
|
||||
return Sort(children, user).ToArray();
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
|
||||
{
|
||||
var children = base.GetRecursiveChildren(user, query, out totalCount);
|
||||
|
||||
@@ -63,6 +63,29 @@ public static class FileSystemHelper
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a single link hop for the specified path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns <c>null</c> if the path is not a symbolic link or the filesystem does not support link resolution (e.g., exFAT).
|
||||
/// </remarks>
|
||||
/// <param name="path">The file path to resolve.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="FileInfo"/> representing the next link target if the path is a link; otherwise, <c>null</c>.
|
||||
/// </returns>
|
||||
private static FileInfo? Resolve(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.ResolveLinkTarget(path, returnFinalTarget: false) as FileInfo;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Filesystem doesn't support links (e.g., exFAT).
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target of the specified file link.
|
||||
/// </summary>
|
||||
@@ -84,23 +107,26 @@ public static class FileSystemHelper
|
||||
|
||||
if (!returnFinalTarget)
|
||||
{
|
||||
return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo;
|
||||
return Resolve(linkPath);
|
||||
}
|
||||
|
||||
if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!targetInfo.Exists)
|
||||
var targetInfo = Resolve(linkPath);
|
||||
if (targetInfo is null || !targetInfo.Exists)
|
||||
{
|
||||
return targetInfo;
|
||||
}
|
||||
|
||||
var currentPath = targetInfo.FullName;
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal) { linkPath, currentPath };
|
||||
while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo)
|
||||
|
||||
while (true)
|
||||
{
|
||||
var linkInfo = Resolve(currentPath);
|
||||
if (linkInfo is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var targetPath = linkInfo.FullName;
|
||||
|
||||
// If an infinite loop is detected, return the file info for the
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -29,7 +30,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
/// </summary>
|
||||
private readonly Lock _taskLock = new();
|
||||
|
||||
private readonly BlockingCollection<TaskQueueItem> _tasks = new();
|
||||
private readonly Channel<TaskQueueItem> _tasks = Channel.CreateUnbounded<TaskQueueItem>();
|
||||
|
||||
private volatile int _workCounter;
|
||||
private Task? _cleanupTask;
|
||||
@@ -77,7 +78,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
|
||||
lock (_taskLock)
|
||||
{
|
||||
if (_tasks.Count > 0 || _workCounter > 0)
|
||||
if (_tasks.Reader.Count > 0 || _workCounter > 0)
|
||||
{
|
||||
_logger.LogDebug("Delay cleanup task, operations still running.");
|
||||
// tasks are still there so its still in use. Reschedule cleanup task.
|
||||
@@ -144,9 +145,9 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
_deadlockDetector.Value = stopToken.TaskStop;
|
||||
try
|
||||
{
|
||||
foreach (var item in _tasks.GetConsumingEnumerable(stopToken.GlobalStop.Token))
|
||||
while (!stopToken.GlobalStop.Token.IsCancellationRequested)
|
||||
{
|
||||
stopToken.GlobalStop.Token.ThrowIfCancellationRequested();
|
||||
var item = await _tasks.Reader.ReadAsync(stopToken.GlobalStop.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var newWorkerLimit = Interlocked.Increment(ref _workCounter) > 0;
|
||||
@@ -242,7 +243,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
if (ShouldForceSequentialOperation())
|
||||
if (ShouldForceSequentialOperation() || _deadlockDetector.Value is not null)
|
||||
{
|
||||
_logger.LogDebug("Process sequentially.");
|
||||
try
|
||||
@@ -264,35 +265,14 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
for (var i = 0; i < workItems.Length; i++)
|
||||
{
|
||||
var item = workItems[i]!;
|
||||
_tasks.Add(item, CancellationToken.None);
|
||||
await _tasks.Writer.WriteAsync(item, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_deadlockDetector.Value is not null)
|
||||
{
|
||||
_logger.LogDebug("Nested invocation detected, process in-place.");
|
||||
try
|
||||
{
|
||||
// we are in a nested loop. There is no reason to spawn a task here as that would just lead to deadlocks and no additional concurrency is achieved
|
||||
while (workItems.Any(e => !e.Done.Task.IsCompleted) && _tasks.TryTake(out var item, 200, _deadlockDetector.Value.Token))
|
||||
{
|
||||
await ProcessItem(item).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_deadlockDetector.Value.IsCancellationRequested)
|
||||
{
|
||||
// operation is cancelled. Do nothing.
|
||||
}
|
||||
|
||||
_logger.LogDebug("process in-place done.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Worker();
|
||||
_logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length);
|
||||
await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false);
|
||||
_logger.LogDebug("{NoWorkers} completed.", workItems.Length);
|
||||
ScheduleTaskCleanup();
|
||||
}
|
||||
Worker();
|
||||
_logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length);
|
||||
await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false);
|
||||
_logger.LogDebug("{NoWorkers} completed.", workItems.Length);
|
||||
ScheduleTaskCleanup();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -304,13 +284,12 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_tasks.CompleteAdding();
|
||||
_tasks.Writer.Complete();
|
||||
foreach (var item in _taskRunners)
|
||||
{
|
||||
await item.Key.CancelAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_tasks.Dispose();
|
||||
if (_cleanupTask is not null)
|
||||
{
|
||||
await _cleanupTask.ConfigureAwait(false);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -2378,6 +2378,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// If SDR is the only supported range, we should not copy any of the HDR streams.
|
||||
// All the following copy check assumes at least one HDR format is supported.
|
||||
if (requestedRangeTypes.Length == 1 && requestHasSDR && videoStream.VideoRangeType != VideoRangeType.SDR)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the client does not support DOVI and the video stream is DOVI without fallback, we should not copy it.
|
||||
if (!requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVI)
|
||||
{
|
||||
@@ -5942,28 +5949,37 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap;
|
||||
var swapOutputWandH = doRkVppTranspose && swapWAndH;
|
||||
var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts
|
||||
var outFormat = doOclTonemap ? "p010" : "nv12";
|
||||
var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var doScaling = !string.IsNullOrEmpty(GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH));
|
||||
|
||||
if (!hasSubs
|
||||
|| doRkVppTranspose
|
||||
|| !isFullAfbcPipeline
|
||||
|| !string.IsNullOrEmpty(doScaling))
|
||||
|| doScaling)
|
||||
{
|
||||
var isScaleRatioSupported = IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f);
|
||||
|
||||
// RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation,
|
||||
// but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it
|
||||
if (!string.IsNullOrEmpty(doScaling)
|
||||
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
|
||||
if (doScaling && !isScaleRatioSupported)
|
||||
{
|
||||
// Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
|
||||
// Use NV15 instead of P010 to avoid the issue.
|
||||
// SDR inputs are using BGRA formats already which is not affected.
|
||||
var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
|
||||
var intermediateFormat = doOclTonemap ? "nv15" : (isMjpegEncoder ? "bgra" : outFormat);
|
||||
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_original_aspect_ratio=increase:force_divisible_by=4:afbc=1";
|
||||
mainFilters.Add(hwScaleFilterFirstPass);
|
||||
}
|
||||
|
||||
// The RKMPP MJPEG encoder on some newer chip models no longer supports RGB input.
|
||||
// Use 2pass here to enable RGA output of full-range YUV in the 2nd pass.
|
||||
if (isMjpegEncoder && !doOclTonemap && ((doScaling && isScaleRatioSupported) || !doScaling))
|
||||
{
|
||||
var hwScaleFilterFirstPass = "vpp_rkrga=format=bgra:afbc=1";
|
||||
mainFilters.Add(hwScaleFilterFirstPass);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose)
|
||||
{
|
||||
hwScaleFilter += $":transpose={transposeDir}";
|
||||
@@ -7023,8 +7039,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var accelType = GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
|
||||
return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty);
|
||||
// there's an issue about AV1 AFBC on RK3588, disable it for now until it's fixed upstream
|
||||
return GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BDInfo.IO;
|
||||
@@ -58,6 +59,8 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsHidden(ReadOnlySpan<char> name) => name.StartsWith('.');
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directories.
|
||||
/// </summary>
|
||||
@@ -65,6 +68,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
public IDirectoryInfo[] GetDirectories()
|
||||
{
|
||||
return _fileSystem.GetDirectories(_impl.FullName)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoDirectoryInfo(_fileSystem, x))
|
||||
.ToArray();
|
||||
}
|
||||
@@ -76,6 +80,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
public IFileInfo[] GetFiles()
|
||||
{
|
||||
return _fileSystem.GetFiles(_impl.FullName)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoFileInfo(x))
|
||||
.ToArray();
|
||||
}
|
||||
@@ -88,6 +93,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
public IFileInfo[] GetFiles(string searchPattern)
|
||||
{
|
||||
return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoFileInfo(x))
|
||||
.ToArray();
|
||||
}
|
||||
@@ -105,6 +111,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
new[] { searchPattern },
|
||||
false,
|
||||
searchOption == SearchOption.AllDirectories)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoFileInfo(x))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
@@ -511,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
|
||||
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
|
||||
|
||||
if (!isAudio && _proberSupportsFirstVideoFrame)
|
||||
if (protocol == MediaProtocol.File && !isAudio && _proberSupportsFirstVideoFrame)
|
||||
{
|
||||
args += " -show_frames -only_first_vframe";
|
||||
}
|
||||
|
||||
@@ -154,12 +154,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
info.Name = tags.GetFirstNotNullNorWhiteSpaceValue("title", "title-eng");
|
||||
info.ForcedSortName = tags.GetFirstNotNullNorWhiteSpaceValue("sort_name", "title-sort", "titlesort");
|
||||
info.Overview = tags.GetFirstNotNullNorWhiteSpaceValue("synopsis", "description", "desc", "comment");
|
||||
info.Overview = tags.GetFirstNotNullNorWhiteSpaceValue("synopsis", "description", "desc");
|
||||
|
||||
info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort");
|
||||
info.ParentIndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "season_number");
|
||||
info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort") ??
|
||||
FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_id");
|
||||
info.ShowName = tags.GetValueOrDefault("show_name", "show");
|
||||
info.ShowName = tags.GetValueOrDefault("show_name");
|
||||
info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
|
||||
|
||||
// Several different forms of retail/premiere date
|
||||
|
||||
@@ -287,5 +287,5 @@ public class ServerConfiguration : BaseApplicationConfiguration
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether old authorization methods are allowed.
|
||||
/// </summary>
|
||||
public bool EnableLegacyAuthorization { get; set; }
|
||||
public bool EnableLegacyAuthorization { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -1250,37 +1250,30 @@ public class StreamInfo
|
||||
|
||||
if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
|
||||
{
|
||||
// Default to using the API URL
|
||||
info.Url = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
|
||||
baseUrl,
|
||||
ItemId,
|
||||
MediaSourceId,
|
||||
stream.Index.ToString(CultureInfo.InvariantCulture),
|
||||
startPositionTicks.ToString(CultureInfo.InvariantCulture),
|
||||
subtitleProfile.Format);
|
||||
info.IsExternalUrl = false; // Default to API URL
|
||||
|
||||
// Check conditions for potentially using the direct path
|
||||
if (stream.IsExternal // Must be external
|
||||
&& MediaSource?.Protocol != MediaProtocol.File // Main media must not be a local file
|
||||
&& string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) // Format must match (no conversion needed)
|
||||
&& !string.IsNullOrEmpty(stream.Path) // Path must exist
|
||||
&& Uri.TryCreate(stream.Path, UriKind.Absolute, out Uri? uriResult) // Path must be an absolute URI
|
||||
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) // Scheme must be HTTP or HTTPS
|
||||
if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
|
||||
{
|
||||
info.Url = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
|
||||
baseUrl,
|
||||
ItemId,
|
||||
MediaSourceId,
|
||||
stream.Index.ToString(CultureInfo.InvariantCulture),
|
||||
startPositionTicks.ToString(CultureInfo.InvariantCulture),
|
||||
subtitleProfile.Format);
|
||||
|
||||
if (!string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
info.Url += "?ApiKey=" + accessToken;
|
||||
}
|
||||
|
||||
info.IsExternalUrl = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// All conditions met, override with the direct path
|
||||
info.Url = stream.Path;
|
||||
info.IsExternalUrl = true;
|
||||
}
|
||||
|
||||
// Append ApiKey only if we are using the API URL
|
||||
if (!info.IsExternalUrl && !string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
// Use "?ApiKey=" as seen in HEAD and other parts of the code
|
||||
info.Url += "?ApiKey=" + accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
||||
@@ -55,22 +54,20 @@ namespace MediaBrowser.Model.Dto
|
||||
/// Gets or sets a value indicating whether this instance has password.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance has password; otherwise, <c>false</c>.</value>
|
||||
[Obsolete("This information is no longer provided")]
|
||||
public bool? HasPassword { get; set; } = true;
|
||||
public bool HasPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance has configured password.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance has configured password; otherwise, <c>false</c>.</value>
|
||||
[Obsolete("This is always true")]
|
||||
public bool? HasConfiguredPassword { get; set; } = true;
|
||||
public bool HasConfiguredPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance has configured easy password.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance has configured easy password; otherwise, <c>false</c>.</value>
|
||||
[Obsolete("Easy Password has been replaced with Quick Connect")]
|
||||
public bool? HasConfiguredEasyPassword { get; set; } = false;
|
||||
public bool HasConfiguredEasyPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether async login is enabled or not.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -88,7 +88,15 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
|
||||
singular.AddRange(item.GetImages(ImageType.Backdrop));
|
||||
foreach (var backdrop in item.GetImages(ImageType.Backdrop))
|
||||
{
|
||||
var imageInMetadataFolder = backdrop.Path.StartsWith(itemMetadataPath, StringComparison.OrdinalIgnoreCase);
|
||||
if (imageInMetadataFolder || canDeleteLocal || item.IsSaveLocalMetadataEnabled())
|
||||
{
|
||||
singular.Add(backdrop);
|
||||
}
|
||||
}
|
||||
|
||||
PruneImages(item, singular);
|
||||
|
||||
return singular.Count > 0;
|
||||
@@ -466,10 +474,36 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
|
||||
if (UpdateMultiImages(item, images, ImageType.Backdrop))
|
||||
bool hasBackdrop = false;
|
||||
bool backdropStoredWithMedia = false;
|
||||
|
||||
foreach (var image in images)
|
||||
{
|
||||
changed = true;
|
||||
foundImageTypes.Add(ImageType.Backdrop);
|
||||
if (image.Type != ImageType.Backdrop)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasBackdrop = true;
|
||||
|
||||
if (item.ContainingFolderPath is not null && item.ContainingFolderPath.Contains(Path.GetDirectoryName(image.FileInfo.FullName), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
backdropStoredWithMedia = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBackdrop)
|
||||
{
|
||||
if (UpdateMultiImages(item, images, ImageType.Backdrop))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (backdropStoredWithMedia)
|
||||
{
|
||||
foundImageTypes.Add(ImageType.Backdrop);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundImageTypes.Count > 0)
|
||||
|
||||
@@ -151,9 +151,9 @@ namespace MediaBrowser.Providers.Manager
|
||||
.ConfigureAwait(false);
|
||||
updateType |= beforeSaveResult;
|
||||
|
||||
if (!isFirstRefresh)
|
||||
if (isFirstRefresh)
|
||||
{
|
||||
updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
|
||||
await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Next run metadata providers
|
||||
|
||||
@@ -721,8 +721,6 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_libraryManager.CreateItem(item, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.12.0")]
|
||||
[assembly: AssemblyFileVersion("10.12.0")]
|
||||
[assembly: AssemblyVersion("10.11.4")]
|
||||
[assembly: AssemblyFileVersion("10.11.4")]
|
||||
|
||||
@@ -209,39 +209,69 @@ public class SkiaEncoder : IImageEncoder
|
||||
return default;
|
||||
}
|
||||
|
||||
using var codec = SKCodec.Create(safePath, out var result);
|
||||
|
||||
switch (result)
|
||||
SKCodec? codec = null;
|
||||
bool isSafePathTemp = !string.Equals(Path.GetFullPath(safePath), Path.GetFullPath(path), StringComparison.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
case SKCodecResult.Success:
|
||||
// Skia/SkiaSharp edge‑case: when the image header is parsed but the actual pixel
|
||||
// decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without color‑transform, etc.)
|
||||
// `SKCodec.Create` returns a *non‑null* codec together with
|
||||
// SKCodecResult.InternalError. The header still contains valid dimensions,
|
||||
// which is all we need here – so we fall back to them instead of aborting.
|
||||
// See e.g. Skia bugs #4139, #6092.
|
||||
case SKCodecResult.InternalError when codec is not null:
|
||||
var info = codec.Info;
|
||||
return new ImageDimensions(info.Width, info.Height);
|
||||
|
||||
case SKCodecResult.Unimplemented:
|
||||
_logger.LogDebug("Image format not supported: {FilePath}", path);
|
||||
return default;
|
||||
|
||||
default:
|
||||
codec = SKCodec.Create(safePath, out var result);
|
||||
switch (result)
|
||||
{
|
||||
var boundsInfo = SKBitmap.DecodeBounds(safePath);
|
||||
case SKCodecResult.Success:
|
||||
// Skia/SkiaSharp edge‑case: when the image header is parsed but the actual pixel
|
||||
// decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without color‑transform, etc.)
|
||||
// `SKCodec.Create` returns a *non‑null* codec together with
|
||||
// SKCodecResult.InternalError. The header still contains valid dimensions,
|
||||
// which is all we need here – so we fall back to them instead of aborting.
|
||||
// See e.g. Skia bugs #4139, #6092.
|
||||
case SKCodecResult.InternalError when codec is not null:
|
||||
var info = codec.Info;
|
||||
return new ImageDimensions(info.Width, info.Height);
|
||||
|
||||
if (boundsInfo.Width > 0 && boundsInfo.Height > 0)
|
||||
case SKCodecResult.Unimplemented:
|
||||
_logger.LogDebug("Image format not supported: {FilePath}", path);
|
||||
return default;
|
||||
|
||||
default:
|
||||
{
|
||||
return new ImageDimensions(boundsInfo.Width, boundsInfo.Height);
|
||||
}
|
||||
var boundsInfo = SKBitmap.DecodeBounds(safePath);
|
||||
if (boundsInfo.Width > 0 && boundsInfo.Height > 0)
|
||||
{
|
||||
return new ImageDimensions(boundsInfo.Width, boundsInfo.Height);
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Unable to determine image dimensions for {FilePath}: {SkCodecResult}",
|
||||
path,
|
||||
result);
|
||||
return default;
|
||||
_logger.LogWarning(
|
||||
"Unable to determine image dimensions for {FilePath}: {SkCodecResult}",
|
||||
path,
|
||||
result);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
codec?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Error by closing codec for {FilePath}", safePath);
|
||||
}
|
||||
|
||||
if (isSafePathTemp)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(safePath))
|
||||
{
|
||||
File.Delete(safePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Unable to remove temporary file '{TempPath}'", safePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,31 @@ namespace Jellyfin.Extensions
|
||||
/// </summary>
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a string from a string dictionary, checking all keys sequentially,
|
||||
/// stopping at the first key that returns a result that's neither null nor blank.
|
||||
/// </summary>
|
||||
/// <param name="dictionary">The dictionary.</param>
|
||||
/// <param name="key1">The first checked key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1)
|
||||
{
|
||||
return dictionary.GetFirstNotNullNorWhiteSpaceValue(key1, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string from a string dictionary, checking all keys sequentially,
|
||||
/// stopping at the first key that returns a result that's neither null nor blank.
|
||||
/// </summary>
|
||||
/// <param name="dictionary">The dictionary.</param>
|
||||
/// <param name="key1">The first checked key.</param>
|
||||
/// <param name="key2">The second checked key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1, string key2)
|
||||
{
|
||||
return dictionary.GetFirstNotNullNorWhiteSpaceValue(key1, key2, string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string from a string dictionary, checking all keys sequentially,
|
||||
/// stopping at the first key that returns a result that's neither null nor blank.
|
||||
@@ -15,9 +40,8 @@ namespace Jellyfin.Extensions
|
||||
/// <param name="key1">The first checked key.</param>
|
||||
/// <param name="key2">The second checked key.</param>
|
||||
/// <param name="key3">The third checked key.</param>
|
||||
/// <param name="key4">The fourth checked key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1, string? key2 = null, string? key3 = null, string? key4 = null)
|
||||
public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1, string key2, string key3)
|
||||
{
|
||||
if (dictionary.TryGetValue(key1, out var val) && !string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
@@ -34,11 +58,6 @@ namespace Jellyfin.Extensions
|
||||
return val;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(key4) && dictionary.TryGetValue(key4, out val) && !string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Extensions</PackageId>
|
||||
<VersionPrefix>10.12.0</VersionPrefix>
|
||||
<VersionPrefix>10.11.4</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -19,7 +19,6 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
[InlineData("/some/path/The Show", "The Show")]
|
||||
[InlineData("/some/path/The Show s02e10 720p hdtv", "The Show")]
|
||||
[InlineData("/some/path/The Show s02e10 the episode 720p hdtv", "The Show")]
|
||||
[InlineData("/some/path/1923 (2022)", "1923")]
|
||||
public void SeriesResolverResolveTest(string path, string name)
|
||||
{
|
||||
var res = SeriesResolver.Resolve(_namingOptions, path);
|
||||
|
||||
@@ -1,30 +1,81 @@
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.Library;
|
||||
|
||||
public class DotIgnoreIgnoreRuleTest
|
||||
{
|
||||
[Fact]
|
||||
public void Test()
|
||||
private static readonly string[] _rule1 = ["SPs"];
|
||||
private static readonly string[] _rule2 = ["SPs", "!thebestshot.mkv"];
|
||||
private static readonly string[] _rule3 = ["*.txt", @"{\colortbl;\red255\green255\blue255;}", "videos/", @"\invalid\escape\sequence", "*.mkv"];
|
||||
private static readonly string[] _rule4 = [@"{\colortbl;\red255\green255\blue255;}", @"\invalid\escape\sequence"];
|
||||
|
||||
public static TheoryData<string[], string, bool, bool> CheckIgnoreRulesTestData =>
|
||||
new()
|
||||
{
|
||||
// Basic pattern matching
|
||||
{ _rule1, "f:/cd/sps/ffffff.mkv", false, true },
|
||||
{ _rule1, "cd/sps/ffffff.mkv", false, true },
|
||||
{ _rule1, "/cd/sps/ffffff.mkv", false, true },
|
||||
|
||||
// Negate pattern
|
||||
{ _rule2, "f:/cd/sps/ffffff.mkv", false, true },
|
||||
{ _rule2, "cd/sps/ffffff.mkv", false, true },
|
||||
{ _rule2, "/cd/sps/ffffff.mkv", false, true },
|
||||
{ _rule2, "f:/cd/sps/thebestshot.mkv", false, false },
|
||||
{ _rule2, "cd/sps/thebestshot.mkv", false, false },
|
||||
{ _rule2, "/cd/sps/thebestshot.mkv", false, false },
|
||||
|
||||
// Mixed valid and invalid patterns - skips invalid, applies valid
|
||||
{ _rule3, "test.txt", false, true },
|
||||
{ _rule3, "videos/movie.mp4", false, true },
|
||||
{ _rule3, "movie.mkv", false, true },
|
||||
{ _rule3, "test.mp3", false, false },
|
||||
|
||||
// Only invalid patterns - falls back to ignore all
|
||||
{ _rule4, "any-file.txt", false, true },
|
||||
{ _rule4, "any/path/to/file.mkv", false, true },
|
||||
};
|
||||
|
||||
public static TheoryData<string[], string, bool, bool> WindowsPathNormalizationTestData =>
|
||||
new()
|
||||
{
|
||||
// Windows paths with backslashes - should match when normalizePath is true
|
||||
{ _rule1, @"C:\cd\sps\ffffff.mkv", false, true },
|
||||
{ _rule1, @"D:\media\sps\movie.mkv", false, true },
|
||||
{ _rule1, @"\\server\share\sps\file.mkv", false, true },
|
||||
|
||||
// Negate pattern with Windows paths
|
||||
{ _rule2, @"C:\cd\sps\ffffff.mkv", false, true },
|
||||
{ _rule2, @"C:\cd\sps\thebestshot.mkv", false, false },
|
||||
|
||||
// Directory matching with Windows paths
|
||||
{ _rule3, @"C:\videos\movie.mp4", false, true },
|
||||
{ _rule3, @"D:\documents\test.txt", false, true },
|
||||
{ _rule3, @"E:\music\song.mp3", false, false },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CheckIgnoreRulesTestData))]
|
||||
public void CheckIgnoreRules_ReturnsExpectedResult(string[] rules, string path, bool isDirectory, bool expectedIgnored)
|
||||
{
|
||||
var ignore = new Ignore.Ignore();
|
||||
ignore.Add("SPs");
|
||||
Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv"));
|
||||
Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv"));
|
||||
Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv"));
|
||||
Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestNegatePattern()
|
||||
[Theory]
|
||||
[MemberData(nameof(WindowsPathNormalizationTestData))]
|
||||
public void CheckIgnoreRules_WithWindowsPaths_NormalizesBackslashes(string[] rules, string path, bool isDirectory, bool expectedIgnored)
|
||||
{
|
||||
var ignore = new Ignore.Ignore();
|
||||
ignore.Add("SPs");
|
||||
ignore.Add("!thebestshot.mkv");
|
||||
Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv"));
|
||||
Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv"));
|
||||
Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv"));
|
||||
Assert.True(!ignore.IsIgnored("f:/cd/sps/thebestshot.mkv"));
|
||||
Assert.True(!ignore.IsIgnored("cd/sps/thebestshot.mkv"));
|
||||
Assert.True(!ignore.IsIgnored("/cd/sps/thebestshot.mkv"));
|
||||
// With normalizePath=true, backslashes should be converted to forward slashes
|
||||
Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory, normalizePath: true));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"C:\cd\sps\ffffff.mkv")]
|
||||
[InlineData(@"D:\media\sps\movie.mkv")]
|
||||
public void CheckIgnoreRules_WithWindowsPaths_WithoutNormalization_DoesNotMatch(string path)
|
||||
{
|
||||
// Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes
|
||||
Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions);
|
||||
Assert.NotNull(users);
|
||||
Assert.Single(users);
|
||||
Assert.False(users![0].HasConfiguredPassword);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -91,6 +92,8 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOptions);
|
||||
Assert.Equal(TestUsername, user!.Name);
|
||||
Assert.False(user.HasPassword);
|
||||
Assert.False(user.HasConfiguredPassword);
|
||||
|
||||
_testUserId = user.Id;
|
||||
|
||||
@@ -146,6 +149,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
|
||||
using var response = await UpdateUserPassword(client, _testUserId, createRequest);
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
|
||||
var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
|
||||
await client.GetStreamAsync("Users"), _jsonOptions);
|
||||
var user = users!.First(x => x.Id.Equals(_testUserId));
|
||||
Assert.True(user.HasPassword);
|
||||
Assert.True(user.HasConfiguredPassword);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -163,6 +172,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
|
||||
using var response = await UpdateUserPassword(client, _testUserId, createRequest);
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
|
||||
var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
|
||||
await client.GetStreamAsync("Users"), _jsonOptions);
|
||||
var user = users!.First(x => x.Id.Equals(_testUserId));
|
||||
Assert.False(user.HasPassword);
|
||||
Assert.False(user.HasConfiguredPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user