Compare commits

..

104 Commits

Author SHA1 Message Date
Cody Robibero
f6709a69e7 Explicitly load related items 2025-12-14 10:08:33 -07:00
Andrew Rabert
4cdd8c8233 Fix unnecessary database JOINs in ApplyNavigations (#15666) 2025-12-13 10:58:08 -07:00
Tim Eisele
6e60634c9f Skip invalid ignore rules (#15746) 2025-12-13 08:39:49 -07:00
theguymadmax
12c5d6b636 Fix backdrop images being deleted when stored with media (#15766) 2025-12-13 08:29:17 -07:00
theguymadmax
b617c62f8e Fix NullReferenceException in ApplyOrder method (#15768) 2025-12-13 08:28:31 -07:00
Nyanmisaka
035b5895b0 Fix AV1 decoding hang regression on RK3588 (#15776) 2025-12-13 08:27:29 -07:00
theguymadmax
22da5187c8 Fix collection display order (#15767) 2025-12-13 08:27:01 -07:00
theguymadmax
5804d6840c Fix parental rating comparison with sub-scores (#15786) 2025-12-13 08:25:48 -07:00
Bond-009
b50ce1ad6b Merge pull request #15752 from Collin-Swish/fix-name-case-insensitivity
Fix case sensitivity edge case
2025-12-12 21:39:22 +01:00
Bond-009
481ee03f35 Merge pull request #15757 from theguymadmax/fix-trickplays-for-alt-versions
Fix trickplay images using wrong item on alternate versions
2025-12-12 21:31:52 +01:00
Bond-009
d91adb5d54 Merge pull request #15662 from SapientGuardian/issue15661
Fix blocking in async context in LimitedConcurrencyLibraryScheduler
2025-12-10 20:37:57 +01:00
theguymadmax
ef7f138a4e Fix trickplay images using wrong item on alternate versions 2025-12-09 14:21:09 -05:00
Collin Swisher
2e8d9a311b Fix case sensitivity edge case 2025-12-08 17:41:48 -06:00
gnattu
4c5a3fbff3 Use original name for MusicAritist matching (#15689) 2025-12-05 19:30:02 -07:00
liszto
636908fc4d Fix thumbnails not being deleted from temp folder 2025-12-05 19:29:54 -07:00
Tim Eisele
997362fc97 Backport dependency updates (#15723) 2025-12-05 19:27:30 -07:00
Noah Potash
c5147341e3 Fixes 15661. Replace BlockingCollection with Channel in LimitedConcurrencyLibraryScheduler to prevent blocking in an asynchronous context. 2025-12-03 21:50:08 -05:00
Noah Potash
ca33bcebf0 Add SapientGuardian to CONTRIBUTORS.md 2025-12-03 21:27:26 -05:00
Ivan Kara
d32f487e8e Fix symlinked file size (#15681) 2025-12-03 19:04:59 -07:00
theguymadmax
fb65f8f853 Fix ItemAdded event triggering when updating metadata (#15680) 2025-12-03 19:02:55 -07:00
martenumberto
2a0b90e385 Fix: Add .ts fallback for video streams to prevent crash (#15690) 2025-12-03 19:02:39 -07:00
myzhysz
dde70fd8a2 Fix stack overflow while scanning (#15698) 2025-12-03 19:02:04 -07:00
Niels van Velzen
98d1d0cb35 Merge pull request #15670 from nyanmisaka/fix-mjpeg-rk3576
Fix the empty output of trickplay on RK3576
2025-12-02 13:48:51 +01:00
Jellyfin Release Bot
ba76a8f3ad Bump version to 10.11.4 2025-11-30 21:33:32 -05:00
Anthony Lavado
8cd5652157 Merge pull request #15672 from jellyfin/openapi-cache-z
Cache OpenApi document generation
2025-11-30 21:22:29 -05:00
crobibero
8aff4227d9 Implement caching for OpenAPI document 2025-11-30 09:19:19 -07:00
nyanmisaka
026f7472cb Fix the empty output of trickplay on RK3576
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2025-11-30 21:38:47 +08:00
MBR-0001
daca285568 Revert "Localization/iso6392.txt: change pob and pop" (#15555) 2025-11-23 19:20:29 +01:00
theguymadmax
fbb9a0b2c7 Fix ResolveLinkTarget crashing on exFAT drives (#15568) 2025-11-21 21:14:39 -07:00
Ziyuan Qu
29b3aa8543 Add hidden file check in bdInfo (#15582) 2025-11-21 21:14:30 -07:00
theguymadmax
94f3725208 Fix isMovie filter logic (#15594) 2025-11-21 21:14:03 -07:00
theguymadmax
0ee81e87be Fix locked fields on not saving (#15564) 2025-11-19 17:02:53 +01:00
theguymadmax
c491a918c2 Save item to database before providers run to prevent FK constraint errors (#15563) 2025-11-19 17:01:13 +01:00
gnattu
1e7e46cb82 Prevent copying HDR streams when only SDR is supported (#15556) 2025-11-18 18:37:35 -07:00
theguymadmax
5ae444d96d Fix NullReferenceException in filesystem path comparison (#15548) 2025-11-18 18:37:09 -07:00
gnattu
ee7ad83427 Restrict first video frame probing to file protocol (#15557) 2025-11-18 18:36:59 -07:00
Jellyfin Release Bot
921d7d3364 Bump version to 10.11.3 2025-11-16 17:40:07 -05:00
theguymadmax
f8e012582a Fix movie titles using folder name when NFOs saver is enabled (#15529) 2025-11-16 13:59:58 -07:00
theguymadmax
def5956cd1 Fix tmdbid not detected in single movie folder (#14955) 2025-11-16 13:36:35 -07:00
theguymadmax
abfbaca336 Fix series DateLastMediaAdded not updating when new episodes are added (#15472) 2025-11-16 13:35:43 -07:00
theguymadmax
6566188e45 Add 1 minute tolerance for NFO change detection (#15514) 2025-11-15 08:39:25 -07:00
theguymadmax
078f9584ed Fix playlist DateCreated and DateLastMediaAdded not being set (#15508) 2025-11-14 15:19:40 -07:00
Iksas
ee34c75386 fix missing font extraction for certain transcoding settings (#15502) 2025-11-13 18:30:18 -07:00
theguymadmax
e8150428b6 Fix .ignore handling for directories (#15501) 2025-11-13 18:23:18 -07:00
theguymadmax
4b38e35bbb Remove InheritedTags and update tag filtering logic (#15493) 2025-11-13 18:23:03 -07:00
Huo Jiacheng
435bb14bb2 Fix gitignore-style not working properly on windows. (#15487) 2025-11-12 19:43:13 -07:00
theguymadmax
2e5ced5098 Improve season folder parsing (#15404) 2025-11-12 17:36:57 -07:00
Bond-009
f4a846aa4d Don't error out when searching for marker files fails (#15466)
Fixes #15445
2025-11-11 15:45:47 -07:00
Joshua M. Boniface
7c1063177f Merge pull request #15462 from theguymadmax/fix-exception-for-empty-strm-files
Fix NullReferenceException in GetPathProtocol when path is null
2025-11-10 19:30:38 -05:00
Joshua M. Boniface
5878b1ffc5 Merge pull request #15468 from Bond-009/carefulWithLastMinChanges
Check if target exists before trying to follow it
2025-11-10 19:12:24 -05:00
Bond_009
3c3c2aee0d Check if target exists before trying to follow it
Exception got caught in ManagedFileSystem and wrong file info got returned
2025-11-10 23:19:17 +01:00
theguymadmax
511223aac4 Fix NullReferenceException in GetPathProtocol when path is null 2025-11-10 02:30:49 -05:00
Mikal S.
3b2d64995a Resolve symlinks for static media source infos (#15263) 2025-11-09 09:45:02 -07:00
theguymadmax
13c4517a66 Fix collection grouping in mixed libraries (#15373) 2025-11-09 09:35:50 -07:00
theguymadmax
177b6464ca Don't clear baseitemids (#15446) 2025-11-09 09:22:09 -07:00
Bond-009
5a9a8363f4 Merge pull request #15441 from IceStormNG/fix-nullreference-role-null-10.11
Fix System.NullReferenceException when people's role is null (10.11.z)
2025-11-08 18:25:03 +01:00
theguymadmax
49efd68fc7 Invalidate parent folder's cache on deletion/creation (#15423) 2025-11-08 08:30:04 -07:00
Carsten Braun
90a8a26c6e Copy-Pasting is sometimes hard.... 2025-11-08 15:00:11 +01:00
Carsten Braun
002c83e6f5 Fix NullReferenceExceltop when role is null. 2025-11-08 14:32:14 +01:00
theguymadmax
7222910b05 Fix filters to use SortName (#15381) 2025-11-07 18:21:41 -07:00
Bond-009
097cb87f6f Don't enforce a minimum amount of free space for the tmp and log dirs (#15390) 2025-11-07 18:21:10 -07:00
JPVenson
91c3b1617e Fixed missing sort argument (#15413) 2025-11-07 18:20:42 -07:00
theguymadmax
8f71922734 Fix item count display for collapsed items (#15380) 2025-11-07 18:20:10 -07:00
Niels van Velzen
d140630208 Update branding in Swagger page (#15422) 2025-11-07 18:19:30 -07:00
theguymadmax
63a3e55297 Fix search terms using diacritics (#15435) 2025-11-07 18:18:24 -07:00
evanreichard
c2e5081d64 feat(sqlite): add timeout config (#15369) 2025-11-07 18:17:43 -07:00
Jellyfin Release Bot
4187c6f620 Bump version to 10.11.2 2025-11-02 21:28:56 -05:00
Tim Eisele
e7dbb3afec Skip too large extracted season numbers (#15326) 2025-11-02 09:11:48 -07:00
vinnyspb
f994dd6211 Update file size when refreshing metadata (#15325) 2025-11-01 14:18:19 -06:00
Cody Robibero
da254ee968 return instead of break, add check to more migrations (#15322) 2025-11-01 14:17:22 -06:00
Bill Thornton
4ad3141875 Update password reset to always return the same response structure (#15254) 2025-11-01 14:17:09 -06:00
evanreichard
b5f0199a25 fix: in optimistic locking, key off table is locked (#15328) 2025-11-01 14:15:26 -06:00
Nyanmisaka
6bf88c049e Ignore initial delay in audio-only containers (#15247) 2025-10-29 20:40:28 -06:00
Jellyfin Release Bot
40a33da2a5 Bump version to 10.11.1 2025-10-26 22:02:09 -04:00
Joshua M. Boniface
3596fc0693 Fix bump_version to handle spaced filename 2025-10-26 21:50:38 -04:00
Jellyfin Release Bot
93824dad97 Bump version to 10.11.1 2025-10-26 21:41:27 -04:00
Tim Eisele
e5656af1f2 Improve symlink handling (#15209) 2025-10-26 15:10:13 -06:00
Niels van Velzen
c127c10458 Merge pull request #15225 from Bond-009/z440ATL
Update dependency z440.atl.core to 7.6.0
2025-10-26 18:50:04 +01:00
Tim Eisele
7d1824ea27 Fix pagination and sorting for folders (#15187) 2025-10-26 11:34:11 -06:00
Cody Robibero
2966d27c97 Skip invalid database migration (#15212) 2025-10-26 11:34:04 -06:00
Ivan Kara
618ec4543e Add season number fallback for OMDB and TMDB plugins (#15113) 2025-10-26 11:33:55 -06:00
Cody Robibero
0e4031ae52 Skip extracting directory entry when restoring (#15196) 2025-10-26 11:33:47 -06:00
CeruleanRed
442af96ed9 Only save chapters that are within the runtime of the video file (#15176) 2025-10-26 10:37:16 -06:00
JJBlue
a305204cfa Skip extracted files in migration if bad timestamp or no access (#15220)
Fixes #15024
2025-10-26 10:30:43 -06:00
theguymadmax
75f472e6a7 Normalize paths in database queries (#15217) 2025-10-26 10:30:12 -06:00
Bond_009
cc32e8f7cb Update dependency z440.atl.core to 7.6.0 2025-10-26 15:16:08 +01:00
MBR-0001
14b3085ff1 Fix Has(Imdb/Tmdb/Tvdb)Id checks (#15126) 2025-10-25 16:00:55 -06:00
Cody Robibero
5691eee4f1 Prefer filting by package id instead of name (#15197) 2025-10-25 09:37:09 -06:00
theguymadmax
1520a697ad Play selected song first with instant mix (#15133) 2025-10-25 09:33:11 -06:00
Cody Robibero
81b8b0ca4a Add the transcode marker during startup instead of first transcode (#15194) 2025-10-25 09:32:15 -06:00
Cody Robibero
ac3fa3c376 Clean up backup service (#15170) 2025-10-24 17:57:34 -06:00
Tim Eisele
7a1c1cd342 Skip extracted files in migration if bad timestamp or no access (#15112) 2025-10-24 17:57:19 -06:00
gnattu
70c32a26fa Make priority class setting more robust (#15177) 2025-10-24 17:57:02 -06:00
Cody Robibero
2b94bb54aa Fix xml formatter (#15164) 2025-10-24 17:56:38 -06:00
Bond-009
0a6e8146be Lower required tmp dir size to 512MiB (#15098) 2025-10-23 16:38:27 -06:00
theguymadmax
305b0fdca3 Make season paths case-insensitive (#15102) 2025-10-23 16:38:06 -06:00
theguymadmax
d738386fe2 Fix LiveTV images not saving to database (#15083) 2025-10-23 16:37:55 -06:00
Tim Eisele
ca830d5be7 Speed-up trickplay migration (#15054) 2025-10-23 16:37:47 -06:00
theguymadmax
a5bc4524d8 Optimize artist query (#15087) 2025-10-23 16:37:29 -06:00
Nyanmisaka
175ee12bbc Fix videos with cropping metadata are probed as anamorphic (#15144) 2025-10-23 16:31:11 -06:00
Nyanmisaka
a725220c21 Reject stream copy of HDR10+ video if the client does not support HDR10 (#15072) 2025-10-21 17:20:56 -06:00
gnattu
a245605152 Log the message more clear when network manager is not ready (#15055) 2025-10-21 17:18:26 -06:00
Tim Eisele
f4a53209f4 Skip invalid keyframe cache data (#15032) 2025-10-21 17:17:56 -06:00
Jellyfin Release Bot
877251bcae Bump version to 10.11.0 2025-10-19 20:45:12 -04:00
95 changed files with 736 additions and 1023 deletions

15
.github/CODEOWNERS vendored
View File

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

View File

@@ -87,11 +87,7 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- 10.11.4
- 10.11.3
- 10.11.2
- 10.11.1
- 10.11.0
- 10.10.0+
- Master
- Unstable
- Older*
@@ -140,14 +136,13 @@ body:
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
- **CPU Model**: [e.g. AMD Ryzen 5 9600X, Intel Core i7-8565U, etc.]
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example]
- **Networking**: [e.g. Host, Bridge/NAT]
- **Jellyfin Data Storage & Filesystem**: [e.g. local SATA SSD - ext4, local HDD - NTFS]
- **Media Storage & Filesystem**: [e.g. Local HDD - ext4, SMB Share]
- **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD]
- **Media Storage**: [e.g. Local HDD, SMB Share]
- **External Integrations**: [e.g. Jellystat, Jellyseerr]
value: |
- OS:
@@ -158,14 +153,13 @@ body:
- FFmpeg Version:
- Playback Method:
- Hardware Acceleration:
- CPU Model:
- GPU Model:
- Plugins:
- Reverse Proxy:
- Base URL:
- Networking:
- Jellyfin Data Storage & Filesystem:
- Media Storage & Filesystem:
- Jellyfin Data Storage:
- Media Storage:
- External Integrations:
render: markdown
validations:

View File

@@ -27,11 +27,11 @@ jobs:
dotnet-version: '9.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6

View File

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

View File

@@ -18,7 +18,7 @@
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.2" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.2" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Ignore" Version="0.2.1" />
@@ -74,9 +74,9 @@
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
<PackageVersion Include="SkiaSharp" Version="[3.119.1]" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="[3.119.1]" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.119.1]" />
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.2.1" />

View File

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

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -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)

View File

@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library
results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
}
if (query.Limit.HasValue && query.Limit.Value > 0)
if (query.Limit.HasValue)
{
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
}

View File

@@ -16,7 +16,7 @@
"Folders": "المجلدات",
"Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم",
"HeaderContinueWatching": "أكمل المشاهدة",
"HeaderContinueWatching": "إستئناف المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة",

View File

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

View File

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

View File

@@ -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",

View File

@@ -129,12 +129,5 @@
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
"TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
"TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
"TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
"CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है"
}

View File

@@ -136,7 +136,5 @@
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
"TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.",
"TaskDownloadMissingLyrics": "누락된 가사 다운로드",
"TaskDownloadMissingLyricsDescription": "가사 다운로드",
"CleanupUserDataTask": "사용자 데이터 정리 작업",
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다."
"TaskDownloadMissingLyricsDescription": "가사 다운로드"
}

View File

@@ -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": "Танисангүй",

View File

@@ -132,10 +132,5 @@
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो",
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
"TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
}

View File

@@ -134,8 +134,6 @@
"TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
"TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
"TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।",
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।",
"CleanupUserDataTaskDescription": "ਘੱਟੋ-ਘੱਟ 90 ਦਿਨਾਂ ਤੋਂ ਮੌਜੂਦ ਨਾ ਹੋਣ ਵਾਲੇ ਮੀਡੀਆ ਤੋਂ ਸਾਰੇ ਉਪਭੋਗਤਾ ਡੇਟਾ (ਵਾਚ ਸਟੇਟ, ਮਨਪਸੰਦ ਸਟੇਟਸ ਆਦਿ) ਨੂੰ ਸਾਫ਼ ਕਰਦਾ ਹੈ।",
"CleanupUserDataTask": "ਯੂਜ਼ਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨ ਦਾ ਕੰਮ"
"TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।",
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।"
}

View File

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

View File

@@ -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",

View File

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

View File

@@ -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": "用戶資料清理工作"
}

View File

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

View File

@@ -266,7 +266,7 @@ namespace Emby.Server.Implementations.TV
items = items.Skip(query.StartIndex.Value);
}
if (query.Limit.HasValue && query.Limit.Value > 0)
if (query.Limit.HasValue)
{
items = items.Take(query.Limit.Value);
}

View File

@@ -1,16 +1,13 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Queries;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Api;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers;
@@ -35,19 +32,10 @@ public class ActivityLogController : BaseJellyfinApiController
/// <summary>
/// Gets activity log entries.
/// </summary>
/// <param name="startIndex">The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">The maximum number of records to return.</param>
/// <param name="minDate">The minimum date.</param>
/// <param name="hasUserId">Filter log entries if it has user id, or not.</param>
/// <param name="name">Filter by name.</param>
/// <param name="overview">Filter by overview.</param>
/// <param name="shortOverview">Filter by short overview.</param>
/// <param name="type">Filter by type.</param>
/// <param name="itemId">Filter by item id.</param>
/// <param name="username">Filter by username.</param>
/// <param name="severity">Filter by log severity.</param>
/// <param name="sortBy">Specify one or more sort orders. Format: SortBy=Name,Type.</param>
/// <param name="sortOrder">Sort Order..</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
/// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
/// <response code="200">Activity log returned.</response>
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
[HttpGet("Entries")]
@@ -56,60 +44,14 @@ public class ActivityLogController : BaseJellyfinApiController
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] DateTime? minDate,
[FromQuery] bool? hasUserId,
[FromQuery] string? name,
[FromQuery] string? overview,
[FromQuery] string? shortOverview,
[FromQuery] string? type,
[FromQuery] Guid? itemId,
[FromQuery] string? username,
[FromQuery] LogLevel? severity,
[FromQuery] ActivityLogSortBy[]? sortBy,
[FromQuery] SortOrder[]? sortOrder)
[FromQuery] bool? hasUserId)
{
var query = new ActivityLogQuery
return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
{
Skip = startIndex,
Limit = limit,
MinDate = minDate,
HasUserId = hasUserId,
Name = name,
Overview = overview,
ShortOverview = shortOverview,
Type = type,
ItemId = itemId,
Username = username,
Severity = severity,
OrderBy = GetOrderBy(sortBy ?? [], sortOrder ?? []),
};
return await _activityManager.GetPagedResultAsync(query).ConfigureAwait(false);
}
private static (ActivityLogSortBy SortBy, SortOrder SortOrder)[] GetOrderBy(
IReadOnlyList<ActivityLogSortBy> sortBy,
IReadOnlyList<SortOrder> requestedSortOrder)
{
if (sortBy.Count == 0)
{
return [];
}
var result = new (ActivityLogSortBy, SortOrder)[sortBy.Count];
var i = 0;
for (; i < requestedSortOrder.Count; i++)
{
result[i] = (sortBy[i], requestedSortOrder[i]);
}
// Add remaining elements with the first specified SortOrder
// or the default one if no SortOrders are specified
var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending;
for (; i < sortBy.Count; i++)
{
result[i] = (sortBy[i], order);
}
return result;
HasUserId = hasUserId
}).ConfigureAwait(false);
}
}

View File

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

View File

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

View File

@@ -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))

View File

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

View File

@@ -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>();

View File

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

View File

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

View File

@@ -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>();

View File

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

View File

@@ -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)

View File

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

View File

@@ -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())

View File

@@ -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) },

View File

@@ -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();

View File

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

View File

@@ -13,7 +13,6 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
@@ -95,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);
}
@@ -134,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);
}
@@ -181,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);
@@ -423,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;
@@ -479,7 +478,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
var dtoOptions = new DtoOptions();
var dtoOptions = new DtoOptions().AddClientFields(User);
return Ok(item
.GetExtras()
@@ -550,6 +549,7 @@ public class UserLibraryController : BaseJellyfinApiController
}
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var list = _userViewManager.GetLatestItems(
@@ -569,7 +569,7 @@ public class UserLibraryController : BaseJellyfinApiController
var item = i.Item2[0];
var childCount = 0;
if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum || i.Item1 is Series ))
if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
{
item = i.Item1;
childCount = i.Item2.Count;

View File

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

View File

@@ -111,6 +111,7 @@ public class VideosController : BaseJellyfinApiController
}
var dtoOptions = new DtoOptions();
dtoOptions = dtoOptions.AddClientFields(User);
BaseItemDto[] items;
if (item is Video video)

View File

@@ -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())
{

View File

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

View File

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

View File

@@ -1,49 +0,0 @@
namespace Jellyfin.Data.Enums;
/// <summary>
/// Activity log sorting options.
/// </summary>
public enum ActivityLogSortBy
{
/// <summary>
/// Sort by name.
/// </summary>
Name = 0,
/// <summary>
/// Sort by overview.
/// </summary>
Overiew = 1,
/// <summary>
/// Sort by short overview.
/// </summary>
ShortOverview = 2,
/// <summary>
/// Sort by type.
/// </summary>
Type = 3,
/*
/// <summary>
/// Sort by item name.
/// </summary>
Item = 4,
*/
/// <summary>
/// Sort by date.
/// </summary>
DateCreated = 5,
/// <summary>
/// Sort by username.
/// </summary>
Username = 6,
/// <summary>
/// Sort by severity.
/// </summary>
LogSeverity = 7
}

View File

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

View File

@@ -1,63 +1,20 @@
using System;
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Data.Queries;
/// <summary>
/// A class representing a query to the activity logs.
/// </summary>
public class ActivityLogQuery : PaginatedQuery
namespace Jellyfin.Data.Queries
{
/// <summary>
/// Gets or sets a value indicating whether to take entries with a user id.
/// A class representing a query to the activity logs.
/// </summary>
public bool? HasUserId { get; set; }
public class ActivityLogQuery : PaginatedQuery
{
/// <summary>
/// Gets or sets a value indicating whether to take entries with a user id.
/// </summary>
public bool? HasUserId { get; set; }
/// <summary>
/// Gets or sets the minimum date to query for.
/// </summary>
public DateTime? MinDate { get; set; }
/// <summary>
/// Gets or sets the name filter.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the overview filter.
/// </summary>
public string? Overview { get; set; }
/// <summary>
/// Gets or sets the short overview filter.
/// </summary>
public string? ShortOverview { get; set; }
/// <summary>
/// Gets or sets the type filter.
/// </summary>
public string? Type { get; set; }
/// <summary>
/// Gets or sets the item filter.
/// </summary>
public Guid? ItemId { get; set; }
/// <summary>
/// Gets or sets the username filter.
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Gets or sets the log level filter.
/// </summary>
public LogLevel? Severity { get; set; }
/// <summary>
/// Gets or sets the result ordering.
/// </summary>
public IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? OrderBy { get; set; }
/// <summary>
/// Gets or sets the minimum date to query for.
/// </summary>
public DateTime? MinDate { get; set; }
}
}

View File

@@ -1,198 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying;
using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations.Activity;
/// <summary>
/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
/// </summary>
public class ActivityManager : IActivityManager
namespace Jellyfin.Server.Implementations.Activity
{
private readonly IDbContextFactory<JellyfinDbContext> _provider;
/// <summary>
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
/// </summary>
/// <param name="provider">The Jellyfin database provider.</param>
public ActivityManager(IDbContextFactory<JellyfinDbContext> provider)
public class ActivityManager : IActivityManager
{
_provider = provider;
}
private readonly IDbContextFactory<JellyfinDbContext> _provider;
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
/// <inheritdoc/>
public async Task CreateAsync(ActivityLog entry)
{
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
/// <summary>
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
/// </summary>
/// <param name="provider">The Jellyfin database provider.</param>
public ActivityManager(IDbContextFactory<JellyfinDbContext> provider)
{
dbContext.ActivityLogs.Add(entry);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
_provider = provider;
}
EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
}
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
/// <inheritdoc/>
public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
{
// TODO allow sorting and filtering by item id. Currently not possible because ActivityLog stores the item id as a string.
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
/// <inheritdoc/>
public async Task CreateAsync(ActivityLog entry)
{
// TODO switch to LeftJoin in .NET 10.
var entries = from a in dbContext.ActivityLogs
join u in dbContext.Users on a.UserId equals u.Id into ugj
from u in ugj.DefaultIfEmpty()
select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
if (query.HasUserId is not null)
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
entries = entries.Where(e => e.ActivityLog.UserId.Equals(default) != query.HasUserId.Value);
dbContext.ActivityLogs.Add(entry);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
if (query.MinDate is not null)
{
entries = entries.Where(e => e.ActivityLog.DateCreated >= query.MinDate.Value);
}
if (!string.IsNullOrEmpty(query.Name))
{
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Name, $"%{query.Name}%"));
}
if (!string.IsNullOrEmpty(query.Overview))
{
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Overview, $"%{query.Overview}%"));
}
if (!string.IsNullOrEmpty(query.ShortOverview))
{
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.ShortOverview, $"%{query.ShortOverview}%"));
}
if (!string.IsNullOrEmpty(query.Type))
{
entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Type, $"%{query.Type}%"));
}
if (!query.ItemId.IsNullOrEmpty())
{
var itemId = query.ItemId.Value.ToString("N");
entries = entries.Where(e => e.ActivityLog.ItemId == itemId);
}
if (!string.IsNullOrEmpty(query.Username))
{
entries = entries.Where(e => EF.Functions.Like(e.Username, $"%{query.Username}%"));
}
if (query.Severity is not null)
{
entries = entries.Where(e => e.ActivityLog.LogSeverity == query.Severity);
}
return new QueryResult<ActivityLogEntry>(
query.Skip,
await entries.CountAsync().ConfigureAwait(false),
await ApplyOrdering(entries, query.OrderBy)
.Skip(query.Skip ?? 0)
.Take(query.Limit ?? 100)
.Select(entity => new ActivityLogEntry(entity.ActivityLog.Name, entity.ActivityLog.Type, entity.ActivityLog.UserId)
{
Id = entity.ActivityLog.Id,
Overview = entity.ActivityLog.Overview,
ShortOverview = entity.ActivityLog.ShortOverview,
ItemId = entity.ActivityLog.ItemId,
Date = entity.ActivityLog.DateCreated,
Severity = entity.ActivityLog.LogSeverity
})
.ToListAsync()
.ConfigureAwait(false));
}
}
/// <inheritdoc />
public async Task CleanAsync(DateTime startDate)
{
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
await dbContext.ActivityLogs
.Where(entry => entry.DateCreated <= startDate)
.ExecuteDeleteAsync()
.ConfigureAwait(false);
}
}
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
{
return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
{
Id = entry.Id,
Overview = entry.Overview,
ShortOverview = entry.ShortOverview,
ItemId = entry.ItemId,
Date = entry.DateCreated,
Severity = entry.LogSeverity
};
}
private IOrderedQueryable<ExpandedActivityLog> ApplyOrdering(IQueryable<ExpandedActivityLog> query, IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? sorting)
{
if (sorting is null || sorting.Count == 0)
{
return query.OrderByDescending(e => e.ActivityLog.DateCreated);
EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
}
IOrderedQueryable<ExpandedActivityLog> ordered = null!;
foreach (var (sortBy, sortOrder) in sorting)
/// <inheritdoc/>
public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
{
var orderBy = MapOrderBy(sortBy);
ordered = sortOrder == SortOrder.Ascending
? (ordered ?? query).OrderBy(orderBy)
: (ordered ?? query).OrderByDescending(orderBy);
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var entries = dbContext.ActivityLogs
.OrderByDescending(entry => entry.DateCreated)
.Where(entry => query.MinDate == null || entry.DateCreated >= query.MinDate)
.Where(entry => !query.HasUserId.HasValue || entry.UserId.Equals(default) != query.HasUserId.Value);
return new QueryResult<ActivityLogEntry>(
query.Skip,
await entries.CountAsync().ConfigureAwait(false),
await entries
.Skip(query.Skip ?? 0)
.Take(query.Limit ?? 100)
.Select(entity => new ActivityLogEntry(entity.Name, entity.Type, entity.UserId)
{
Id = entity.Id,
Overview = entity.Overview,
ShortOverview = entity.ShortOverview,
ItemId = entity.ItemId,
Date = entity.DateCreated,
Severity = entity.LogSeverity
})
.ToListAsync()
.ConfigureAwait(false));
}
}
return ordered;
}
private Expression<Func<ExpandedActivityLog, object?>> MapOrderBy(ActivityLogSortBy sortBy)
{
return sortBy switch
/// <inheritdoc />
public async Task CleanAsync(DateTime startDate)
{
ActivityLogSortBy.Name => e => e.ActivityLog.Name,
ActivityLogSortBy.Overiew => e => e.ActivityLog.Overview,
ActivityLogSortBy.ShortOverview => e => e.ActivityLog.ShortOverview,
ActivityLogSortBy.Type => e => e.ActivityLog.Type,
ActivityLogSortBy.DateCreated => e => e.ActivityLog.DateCreated,
ActivityLogSortBy.Username => e => e.Username,
ActivityLogSortBy.LogSeverity => e => e.ActivityLog.LogSeverity,
_ => throw new ArgumentOutOfRangeException(nameof(sortBy), sortBy, "Unhandled ActivityLogSortBy")
};
}
var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
await dbContext.ActivityLogs
.Where(entry => entry.DateCreated <= startDate)
.ExecuteDeleteAsync()
.ConfigureAwait(false);
}
}
private class ExpandedActivityLog
{
public ActivityLog ActivityLog { get; set; } = null!;
public string? Username { get; set; }
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
{
return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
{
Id = entry.Id,
Overview = entry.Overview,
ShortOverview = entry.ShortOverview,
ItemId = entry.ItemId,
Date = entry.DateCreated,
Severity = entry.LogSeverity
};
}
}
}

View File

@@ -158,7 +158,7 @@ namespace Jellyfin.Server.Implementations.Devices
devices = devices.Skip(query.Skip.Value);
}
if (query.Limit.HasValue && query.Limit.Value > 0)
if (query.Limit.HasValue)
{
devices = devices.Take(query.Limit.Value);
}

View File

@@ -250,7 +250,7 @@ public sealed class BaseItemRepository
public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
{
ArgumentNullException.ThrowIfNull(filter);
if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0))
if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0))
{
var returnList = GetItemList(filter);
return new QueryResult<BaseItemDto>(
@@ -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/>
@@ -326,7 +324,7 @@ public sealed class BaseItemRepository
.OrderByDescending(g => g.MaxDateCreated)
.Select(g => g);
if (filter.Limit.HasValue && filter.Limit.Value > 0)
if (filter.Limit.HasValue)
{
subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
}
@@ -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 />
@@ -367,7 +363,7 @@ public sealed class BaseItemRepository
.OrderByDescending(g => g.LastPlayedDate)
.Select(g => g.Key!);
if (filter.Limit.HasValue && filter.Limit.Value > 0)
if (filter.Limit.HasValue)
{
query = query.Take(filter.Limit.Value);
}
@@ -408,31 +404,21 @@ 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.StartIndex.HasValue && filter.StartIndex.Value > 0)
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
{
dbQuery = dbQuery.Skip(filter.StartIndex.Value);
}
var offset = filter.StartIndex ?? 0;
if (filter.Limit.HasValue && filter.Limit.Value > 0)
{
dbQuery = dbQuery.Take(filter.Limit.Value);
if (offset > 0)
{
dbQuery = dbQuery.Skip(offset);
}
if (filter.Limit.HasValue)
{
dbQuery = dbQuery.Take(filter.Limit.Value);
}
}
return dbQuery;
@@ -443,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)
{
@@ -1185,7 +1203,7 @@ public sealed class BaseItemRepository
{
ArgumentNullException.ThrowIfNull(filter);
if (!(filter.Limit.HasValue && filter.Limit.Value > 0))
if (!filter.Limit.HasValue)
{
filter.EnableTotalRecordCount = false;
}
@@ -1264,14 +1282,19 @@ public sealed class BaseItemRepository
result.TotalRecordCount = query.Count();
}
if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
{
query = query.Skip(filter.StartIndex.Value);
}
var offset = filter.StartIndex ?? 0;
if (filter.Limit.HasValue && filter.Limit.Value > 0)
{
query = query.Take(filter.Limit.Value);
if (offset > 0)
{
query = query.Skip(offset);
}
if (filter.Limit.HasValue)
{
query = query.Take(filter.Limit.Value);
}
}
IQueryable<BaseItemEntity>? itemCountQuery = null;
@@ -1352,7 +1375,7 @@ public sealed class BaseItemRepository
private static void PrepareFilterQuery(InternalItemsQuery query)
{
if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey)
if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
{
query.Limit = query.Limit.Value + 4;
}
@@ -1363,54 +1386,14 @@ public sealed class BaseItemRepository
}
}
/// <summary>
/// Gets the clean value for search and sorting purposes.
/// </summary>
/// <param name="value">The value to clean.</param>
/// <returns>The cleaned value.</returns>
public static string GetCleanValue(string value)
private string GetCleanValue(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return value;
}
var noDiacritics = value.RemoveDiacritics();
// Build a string where any punctuation or symbol is treated as a separator (space).
var sb = new StringBuilder(noDiacritics.Length);
var previousWasSpace = false;
foreach (var ch in noDiacritics)
{
char outCh;
if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
{
outCh = ch;
}
else
{
outCh = ' ';
}
// normalize any whitespace character to a single ASCII space.
if (char.IsWhiteSpace(outCh))
{
if (!previousWasSpace)
{
sb.Append(' ');
previousWasSpace = true;
}
}
else
{
sb.Append(outCh);
previousWasSpace = false;
}
}
// trim leading/trailing spaces that may have been added.
var collapsed = sb.ToString().Trim();
return collapsed.ToLowerInvariant();
return value.RemoveDiacritics().ToLowerInvariant();
}
private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
@@ -1559,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);
}
@@ -1960,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

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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,

View File

@@ -1,5 +1,5 @@
using System;
using AsyncKeyedLock;
using System.Threading;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
@@ -17,7 +17,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider
private const string CacheKey = "openapi.json";
private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) };
private static readonly AsyncNonKeyedLocker _lock = new(1);
private static readonly SemaphoreSlim _lock = new(1, 1);
private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1);
private readonly IMemoryCache _memoryCache;
@@ -50,20 +50,30 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider
return AdjustDocument(openApiDocument, host, basePath);
}
using var acquired = _lock.LockOrNull(_lockTimeout);
if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null)
var acquired = _lock.Wait(_lockTimeout);
try
{
if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null)
{
return AdjustDocument(openApiDocument, host, basePath);
}
if (!acquired)
{
throw new InvalidOperationException("OpenApi document is generating");
}
openApiDocument = _swaggerGenerator.GetSwagger(documentName);
_memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
return AdjustDocument(openApiDocument, host, basePath);
}
if (acquired is null)
finally
{
throw new InvalidOperationException("OpenApi document is generating");
if (acquired)
{
_lock.Release();
}
}
openApiDocument = _swaggerGenerator.GetSwagger(documentName);
_memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
return AdjustDocument(openApiDocument, host, basePath);
}
private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath)

View File

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

View File

@@ -1,105 +0,0 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.ServerSetupApp;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to refresh CleanName values for all library items.
/// </summary>
[JellyfinMigration("2025-10-08T12:00:00", nameof(RefreshCleanNames))]
[JellyfinMigrationBackup(JellyfinDb = true)]
public class RefreshCleanNames : IAsyncMigrationRoutine
{
private readonly IStartupLogger<RefreshCleanNames> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
/// <summary>
/// Initializes a new instance of the <see cref="RefreshCleanNames"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public RefreshCleanNames(
IStartupLogger<RefreshCleanNames> logger,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
_logger = logger;
_dbProvider = dbProvider;
}
/// <inheritdoc />
public async Task PerformAsync(CancellationToken cancellationToken)
{
const int Limit = 1000;
int itemCount = 0;
var sw = Stopwatch.StartNew();
using var context = _dbProvider.CreateDbContext();
var records = context.BaseItems.Count(b => !string.IsNullOrEmpty(b.Name));
_logger.LogInformation("Refreshing CleanName for {Count} library items", records);
var processedInPartition = 0;
await foreach (var item in context.BaseItems
.Where(b => !string.IsNullOrEmpty(b.Name))
.OrderBy(e => e.Id)
.WithPartitionProgress((partition) => _logger.LogInformation("Processed: {Offset}/{Total} - Updated: {UpdatedCount} - Time: {Elapsed}", partition * Limit, records, itemCount, sw.Elapsed))
.PartitionEagerAsync(Limit, cancellationToken)
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
try
{
var newCleanName = string.IsNullOrWhiteSpace(item.Name) ? string.Empty : BaseItemRepository.GetCleanValue(item.Name);
if (!string.Equals(newCleanName, item.CleanName, StringComparison.Ordinal))
{
_logger.LogDebug(
"Updating CleanName for item {Id}: '{OldValue}' -> '{NewValue}'",
item.Id,
item.CleanName,
newCleanName);
item.CleanName = newCleanName;
itemCount++;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to update CleanName for item {Id} ({Name})", item.Id, item.Name);
}
processedInPartition++;
if (processedInPartition >= Limit)
{
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
// Clear tracked entities to avoid memory growth across partitions
context.ChangeTracker.Clear();
processedInPartition = 0;
}
}
// Save any remaining changes after the loop
if (processedInPartition > 0)
{
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
context.ChangeTracker.Clear();
}
_logger.LogInformation(
"Refreshed CleanName for {UpdatedCount} out of {TotalCount} items in {Time}",
itemCount,
records,
sw.Elapsed);
}
}

View File

@@ -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}"
}
},
{

View File

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

View File

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

View File

@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11222.15 d18.0
# Visual Studio Version 16
VisualStudioVersion = 16.0.30503.244
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}"
EndProject
@@ -30,7 +30,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{41093F42-C7CC-4D07-956B-6182CBEDE2EC}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
Directory.Packages.props = Directory.Packages.props
SharedVersion.cs = SharedVersion.cs
EndProjectSection
EndProject

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
@@ -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>

View File

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

View File

@@ -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()

View File

@@ -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.PremiereDate ?? DateTime.MaxValue)
.ToArray();
}
var childCount = realChildren.Length;
if (result.Count < limit)
{

View File

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

View File

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

View File

@@ -455,7 +455,7 @@ namespace MediaBrowser.Controller.Entities
var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
var totalCount = itemsArray.Length;
if (query.Limit.HasValue && query.Limit.Value > 0)
if (query.Limit.HasValue)
{
itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray();
}

View File

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

View File

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

View File

@@ -7039,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);
}
}

View File

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

View File

@@ -13,10 +13,8 @@ using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
@@ -39,7 +37,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private readonly IMediaSourceManager _mediaSourceManager;
private readonly ISubtitleParser _subtitleParser;
private readonly IPathManager _pathManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// The _semaphoreLocks.
@@ -57,8 +54,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
IHttpClientFactory httpClientFactory,
IMediaSourceManager mediaSourceManager,
ISubtitleParser subtitleParser,
IPathManager pathManager,
IServerConfigurationManager serverConfigurationManager)
IPathManager pathManager)
{
_logger = logger;
_fileSystem = fileSystem;
@@ -67,7 +63,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
_mediaSourceManager = mediaSourceManager;
_subtitleParser = subtitleParser;
_pathManager = pathManager;
_serverConfigurationManager = serverConfigurationManager;
}
private MemoryStream ConvertSubtitles(
@@ -399,8 +394,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
exitCode = process.ExitCode;
}
catch (OperationCanceledException)
@@ -683,8 +677,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
exitCode = process.ExitCode;
}
catch (OperationCanceledException)
@@ -835,8 +828,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
exitCode = process.ExitCode;
}
catch (OperationCanceledException)

View File

@@ -1,3 +1,5 @@
#pragma warning disable CS1591
using System;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
@@ -5,36 +7,21 @@ using Jellyfin.Data.Queries;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Model.Activity;
/// <summary>
/// Interface for the activity manager.
/// </summary>
public interface IActivityManager
namespace MediaBrowser.Model.Activity
{
/// <summary>
/// The event that is triggered when an entity is created.
/// </summary>
event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
public interface IActivityManager
{
event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
/// <summary>
/// Create a new activity log entry.
/// </summary>
/// <param name="entry">The entry to create.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task CreateAsync(ActivityLog entry);
Task CreateAsync(ActivityLog entry);
/// <summary>
/// Get a paged list of activity log entries.
/// </summary>
/// <param name="query">The activity log query.</param>
/// <returns>The page of entries.</returns>
Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query);
Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query);
/// <summary>
/// Remove all activity logs before the specified date.
/// </summary>
/// <param name="startDate">Activity log start date.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task CleanAsync(DateTime startDate);
/// <summary>
/// Remove all activity logs before the specified date.
/// </summary>
/// <param name="startDate">Activity log start date.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task CleanAsync(DateTime startDate);
}
}

View File

@@ -57,7 +57,6 @@ public class EncodingOptions
AllowHevcEncoding = false;
AllowAv1Encoding = false;
EnableSubtitleExtraction = true;
SubtitleExtractionTimeoutMinutes = 30;
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = ["mkv"];
HardwareDecodingCodecs = ["h264", "vc1"];
}
@@ -287,11 +286,6 @@ public class EncodingOptions
/// </summary>
public bool EnableSubtitleExtraction { get; set; }
/// <summary>
/// Gets or sets the timeout for subtitle extraction in minutes.
/// </summary>
public int SubtitleExtractionTimeoutMinutes { get; set; }
/// <summary>
/// Gets or sets the codecs hardware encoding is used for.
/// </summary>

