mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-22 07:30:52 +01:00
Compare commits
55 Commits
renovate/s
...
v12.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e80648fd3 | ||
|
|
f08a3f9fd9 | ||
|
|
083f9d291a | ||
|
|
e4383493a9 | ||
|
|
ce58e4400e | ||
|
|
3741d71965 | ||
|
|
11f642594d | ||
|
|
8d15529df7 | ||
|
|
e75161c557 | ||
|
|
308981cc0d | ||
|
|
bebb7ce803 | ||
|
|
1a6f019cfd | ||
|
|
751b763838 | ||
|
|
64e02c0e28 | ||
|
|
49f8a96360 | ||
|
|
364f1e12c0 | ||
|
|
ada11f5692 | ||
|
|
5036bf7db0 | ||
|
|
1c4dea4b2c | ||
|
|
e525fc7c4b | ||
|
|
3307406ac8 | ||
|
|
e86b502cbc | ||
|
|
4c228eaf63 | ||
|
|
1176c2d329 | ||
|
|
b9271eb199 | ||
|
|
e2433e2c79 | ||
|
|
0022508889 | ||
|
|
a9a02719ab | ||
|
|
d50205cc9f | ||
|
|
e4edce9a70 | ||
|
|
ac92da233b | ||
|
|
f6bb086415 | ||
|
|
9375f31bd3 | ||
|
|
a0862a4cb5 | ||
|
|
2d8ab1e2ec | ||
|
|
068bbb7981 | ||
|
|
f9644f24d2 | ||
|
|
8d0003533e | ||
|
|
1dd5a85080 | ||
|
|
f4bab458a2 | ||
|
|
d8f8dbabcb | ||
|
|
a9dc8f6f74 | ||
|
|
88602ce905 | ||
|
|
4cb0385745 | ||
|
|
438d992c8b | ||
|
|
1adf441f1c | ||
|
|
490bf347cb | ||
|
|
fd6e48603b | ||
|
|
b09c9655fd | ||
|
|
790220ef6b | ||
|
|
c08b1a4595 | ||
|
|
622b60064d | ||
|
|
91b2b7fc3d | ||
|
|
0b4854c5ef | ||
|
|
d6a1c8413c |
@@ -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" />
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
|
||||
"NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
|
||||
"NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
|
||||
"NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
|
||||
"NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
25
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
Normal file → Executable file
25
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
Normal file → Executable 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 />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
58
tests/Jellyfin.Naming.Tests/Book/BookResolverTests.cs
Normal file
58
tests/Jellyfin.Naming.Tests/Book/BookResolverTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user