Compare commits

...

55 Commits

Author SHA1 Message Date
Bond-009
4e80648fd3 Merge pull request #17146 from theguymadmax/fix-identify-search
Some checks failed
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Fix Identify returning wrong results
2026-06-21 19:03:52 +02:00
Bond-009
f08a3f9fd9 Merge pull request #17090 from moontwister/fix/audio-sample-rate-non-opus-17026
Fix audio sample rate forced to 48 kHz for non-Opus codecs
2026-06-21 19:01:18 +02:00
Bond-009
083f9d291a Merge pull request #17094 from moontwister/fix/trailers-nullref-controllercontext-17065
Deprecate the redundant /Trailers endpoint
2026-06-21 19:00:23 +02:00
danne
e4383493a9 Fix audio sample rate forced to 48 kHz for non-Opus codecs
GetProgressiveAudioFullCommandLine applied the libopus-only sample rate
quantization to every codec except Opus, inverting the intended guard.
A requested rate such as 44100 Hz was therefore snapped to 48000 Hz for
AAC/MP3/FLAC, while Opus (which actually requires the quantization) was
skipped entirely.

Apply the quantization only when the output codec is Opus, and pass the
requested sample rate through unchanged for all other codecs.

Fixes #17026

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 09:43:31 +02:00
theguymadmax
ce58e4400e Fix Identify returning wrong results 2026-06-20 22:30:52 -04:00
Bond-009
3741d71965 Merge pull request #17116 from theguymadmax/fix-root-folder-parsing
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
2026-06-21 00:10:26 +02:00
Žiga Ules
11f642594d Translated using Weblate (Slovenian)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sl/
2026-06-20 12:46:03 +00:00
AfmanS
8d15529df7 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2026-06-20 12:46:03 +00:00
danne
e75161c557 Deprecate the redundant /Trailers endpoint
GET /Trailers is a thin alias for GET /Items with includeItemTypes=Trailer;
it just forwards to the injected ItemsController. Per the PR review the agreed
direction is to deprecate it rather than keep maintaining the delegation.

Mark the action [Obsolete] so it is flagged as deprecated in the OpenAPI spec;
clients should use the GetItems operation with includeItemTypes=Trailer instead.

Re #17065
2026-06-19 07:03:58 +02:00
Bond-009
308981cc0d Merge pull request #14935 from JadedRain/master
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Fixed "Deleting media that is still being watched in SyncPlay results in errors"
2026-06-18 17:46:33 +02:00
Bond-009
bebb7ce803 Merge pull request #17112 from theguymadmax/add-year-to-series-resolver
Fix series year lost during name parsing
2026-06-18 17:46:23 +02:00
Bond-009
1a6f019cfd Merge pull request #17121 from theguymadmax/fix-date-offset
Fix episode air date offset after initial scan
2026-06-18 17:46:07 +02:00
Bond-009
751b763838 Merge pull request #17099 from Bond-009/libraryimport
Follow native interoperability best practices
2026-06-18 17:45:55 +02:00
theguymadmax
64e02c0e28 Apply review feedback 2026-06-17 15:41:31 -04:00
Bond-009
49f8a96360 Merge pull request #17087 from dkanada/book-resolver
Some checks failed
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
improve book resolution from filename
2026-06-17 20:54:49 +02:00
Bond-009
364f1e12c0 Merge pull request #17106 from Shadowghost/extend-transcoding-reason-reporting
Extend TranscodingReason reporting
2026-06-17 20:49:30 +02:00
Bond-009
ada11f5692 Always apply recursive when filters are requested (#17088) 2026-06-17 20:45:52 +02:00
Rant423
5036bf7db0 Fetch TV Shows creators from TMDB (#17107)
Fetch TV Shows creators from TMDB
2026-06-17 20:39:33 +02:00
Bond-009
1c4dea4b2c Merge pull request #17118 from jellyfin/renovate/sharpfuzz-2.x
Update dependency SharpFuzz to 2.3.0
2026-06-17 20:36:22 +02:00
theguymadmax
e525fc7c4b Fix episode air date offset after initial scan 2026-06-16 18:02:26 -04:00
Darren
3307406ac8 Translated using Weblate (Indonesian)
Some checks failed
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/id/
2026-06-16 16:23:01 +00:00
Bond_009
e86b502cbc Strip null-terminator 2026-06-16 17:54:23 +02:00
Bond_009
4c228eaf63 Make sure we don't include the null terminator 2026-06-16 17:45:22 +02:00
renovate[bot]
1176c2d329 Update dependency SharpFuzz to 2.3.0 2026-06-16 11:32:11 +00:00
theguymadmax
b9271eb199 Skip parsing root-level folders in SeriesResolver 2026-06-15 19:37:39 -04:00
Rohith
e2433e2c79 Translated using Weblate (Kannada)
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/kn/
2026-06-15 20:31:51 +00:00
Bond_009
0022508889 Add regression test 2026-06-15 21:20:06 +02:00
Bond_009
a9a02719ab Fix type of length arguments 2026-06-15 21:01:28 +02:00
Bond_009
d50205cc9f Follow native interoperability best practices
https://learn.microsoft.com/en-us/dotnet/standard/native-interop/best-practices
2026-06-15 21:01:28 +02:00
Bond-009
e4edce9a70 Merge pull request #17074 from jellyfin/renovate/sharpcompress-0.x
Some checks failed
Stale Issue Labeler / Check for stale issues (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
Format / format-check (push) Has been cancelled
Tests / run-tests (macos-latest) (push) Has been cancelled
Tests / run-tests (ubuntu-latest) (push) Has been cancelled
Tests / run-tests (windows-latest) (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Artifact (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Unstable Spec (push) Has been cancelled
OpenAPI Publish / OpenAPI - Publish Stable Spec (push) Has been cancelled
Project Automation / Project board (push) Has been cancelled
Merge Conflict Labeler / main (push) Has been cancelled
Stale PR Check / Check PRs with merge conflicts (push) Has been cancelled
Update dependency SharpCompress to 0.49.1
2026-06-15 20:56:39 +02:00
Bond_009
ac92da233b await instead of returning Task 2026-06-15 20:41:30 +02:00
Bond_009
f6bb086415 fix build errors 2026-06-15 18:52:20 +02:00
renovate[bot]
9375f31bd3 Update dependency SharpCompress to 0.49.1 2026-06-15 16:28:58 +00:00
Bond-009
a0862a4cb5 Merge pull request #17109 from jellyfin/renovate/serilog.settings.configuration-10.x
Update dependency Serilog.Settings.Configuration to 10.0.1
2026-06-15 18:23:06 +02:00
Bond-009
2d8ab1e2ec Merge pull request #17089 from Bond-009/sharpcompress
Replace usage of SharpCompress
2026-06-15 18:18:49 +02:00
theguymadmax
068bbb7981 Fix series year lost during parsing 2026-06-15 11:42:17 -04:00
Shadowghost
f9644f24d2 Fix tests 2026-06-15 11:42:48 +02:00
renovate[bot]
8d0003533e Update dependency Serilog.Settings.Configuration to 10.0.1 2026-06-15 08:45:54 +00:00
Shadowghost
1dd5a85080 Extend TranscodingReason reporting 2026-06-15 09:29:24 +02:00
dkanada
f4bab458a2 improve book resolution from filename 2026-06-15 11:31:49 +09:00
Bond_009
d8f8dbabcb Replace usage of SharpCompress
ComicImageProvider is the last user of SharpCompress after this PR
2026-06-13 22:19:30 +02:00
Shadowghost
a9dc8f6f74 Always apply recursive when filters are requested 2026-06-13 18:06:15 +02:00
Christopher Young
88602ce905 Refactored GroupTests. Removed duplicate mock object declarations 2025-11-08 12:35:37 -07:00
Christopher Young
4cb0385745 Merge branch 'master' of https://github.com/JadedRain/jellyfin 2025-11-08 07:31:02 -07:00
Christopher Young
438d992c8b Fixed group tests to use a file scoped namespace 2025-11-08 07:30:58 -07:00
Logan Douglas
1adf441f1c Merge branch 'jellyfin:master' into master 2025-11-05 13:06:57 -07:00
Logan Douglas
490bf347cb Merge branch 'jellyfin:master' into master 2025-10-31 13:06:17 -06:00
Logan Douglas
fd6e48603b Merge branch 'jellyfin:master' into master 2025-10-10 12:08:11 -06:00
pokreman06
b09c9655fd Update tests/Jellyfin.Server.Implementations.Tests/SyncPlay/GroupTests.cs
Co-authored-by: Bond-009 <bond.009@outlook.com>
2025-10-10 10:38:36 -06:00
pokreman06
790220ef6b Update tests/Jellyfin.Server.Implementations.Tests/SyncPlay/GroupTests.cs
Co-authored-by: Bond-009 <bond.009@outlook.com>
2025-10-10 10:38:29 -06:00
pokreman06
c08b1a4595 Update Emby.Server.Implementations/SyncPlay/Group.cs
Co-authored-by: Bond-009 <bond.009@outlook.com>
2025-10-10 10:35:46 -06:00
Christopher Young
622b60064d Merge branch 'master' of https://github.com/JadedRain/jellyfin 2025-10-08 12:27:51 -06:00
Christopher Young
91b2b7fc3d added tests 2025-10-08 12:27:46 -06:00
pokreman06
0b4854c5ef Merge branch 'jellyfin:master' into master 2025-10-02 11:07:05 -06:00
Christopher Young
d6a1c8413c fixed logic in HasAccessToQueue. If we receive a null response from IsVisibleStandalone then it should be false 2025-09-30 12:27:25 -06:00
35 changed files with 843 additions and 394 deletions

View File

@@ -61,14 +61,14 @@
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.1" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpCompress" Version="0.38.0" />
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
<PackageVersion Include="SharpCompress" Version="0.49.1" />
<PackageVersion Include="SharpFuzz" Version="2.3.0" />
<PackageVersion Include="SkiaSharp" Version="3.119.4" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.119.4" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />

View File

@@ -1,3 +1,4 @@
using System;
using System.Text.RegularExpressions;
namespace Emby.Naming.Book
@@ -5,7 +6,7 @@ namespace Emby.Naming.Book
/// <summary>
/// Helper class to retrieve basic metadata from a book filename.
/// </summary>
public static class BookFileNameParser
public static partial class BookFileNameParser
{
private const string NameMatchGroup = "name";
private const string IndexMatchGroup = "index";
@@ -15,14 +16,17 @@ namespace Emby.Naming.Book
private static readonly Regex[] _nameMatches =
[
// seriesName (seriesYear) #index (of count) (year) where only seriesName and index are required
new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>[0-9]{4})\))?)\s#(?<index>[0-9]+)((\s\(of\s(?<count>[0-9]+)\))?)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>[0-9]+)\)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"^(?<index>[0-9]+)\s\-\s(?<name>.+?)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>[0-9]{4})\))?)\s#(?<index>[0-9]+)(?:\.0)?((\s\(of\s(?<count>[0-9]+)\))?)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>[0-9]+)\)(?:\.0)?((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"^(?<index>[0-9]+)(?:\.0)?\s\-\s(?<name>.+?)((\s\((?<year>[0-9]{4})\))?)$"),
new Regex(@"(?<name>.*)\((?<year>[0-9]{4})\)"),
// last resort matches the whole string as the name
new Regex(@"(?<name>.*)")
];
[GeneratedRegex(@"^(?<name>.+?)(\sv(?<volume>[0-9]+))?(\sc(?<chapter>[0-9]+))?$")]
private static partial Regex ComicRegex();
/// <summary>
/// Parse a filename name to retrieve the book name, series name, index, and year.
/// </summary>
@@ -48,7 +52,22 @@ namespace Emby.Naming.Book
if (match.Groups.TryGetValue(NameMatchGroup, out Group? nameGroup) && nameGroup.Success)
{
result.Name = nameGroup.Value.Trim();
var comicMatch = ComicRegex().Match(nameGroup.Value.Trim());
if (comicMatch.Success)
{
if (comicMatch.Groups.TryGetValue("volume", out Group? volumeGroup) && volumeGroup.Success && int.TryParse(volumeGroup.ValueSpan, out var volume))
{
result.ParentIndex = volume;
}
if (comicMatch.Groups.TryGetValue("chapter", out Group? chapterGroup) && chapterGroup.Success && int.TryParse(chapterGroup.ValueSpan, out var chapter))
{
result.Index = chapter;
}
}
result.Name = nameGroup.ValueSpan.Trim().ToString();
}
if (match.Groups.TryGetValue(IndexMatchGroup, out Group? indexGroup) && indexGroup.Success && int.TryParse(indexGroup.Value, out var index))

View File

@@ -1,5 +1,3 @@
using System;
namespace Emby.Naming.Book
{
/// <summary>
@@ -14,6 +12,7 @@ namespace Emby.Naming.Book
{
Name = null;
Index = null;
ParentIndex = null;
Year = null;
SeriesName = null;
}
@@ -28,6 +27,11 @@ namespace Emby.Naming.Book
/// </summary>
public int? Index { get; set; }
/// <summary>
/// Gets or sets the parent index number.
/// </summary>
public int? ParentIndex { get; set; }
/// <summary>
/// Gets or sets the publication year.
/// </summary>

View File

@@ -25,5 +25,11 @@ namespace Emby.Naming.TV
/// </summary>
/// <value>The name of the series.</value>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the year of the series.
/// </summary>
/// <value>The year of the series.</value>
public int? Year { get; set; }
}
}

View File

@@ -21,7 +21,7 @@ namespace Emby.Naming.TV
/// 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}\)")]
[GeneratedRegex(@"(?<title>.+?)\s*\((?<year>[0-9]{4})\)")]
private static partial Regex TitleWithYearRegex();
/// <summary>
@@ -43,7 +43,8 @@ namespace Emby.Naming.TV
seriesName = titleWithYearMatch.Groups["title"].Value.Trim();
return new SeriesInfo(path)
{
Name = seriesName
Name = seriesName,
Year = int.TryParse(titleWithYearMatch.Groups["year"].ValueSpan, out var year) ? year : null
};
}
}