View File

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

View File

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

View File

@@ -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.

View File

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

View File

@@ -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)

View File

@@ -721,8 +721,6 @@ namespace MediaBrowser.Providers.Manager
}
}
}
_libraryManager.CreateItem(item, null);
}
/// <summary>

View File

@@ -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")]

View File

@@ -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 edgecase: when the image header is parsed but the actual pixel
// decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without colortransform, etc.)
// `SKCodec.Create` returns a *nonnull* 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 edgecase: when the image header is parsed but the actual pixel
// decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without colortransform, etc.)
// `SKCodec.Create` returns a *nonnull* 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);
}
}
}
}

View File

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

View File

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

View File

@@ -240,9 +240,12 @@ namespace Jellyfin.LiveTv.Channels
var all = channels;
var totalCount = all.Count;
int startIndex = query.StartIndex ?? 0;
int count = (query.Limit ?? 0) > 0 ? Math.Min(query.Limit.Value, totalCount - startIndex) : totalCount - startIndex;
all = all.GetRange(query.StartIndex ?? 0, count);
if (query.StartIndex.HasValue || query.Limit.HasValue)
{
int startIndex = query.StartIndex ?? 0;
int count = query.Limit is null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
all = all.GetRange(startIndex, count);
}
if (query.RefreshLatestChannelItems)
{

View File

@@ -287,7 +287,7 @@ namespace Jellyfin.LiveTv
GenreIds = query.GenreIds
};
if (query.Limit.HasValue && query.Limit.Value > 0)
if (query.Limit.HasValue)
{
internalQuery.Limit = Math.Max(query.Limit.Value * 4, 200);
}
@@ -305,7 +305,7 @@ namespace Jellyfin.LiveTv
IEnumerable<BaseItem> programs = orderedPrograms;
if (query.Limit.HasValue && query.Limit.Value > 0)
if (query.Limit.HasValue)
{
programs = programs.Take(query.Limit.Value);
}