View File

@@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -18,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
protected override Book Resolve(ItemResolveArgs args)
protected override Book? Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
@@ -47,13 +45,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
Path = args.Path,
Name = result.Name ?? string.Empty,
IndexNumber = result.Index,
ParentIndexNumber = result.ParentIndex,
ProductionYear = result.Year,
SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)),
IsInMixedFolder = true,
};
}
private Book GetBook(ItemResolveArgs args)
private Book? GetBook(ItemResolveArgs args)
{
var bookFiles = args.FileSystemChildren.Where(f =>
{
@@ -78,6 +77,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
Path = bookFiles[0].FullName,
Name = result.Name ?? string.Empty,
IndexNumber = result.Index,
ParentIndexNumber = result.ParentIndex,
ProductionYear = result.Year,
SeriesName = result.SeriesName ?? string.Empty,
};

View File

@@ -57,6 +57,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
if (args.Parent is not null && args.Parent.IsRoot)
{
return null;
}
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
var collectionType = args.GetCollectionType();
@@ -69,7 +74,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return new Series
{
Path = args.Path,
Name = seriesInfo.Name
Name = seriesInfo.Name,
ProductionYear = seriesInfo.Year
};
}
}

View File

@@ -106,5 +106,7 @@
"TaskExtractMediaSegments": "Scan Segmen media",
"TaskMoveTrickplayImages": "Migrasikan Lokasi Gambar Trickplay",
"TaskDownloadMissingLyrics": "Unduh Lirik yang Hilang",
"CleanupUserDataTask": "Tugas Pembersihan Data Pengguna"
"CleanupUserDataTask": "Tugas Pembersihan Data Pengguna",
"LyricDownloadFailureFromForItem": "Lirik gagal di download dari {0} untuk {1}",
"Original": "Asli"
}

View File

@@ -80,7 +80,7 @@
"NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
"NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
"NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
"NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
"NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
"NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
"NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
"NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",

View File

@@ -107,5 +107,6 @@
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
"CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
"CleanupUserDataTask": "Limpeza de dados de utilizador",
"Original": "Original"
"Original": "Original",
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
}

View File

@@ -106,5 +106,7 @@
"TaskAudioNormalization": "Normalizacija zvoka",
"TaskAudioNormalizationDescription": "Pregled datotek za podatke o normalizaciji zvoka.",
"CleanupUserDataTask": "Čiščenje uporabniških podatkov",
"CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo."
"CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo.",
"LyricDownloadFailureFromForItem": "Besedila ni bilo mogoče prenesti iz {0} za {1}",
"Original": "Original"
}

View File

@@ -206,7 +206,8 @@ namespace Emby.Server.Implementations.SyncPlay
foreach (var itemId in queue)
{
var item = _libraryManager.GetItemById(itemId);
if (!item.IsVisibleStandalone(user))
if (item is null || !item.IsVisibleStandalone(user))
{
return false;
}

View File

@@ -321,24 +321,21 @@ public class ItemsController : BaseJellyfinApiController
recursive = true;
includeItemTypes = [BaseItemKind.Playlist];
}
else if (folder is ICollectionFolder)
else if (folder is ICollectionFolder && includeItemTypes.Length == 0)
{
if (includeItemTypes.Length == 0)
includeItemTypes = collectionType switch
{
includeItemTypes = collectionType switch
{
CollectionType.boxsets => [BaseItemKind.BoxSet],
null => [BaseItemKind.Movie, BaseItemKind.Series],
_ => []
};
}
CollectionType.boxsets => [BaseItemKind.BoxSet],
null => [BaseItemKind.Movie, BaseItemKind.Series],
_ => []
};
}
// When the client doesn't specify recursive/includeItemTypes, force the query
// through the database path where all filters (IsHD, genres, etc.) are applied.
if (includeItemTypes.Length > 0)
{
recursive ??= true;
}
// includeItemTypes on a library lists its contents recursively rather than just its
// immediate children, so default to a recursive query when the client didn't choose.
if (folder is ICollectionFolder && includeItemTypes.Length > 0)
{
recursive ??= true;
}
if (item is not UserRootFolder
@@ -351,246 +348,248 @@ public class ItemsController : BaseJellyfinApiController
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
}
if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder)
// Build the query up front so the dispatch below can decide the path from it.
// Use search providers when searchTerm is provided. Providers return only IDs and scores;
// items are loaded server-side via folder.GetItems below, which applies user-access filtering.
Dictionary<Guid, float>? searchResultScores = null;
Guid[] itemIds = ids;
if (!string.IsNullOrWhiteSpace(searchTerm))
{
// Use search providers when searchTerm is provided. Providers return only IDs and scores;
// items are loaded server-side via folder.GetItems below, which applies user-access filtering.
Dictionary<Guid, float>? searchResultScores = null;
Guid[] itemIds = ids;
if (!string.IsNullOrWhiteSpace(searchTerm))
var searchProviderQuery = new SearchProviderQuery
{
var searchProviderQuery = new SearchProviderQuery
{
SearchTerm = searchTerm,
UserId = userId,
IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = excludeItemTypes,
MediaTypes = mediaTypes,
Limit = limit.HasValue ? limit.Value * 3 : null,
ParentId = parentId
};
var searchResults = await _searchManager.GetSearchResultsAsync(searchProviderQuery, HttpContext.RequestAborted).ConfigureAwait(false);
if (searchResults.Count > 0)
{
searchResultScores = searchResults.ToDictionary(r => r.ItemId, r => r.Score);
itemIds = ids.Length > 0
? ids.Concat(searchResultScores.Keys).Distinct().ToArray()
: searchResultScores.Keys.ToArray();
}
}
var query = new InternalItemsQuery(user)
{
IsPlayed = isPlayed,
MediaTypes = mediaTypes,
SearchTerm = searchTerm,
UserId = userId,
IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = excludeItemTypes,
Recursive = recursive ?? false,
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
IsFavorite = isFavorite,
Limit = searchResultScores is null ? limit : null,
StartIndex = searchResultScores is null ? startIndex : null,
IsMissing = isMissing,
IsUnaired = isUnaired,
CollapseBoxSetItems = collapseBoxSetItems,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
HasImdbId = hasImdbId,
IsPlaceHolder = isPlaceHolder,
IsLocked = isLocked,
MinWidth = minWidth,
MinHeight = minHeight,
MaxWidth = maxWidth,
MaxHeight = maxHeight,
Is3D = is3D,
HasTvdbId = hasTvdbId,
HasTmdbId = hasTmdbId,
IsMovie = isMovie,
IsSeries = isSeries,
IsNews = isNews,
IsKids = isKids,
IsSports = isSports,
HasOverview = hasOverview,
HasOfficialRating = hasOfficialRating,
HasParentalRating = hasParentalRating,
HasSpecialFeature = hasSpecialFeature,
HasSubtitles = hasSubtitles,
HasThemeSong = hasThemeSong,
HasThemeVideo = hasThemeVideo,
HasTrailer = hasTrailer,
IsHD = isHd,
Is4K = is4K,
Tags = tags,
OfficialRatings = officialRatings,
Genres = genres,
ArtistIds = artistIds,
AlbumArtistIds = albumArtistIds,
ContributingArtistIds = contributingArtistIds,
GenreIds = genreIds,
StudioIds = studioIds,
Person = person,
PersonIds = personIds,
PersonTypes = personTypes,
Years = years,
ImageTypes = imageTypes,
VideoTypes = videoTypes,
AdjacentTo = adjacentTo,
ItemIds = itemIds,
MinCommunityRating = minCommunityRating,
MinCriticRating = minCriticRating,
ParentId = parentId ?? Guid.Empty,
IndexNumber = indexNumber,
ParentIndexNumber = parentIndexNumber,
EnableTotalRecordCount = enableTotalRecordCount,
ExcludeItemIds = excludeItemIds,
DtoOptions = dtoOptions,
SearchTerm = searchResultScores is null ? searchTerm : null,
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
MinPremiereDate = minPremiereDate?.ToUniversalTime(),
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
AudioLanguages = audioLanguages,
SubtitleLanguages = subtitleLanguages,
LinkedChildAncestorIds = linkedChildAncestorIds,
MediaTypes = mediaTypes,
Limit = limit.HasValue ? limit.Value * 3 : null,
ParentId = parentId
};
if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
var searchResults = await _searchManager.GetSearchResultsAsync(searchProviderQuery, HttpContext.RequestAborted).ConfigureAwait(false);
if (searchResults.Count > 0)
{
query.CollapseBoxSetItems = false;
searchResultScores = searchResults.ToDictionary(r => r.ItemId, r => r.Score);
itemIds = ids.Length > 0
? ids.Concat(searchResultScores.Keys).Distinct().ToArray()
: searchResultScores.Keys.ToArray();
}
}
if (query.SubtitleLanguages.Count > 0 && query.HasSubtitles.HasValue)
var query = new InternalItemsQuery(user)
{
IsPlayed = isPlayed,
MediaTypes = mediaTypes,
IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = excludeItemTypes,
Recursive = recursive ?? false,
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
IsFavorite = isFavorite,
Limit = searchResultScores is null ? limit : null,
StartIndex = searchResultScores is null ? startIndex : null,
IsMissing = isMissing,
IsUnaired = isUnaired,
CollapseBoxSetItems = collapseBoxSetItems,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
HasImdbId = hasImdbId,
IsPlaceHolder = isPlaceHolder,
IsLocked = isLocked,
MinWidth = minWidth,
MinHeight = minHeight,
MaxWidth = maxWidth,
MaxHeight = maxHeight,
Is3D = is3D,
HasTvdbId = hasTvdbId,
HasTmdbId = hasTmdbId,
IsMovie = isMovie,
IsSeries = isSeries,
IsNews = isNews,
IsKids = isKids,
IsSports = isSports,
HasOverview = hasOverview,
HasOfficialRating = hasOfficialRating,
HasParentalRating = hasParentalRating,
HasSpecialFeature = hasSpecialFeature,
HasSubtitles = hasSubtitles,
HasThemeSong = hasThemeSong,
HasThemeVideo = hasThemeVideo,
HasTrailer = hasTrailer,
IsHD = isHd,
Is4K = is4K,
Tags = tags,
OfficialRatings = officialRatings,
Genres = genres,
ArtistIds = artistIds,
AlbumArtistIds = albumArtistIds,
ContributingArtistIds = contributingArtistIds,
GenreIds = genreIds,
StudioIds = studioIds,
Person = person,
PersonIds = personIds,
PersonTypes = personTypes,
Years = years,
ImageTypes = imageTypes,
VideoTypes = videoTypes,
AdjacentTo = adjacentTo,
ItemIds = itemIds,
MinCommunityRating = minCommunityRating,
MinCriticRating = minCriticRating,
ParentId = parentId ?? Guid.Empty,
IndexNumber = indexNumber,
ParentIndexNumber = parentIndexNumber,
EnableTotalRecordCount = enableTotalRecordCount,
ExcludeItemIds = excludeItemIds,
DtoOptions = dtoOptions,
SearchTerm = searchResultScores is null ? searchTerm : null,
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
MinPremiereDate = minPremiereDate?.ToUniversalTime(),
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
AudioLanguages = audioLanguages,
SubtitleLanguages = subtitleLanguages,
LinkedChildAncestorIds = linkedChildAncestorIds,
};
if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
{
query.CollapseBoxSetItems = false;
}
if (query.SubtitleLanguages.Count > 0 && query.HasSubtitles.HasValue)
{
if (query.HasSubtitles.Value)
{
if (query.HasSubtitles.Value)
// if we check for specific subtitles we don't need a separate check for subtitle existence
query.HasSubtitles = null;
}
else
{
// if we search for items without subtitles, we don't need to check for subtitles of a specific language
query.SubtitleLanguages = [];
}
}
// for filter values that rely on media streams, we need to include alternative and linked versions
if (query.HasSubtitles.HasValue
|| query.SubtitleLanguages.Count > 0
|| query.AudioLanguages.Count > 0
|| query.Is3D.HasValue
|| query.IsHD.HasValue
|| query.Is4K.HasValue
|| query.VideoTypes.Length > 0
)
{
query.IncludeOwnedItems = true;
}
query.ApplyFilters(filters);
// Filter by Series Status
if (seriesStatus.Length != 0)
{
query.SeriesStatuses = seriesStatus;
}
// Exclude Blocked Unrated Items
var blockedUnratedItems = user?.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems);
if (blockedUnratedItems is not null)
{
query.BlockUnratedItems = blockedUnratedItems;
}
// ExcludeLocationTypes
if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
{
query.IsVirtualItem = false;
}
if (locationTypes.Length > 0 && locationTypes.Length < 4)
{
query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
}
// Min official rating
if (!string.IsNullOrWhiteSpace(minOfficialRating))
{
query.MinParentalRating = _localization.GetRatingScore(minOfficialRating);
}
// Max official rating
if (!string.IsNullOrWhiteSpace(maxOfficialRating))
{
query.MaxParentalRating = _localization.GetRatingScore(maxOfficialRating);
}
// Artists
if (artists.Length != 0)
{
query.ArtistIds = artists.Select(i =>
{
try
{
// if we check for specific subtitles we don't need a separate check for subtitle existence
query.HasSubtitles = null;
return _libraryManager.GetArtist(i, new DtoOptions(false));
}
else
catch
{
// if we search for items without subtitles, we don't need to check for subtitles of a specific language
query.SubtitleLanguages = [];
return null;
}
}
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
}
// for filter values that rely on media streams, we need to include alternative and linked versions
if (query.HasSubtitles.HasValue
|| query.SubtitleLanguages.Count > 0
|| query.AudioLanguages.Count > 0
|| query.Is3D.HasValue
|| query.IsHD.HasValue
|| query.Is4K.HasValue
|| query.VideoTypes.Length > 0
)
// ExcludeArtistIds
if (excludeArtistIds.Length != 0)
{
query.ExcludeArtistIds = excludeArtistIds;
}
if (albumIds.Length != 0)
{
query.AlbumIds = albumIds;
}
// Albums
if (albums.Length != 0)
{
query.AlbumIds = albums.SelectMany(i =>
{
query.IncludeOwnedItems = true;
}
return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Name = i, Limit = 1 });
}).ToArray();
}
query.ApplyFilters(filters);
// Filter by Series Status
if (seriesStatus.Length != 0)
// Studios
if (studios.Length != 0)
{
query.StudioIds = studios.Select(i =>
{
query.SeriesStatuses = seriesStatus;
}
// Exclude Blocked Unrated Items
var blockedUnratedItems = user?.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems);
if (blockedUnratedItems is not null)
{
query.BlockUnratedItems = blockedUnratedItems;
}
// ExcludeLocationTypes
if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
{
query.IsVirtualItem = false;
}
if (locationTypes.Length > 0 && locationTypes.Length < 4)
{
query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
}
// Min official rating
if (!string.IsNullOrWhiteSpace(minOfficialRating))
{
query.MinParentalRating = _localization.GetRatingScore(minOfficialRating);
}
// Max official rating
if (!string.IsNullOrWhiteSpace(maxOfficialRating))
{
query.MaxParentalRating = _localization.GetRatingScore(maxOfficialRating);
}
// Artists
if (artists.Length != 0)
{
query.ArtistIds = artists.Select(i =>
try
{
try
{
return _libraryManager.GetArtist(i, new DtoOptions(false));
}
catch
{
return null;
}
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
}
// ExcludeArtistIds
if (excludeArtistIds.Length != 0)
{
query.ExcludeArtistIds = excludeArtistIds;
}
if (albumIds.Length != 0)
{
query.AlbumIds = albumIds;
}
// Albums
if (albums.Length != 0)
{
query.AlbumIds = albums.SelectMany(i =>
{
return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Name = i, Limit = 1 });
}).ToArray();
}
// Studios
if (studios.Length != 0)
{
query.StudioIds = studios.Select(i =>
{
try
{
return _libraryManager.GetStudio(i);
}
catch
{
return null;
}
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
}
// Apply default sorting if none requested
if (query.OrderBy.Count == 0)
{
// Albums by artist
if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum)
{
query.OrderBy = [(ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Ascending)];
return _libraryManager.GetStudio(i);
}
catch
{
return null;
}
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
}
// Apply default sorting if none requested
if (query.OrderBy.Count == 0)
{
// Albums by artist
if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum)
{
query.OrderBy = [(ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Ascending)];
}
}
query.Parent = null;
query.Parent = null;
// At the user root an unfiltered, non-recursive request is a plain listing of the user's libraries
if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder || query.HasFilters)
{
// folder.GetItems applies user-access filtering via the InternalItemsQuery's User.
result = folder.GetItems(query);
if (searchResultScores is not null && searchResultScores.Count > 0)

View File

@@ -122,6 +122,7 @@ public class TrailersController : BaseJellyfinApiController
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Use GetItems with includeItemTypes=Trailer instead.")]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetTrailers(
[FromQuery] Guid? userId,
[FromQuery] string? maxOfficialRating,

View File

@@ -72,6 +72,102 @@ namespace MediaBrowser.Controller.Entities
}
}
/// <summary>
/// Gets a value indicating whether the query carries any criteria that narrows the
/// result set, as opposed to user context, pagination, sorting or DTO options.
/// </summary>
public bool HasFilters =>
IncludeItemTypes.Length > 0
|| ExcludeItemTypes.Length > 0
|| Genres.Count > 0
|| GenreIds.Count > 0
|| Years.Length > 0
|| Tags.Length > 0
|| ExcludeTags.Length > 0
|| OfficialRatings.Length > 0
|| StudioIds.Length > 0
|| ArtistIds.Length > 0
|| AlbumArtistIds.Length > 0
|| ContributingArtistIds.Length > 0
|| ExcludeArtistIds.Length > 0
|| AlbumIds.Length > 0
|| PersonIds.Length > 0
|| PersonTypes.Length > 0
|| MediaTypes.Length > 0
|| VideoTypes.Length > 0
|| ImageTypes.Length > 0
|| SeriesStatuses.Length > 0
|| ItemIds.Length > 0
|| ExcludeItemIds.Length > 0
|| AudioLanguages.Count > 0
|| SubtitleLanguages.Count > 0
|| LinkedChildAncestorIds.Length > 0
|| AncestorIds.Length > 0
|| IsFavorite.HasValue
|| IsFavoriteOrLiked.HasValue
|| IsLiked.HasValue
|| IsPlayed.HasValue
|| IsResumable.HasValue
|| IsFolder.HasValue
|| IsMissing.HasValue
|| IsUnaired.HasValue
|| IsSpecialSeason.HasValue
|| Is3D.HasValue
|| IsHD.HasValue
|| Is4K.HasValue
|| IsLocked.HasValue
|| IsPlaceHolder.HasValue
|| IsMovie.HasValue
|| IsSports.HasValue
|| IsKids.HasValue
|| IsNews.HasValue
|| IsSeries.HasValue
|| IsAiring.HasValue
|| IsVirtualItem.HasValue
|| HasImdbId.HasValue
|| HasTmdbId.HasValue
|| HasTvdbId.HasValue
|| HasOverview.HasValue
|| HasOfficialRating.HasValue
|| HasParentalRating.HasValue
|| HasThemeSong.HasValue
|| HasThemeVideo.HasValue
|| HasSubtitles.HasValue
|| HasSpecialFeature.HasValue
|| HasTrailer.HasValue
|| HasChapterImages.HasValue
|| MinCriticRating.HasValue
|| MinCommunityRating.HasValue
|| MinParentalRating is not null
|| MinIndexNumber.HasValue
|| MinParentAndIndexNumber.HasValue
|| IndexNumber.HasValue
|| ParentIndexNumber.HasValue
|| AiredDuringSeason.HasValue
|| MinWidth.HasValue
|| MinHeight.HasValue
|| MaxWidth.HasValue
|| MaxHeight.HasValue
|| MinPremiereDate.HasValue
|| MaxPremiereDate.HasValue
|| MinStartDate.HasValue
|| MaxStartDate.HasValue
|| MinEndDate.HasValue
|| MaxEndDate.HasValue
|| MinDateCreated.HasValue
|| MinDateLastSaved.HasValue
|| MinDateLastSavedForUser.HasValue
|| AdjacentTo.HasValue
|| !string.IsNullOrEmpty(NameStartsWith)
|| !string.IsNullOrEmpty(NameStartsWithOrGreater)
|| !string.IsNullOrEmpty(NameLessThan)
|| !string.IsNullOrEmpty(NameContains)
|| !string.IsNullOrEmpty(MinSortName)
|| !string.IsNullOrEmpty(Name)
|| !string.IsNullOrEmpty(Person)
|| !string.IsNullOrEmpty(SearchTerm)
|| !string.IsNullOrEmpty(Path);
public bool Recursive { get; set; }
public int? StartIndex { get; set; }