View File

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

View File

@@ -1,109 +0,0 @@
using System;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Controller.Entities.TV;
using Microsoft.Extensions.Configuration;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Data
{
public class SearchPunctuationTests
{
private readonly IFixture _fixture;
private readonly BaseItemRepository _repo;
public SearchPunctuationTests()
{
var appHost = new Mock<MediaBrowser.Controller.IServerApplicationHost>();
appHost.Setup(x => x.ExpandVirtualPath(It.IsAny<string>()))
.Returns((string x) => x);
appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>()))
.Returns((string x) => x);
var configSection = new Mock<IConfigurationSection>();
configSection
.SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)])
.Returns("0");
var config = new Mock<IConfiguration>();
config
.Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)))
.Returns(configSection.Object);
_fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
_fixture.Inject(appHost.Object);
_fixture.Inject(config.Object);
_repo = _fixture.Create<BaseItemRepository>();
}
[Fact]
public void CleanName_keeps_punctuation_and_search_without_punctuation_passes()
{
var series = new Series
{
Id = Guid.NewGuid(),
Name = "Mr. Robot"
};
series.SortName = "Mr. Robot";
var entity = _repo.Map(series);
Assert.Equal("mr robot", entity.CleanName);
var searchTerm = "Mr Robot".ToLowerInvariant();
Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
[Theory]
[InlineData("Spider-Man: Homecoming", "spider man homecoming")]
[InlineData("Beyoncé — Live!", "beyonce live")]
[InlineData("Hello, World!", "hello world")]
[InlineData("(The) Good, the Bad & the Ugly", "the good the bad the ugly")]
[InlineData("Wall-E", "wall e")]
[InlineData("No. 1: The Beginning", "no 1 the beginning")]
[InlineData("Café-au-lait", "cafe au lait")]
public void CleanName_normalizes_various_punctuation(string title, string expectedClean)
{
var series = new Series
{
Id = Guid.NewGuid(),
Name = title
};
series.SortName = title;
var entity = _repo.Map(series);
Assert.Equal(expectedClean, entity.CleanName);
// Ensure a search term without punctuation would match
var searchTerm = expectedClean;
Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
[Theory]
[InlineData("Face/Off", "face off")]
[InlineData("V/H/S", "v h s")]
public void CleanName_normalizes_titles_withslashes(string title, string expectedClean)
{
var series = new Series
{
Id = Guid.NewGuid(),
Name = title
};
series.SortName = title;
var entity = _repo.Map(series);
Assert.Equal(expectedClean, entity.CleanName);
// Ensure a search term without punctuation would match
var searchTerm = expectedClean;
Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

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

View File

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