View File

@@ -69,8 +69,14 @@ namespace MediaBrowser.Controller.Entities
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
if (query.Recursive)
// The user root holds no items of its own - a plain listing returns the user's
// views. But a request carrying any filter is a search across the libraries, so
// resolve it through the recursive query path even when Recursive wasn't set;
// otherwise the filters would be silently dropped. Recursive is set so the
// downstream query (ancestor/top-parent scoping) treats it as a recursive search.
if (query.Recursive || query.HasFilters)
{
query.Recursive = true;
return QueryRecursive(query);
}

View File

@@ -25,6 +25,7 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Configuration;
using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
@@ -2611,56 +2612,66 @@ namespace MediaBrowser.Controller.MediaEncoding
}
public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, IEnumerable<string> supportedAudioCodecs)
=> CanStreamCopyAudio(state, audioStream, supportedAudioCodecs, out _);
/// <summary>
/// Determines whether the given audio stream can be stream-copied and, regardless of the outcome,
/// reports the codec/parameter incompatibilities that would force a re-encode via <paramref name="failureReasons"/>.
/// </summary>
/// <param name="state">The encoding job state.</param>
/// <param name="audioStream">The source audio stream.</param>
/// <param name="supportedAudioCodecs">The audio codecs the target supports.</param>
/// <param name="failureReasons">The codec/parameter incompatibilities preventing a copy, or <c>0</c> if the stream is copy-compatible.</param>
/// <returns><c>true</c> if the audio stream can be stream-copied; otherwise, <c>false</c>.</returns>
public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, IEnumerable<string> supportedAudioCodecs, out TranscodeReason failureReasons)
{
var request = state.BaseRequest;
if (!request.AllowAudioStreamCopy)
{
return false;
}
// Policy-independent compatibility check, so the reasons are reported even when a policy gate is what ultimately prevents the copy.
failureReasons = GetAudioStreamCopyFailureReasons(state, audioStream, supportedAudioCodecs);
return request.AllowAudioStreamCopy
&& request.EnableAutoStreamCopy
&& failureReasons == 0;
}
private static TranscodeReason GetAudioStreamCopyFailureReasons(EncodingJobInfo state, MediaStream audioStream, IEnumerable<string> supportedAudioCodecs)
{
var request = state.BaseRequest;
TranscodeReason reasons = 0;
var maxBitDepth = state.GetRequestedAudioBitDepth(audioStream.Codec);
if (maxBitDepth.HasValue
&& audioStream.BitDepth.HasValue
&& audioStream.BitDepth.Value > maxBitDepth.Value)
{
return false;
reasons |= TranscodeReason.AudioBitDepthNotSupported;
}
// Source and target codecs must match
if (string.IsNullOrEmpty(audioStream.Codec)
|| !supportedAudioCodecs.Contains(audioStream.Codec, StringComparison.OrdinalIgnoreCase))
{
return false;
reasons |= TranscodeReason.AudioCodecNotSupported;
}
// Channels must fall within requested value
var channels = state.GetRequestedAudioChannels(audioStream.Codec);
if (channels.HasValue)
if (channels.HasValue
&& (!audioStream.Channels.HasValue
|| audioStream.Channels.Value <= 0
|| audioStream.Channels.Value > channels.Value))
{
if (!audioStream.Channels.HasValue || audioStream.Channels.Value <= 0)
{
return false;
}
if (audioStream.Channels.Value > channels.Value)
{
return false;
}
reasons |= TranscodeReason.AudioChannelsNotSupported;
}
// Sample rate must fall within requested value
if (request.AudioSampleRate.HasValue)
if (request.AudioSampleRate.HasValue
&& (!audioStream.SampleRate.HasValue
|| audioStream.SampleRate.Value <= 0
|| audioStream.SampleRate.Value > request.AudioSampleRate.Value))
{
if (!audioStream.SampleRate.HasValue || audioStream.SampleRate.Value <= 0)
{
return false;
}
if (audioStream.SampleRate.Value > request.AudioSampleRate.Value)
{
return false;
}
reasons |= TranscodeReason.AudioSampleRateNotSupported;
}
// Audio bitrate must fall within requested value
@@ -2668,10 +2679,10 @@ namespace MediaBrowser.Controller.MediaEncoding
&& audioStream.BitRate.HasValue
&& audioStream.BitRate.Value > request.AudioBitRate.Value)
{
return false;
reasons |= TranscodeReason.AudioBitrateNotSupported;
}
return request.EnableAutoStreamCopy;
return reasons;
}
public int GetVideoBitrateParamValue(BaseEncodingJobOptions request, MediaStream videoStream, string outputVideoCodec)
@@ -7217,8 +7228,9 @@ namespace MediaBrowser.Controller.MediaEncoding
&& !IsCopyCodec(state.OutputVideoCodec)
&& options.HlsAudioSeekStrategy is HlsAudioSeekStrategy.TranscodeAudio;
TranscodeReason audioCopyFailureReasons = 0;
if (state.AudioStream is not null
&& CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs)
&& CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs, out audioCopyFailureReasons)
&& !preventHlsAudioCopy)
{
state.OutputAudioCodec = "copy";
@@ -7232,6 +7244,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{
state.OutputAudioCodec = "copy";
}
else if (state.AudioStream is not null && !IsCopyCodec(state.OutputAudioCodec))
{
// Audio is actually being re-encoded although the playback determination may have considered the source copyable.
// Only carry the primary "cannot be passed through" cause - the codec mismatch.
// Bitrate/channels/sample-rate/bit-depth copy refusals are consequences of the chosen transcode target.
state.AddTranscodeReason(audioCopyFailureReasons & TranscodeReason.AudioCodecNotSupported);
}
}
}
@@ -7851,13 +7870,14 @@ namespace MediaBrowser.Controller.MediaEncoding
audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate);
}
if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
var sampleRate = state.OutputAudioSampleRate;
if (sampleRate.HasValue)
{
// opus only supports specific sampling rates
var sampleRate = state.OutputAudioSampleRate;
if (sampleRate.HasValue)
var sampleRateValue = sampleRate.Value;
if (string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
{
var sampleRateValue = sampleRate.Value switch
// opus only supports specific sampling rates
sampleRateValue = sampleRate.Value switch
{
<= 8000 => 8000,
<= 12000 => 12000,
@@ -7865,9 +7885,9 @@ namespace MediaBrowser.Controller.MediaEncoding
<= 24000 => 24000,
_ => 48000
};
audioTranscodeParams.Add("-ar " + sampleRateValue.ToString(CultureInfo.InvariantCulture));
}
audioTranscodeParams.Add("-ar " + sampleRateValue.ToString(CultureInfo.InvariantCulture));
}
// Copy the movflags from GetProgressiveVideoFullCommandLine

View File

@@ -515,6 +515,15 @@ namespace MediaBrowser.Controller.MediaEncoding
public int HlsListSize => 0;
/// <summary>
/// Adds the specified reason(s) to <see cref="TranscodeReasons"/>.
/// </summary>
/// <param name="reason">The transcode reason(s) to add.</param>
public void AddTranscodeReason(TranscodeReason reason)
{
_transcodeReasons = TranscodeReasons | reason;
}
private int? GetMediaStreamCount(MediaStreamType type, int limit)
{
var count = MediaSource.GetStreamCount(type);

View File

@@ -1,9 +1,11 @@
#pragma warning disable CA1031
using System;
using System.Buffers;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Encoder;
@@ -12,43 +14,43 @@ namespace MediaBrowser.MediaEncoding.Encoder;
/// Helper class for Apple platform specific operations.
/// </summary>
[SupportedOSPlatform("macos")]
public static class ApplePlatformHelper
public static partial class ApplePlatformHelper
{
private static readonly string[] _av1DecodeBlacklistedCpuClass = ["M1", "M2"];
private static string GetSysctlValue(ReadOnlySpan<byte> name)
internal static string GetSysctlValue(string name)
{
IntPtr length = IntPtr.Zero;
nuint length = 0;
// Get length of the value
int osStatus = SysctlByName(name, IntPtr.Zero, ref length, IntPtr.Zero, 0);
if (osStatus != 0)
int osStatus = sysctlbyname(name, Span<byte>.Empty, ref length, IntPtr.Zero, 0);
if (osStatus != 0 || length == 0)
{
throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}");
throw new NotSupportedException($"Failed to get sysctl value for {name} with error {osStatus}");
}
IntPtr buffer = Marshal.AllocHGlobal(length.ToInt32());
byte[] buffer = ArrayPool<byte>.Shared.Rent((int)length);
try
{
osStatus = SysctlByName(name, buffer, ref length, IntPtr.Zero, 0);
osStatus = sysctlbyname(name, buffer.AsSpan()[..(int)length], ref length, IntPtr.Zero, 0);
if (osStatus != 0)
{
throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}");
throw new NotSupportedException($"Failed to get sysctl value for {name} with error {osStatus}");
}
return Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
if (length < 1)
{
return string.Empty;
}
ReadOnlySpan<byte> data = buffer.AsSpan()[..(int)(length - 1)];
return Encoding.UTF8.GetString(data);
}
finally
{
Marshal.FreeHGlobal(buffer);
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static int SysctlByName(ReadOnlySpan<byte> name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen)
{
return NativeMethods.SysctlByName(name.ToArray(), oldp, ref oldlenp, newp, newlen);
}
/// <summary>
/// Check if the current system has hardware acceleration for AV1 decoding.
/// </summary>
@@ -63,7 +65,7 @@ public static class ApplePlatformHelper
try
{
string cpuBrandString = GetSysctlValue("machdep.cpu.brand_string"u8);
string cpuBrandString = GetSysctlValue("machdep.cpu.brand_string");
return !_av1DecodeBlacklistedCpuClass.Any(blacklistedCpuClass => cpuBrandString.Contains(blacklistedCpuClass, StringComparison.OrdinalIgnoreCase));
}
catch (NotSupportedException e)
@@ -78,10 +80,7 @@ public static class ApplePlatformHelper
return false;
}
private static class NativeMethods
{
[DllImport("libc", EntryPoint = "sysctlbyname", SetLastError = true)]
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
internal static extern int SysctlByName(byte[] name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen);
}
[LibraryImport("libc", EntryPoint = "sysctlbyname", SetLastError = true)]
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
internal static partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, Span<byte> oldp, ref nuint oldlenp, IntPtr newp, nuint newlen);
}

View File

@@ -9,6 +9,7 @@
<TargetFramework>net10.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

View File

@@ -951,6 +951,10 @@ namespace MediaBrowser.Model.Dlna
}
playlistItem.VideoCodecs = videoCodecs;
if (videoStream is not null && !ContainerHelper.ContainsContainer(videoCodecs, false, videoStream.Codec))
{
playlistItem.TranscodeReasons |= TranscodeReason.VideoCodecNotSupported;
}
// Copy video codec options as a starting point, this applies to transcode and direct-stream
playlistItem.MaxFramerate = videoStream?.ReferenceFrameRate;
@@ -999,6 +1003,10 @@ namespace MediaBrowser.Model.Dlna
var directAudioFailures = audioStreamWithSupportedCodec is null ? default : GetCompatibilityAudioCodec(options, item, container ?? string.Empty, audioStreamWithSupportedCodec, null, true, false);
playlistItem.TranscodeReasons |= directAudioFailures;
if (audioStream is not null && audioStreamWithSupportedCodec is null)
{
playlistItem.TranscodeReasons |= TranscodeReason.AudioCodecNotSupported;
}
var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
&& directAudioFailures == 0;

View File

@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json;
using System.Threading;
@@ -12,7 +13,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Books.ComicBookInfo.Models;
using Microsoft.Extensions.Logging;
using SharpCompress.Archives.Zip;
namespace MediaBrowser.Providers.Books.ComicBookInfo;
@@ -48,35 +48,27 @@ public class ComicBookInfoProvider : IComicProvider
try
{
Stream stream = File.OpenRead(path);
// not yet async: https://github.com/adamhathcock/sharpcompress/pull/565
Stream stream = AsyncFile.OpenRead(path);
await using (stream.ConfigureAwait(false))
using (var archive = ZipArchive.Open(stream))
{
if (!archive.IsComplete)
var archive = await ZipArchive.CreateAsync(stream, ZipArchiveMode.Read, false, null, cancellationToken).ConfigureAwait(false);
await using (archive.ConfigureAwait(false))
{
_logger.LogError("incomplete comic archive: {Path}", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
if (archive.Comment is null)
{
_logger.LogInformation("missing ComicBookInfo in archive comment: {Path}", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
}
var comicBookMetadata = JsonSerializer.Deserialize<ComicBookInfoFormat>(archive.Comment, JsonDefaults.Options);
if (comicBookMetadata is null)
{
_logger.LogError("ComicBookInfo deserialization failure: {Path}", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
}
return SaveMetadata(comicBookMetadata);
}
var volume = archive.Volumes.First();
if (volume.Comment is null)
{
_logger.LogInformation("missing ComicBookInfo in archive comment: {Path}", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
}
var comicBookMetadata = JsonSerializer.Deserialize<ComicBookInfoFormat>(volume.Comment, JsonDefaults.Options);
if (comicBookMetadata is null)
{
_logger.LogError("ComicBookInfo deserialization failure: {Path}", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
}
return SaveMetadata(comicBookMetadata);
}
}
catch (Exception ex)

View File

@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using SharpCompress.Archives;
@@ -38,16 +39,16 @@ public class ComicImageProvider : IDynamicImageProvider
public string Name => "Comic Book Archive Cover Extractor";
/// <inheritdoc />
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
public async Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
var extension = Path.GetExtension(item.Path);
if (_comicBookExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
return LoadCover(item);
return await LoadCoverAsync(item, cancellationToken).ConfigureAwait(false);
}
return Task.FromResult(new DynamicImageResponse { HasImage = false });
return new DynamicImageResponse { HasImage = false };
}
/// <inheritdoc />
@@ -67,7 +68,8 @@ public class ComicImageProvider : IDynamicImageProvider
/// with no image if nothing is found.
/// </summary>
/// <param name="item">Item to check for covers.</param>
private async Task<DynamicImageResponse> LoadCover(BaseItem item)
/// <param name="cancellationToken">The cancellation token.</param>
private async Task<DynamicImageResponse> LoadCoverAsync(BaseItem item, CancellationToken cancellationToken)
{
var memoryStream = new MemoryStream();
@@ -75,14 +77,22 @@ public class ComicImageProvider : IDynamicImageProvider
{
ImageFormat imageFormat;
using (Stream stream = File.OpenRead(item.Path))
using (var archive = ArchiveFactory.Open(stream))
using (Stream stream = AsyncFile.OpenRead(item.Path))
{
// throw exception to log results if no cover is found
(var cover, imageFormat) = FindCoverEntryInArchive(archive) ?? throw new InvalidOperationException("no supported cover found");
var archive = await ArchiveFactory.OpenAsyncArchive(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
await using (archive.ConfigureAwait(false))
{
// throw exception to log results if no cover is found
(var cover, imageFormat) = await FindCoverEntryInArchiveAsync(archive).ConfigureAwait(false)
?? throw new InvalidOperationException("no supported cover found");
// copy the cover to memory stream
await cover.OpenEntryStream().CopyToAsync(memoryStream).ConfigureAwait(false);
// copy the cover to memory stream
var coverStream = await cover.OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
await using (coverStream.ConfigureAwait(false))
{
await coverStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
}
}
}
// reset stream position after copying
@@ -102,7 +112,7 @@ public class ComicImageProvider : IDynamicImageProvider
/// </summary>
/// <param name="archive">The archive to search.</param>
/// <returns>The search result.</returns>
private (IArchiveEntry CoverEntry, ImageFormat ImageFormat)? FindCoverEntryInArchive(IArchive archive)
private async ValueTask<(IArchiveEntry CoverEntry, ImageFormat ImageFormat)?> FindCoverEntryInArchiveAsync(IAsyncArchive archive)
{
IArchiveEntry? cover;
@@ -110,7 +120,7 @@ public class ComicImageProvider : IDynamicImageProvider
// in many cases the cover will simply be the first image in the archive
foreach (var extension in _coverExtensions)
{
cover = archive.Entries.FirstOrDefault(e => e.Key == "cover" + extension);
cover = await archive.EntriesAsync.FirstOrDefaultAsync(e => e.Key == "cover" + extension).ConfigureAwait(false);
if (cover is not null)
{
@@ -120,7 +130,9 @@ public class ComicImageProvider : IDynamicImageProvider
}
}
cover = archive.Entries.OrderBy(x => x.Key).FirstOrDefault(x => _coverExtensions.Contains(Path.GetExtension(x.Key), StringComparison.OrdinalIgnoreCase));
cover = await archive.EntriesAsync.OrderBy(x => x.Key)
.FirstOrDefaultAsync(x => _coverExtensions.Contains(Path.GetExtension(x.Key), StringComparison.OrdinalIgnoreCase))
.ConfigureAwait(false);
if (cover is not null)
{

View File

@@ -7,7 +7,6 @@ using System.Xml.XPath;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using SharpCompress;
namespace MediaBrowser.Providers.Books.ComicInfo;
@@ -41,7 +40,13 @@ public static class ComicInfoReader
hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Summary", summary => book.Overview = summary);
hasFoundMetadata |= ReadIntInto(xml, "ComicInfo/Year", year => book.ProductionYear = year);
hasFoundMetadata |= ReadThreePartDateInto(xml, "ComicInfo/Year", "ComicInfo/Month", "ComicInfo/Day", dateTime => book.PremiereDate = dateTime);
hasFoundMetadata |= ReadCommaSeparatedStringsInto(xml, "ComicInfo/Genre", genres => genres.ForEach(genre => book.AddGenre(genre)));
hasFoundMetadata |= ReadCommaSeparatedStringsInto(xml, "ComicInfo/Genre", genres =>
{
foreach (var genre in genres)
{
book.AddGenre(genre);
}
});
hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/Publisher", publisher => book.SetStudios([publisher]));
hasFoundMetadata |= ReadStringInto(xml, "ComicInfo/AlternateSeries", title =>
@@ -71,32 +76,50 @@ public static class ComicInfoReader
{
ReadCommaSeparatedStringsInto(xml, "ComicInfo/Writer", authors =>
{
authors.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Author }));
foreach (var p in authors)
{
metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Author });
}
});
ReadCommaSeparatedStringsInto(xml, "ComicInfo/Penciller", pencillers =>
{
pencillers.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Penciller }));
foreach (var p in pencillers)
{
metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Penciller });
}
});
ReadCommaSeparatedStringsInto(xml, "ComicInfo/Inker", inkers =>
{
inkers.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Inker }));
foreach (var p in inkers)
{
metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Inker });
}
});
ReadCommaSeparatedStringsInto(xml, "ComicInfo/Letterer", letterers =>
{
letterers.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Letterer }));
foreach (var p in letterers)
{
metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Letterer });
}
});
ReadCommaSeparatedStringsInto(xml, "ComicInfo/CoverArtist", artists =>
{
artists.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.CoverArtist }));
foreach (var p in artists)
{
metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.CoverArtist });
}
});
ReadCommaSeparatedStringsInto(xml, "ComicInfo/Colourist", colorists =>
{
colorists.ForEach(p => metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Colorist }));
foreach (var p in colorists)
{
metadataResult.AddPerson(new PersonInfo { Name = p, Type = PersonKind.Colorist });
}
});
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

View File

@@ -831,8 +831,16 @@ namespace MediaBrowser.Providers.Manager
var isLocalLocked = temp.Item.IsLocked;
if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly))
{
var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
.ConfigureAwait(false);
var remoteProviders = providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>();
// When identifying, run the provider the user picked first so the correct IDs are used.
if (!string.IsNullOrEmpty(options.SearchResult?.SearchProviderName))
{
remoteProviders = remoteProviders
.OrderBy(i => string.Equals(i.Name, options.SearchResult.SearchProviderName, StringComparison.OrdinalIgnoreCase) ? 0 : 1);
}
var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, remoteProviders, cancellationToken).ConfigureAwait(false);
refreshResult.UpdateType |= remoteResult.UpdateType;
refreshResult.ErrorMessage = remoteResult.ErrorMessage;

View File

@@ -181,7 +181,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
ParentIndexNumber = seasonNumber,
IndexNumberEnd = info.IndexNumberEnd,
Name = episodeResult.Name,
PremiereDate = episodeResult.AirDate,
PremiereDate = episodeResult.AirDate.HasValue
? DateTime.SpecifyKind(episodeResult.AirDate.Value, DateTimeKind.Local).ToUniversalTime()
: null,
ProductionYear = episodeResult.AirDate?.Year,
Overview = episodeResult.Overview,
CommunityRating = Convert.ToSingle(episodeResult.VoteAverage)

View File

@@ -417,6 +417,31 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
yield return personInfo;
}
}
if (seriesResult.CreatedBy is not null)
{
foreach (var person in seriesResult.CreatedBy)
{
if (string.IsNullOrWhiteSpace(person.Name))
{
continue;
}
var personInfo = new PersonInfo
{
Name = person.Name.Trim(),
Type = PersonKind.Creator,
ImageUrl = _tmdbClientManager.GetProfileUrl(person.ProfilePath)
};
if (person.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
}
yield return personInfo;
}
}
}
/// <inheritdoc />

View File

@@ -11,6 +11,7 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using Moq;
using Xunit;
@@ -203,6 +204,50 @@ public class EncodingHelperTests
}
}
[Theory]
[InlineData("aac", 44100, 44100)] // non-opus: requested rate must be preserved (issue #17026)
[InlineData("aac", 48000, 48000)]
[InlineData("mp3", 22050, 22050)]
[InlineData("flac", 96000, 96000)]
[InlineData("opus", 44100, 48000)] // opus: must snap to a libopus-supported rate
[InlineData("opus", 22050, 24000)]
[InlineData("opus", 8000, 8000)]
public void GetProgressiveAudioFullCommandLine_SampleRate_OnlyClampedForOpus(
string audioCodec,
int requestedSampleRate,
int expectedSampleRate)
{
var state = BuildAudioState(audioCodec, requestedSampleRate);
var args = CreateHelper().GetProgressiveAudioFullCommandLine(state, new EncodingOptions(), "/tmp/out");
Assert.Contains("-ar " + expectedSampleRate, args, StringComparison.Ordinal);
}
private static EncodingJobInfo BuildAudioState(string audioCodec, int requestedSampleRate)
{
var audio = new MediaStream { Index = 0, Type = MediaStreamType.Audio, Codec = "flac", SampleRate = 96000 };
return new EncodingJobInfo(TranscodingJobType.Progressive)
{
MediaSource = new MediaSourceInfo
{
Container = "flac",
MediaStreams = new List<MediaStream> { audio },
Path = "/media/track.flac",
Protocol = MediaProtocol.File,
},
AudioStream = audio,
OutputAudioCodec = audioCodec,
BaseRequest = new VideoRequestDto
{
AudioCodec = audioCodec,
AudioSampleRate = requestedSampleRate,
},
IsVideoRequest = false,
IsInputVideo = false,
};
}
private static EncodingJobInfo BuildState(
MediaStream? subtitle,
SubtitleDeliveryMethod? deliveryMethod,

View File

@@ -0,0 +1,22 @@
using System;
using System.Runtime.Versioning;
using MediaBrowser.MediaEncoding.Encoder;
using Xunit;
namespace Jellyfin.MediaEncoding.Tests;
[SupportedOSPlatform("macos")]
public class ApplePlatformHelperTests
{
[Fact]
public void GetSysctlValue_CpuBrand_NotEmpty()
{
Assert.SkipUnless(OperatingSystem.IsMacOS(), "macOS-only test");
var value = ApplePlatformHelper.GetSysctlValue("machdep.cpu.brand_string");
Assert.NotEmpty(value);
// Make sure we don't include the null terminator
Assert.DoesNotContain("\0", value, StringComparison.Ordinal);
}
}

View File

@@ -81,25 +81,25 @@ namespace Jellyfin.Model.Tests
[InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
// Yatse
[InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
[InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)]
[InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("Yatse", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("Yatse", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
// RokuSSPlus
[InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 should be DirectPlay
[InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("RokuSSPlus", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
// JellyfinMediaPlayer
[InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
@@ -118,21 +118,21 @@ namespace Jellyfin.Model.Tests
[InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")]
[InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "http")]
[InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")]
[InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported, "DirectStream", "http")] // webm requested, aac not supported
[InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // webm requested, aac not supported
[InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.AudioCodecNotSupported, "DirectStream", "http")] // #6450
[InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux", "http")] // #6450
// TranscodeMedia
[InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")]
[InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")]
[InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported | TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")]
[InlineData("TranscodeMedia", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")]
[InlineData("TranscodeMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")]
[InlineData("TranscodeMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported | TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")]
[InlineData("TranscodeMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")]
[InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")]
[InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported | TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")]
[InlineData("TranscodeMedia", "mp4-hevc-ac3-aacDef-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")]
[InlineData("TranscodeMedia", "mkv-av1-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")]
[InlineData("TranscodeMedia", "mkv-av1-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported | TranscodeReason.DirectPlayError, "DirectStream", "http")]
[InlineData("TranscodeMedia", "mkv-av1-vorbis-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "http")]
[InlineData("TranscodeMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")]
[InlineData("TranscodeMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")]
[InlineData("TranscodeMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported | TranscodeReason.DirectPlayError, "DirectStream", "http")]
[InlineData("TranscodeMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported | TranscodeReason.DirectPlayError, "DirectStream", "http")]
[InlineData("TranscodeMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "http")]
// DirectMedia
[InlineData("DirectMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
@@ -150,9 +150,9 @@ namespace Jellyfin.Model.Tests
[InlineData("LowBandwidth", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("LowBandwidth", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("LowBandwidth", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("LowBandwidth", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("LowBandwidth", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("LowBandwidth", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("LowBandwidth", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("LowBandwidth", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("LowBandwidth", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported | TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
// Null
[InlineData("Null", "mp4-h264-aac-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
[InlineData("Null", "mp4-h264-ac3-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
@@ -170,10 +170,10 @@ namespace Jellyfin.Model.Tests
[InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9
[InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9
[InlineData("AndroidTVExoPlayer", "mp4-hevc-aac-4000k-r180", PlayMethod.DirectPlay)] // #13712
// AndroidTV NoHevcRotation
[InlineData("AndroidTVExoPlayer-NoHevcRotation", "mp4-hevc-aac-4000k-r180", PlayMethod.Transcode, TranscodeReason.VideoRotationNotSupported, "Transcode")] // #13712
[InlineData("AndroidTVExoPlayer-NoHevcRotation", "mp4-hevc-aac-4000k-r180", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.VideoRotationNotSupported, "Transcode")] // #13712
// Tizen 3 Stereo
[InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
@@ -255,23 +255,23 @@ namespace Jellyfin.Model.Tests
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
[InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")]
// Yatse
[InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)]
[InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
// RokuSSPlus
[InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay
[InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported)] // #6450
[InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
// JellyfinMediaPlayer
[InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
@@ -289,7 +289,7 @@ namespace Jellyfin.Model.Tests
[InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9
[InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9
// Tizen 3 Stereo
[InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
@@ -336,7 +336,7 @@ namespace Jellyfin.Model.Tests
// Yatse
[InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450
[InlineData("Yatse", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Remux")]
[InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
[InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow hevc
// RokuSSPlus
[InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
[InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450

View File

@@ -0,0 +1,58 @@
using Emby.Naming.Book;
using Xunit;
namespace Jellyfin.Naming.Tests.Book;
public class BookResolverTests
{
[Theory]
// seriesName (seriesYear?) #index (of count?) (year?)
[InlineData("Sherlock Holmes (1887) #1 (of 4) (1887)", null, "Sherlock Holmes", 1, 1887)]
[InlineData("Sherlock Holmes #2", null, "Sherlock Holmes", 2, null)]
[InlineData("Sherlock Holmes (1887) #1", null, "Sherlock Holmes", 1, null)]
[InlineData("Sherlock Holmes #2 (1890)", null, "Sherlock Holmes", 2, 1890)]
// name (seriesName, #index) (year?)
[InlineData("A Study in Scarlet (Sherlock Holmes, #1) (1887)", "A Study in Scarlet", "Sherlock Holmes", 1, 1887)]
[InlineData("The Adventures of Sherlock Holmes (Sherlock Holmes, #5)", "The Adventures of Sherlock Holmes", "Sherlock Holmes", 5, null)]
// name (year)
[InlineData("The Sign of the Four (1890)", "The Sign of the Four", null, null, 1890)]
[InlineData("The Valley of Fear (1915)", "The Valley of Fear", null, null, 1915)]
// index - name (year?)
[InlineData("2 - The Sign of the Four (1890)", "The Sign of the Four", null, 2, 1890)]
[InlineData("4 - The Valley of Fear", "The Valley of Fear", null, 4, null)]
// parse entire string as book name
[InlineData("A Study in Scarlet", "A Study in Scarlet", null, null, null)]
[InlineData("The Adventures of Sherlock Holmes", "The Adventures of Sherlock Holmes", null, null, null)]
// leading zeros on index number
[InlineData("00 - Dracula's Guest (1914)", "Dracula's Guest", null, 0, 1914)]
[InlineData("01 - Dracula (1897)", "Dracula", null, 1, 1897)]
// basic decimal support for prequels and novellas
[InlineData("2.0 - Twenty Thousand Leagues Under the Sea", "Twenty Thousand Leagues Under the Sea", null, 2, null)]
// TODO decide how to process non-zero decimals
[InlineData("2.1 - The Blockade Runners", "2.1 - The Blockade Runners", null, null, null)]
public void Resolve_Books(string input, string? name, string? series, int? index, int? year)
{
var result = BookFileNameParser.Parse(input);
Assert.Equal(name, result.Name);
Assert.Equal(series, result.SeriesName);
Assert.Equal(index, result.Index);
Assert.Equal(year, result.Year);
}
[Theory]
// name volume? chapter? (year?)
[InlineData("Captain Marvel Adventures v01 (1941)", "Captain Marvel Adventures v01", null, null, 1, 1941)]
[InlineData("Captain Marvel Adventures c120", "Captain Marvel Adventures c120", null, 120, null, null)]
[InlineData("Captain Marvel Adventures v01 c120", "Captain Marvel Adventures v01 c120", null, 120, 1, null)]
public void Resolve_Comics(string input, string? name, string? series, int? chapter, int? volume, int? year)
{
var result = BookFileNameParser.Parse(input);
Assert.Equal(name, result.Name);
Assert.Equal(series, result.SeriesName);
Assert.Equal(chapter, result.Index);
Assert.Equal(volume, result.ParentIndex);
Assert.Equal(year, result.Year);
}
}

View File

@@ -10,7 +10,7 @@ using Xunit;
namespace Jellyfin.Server.Implementations.Tests.IO;
public class ManagedFileSystemTests
public partial class ManagedFileSystemTests
{
private readonly IFixture _fixture;
private readonly ManagedFileSystem _sut;
@@ -117,7 +117,7 @@ public class ManagedFileSystemTests
}
[SuppressMessage("Naming Rules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Have to")]
[DllImport("libc", SetLastError = true, CharSet = CharSet.Ansi)]
[LibraryImport("libc", SetLastError = true)]
[DefaultDllImportSearchPaths(DllImportSearchPath.UserDirectories)]
private static extern int symlink(string target, string linkpath);
private static partial int symlink([MarshalAs(UnmanagedType.LPStr)] string target, [MarshalAs(UnmanagedType.LPStr)] string linkpath);
}

View File

@@ -4,6 +4,7 @@
<PropertyGroup>
<ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using Emby.Server.Implementations.SyncPlay;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.SyncPlay;
public class GroupTests
{
public GroupTests()
{
var mockLogger = new Mock<ILogger<Emby.Server.Implementations.SyncPlay.Group>>();
MockLoggerFactory = new Mock<ILoggerFactory>();
MockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
MockUserManager = new Mock<IUserManager>();
MockSessionManager = new Mock<ISessionManager>();
MockLibraryManager = new Mock<ILibraryManager>();
MockItem = new Mock<BaseItem>();
MockItem.Setup(i => i.IsVisibleStandalone(It.IsAny<User>())).Returns(true);
}
private Mock<ILoggerFactory> MockLoggerFactory { get; }
private Mock<IUserManager> MockUserManager { get; }
private Mock<ISessionManager> MockSessionManager { get; }
private Mock<ILibraryManager> MockLibraryManager { get; }
private Mock<BaseItem> MockItem { get; }
[Fact]
public void HasAccessToPlayQueue_ReturnsTrue_WhenItemsAreVisible()
{
MockLibraryManager.Setup(m => m.GetItemById(It.IsAny<Guid>())).Returns(MockItem.Object);
var group = new Emby.Server.Implementations.SyncPlay.Group(MockLoggerFactory.Object, MockUserManager.Object, MockSessionManager.Object, MockLibraryManager.Object);
var itemId = Guid.NewGuid();
var playlist = new List<Guid> { itemId };
group.PlayQueue.Reset();
group.PlayQueue.SetPlaylist(playlist);
Assert.Single(group.PlayQueue.GetPlaylist());
Assert.Equal(itemId, group.PlayQueue.GetPlaylist()[0].ItemId);
var user = new User("test-user", "auth-provider", "pwdreset-provider");
var result = group.HasAccessToPlayQueue(user);
Assert.True(result);
}
[Fact]
public void HasAccessToPlayQueue_ReturnsFalse_WhenLibraryReturnsNullForItem()
{
MockLibraryManager.Setup(m => m.GetItemById(It.IsAny<Guid>())).Returns((BaseItem?)null);
Assert.Null(MockLibraryManager.Object.GetItemById(Guid.NewGuid()));
var group = new Emby.Server.Implementations.SyncPlay.Group(MockLoggerFactory.Object, MockUserManager.Object, MockSessionManager.Object, MockLibraryManager.Object);
var itemId = Guid.NewGuid();
var playlist = new List<Guid> { itemId };
group.PlayQueue.Reset();
group.PlayQueue.SetPlaylist(playlist);
Assert.Single(group.PlayQueue.GetPlaylist());
Assert.Equal(itemId, group.PlayQueue.GetPlaylist()[0].ItemId);
var user = new User("test-user", "auth-provider", "pwdreset-provider");
var result = group.HasAccessToPlayQueue(user);
Assert.False(result);
}
}