mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-29 20:08:27 +01:00
Compare commits
2 Commits
v10.11.9
...
explicit-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dbec6f4f5 | ||
|
|
f6709a69e7 |
@@ -207,7 +207,6 @@
|
|||||||
- [TokerX](https://github.com/TokerX)
|
- [TokerX](https://github.com/TokerX)
|
||||||
- [GeneMarks](https://github.com/GeneMarks)
|
- [GeneMarks](https://github.com/GeneMarks)
|
||||||
- [martenumberto](https://github.com/martenumberto)
|
- [martenumberto](https://github.com/martenumberto)
|
||||||
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
|
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.11.9</VersionPrefix>
|
<VersionPrefix>10.11.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1051,16 +1051,16 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
||||||
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
||||||
var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))])
|
||||||
|
.Where(e => e.Value.Length > 0)
|
||||||
dto.ArtistItems = hasArtist.Artists
|
.Select(i =>
|
||||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
{
|
||||||
.Distinct()
|
return new NameGuidPair
|
||||||
.Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0
|
{
|
||||||
? new NameGuidPair { Name = name, Id = artists[0].Id }
|
Name = i.Key,
|
||||||
: null)
|
Id = i.Value.First().Id
|
||||||
.Where(item => item is not null)
|
};
|
||||||
.ToArray();
|
}).Where(i => i is not null).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is IHasAlbumArtist hasAlbumArtist)
|
if (item is IHasAlbumArtist hasAlbumArtist)
|
||||||
@@ -1085,16 +1085,31 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
// })
|
// })
|
||||||
// .ToList();
|
// .ToList();
|
||||||
|
|
||||||
var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
|
||||||
|
|
||||||
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
||||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
// .Except(foundArtists, new DistinctNameComparer())
|
||||||
.Distinct()
|
.Select(i =>
|
||||||
.Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0
|
{
|
||||||
? new NameGuidPair { Name = name, Id = albumArtists[0].Id }
|
// This should not be necessary but we're seeing some cases of it
|
||||||
: null)
|
if (string.IsNullOrEmpty(i))
|
||||||
.Where(item => item is not null)
|
{
|
||||||
.ToArray();
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
|
||||||
|
{
|
||||||
|
EnableImages = false
|
||||||
|
});
|
||||||
|
if (artist is not null)
|
||||||
|
{
|
||||||
|
return new NameGuidPair
|
||||||
|
{
|
||||||
|
Name = artist.Name,
|
||||||
|
Id = artist.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}).Where(i => i is not null).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add video info
|
// Add video info
|
||||||
|
|||||||
@@ -352,12 +352,6 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
|
||||||
if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
||||||
foreach (var i in _tempIgnoredPaths.Keys)
|
foreach (var i in _tempIgnoredPaths.Keys)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -267,24 +267,22 @@ namespace Emby.Server.Implementations.Images
|
|||||||
{
|
{
|
||||||
var image = item.GetImageInfo(type, 0);
|
var image = item.GetImageInfo(type, 0);
|
||||||
|
|
||||||
if (image is null)
|
if (image is not null)
|
||||||
{
|
{
|
||||||
return GetItemsWithImages(item).Count is not 0;
|
if (!image.IsLocalFile)
|
||||||
}
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!image.IsLocalFile)
|
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
|
if (!HasChangedByDate(item, image))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!HasChangedByDate(item, image))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -98,11 +98,5 @@ namespace Emby.Server.Implementations.Images
|
|||||||
|
|
||||||
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
|
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
|
|
||||||
{
|
|
||||||
var age = DateTime.UtcNow - image.DateModified;
|
|
||||||
return age.TotalDays > 7;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,6 @@ namespace Emby.Server.Implementations.Library
|
|||||||
"**/lost+found",
|
"**/lost+found",
|
||||||
"**/subs/**",
|
"**/subs/**",
|
||||||
"**/subs",
|
"**/subs",
|
||||||
"**/.snapshots/**",
|
|
||||||
"**/.snapshots",
|
|
||||||
"**/.snapshot/**",
|
|
||||||
"**/.snapshot",
|
|
||||||
|
|
||||||
// Trickplay files
|
// Trickplay files
|
||||||
"**/*.trickplay",
|
"**/*.trickplay",
|
||||||
|
|||||||
@@ -2202,12 +2202,6 @@ namespace Emby.Server.Implementations.Library
|
|||||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||||
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
|
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
|
||||||
{
|
{
|
||||||
if (item.IsFileProtocol)
|
if (item.IsFileProtocol)
|
||||||
@@ -3201,7 +3195,19 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||||
|
|
||||||
CreateShortcut(virtualFolderPath, pathInfo);
|
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||||
|
|
||||||
|
while (File.Exists(lnk))
|
||||||
|
{
|
||||||
|
shortcutFilename += "1";
|
||||||
|
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
|
||||||
|
|
||||||
|
RemoveContentTypeOverrides(path);
|
||||||
|
|
||||||
if (saveLibraryOptions)
|
if (saveLibraryOptions)
|
||||||
{
|
{
|
||||||
@@ -3366,24 +3372,5 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
return item is UserRootFolder || item.IsVisibleStandalone(user);
|
return item is UserRootFolder || item.IsVisibleStandalone(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
|
|
||||||
{
|
|
||||||
var path = pathInfo.Path;
|
|
||||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
|
||||||
|
|
||||||
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
|
|
||||||
|
|
||||||
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
|
||||||
|
|
||||||
while (File.Exists(lnk))
|
|
||||||
{
|
|
||||||
shortcutFilename += "1";
|
|
||||||
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
|
||||||
}
|
|
||||||
|
|
||||||
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
|
|
||||||
RemoveContentTypeOverrides(path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
|
|
||||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, CultureDto?> _cultureCache = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private List<CultureDto> _cultures = [];
|
private List<CultureDto> _cultures = [];
|
||||||
|
|
||||||
private FrozenDictionary<string, string> _iso6392BtoT = null!;
|
private FrozenDictionary<string, string> _iso6392BtoT = null!;
|
||||||
@@ -162,7 +161,6 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames));
|
list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
_cultureCache.Clear();
|
|
||||||
_cultures = list;
|
_cultures = list;
|
||||||
_iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
_iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
@@ -171,31 +169,20 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CultureDto? FindLanguageInfo(string language)
|
public CultureDto? FindLanguageInfo(string language)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(language))
|
// TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
|
||||||
|
for (var i = 0; i < _cultures.Count; i++)
|
||||||
{
|
{
|
||||||
return null;
|
var culture = _cultures[i];
|
||||||
|
if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return culture;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _cultureCache.GetOrAdd(
|
return default;
|
||||||
language,
|
|
||||||
static (lang, cultures) =>
|
|
||||||
{
|
|
||||||
// TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
|
|
||||||
for (var i = 0; i < cultures.Count; i++)
|
|
||||||
{
|
|
||||||
var culture = cultures[i];
|
|
||||||
if (lang.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| lang.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| culture.ThreeLetterISOLanguageNames.Contains(lang, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| lang.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return culture;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
_cultures);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -324,19 +311,15 @@ namespace Emby.Server.Implementations.Localization
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fall back to server default language for ratings check
|
// Fall back to server default language for ratings check
|
||||||
var ratingsDictionary = GetParentalRatingsDictionary();
|
// If it has no ratings, use the US ratings
|
||||||
|
var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
|
||||||
if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
|
if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't find anything, check all ratings systems, starting with US
|
// If we don't find anything, check all ratings systems
|
||||||
if (_allParentalRatings.TryGetValue("us", out var usRatings) && usRatings.TryGetValue(rating, out var usValue))
|
|
||||||
{
|
|
||||||
return usValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var dictionary in _allParentalRatings.Values)
|
foreach (var dictionary in _allParentalRatings.Values)
|
||||||
{
|
{
|
||||||
if (dictionary.TryGetValue(rating, out var value))
|
if (dictionary.TryGetValue(rating, out var value))
|
||||||
|
|||||||
@@ -271,9 +271,9 @@ namespace Emby.Server.Implementations.Session
|
|||||||
user.LastActivityDate = activityDate;
|
user.LastActivityDate = activityDate;
|
||||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (DbUpdateConcurrencyException)
|
catch (DbUpdateConcurrencyException e)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Error updating user's last activity date due to concurrency conflict. This is an expected event.");
|
_logger.LogDebug(e, "Error updating user's last activity date.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1175,8 +1175,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
|
||||||
public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
|
|
||||||
{
|
{
|
||||||
return new SessionInfoDto
|
return new SessionInfoDto
|
||||||
{
|
{
|
||||||
@@ -2043,7 +2042,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
{
|
{
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
var adminUserIds = _userManager.GetUsers()
|
var adminUserIds = _userManager.Users
|
||||||
.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
|
.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
|
||||||
.Select(i => i.Id)
|
.Select(i => i.Id)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|||||||
@@ -156,11 +156,6 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
|
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
|
||||||
return Array.Empty<PackageInfo>();
|
return Array.Empty<PackageInfo>();
|
||||||
}
|
}
|
||||||
catch (NotSupportedException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}", manifest);
|
|
||||||
return Array.Empty<PackageInfo>();
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
||||||
|
|||||||
@@ -92,18 +92,18 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[ProducesAudioFile]
|
[ProducesAudioFile]
|
||||||
public async Task<ActionResult> GetAudioStream(
|
public async Task<ActionResult> GetAudioStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -114,7 +114,7 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -133,8 +133,8 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -259,18 +259,18 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[ProducesAudioFile]
|
[ProducesAudioFile]
|
||||||
public async Task<ActionResult> GetAudioStreamByContainer(
|
public async Task<ActionResult> GetAudioStreamByContainer(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -281,7 +281,7 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -300,8 +300,8 @@ public class AudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
|
|||||||
@@ -167,18 +167,18 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[ProducesPlaylistFile]
|
[ProducesPlaylistFile]
|
||||||
public async Task<ActionResult> GetLiveHlsStream(
|
public async Task<ActionResult> GetLiveHlsStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -189,7 +189,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -208,8 +208,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -416,12 +416,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery, Required] string mediaSourceId,
|
[FromQuery, Required] string mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -432,7 +432,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -453,8 +453,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -592,12 +592,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery, Required] string mediaSourceId,
|
[FromQuery, Required] string mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -609,7 +609,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -628,8 +628,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -762,12 +762,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -778,7 +778,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -799,8 +799,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -934,12 +934,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -951,7 +951,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -970,8 +970,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -1107,7 +1107,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string playlistId,
|
[FromRoute, Required] string playlistId,
|
||||||
[FromRoute, Required] int segmentId,
|
[FromRoute, Required] int segmentId,
|
||||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery, Required] long runtimeTicks,
|
[FromQuery, Required] long runtimeTicks,
|
||||||
[FromQuery, Required] long actualSegmentLengthTicks,
|
[FromQuery, Required] long actualSegmentLengthTicks,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
@@ -1115,12 +1115,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -1131,7 +1131,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -1152,8 +1152,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -1292,7 +1292,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string playlistId,
|
[FromRoute, Required] string playlistId,
|
||||||
[FromRoute, Required] int segmentId,
|
[FromRoute, Required] int segmentId,
|
||||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery, Required] long runtimeTicks,
|
[FromQuery, Required] long runtimeTicks,
|
||||||
[FromQuery, Required] long actualSegmentLengthTicks,
|
[FromQuery, Required] long actualSegmentLengthTicks,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
@@ -1300,12 +1300,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -1317,7 +1317,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -1336,8 +1336,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -1421,20 +1421,10 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
cancellationTokenSource.Token)
|
cancellationTokenSource.Token)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
var mediaSourceId = state.BaseRequest.MediaSourceId;
|
var mediaSourceId = state.BaseRequest.MediaSourceId;
|
||||||
double fps = state.TargetFramerate ?? 0.0f;
|
|
||||||
int segmentLength = state.SegmentLength * 1000;
|
|
||||||
|
|
||||||
// If video is transcoded and framerate is fractional (i.e. 23.976), we need to slightly adjust segment length
|
|
||||||
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001)
|
|
||||||
{
|
|
||||||
double nearestIntFramerate = Math.Ceiling(fps);
|
|
||||||
segmentLength = (int)Math.Ceiling(segmentLength * (nearestIntFramerate / fps));
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = new CreateMainPlaylistRequest(
|
var request = new CreateMainPlaylistRequest(
|
||||||
mediaSourceId is null ? null : Guid.Parse(mediaSourceId),
|
mediaSourceId is null ? null : Guid.Parse(mediaSourceId),
|
||||||
state.MediaPath,
|
state.MediaPath,
|
||||||
segmentLength,
|
state.SegmentLength * 1000,
|
||||||
state.RunTimeTicks ?? 0,
|
state.RunTimeTicks ?? 0,
|
||||||
state.Request.SegmentContainer ?? string.Empty,
|
state.Request.SegmentContainer ?? string.Empty,
|
||||||
"hls1/main/",
|
"hls1/main/",
|
||||||
@@ -1849,9 +1839,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
if (isActualOutputVideoCodecHevc)
|
if (isActualOutputVideoCodecHevc)
|
||||||
{
|
{
|
||||||
// Use hvc1 for 8.4. This is what Dolby uses for its official sample streams. Tagging with dvh1 would break some players with strict tag checking like Apple Safari.
|
// Prefer dvh1 to dvhe
|
||||||
var codecTag = state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG ? "hvc1" : "dvh1";
|
args += " -tag:v:0 dvh1 -strict -2";
|
||||||
args += $" -tag:v:0 {codecTag} -strict -2";
|
|
||||||
}
|
}
|
||||||
else if (isActualOutputVideoCodecAv1)
|
else if (isActualOutputVideoCodecAv1)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||||
{
|
{
|
||||||
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim());
|
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +426,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
if (item is IHasArtist hasArtists)
|
if (item is IHasArtist hasArtists)
|
||||||
{
|
{
|
||||||
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim());
|
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -342,17 +342,6 @@ public class LibraryStructureController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryOptions options = item.GetLibraryOptions();
|
|
||||||
foreach (var mediaPath in request.LibraryOptions!.PathInfos)
|
|
||||||
{
|
|
||||||
if (options.PathInfos.Any(i => i.Path == mediaPath.Path))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_libraryManager.CreateShortcut(item.Path, mediaPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.UpdateLibraryOptions(request.LibraryOptions);
|
item.UpdateLibraryOptions(request.LibraryOptions);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Tuners/{tunerId}/Reset")]
|
[HttpPost("Tuners/{tunerId}/Reset")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
|
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
|
||||||
{
|
{
|
||||||
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
|
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
|
||||||
@@ -983,7 +983,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
/// <response code="200">Created tuner host returned.</response>
|
/// <response code="200">Created tuner host returned.</response>
|
||||||
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
|
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
|
||||||
[HttpPost("TunerHosts")]
|
[HttpPost("TunerHosts")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
|
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
|
||||||
=> await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
|
=> await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
|
||||||
@@ -995,7 +995,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
/// <response code="204">Tuner host deleted.</response>
|
/// <response code="204">Tuner host deleted.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpDelete("TunerHosts")]
|
[HttpDelete("TunerHosts")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult DeleteTunerHost([FromQuery] string? id)
|
public ActionResult DeleteTunerHost([FromQuery] string? id)
|
||||||
{
|
{
|
||||||
@@ -1028,7 +1028,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
/// <response code="200">Created listings provider returned.</response>
|
/// <response code="200">Created listings provider returned.</response>
|
||||||
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
||||||
[HttpPost("ListingProviders")]
|
[HttpPost("ListingProviders")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
||||||
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
|
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
|
||||||
@@ -1054,7 +1054,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
/// <response code="204">Listing provider deleted.</response>
|
/// <response code="204">Listing provider deleted.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpDelete("ListingProviders")]
|
[HttpDelete("ListingProviders")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult DeleteListingProvider([FromQuery] string? id)
|
public ActionResult DeleteListingProvider([FromQuery] string? id)
|
||||||
{
|
{
|
||||||
@@ -1087,7 +1087,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
/// <response code="200">Available countries returned.</response>
|
/// <response code="200">Available countries returned.</response>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
|
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
|
||||||
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
|
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
[ProducesFile(MediaTypeNames.Application.Json)]
|
||||||
public async Task<ActionResult> GetSchedulesDirectCountries()
|
public async Task<ActionResult> GetSchedulesDirectCountries()
|
||||||
@@ -1108,7 +1108,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
/// <response code="200">Channel mapping options returned.</response>
|
/// <response code="200">Channel mapping options returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
|
||||||
[HttpGet("ChannelMappingOptions")]
|
[HttpGet("ChannelMappingOptions")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId)
|
public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId)
|
||||||
=> _listingsManager.GetChannelMappingOptions(providerId);
|
=> _listingsManager.GetChannelMappingOptions(providerId);
|
||||||
@@ -1120,7 +1120,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
/// <response code="200">Created channel mapping returned.</response>
|
/// <response code="200">Created channel mapping returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
||||||
[HttpPost("ChannelMappings")]
|
[HttpPost("ChannelMappings")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto)
|
public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto)
|
||||||
=> _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId);
|
=> _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId);
|
||||||
@@ -1144,7 +1144,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
|
||||||
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
|
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
|
||||||
[HttpGet("Tuners/Discover")]
|
[HttpGet("Tuners/Discover")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public IAsyncEnumerable<TunerHostInfo> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
|
public IAsyncEnumerable<TunerHostInfo> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
|
||||||
=> _tunerHostManager.DiscoverTuners(newDevicesOnly);
|
=> _tunerHostManager.DiscoverTuners(newDevicesOnly);
|
||||||
@@ -1192,7 +1192,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public ActionResult GetLiveStreamFile(
|
public ActionResult GetLiveStreamFile(
|
||||||
[FromRoute, Required] string streamId,
|
[FromRoute, Required] string streamId,
|
||||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container)
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container)
|
||||||
{
|
{
|
||||||
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
|
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
|
||||||
if (liveStreamInfo is null)
|
if (liveStreamInfo is null)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Models.StartupDtos;
|
using Jellyfin.Api.Models.StartupDtos;
|
||||||
@@ -111,7 +111,7 @@ public class StartupController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
// TODO: Remove this method when startup wizard no longer requires an existing user.
|
// TODO: Remove this method when startup wizard no longer requires an existing user.
|
||||||
await _userManager.InitializeAsync().ConfigureAwait(false);
|
await _userManager.InitializeAsync().ConfigureAwait(false);
|
||||||
var user = _userManager.GetFirstUser() ?? throw new InvalidOperationException("No user exists after initialization.");
|
var user = _userManager.Users.First();
|
||||||
return new StartupUserDto
|
return new StartupUserDto
|
||||||
{
|
{
|
||||||
Name = user.Username
|
Name = user.Username
|
||||||
@@ -131,12 +131,7 @@ public class StartupController : BaseJellyfinApiController
|
|||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto)
|
public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto)
|
||||||
{
|
{
|
||||||
var user = _userManager.GetFirstUser();
|
var user = _userManager.Users.First();
|
||||||
if (user is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(startupUserDto.Password))
|
if (string.IsNullOrWhiteSpace(startupUserDto.Password))
|
||||||
{
|
{
|
||||||
return BadRequest("Password must not be empty");
|
return BadRequest("Password must not be empty");
|
||||||
@@ -151,7 +146,7 @@ public class StartupController : BaseJellyfinApiController
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(startupUserDto.Password))
|
if (!string.IsNullOrEmpty(startupUserDto.Password))
|
||||||
{
|
{
|
||||||
await _userManager.ChangePassword(user.Id, startupUserDto.Password).ConfigureAwait(false);
|
await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public class SyncPlayController : BaseJellyfinApiController
|
|||||||
[FromBody, Required] NewGroupRequestDto requestData)
|
[FromBody, Required] NewGroupRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new NewGroupRequest(requestData.GroupName.Trim());
|
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
|
||||||
return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None));
|
return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] int? transcodingAudioChannels,
|
[FromQuery] int? transcodingAudioChannels,
|
||||||
[FromQuery] int? maxStreamingBitrate,
|
[FromQuery] int? maxStreamingBitrate,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] int? audioBitRate,
|
||||||
[FromQuery] long? startTimeTicks,
|
[FromQuery] long? startTimeTicks,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer,
|
||||||
[FromQuery] MediaStreamProtocol? transcodingProtocol,
|
[FromQuery] MediaStreamProtocol? transcodingProtocol,
|
||||||
[FromQuery] int? maxAudioSampleRate,
|
[FromQuery] int? maxAudioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ public class UserController : BaseJellyfinApiController
|
|||||||
|
|
||||||
if (request.ResetPassword)
|
if (request.ResetPassword)
|
||||||
{
|
{
|
||||||
await _userManager.ResetPassword(user.Id).ConfigureAwait(false);
|
await _userManager.ResetPassword(user).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -306,7 +306,7 @@ public class UserController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userManager.ChangePassword(user.Id, request.NewPw ?? string.Empty).ConfigureAwait(false);
|
await _userManager.ChangePassword(user, request.NewPw ?? string.Empty).ConfigureAwait(false);
|
||||||
|
|
||||||
var currentToken = User.GetToken();
|
var currentToken = User.GetToken();
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@ public class UserController : BaseJellyfinApiController
|
|||||||
|
|
||||||
if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
|
if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
await _userManager.RenameUser(user.Id, user.Username, updateUser.Name).ConfigureAwait(false);
|
await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userManager.UpdateConfigurationAsync(requestUserId, updateUser.Configuration).ConfigureAwait(false);
|
await _userManager.UpdateConfigurationAsync(requestUserId, updateUser.Configuration).ConfigureAwait(false);
|
||||||
@@ -448,7 +448,7 @@ public class UserController : BaseJellyfinApiController
|
|||||||
// If removing admin access
|
// If removing admin access
|
||||||
if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
|
if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
|
||||||
{
|
{
|
||||||
if (_userManager.GetUsers().Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
|
if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
|
||||||
{
|
{
|
||||||
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
|
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
|
||||||
}
|
}
|
||||||
@@ -463,7 +463,7 @@ public class UserController : BaseJellyfinApiController
|
|||||||
// If disabling
|
// If disabling
|
||||||
if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
|
if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
|
||||||
{
|
{
|
||||||
if (_userManager.GetUsers().Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
|
if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
|
||||||
{
|
{
|
||||||
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
|
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
|
||||||
}
|
}
|
||||||
@@ -545,7 +545,7 @@ public class UserController : BaseJellyfinApiController
|
|||||||
// no need to authenticate password for new user
|
// no need to authenticate password for new user
|
||||||
if (request.Password is not null)
|
if (request.Password is not null)
|
||||||
{
|
{
|
||||||
await _userManager.ChangePassword(newUser.Id, request.Password).ConfigureAwait(false);
|
await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIP().ToString());
|
var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIP().ToString());
|
||||||
@@ -620,7 +620,7 @@ public class UserController : BaseJellyfinApiController
|
|||||||
|
|
||||||
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
|
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
|
||||||
{
|
{
|
||||||
var users = _userManager.GetUsers();
|
var users = _userManager.Users;
|
||||||
|
|
||||||
if (isDisabled.HasValue)
|
if (isDisabled.HasValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -315,18 +315,18 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public async Task<ActionResult> GetVideoStream(
|
public async Task<ActionResult> GetVideoStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
[FromQuery, ParameterObsolete] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -337,7 +337,7 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -358,8 +358,8 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
@@ -556,18 +556,18 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public Task<ActionResult> GetVideoStreamByContainer(
|
public Task<ActionResult> GetVideoStreamByContainer(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
|
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] string? deviceProfileId,
|
[FromQuery] string? deviceProfileId,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
@@ -578,7 +578,7 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
|
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] bool? copyTimestamps,
|
||||||
@@ -599,8 +599,8 @@ public class VideosController : BaseJellyfinApiController
|
|||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
|
||||||
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
|
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ public class DynamicHlsHelper
|
|||||||
// from universal audio service, need to override the AudioCodec when the actual request differs from original query
|
// from universal audio service, need to override the AudioCodec when the actual request differs from original query
|
||||||
if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
|
var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString());
|
||||||
newQuery["AudioCodec"] = state.OutputAudioCodec;
|
newQuery["AudioCodec"] = state.OutputAudioCodec;
|
||||||
queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
|
queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
|
||||||
}
|
}
|
||||||
@@ -173,21 +173,10 @@ public class DynamicHlsHelper
|
|||||||
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
|
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video rotation metadata is only supported in fMP4 remuxing
|
|
||||||
if (state.VideoStream is not null
|
|
||||||
&& state.VideoRequest is not null
|
|
||||||
&& (state.VideoStream?.Rotation ?? 0) != 0
|
|
||||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
|
||||||
&& !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
|
|
||||||
&& !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
queryString += "&AllowVideoStreamCopy=false";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main stream
|
// Main stream
|
||||||
var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
|
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
|
||||||
var playlistUrl = baseUrl + queryString;
|
|
||||||
var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
|
playlistUrl += queryString;
|
||||||
|
|
||||||
var subtitleStreams = state.MediaSource
|
var subtitleStreams = state.MediaSource
|
||||||
.MediaStreams
|
.MediaStreams
|
||||||
@@ -209,36 +198,37 @@ public class DynamicHlsHelper
|
|||||||
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
|
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Video rotation metadata is only supported in fMP4 remuxing
|
||||||
|
if (state.VideoStream is not null
|
||||||
|
&& state.VideoRequest is not null
|
||||||
|
&& (state.VideoStream?.Rotation ?? 0) != 0
|
||||||
|
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
|
&& !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
|
||||||
|
&& !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
playlistUrl += "&AllowVideoStreamCopy=false";
|
||||||
|
}
|
||||||
|
|
||||||
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
||||||
|
|
||||||
if (state.VideoStream is not null && state.VideoRequest is not null)
|
if (state.VideoStream is not null && state.VideoRequest is not null)
|
||||||
{
|
{
|
||||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||||
|
|
||||||
// Provide AV1 and HEVC SDR entrances for backward compatibility.
|
// Provide SDR HEVC entrance for backward compatibility.
|
||||||
foreach (var sdrVideoCodec in new[] { "av1", "hevc" })
|
if (encodingOptions.AllowHevcEncoding
|
||||||
|
&& !encodingOptions.AllowAv1Encoding
|
||||||
|
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
|
&& state.VideoStream.VideoRange == VideoRange.HDR
|
||||||
|
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
|
var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
|
||||||
&& string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)
|
if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0)
|
||||||
&& string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase);
|
|
||||||
var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding
|
|
||||||
&& string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase);
|
|
||||||
var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed;
|
|
||||||
|
|
||||||
if (isEncodingAllowed
|
|
||||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
|
||||||
&& state.VideoStream.VideoRange == VideoRange.HDR)
|
|
||||||
{
|
{
|
||||||
// Force AV1 and HEVC Main Profile and disable video stream copy.
|
// Force HEVC Main Profile and disable video stream copy.
|
||||||
state.OutputVideoCodec = sdrVideoCodec;
|
state.OutputVideoCodec = "hevc";
|
||||||
|
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
|
||||||
var sdrPlaylistQuery = playlistQuery;
|
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
||||||
sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec;
|
|
||||||
sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main";
|
|
||||||
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
|
|
||||||
|
|
||||||
var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
|
|
||||||
|
|
||||||
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
|
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
|
||||||
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
|
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
|
||||||
@@ -248,30 +238,12 @@ public class DynamicHlsHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide H.264 SDR entrance for backward compatibility.
|
|
||||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
|
||||||
&& state.VideoStream.VideoRange == VideoRange.HDR)
|
|
||||||
{
|
|
||||||
// Force H.264 and disable video stream copy.
|
|
||||||
state.OutputVideoCodec = "h264";
|
|
||||||
|
|
||||||
var sdrPlaylistQuery = playlistQuery;
|
|
||||||
sdrPlaylistQuery["VideoCodec"] = "h264";
|
|
||||||
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
|
|
||||||
|
|
||||||
var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
|
|
||||||
|
|
||||||
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
|
|
||||||
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
|
|
||||||
|
|
||||||
// Restore the video codec
|
|
||||||
state.OutputVideoCodec = "copy";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide Level 5.0 entrance for backward compatibility.
|
// Provide Level 5.0 entrance for backward compatibility.
|
||||||
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
|
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
|
||||||
// but in fact it is capable of playing videos up to Level 6.1.
|
// but in fact it is capable of playing videos up to Level 6.1.
|
||||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
if (encodingOptions.AllowHevcEncoding
|
||||||
|
&& !encodingOptions.AllowAv1Encoding
|
||||||
|
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
&& state.VideoStream.Level.HasValue
|
&& state.VideoStream.Level.HasValue
|
||||||
&& state.VideoStream.Level > 150
|
&& state.VideoStream.Level > 150
|
||||||
&& state.VideoStream.VideoRange == VideoRange.SDR
|
&& state.VideoStream.VideoRange == VideoRange.SDR
|
||||||
@@ -301,15 +273,12 @@ public class DynamicHlsHelper
|
|||||||
var variation = GetBitrateVariation(totalBitrate);
|
var variation = GetBitrateVariation(totalBitrate);
|
||||||
|
|
||||||
var newBitrate = totalBitrate - variation;
|
var newBitrate = totalBitrate - variation;
|
||||||
var variantQuery = playlistQuery;
|
var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
||||||
variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
|
|
||||||
var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
|
|
||||||
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
||||||
|
|
||||||
variation *= 2;
|
variation *= 2;
|
||||||
newBitrate = totalBitrate - variation;
|
newBitrate = totalBitrate - variation;
|
||||||
variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
|
variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
||||||
variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
|
|
||||||
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -894,6 +863,23 @@ public class DynamicHlsHelper
|
|||||||
return variation;
|
return variation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
|
||||||
|
{
|
||||||
|
return url.Replace(
|
||||||
|
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
|
||||||
|
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
|
||||||
|
{
|
||||||
|
string profileStr = codec + "-profile=";
|
||||||
|
return url.Replace(
|
||||||
|
profileStr + oldValue,
|
||||||
|
profileStr + newValue,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
|
private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
|
||||||
{
|
{
|
||||||
var oldPlaylist = playlist.ToString();
|
var oldPlaylist = playlist.ToString();
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
@@ -18,7 +17,9 @@ using MediaBrowser.Controller.MediaEncoding;
|
|||||||
using MediaBrowser.Controller.Streaming;
|
using MediaBrowser.Controller.Streaming;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Helpers;
|
namespace Jellyfin.Api.Helpers;
|
||||||
@@ -421,18 +422,14 @@ public static class StreamingHelpers
|
|||||||
request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
if (videoRequest is not null && IsValidCodecName(val))
|
if (videoRequest is not null)
|
||||||
{
|
{
|
||||||
videoRequest.VideoCodec = val;
|
videoRequest.VideoCodec = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
if (IsValidCodecName(val))
|
request.AudioCodec = val;
|
||||||
{
|
|
||||||
request.AudioCodec = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
if (videoRequest is not null)
|
if (videoRequest is not null)
|
||||||
@@ -486,7 +483,7 @@ public static class StreamingHelpers
|
|||||||
request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
|
request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
|
||||||
break;
|
break;
|
||||||
case 15:
|
case 15:
|
||||||
if (videoRequest is not null && Regex.IsMatch(val, EncodingHelper.LevelValidationRegexStr))
|
if (videoRequest is not null)
|
||||||
{
|
{
|
||||||
videoRequest.Level = val;
|
videoRequest.Level = val;
|
||||||
}
|
}
|
||||||
@@ -507,7 +504,7 @@ public static class StreamingHelpers
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case 18:
|
case 18:
|
||||||
if (videoRequest is not null && IsValidCodecName(val))
|
if (videoRequest is not null)
|
||||||
{
|
{
|
||||||
videoRequest.Profile = val;
|
videoRequest.Profile = val;
|
||||||
}
|
}
|
||||||
@@ -566,11 +563,7 @@ public static class StreamingHelpers
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case 30:
|
case 30:
|
||||||
if (IsValidCodecName(val))
|
request.SubtitleCodec = val;
|
||||||
{
|
|
||||||
request.SubtitleCodec = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 31:
|
case 31:
|
||||||
if (videoRequest is not null)
|
if (videoRequest is not null)
|
||||||
@@ -593,11 +586,6 @@ public static class StreamingHelpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidCodecName(string val)
|
|
||||||
{
|
|
||||||
return EncodingHelper.ContainerValidationRegex().IsMatch(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses the container into its file extension.
|
/// Parses the container into its file extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Models.SyncPlayDtos;
|
namespace Jellyfin.Api.Models.SyncPlayDtos;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -19,6 +17,5 @@ public class NewGroupRequestDto
|
|||||||
/// Gets or sets the group name.
|
/// Gets or sets the group name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The name of the new group.</value>
|
/// <value>The name of the new group.</value>
|
||||||
[StringLength(200, ErrorMessage = "Group name must not exceed 200 characters.")]
|
|
||||||
public string GroupName { get; set; }
|
public string GroupName { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using MediaBrowser.Controller.Authentication;
|
|||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ namespace Jellyfin.Api.WebSocketListeners;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class SessionInfoWebSocketListener.
|
/// Class SessionInfoWebSocketListener.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfoDto>, WebSocketListenerState>
|
public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
|
||||||
{
|
{
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
@@ -53,26 +52,24 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
|
|||||||
/// Gets the data to send.
|
/// Gets the data to send.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Task{SystemInfo}.</returns>
|
/// <returns>Task{SystemInfo}.</returns>
|
||||||
protected override Task<IEnumerable<SessionInfoDto>> GetDataToSend()
|
protected override Task<IEnumerable<SessionInfo>> GetDataToSend()
|
||||||
{
|
{
|
||||||
return Task.FromResult(_sessionManager.Sessions.Select(_sessionManager.ToSessionInfoDto));
|
return Task.FromResult(_sessionManager.Sessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task<IEnumerable<SessionInfoDto>> GetDataToSendForConnection(IWebSocketConnection connection)
|
protected override Task<IEnumerable<SessionInfo>> GetDataToSendForConnection(IWebSocketConnection connection)
|
||||||
{
|
{
|
||||||
var sessions = _sessionManager.Sessions;
|
|
||||||
|
|
||||||
// For non-admin users, filter the sessions to only include their own sessions
|
// For non-admin users, filter the sessions to only include their own sessions
|
||||||
if (connection.AuthorizationInfo?.User is not null &&
|
if (connection.AuthorizationInfo?.User is not null &&
|
||||||
!connection.AuthorizationInfo.IsApiKey &&
|
!connection.AuthorizationInfo.IsApiKey &&
|
||||||
!connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
!connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
||||||
{
|
{
|
||||||
var userId = connection.AuthorizationInfo.User.Id;
|
var userId = connection.AuthorizationInfo.User.Id;
|
||||||
sessions = sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId));
|
return Task.FromResult(_sessionManager.Sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(sessions.Select(_sessionManager.ToSessionInfoDto));
|
return Task.FromResult(_sessionManager.Sessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.11.9</VersionPrefix>
|
<VersionPrefix>10.11.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ public static class UserEntityExtensions
|
|||||||
entity.Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true));
|
entity.Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true));
|
||||||
entity.Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true));
|
entity.Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true));
|
||||||
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true));
|
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true));
|
||||||
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, false));
|
entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true));
|
||||||
entity.Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true));
|
entity.Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true));
|
||||||
entity.Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
|
entity.Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
|
||||||
entity.Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
|
entity.Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
|
||||||
|
|||||||
@@ -118,21 +118,15 @@ public class BackupService : IBackupService
|
|||||||
throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
|
throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
|
||||||
}
|
}
|
||||||
|
|
||||||
void CopyDirectory(string source, string target, string[]? exclude = null)
|
void CopyDirectory(string source, string target)
|
||||||
{
|
{
|
||||||
var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
|
var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
|
||||||
var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
|
var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
|
||||||
var excludePaths = exclude?.Select(e => $"{source}/{e}/").ToArray();
|
|
||||||
foreach (var item in zipArchive.Entries)
|
foreach (var item in zipArchive.Entries)
|
||||||
{
|
{
|
||||||
var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
|
var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
|
||||||
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
|
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
|
||||||
|
|
||||||
if (excludePaths is not null && excludePaths.Any(e => item.FullName.StartsWith(e, StringComparison.Ordinal)))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|
||||||
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|
||||||
|| Path.EndsInDirectorySeparator(item.FullName))
|
|| Path.EndsInDirectorySeparator(item.FullName))
|
||||||
@@ -148,10 +142,8 @@ public class BackupService : IBackupService
|
|||||||
}
|
}
|
||||||
|
|
||||||
CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
|
CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
|
||||||
CopyDirectory("Data", _applicationPaths.DataPath, exclude: ["metadata", "metadata-default"]);
|
CopyDirectory("Data", _applicationPaths.DataPath);
|
||||||
CopyDirectory("Root", _applicationPaths.RootFolderPath);
|
CopyDirectory("Root", _applicationPaths.RootFolderPath);
|
||||||
CopyDirectory("Data/metadata", _applicationPaths.InternalMetadataPath);
|
|
||||||
CopyDirectory("Data/metadata-default", _applicationPaths.DefaultInternalMetadataPath);
|
|
||||||
|
|
||||||
if (manifest.Options.Database)
|
if (manifest.Options.Database)
|
||||||
{
|
{
|
||||||
@@ -411,15 +403,6 @@ public class BackupService : IBackupService
|
|||||||
if (backupOptions.Metadata)
|
if (backupOptions.Metadata)
|
||||||
{
|
{
|
||||||
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
|
||||||
|
|
||||||
// If a custom metadata path is configured, the default location may still contain data.
|
|
||||||
if (!string.Equals(
|
|
||||||
Path.GetFullPath(_applicationPaths.DefaultInternalMetadataPath),
|
|
||||||
Path.GetFullPath(_applicationPaths.InternalMetadataPath),
|
|
||||||
StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
CopyDirectory(Path.Combine(_applicationPaths.DefaultInternalMetadataPath), Path.Combine("Data", "metadata-default"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
|
var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ using MediaBrowser.Controller.LiveTv;
|
|||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Globalization;
|
|
||||||
using MediaBrowser.Model.LiveTv;
|
using MediaBrowser.Model.LiveTv;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -70,7 +69,6 @@ public sealed class BaseItemRepository
|
|||||||
private readonly IItemTypeLookup _itemTypeLookup;
|
private readonly IItemTypeLookup _itemTypeLookup;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly ILogger<BaseItemRepository> _logger;
|
private readonly ILogger<BaseItemRepository> _logger;
|
||||||
private readonly ILocalizationManager _localizationManager;
|
|
||||||
|
|
||||||
private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType.AlbumArtist];
|
private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType.AlbumArtist];
|
||||||
private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
|
private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
|
||||||
@@ -87,21 +85,18 @@ public sealed class BaseItemRepository
|
|||||||
/// <param name="itemTypeLookup">The static type lookup.</param>
|
/// <param name="itemTypeLookup">The static type lookup.</param>
|
||||||
/// <param name="serverConfigurationManager">The server Configuration manager.</param>
|
/// <param name="serverConfigurationManager">The server Configuration manager.</param>
|
||||||
/// <param name="logger">System logger.</param>
|
/// <param name="logger">System logger.</param>
|
||||||
/// <param name="localizationManager">Localization manager.</param>
|
|
||||||
public BaseItemRepository(
|
public BaseItemRepository(
|
||||||
IDbContextFactory<JellyfinDbContext> dbProvider,
|
IDbContextFactory<JellyfinDbContext> dbProvider,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
IItemTypeLookup itemTypeLookup,
|
IItemTypeLookup itemTypeLookup,
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
ILogger<BaseItemRepository> logger,
|
ILogger<BaseItemRepository> logger)
|
||||||
ILocalizationManager localizationManager)
|
|
||||||
{
|
{
|
||||||
_dbProvider = dbProvider;
|
_dbProvider = dbProvider;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_itemTypeLookup = itemTypeLookup;
|
_itemTypeLookup = itemTypeLookup;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_localizationManager = localizationManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -280,9 +275,8 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
|
||||||
|
|
||||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
result.Items = GetEntities(dbQuery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||||
result.StartIndex = filter.StartIndex ?? 0;
|
result.StartIndex = filter.StartIndex ?? 0;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -301,27 +295,7 @@ public sealed class BaseItemRepository
|
|||||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||||
|
|
||||||
var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
|
return GetEntities(dbQuery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||||
if (hasRandomSort)
|
|
||||||
{
|
|
||||||
var orderedIds = dbQuery.Select(e => e.Id).ToList();
|
|
||||||
if (orderedIds.Count == 0)
|
|
||||||
{
|
|
||||||
return Array.Empty<BaseItemDto>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemsById = ApplyNavigations(context.BaseItems.Where(e => orderedIds.Contains(e.Id)), filter)
|
|
||||||
.AsEnumerable()
|
|
||||||
.Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
|
|
||||||
.Where(dto => dto is not null)
|
|
||||||
.ToDictionary(i => i!.Id);
|
|
||||||
|
|
||||||
return orderedIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
|
||||||
|
|
||||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -363,9 +337,7 @@ public sealed class BaseItemRepository
|
|||||||
mainquery = ApplyGroupingFilter(context, mainquery, filter);
|
mainquery = ApplyGroupingFilter(context, mainquery, filter);
|
||||||
mainquery = ApplyQueryPaging(mainquery, filter);
|
mainquery = ApplyQueryPaging(mainquery, filter);
|
||||||
|
|
||||||
mainquery = ApplyNavigations(mainquery, filter);
|
return GetEntities(mainquery, context, filter).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||||
|
|
||||||
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -432,36 +404,6 @@ public sealed class BaseItemRepository
|
|||||||
return dbQuery;
|
return dbQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
|
|
||||||
{
|
|
||||||
if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
|
|
||||||
{
|
|
||||||
dbQuery = dbQuery.Include(e => e.TrailerTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
|
|
||||||
{
|
|
||||||
dbQuery = dbQuery.Include(e => e.Provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.DtoOptions.ContainsField(ItemFields.Settings))
|
|
||||||
{
|
|
||||||
dbQuery = dbQuery.Include(e => e.LockedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.DtoOptions.EnableUserData)
|
|
||||||
{
|
|
||||||
dbQuery = dbQuery.Include(e => e.UserData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.DtoOptions.EnableImages)
|
|
||||||
{
|
|
||||||
dbQuery = dbQuery.Include(e => e.Images);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
|
private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
|
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
|
||||||
@@ -487,18 +429,50 @@ public sealed class BaseItemRepository
|
|||||||
dbQuery = TranslateQuery(dbQuery, context, filter);
|
dbQuery = TranslateQuery(dbQuery, context, filter);
|
||||||
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
|
||||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
|
||||||
return dbQuery;
|
return dbQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
|
private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
|
IQueryable<BaseItemEntity> dbQuery = context.BaseItems;
|
||||||
dbQuery = dbQuery.AsSingleQuery();
|
dbQuery = dbQuery.AsSingleQuery();
|
||||||
|
|
||||||
return dbQuery;
|
return dbQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<BaseItemEntity> GetEntities(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter)
|
||||||
|
{
|
||||||
|
var items = dbQuery.AsEnumerable().Where(e => e is not null).ToArray();
|
||||||
|
var itemIds = items.Select(e => e.Id).ToArray();
|
||||||
|
|
||||||
|
if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
|
||||||
|
{
|
||||||
|
context.BaseItemTrailerTypes.WhereOneOrMany(itemIds, e => e.ItemId).Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds))
|
||||||
|
{
|
||||||
|
context.BaseItemProviders.WhereOneOrMany(itemIds, e => e.ItemId).Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.DtoOptions.ContainsField(ItemFields.Settings))
|
||||||
|
{
|
||||||
|
context.BaseItemMetadataFields.WhereOneOrMany(itemIds, e => e.ItemId).Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.DtoOptions.EnableImages)
|
||||||
|
{
|
||||||
|
context.BaseItemImageInfos.WhereOneOrMany(itemIds, e => e.ItemId).Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.DtoOptions.EnableUserData)
|
||||||
|
{
|
||||||
|
context.UserData.WhereOneOrMany(itemIds, e => e.ItemId).Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public int GetCount(InternalItemsQuery filter)
|
public int GetCount(InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
@@ -641,6 +615,7 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
var ids = tuples.Select(f => f.Item.Id).ToArray();
|
var ids = tuples.Select(f => f.Item.Id).ToArray();
|
||||||
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
|
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
|
||||||
|
var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
|
||||||
|
|
||||||
foreach (var item in tuples)
|
foreach (var item in tuples)
|
||||||
{
|
{
|
||||||
@@ -674,6 +649,19 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
|
||||||
|
foreach (var item in newItems)
|
||||||
|
{
|
||||||
|
// reattach old userData entries
|
||||||
|
var userKeys = item.UserDataKey.ToArray();
|
||||||
|
var retentionDate = (DateTime?)null;
|
||||||
|
context.UserData
|
||||||
|
.Where(e => e.ItemId == PlaceholderId)
|
||||||
|
.Where(e => userKeys.Contains(e.CustomDataKey))
|
||||||
|
.ExecuteUpdate(e => e
|
||||||
|
.SetProperty(f => f.ItemId, item.Item.Id)
|
||||||
|
.SetProperty(f => f.RetentionDate, retentionDate));
|
||||||
|
}
|
||||||
|
|
||||||
var itemValueMaps = tuples
|
var itemValueMaps = tuples
|
||||||
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
@@ -769,43 +757,6 @@ public sealed class BaseItemRepository
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(item);
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
await using (transaction.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
var userKeys = item.GetUserDataKeys().ToArray();
|
|
||||||
var retentionDate = (DateTime?)null;
|
|
||||||
|
|
||||||
await dbContext.UserData
|
|
||||||
.Where(e => e.ItemId == PlaceholderId)
|
|
||||||
.Where(e => userKeys.Contains(e.CustomDataKey))
|
|
||||||
.ExecuteUpdateAsync(
|
|
||||||
e => e
|
|
||||||
.SetProperty(f => f.ItemId, item.Id)
|
|
||||||
.SetProperty(f => f.RetentionDate, retentionDate),
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Rehydrate the cached userdata
|
|
||||||
item.UserData = await dbContext.UserData
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(e => e.ItemId == item.Id)
|
|
||||||
.ToArrayAsync(cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public BaseItemDto? RetrieveItem(Guid id)
|
public BaseItemDto? RetrieveItem(Guid id)
|
||||||
{
|
{
|
||||||
@@ -913,7 +864,7 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
|
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
|
||||||
dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
|
dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
|
||||||
dto.Studios = entity.Studios?.Split('|') ?? [];
|
dto.Studios = entity.Studios?.Split('|') ?? [];
|
||||||
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
|
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
|
||||||
|
|
||||||
@@ -1075,7 +1026,7 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
|
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
|
||||||
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null;
|
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null;
|
||||||
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
|
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
|
||||||
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
|
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
|
||||||
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
|
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
|
||||||
@@ -1605,36 +1556,29 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
|
IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
|
||||||
|
|
||||||
// When searching, prioritize by match quality: exact match > prefix match > contains
|
|
||||||
if (hasSearch)
|
|
||||||
{
|
|
||||||
orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!));
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstOrdering = orderBy.FirstOrDefault();
|
var firstOrdering = orderBy.FirstOrDefault();
|
||||||
if (firstOrdering != default)
|
if (firstOrdering != default)
|
||||||
{
|
{
|
||||||
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
|
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
|
||||||
if (orderedQuery is null)
|
if (firstOrdering.SortOrder == SortOrder.Ascending)
|
||||||
{
|
{
|
||||||
// No search relevance ordering, start fresh
|
orderedQuery = query.OrderBy(expression);
|
||||||
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
|
|
||||||
? query.OrderBy(expression)
|
|
||||||
: query.OrderByDescending(expression);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Search relevance ordering already applied, chain with ThenBy
|
orderedQuery = query.OrderByDescending(expression);
|
||||||
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
|
|
||||||
? orderedQuery.ThenBy(expression)
|
|
||||||
: orderedQuery.ThenByDescending(expression);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
|
if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
|
||||||
{
|
{
|
||||||
orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending
|
if (firstOrdering.SortOrder is SortOrder.Ascending)
|
||||||
? orderedQuery.ThenBy(e => e.Name)
|
{
|
||||||
: orderedQuery.ThenByDescending(e => e.Name);
|
orderedQuery = orderedQuery.ThenBy(e => e.Name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2302,42 +2246,26 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
|
if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
|
||||||
{
|
{
|
||||||
var lang = _localizationManager.FindLanguageInfo(filter.HasNoAudioTrackWithLanguage);
|
baseQuery = baseQuery
|
||||||
if (lang is not null)
|
.Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filter.HasNoAudioTrackWithLanguage));
|
||||||
{
|
|
||||||
baseQuery = baseQuery
|
|
||||||
.Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && lang.ThreeLetterISOLanguageNames.Contains(f.Language)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
|
if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
|
||||||
{
|
{
|
||||||
var lang = _localizationManager.FindLanguageInfo(filter.HasNoInternalSubtitleTrackWithLanguage);
|
baseQuery = baseQuery
|
||||||
if (lang is not null)
|
.Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal && f.Language == filter.HasNoInternalSubtitleTrackWithLanguage));
|
||||||
{
|
|
||||||
baseQuery = baseQuery
|
|
||||||
.Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal && lang.ThreeLetterISOLanguageNames.Contains(f.Language)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
|
if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
|
||||||
{
|
{
|
||||||
var lang = _localizationManager.FindLanguageInfo(filter.HasNoExternalSubtitleTrackWithLanguage);
|
baseQuery = baseQuery
|
||||||
if (lang is not null)
|
.Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && f.Language == filter.HasNoExternalSubtitleTrackWithLanguage));
|
||||||
{
|
|
||||||
baseQuery = baseQuery
|
|
||||||
.Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && lang.ThreeLetterISOLanguageNames.Contains(f.Language)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
|
if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
|
||||||
{
|
{
|
||||||
var lang = _localizationManager.FindLanguageInfo(filter.HasNoSubtitleTrackWithLanguage);
|
baseQuery = baseQuery
|
||||||
if (lang is not null)
|
.Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == filter.HasNoSubtitleTrackWithLanguage));
|
||||||
{
|
|
||||||
baseQuery = baseQuery
|
|
||||||
.Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && lang.ThreeLetterISOLanguageNames.Contains(f.Language)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.HasSubtitles.HasValue)
|
if (filter.HasSubtitles.HasValue)
|
||||||
@@ -2517,24 +2445,35 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
if (filter.ExcludeInheritedTags.Length > 0)
|
if (filter.ExcludeInheritedTags.Length > 0)
|
||||||
{
|
{
|
||||||
var excludedTags = filter.ExcludeInheritedTags;
|
|
||||||
baseQuery = baseQuery.Where(e =>
|
baseQuery = baseQuery.Where(e =>
|
||||||
!e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
|
!e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||||
&& (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))));
|
&& (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue ||
|
||||||
|
!context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.IncludeInheritedTags.Length > 0)
|
if (filter.IncludeInheritedTags.Length > 0)
|
||||||
{
|
{
|
||||||
var includeTags = filter.IncludeInheritedTags;
|
// For seasons and episodes, we also need to check the parent series' tags.
|
||||||
var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist;
|
if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
|
||||||
baseQuery = baseQuery.Where(e =>
|
{
|
||||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
|
baseQuery = baseQuery.Where(e =>
|
||||||
|
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||||
|
|| (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
|
||||||
|
}
|
||||||
|
|
||||||
// For seasons and episodes, we also need to check the parent series' tags.
|
// A playlist should be accessible to its owner regardless of allowed tags.
|
||||||
|| (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue)))
|
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
|
||||||
|
{
|
||||||
// A playlist should be accessible to its owner regardless of allowed tags
|
baseQuery = baseQuery.Where(e =>
|
||||||
|| (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
|
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||||
|
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
|
||||||
|
// d ^^ this is stupid it hate this.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
baseQuery = baseQuery.Where(e =>
|
||||||
|
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.SeriesStatuses.Length > 0)
|
if (filter.SeriesStatuses.Length > 0)
|
||||||
@@ -2688,21 +2627,6 @@ public sealed class BaseItemRepository
|
|||||||
.Where(e => artistNames.Contains(e.Name))
|
.Where(e => artistNames.Contains(e.Name))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var lookup = artists
|
return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray());
|
||||||
.GroupBy(e => e.Name!)
|
|
||||||
.ToDictionary(
|
|
||||||
g => g.Key,
|
|
||||||
g => g.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
|
|
||||||
|
|
||||||
var result = new Dictionary<string, MusicArtist[]>(artistNames.Count);
|
|
||||||
foreach (var name in artistNames)
|
|
||||||
{
|
|
||||||
if (lookup.TryGetValue(name, out var artistArray))
|
|
||||||
{
|
|
||||||
result[name] = artistArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using System.Linq.Expressions;
|
|||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Database.Implementations;
|
using Jellyfin.Database.Implementations;
|
||||||
using Jellyfin.Database.Implementations.Entities;
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
using Jellyfin.Extensions;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -69,30 +68,4 @@ public static class OrderMapper
|
|||||||
_ => e => e.SortName
|
_ => e => e.SortName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an expression to order search results by match quality.
|
|
||||||
/// Prioritizes: exact match (0) > prefix match with word boundary (1) > prefix match (2) > contains (3).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="searchTerm">The search term to match against.</param>
|
|
||||||
/// <returns>An expression that returns an integer representing match quality (lower is better).</returns>
|
|
||||||
public static Expression<Func<BaseItemEntity, int>> MapSearchRelevanceOrder(string searchTerm)
|
|
||||||
{
|
|
||||||
var cleanSearchTerm = GetCleanValue(searchTerm);
|
|
||||||
var searchPrefix = cleanSearchTerm + " ";
|
|
||||||
return e =>
|
|
||||||
e.CleanName == cleanSearchTerm ? 0 :
|
|
||||||
e.CleanName!.StartsWith(searchPrefix) ? 1 :
|
|
||||||
e.CleanName!.StartsWith(cleanSearchTerm) ? 2 : 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetCleanValue(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.RemoveDiacritics().ToLowerInvariant();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
var resetUser = userManager.GetUserByName(spr.UserName)
|
var resetUser = userManager.GetUserByName(spr.UserName)
|
||||||
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
|
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
|
||||||
|
|
||||||
await userManager.ChangePassword(resetUser.Id, pin).ConfigureAwait(false);
|
await userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
|
||||||
usersReset.Add(resetUser.Username);
|
usersReset.Add(resetUser.Username);
|
||||||
File.Delete(resetFile);
|
File.Delete(resetFile);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
#pragma warning disable CA1307
|
#pragma warning disable CA1307
|
||||||
#pragma warning disable RS0030 // Do not use banned APIs
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AsyncKeyedLock;
|
|
||||||
using Jellyfin.Data;
|
using Jellyfin.Data;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
@@ -37,7 +35,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages the creation and retrieval of <see cref="User"/> instances.
|
/// Manages the creation and retrieval of <see cref="User"/> instances.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class UserManager : IUserManager, IDisposable
|
public partial class UserManager : IUserManager
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||||
private readonly IEventManager _eventManager;
|
private readonly IEventManager _eventManager;
|
||||||
@@ -52,7 +50,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
|
||||||
private readonly AsyncKeyedLocker<Guid> _userLock = new();
|
private readonly IDictionary<Guid, User> _users;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UserManager"/> class.
|
/// Initializes a new instance of the <see cref="UserManager"/> class.
|
||||||
@@ -91,28 +89,29 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
|
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
|
||||||
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
||||||
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
||||||
|
|
||||||
|
_users = new ConcurrentDictionary<Guid, User>();
|
||||||
|
using var dbContext = _dbProvider.CreateDbContext();
|
||||||
|
foreach (var user in dbContext.Users
|
||||||
|
.AsSplitQuery()
|
||||||
|
.Include(user => user.Permissions)
|
||||||
|
.Include(user => user.Preferences)
|
||||||
|
.Include(user => user.AccessSchedules)
|
||||||
|
.Include(user => user.ProfileImage)
|
||||||
|
.AsEnumerable())
|
||||||
|
{
|
||||||
|
_users.Add(user.Id, user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
|
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<User> GetUsers()
|
public IEnumerable<User> Users => _users.Values;
|
||||||
{
|
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
|
||||||
return UserQuery(dbContext)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<Guid> GetUsersIds()
|
public IEnumerable<Guid> UsersIds => _users.Keys;
|
||||||
{
|
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
|
||||||
return dbContext.Users
|
|
||||||
.AsNoTracking()
|
|
||||||
.Select(user => user.Id)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
|
||||||
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
|
||||||
@@ -128,28 +127,8 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new ArgumentException("Guid can't be empty", nameof(id));
|
throw new ArgumentException("Guid can't be empty", nameof(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
_users.TryGetValue(id, out var user);
|
||||||
return UserQuery(dbContext)
|
return user;
|
||||||
.FirstOrDefault(user => user.Id == id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IQueryable<User> UserQuery(JellyfinDbContext dbContext)
|
|
||||||
{
|
|
||||||
return dbContext.Users
|
|
||||||
.AsSingleQuery()
|
|
||||||
.Include(user => user.Permissions)
|
|
||||||
.Include(user => user.Preferences)
|
|
||||||
.Include(user => user.AccessSchedules)
|
|
||||||
.Include(user => user.ProfileImage)
|
|
||||||
.AsNoTracking();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public User? GetFirstUser()
|
|
||||||
{
|
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
|
||||||
return UserQuery(dbContext)
|
|
||||||
.FirstOrDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -160,58 +139,42 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new ArgumentException("Invalid username", nameof(name));
|
throw new ArgumentException("Invalid username", nameof(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
|
||||||
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
|
||||||
#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
|
||||||
#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
|
||||||
return UserQuery(dbContext)
|
|
||||||
.FirstOrDefault(u => u.Username.ToUpper() == name.ToUpper());
|
|
||||||
#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
|
||||||
#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
|
||||||
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task RenameUser(Guid userId, string oldName, string newName)
|
public async Task RenameUser(User user, string newName)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
|
|
||||||
ThrowIfInvalidUsername(newName);
|
ThrowIfInvalidUsername(newName);
|
||||||
|
|
||||||
if (oldName.Equals(newName, StringComparison.OrdinalIgnoreCase))
|
if (user.Username.Equals(newName, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("The new and old names must be different.");
|
throw new ArgumentException("The new and old names must be different.");
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = null!; // user is never actually null where its used afterwards so we can just ignore.
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
using (await _userLock.LockAsync(userId).ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
||||||
#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
||||||
#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
||||||
if (await dbContext.Users
|
if (await dbContext.Users
|
||||||
.AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && u.Id != userId)
|
.AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && !u.Id.Equals(user.Id))
|
||||||
.ConfigureAwait(false))
|
.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
throw new ArgumentException(string.Format(
|
throw new ArgumentException(string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"A user with the name '{0}' already exists.",
|
"A user with the name '{0}' already exists.",
|
||||||
newName));
|
newName));
|
||||||
}
|
}
|
||||||
#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
||||||
#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
||||||
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
||||||
|
|
||||||
user = await UserQuery(dbContext)
|
user.Username = newName;
|
||||||
.AsTracking()
|
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
|
||||||
.FirstOrDefaultAsync(u => u.Id == userId)
|
|
||||||
.ConfigureAwait(false)
|
|
||||||
?? throw new ResourceNotFoundException(nameof(userId));
|
|
||||||
|
|
||||||
user.Username = newName;
|
|
||||||
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var eventArgs = new UserUpdatedEventArgs(user);
|
var eventArgs = new UserUpdatedEventArgs(user);
|
||||||
@@ -222,9 +185,10 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task UpdateUserAsync(User user)
|
public async Task UpdateUserAsync(User user)
|
||||||
{
|
{
|
||||||
using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await UpdateUserInternalAsync(user).ConfigureAwait(false);
|
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,30 +218,23 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
{
|
{
|
||||||
ThrowIfInvalidUsername(name);
|
ThrowIfInvalidUsername(name);
|
||||||
|
|
||||||
|
if (Users.Any(u => u.Username.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"A user with the name '{0}' already exists.",
|
||||||
|
name));
|
||||||
|
}
|
||||||
|
|
||||||
User newUser;
|
User newUser;
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
|
||||||
#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
|
||||||
#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
|
||||||
if (await dbContext.Users
|
|
||||||
.AnyAsync(u => u.Username.ToUpper() == name.ToUpper())
|
|
||||||
.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
throw new ArgumentException(string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"A user with the name '{0}' already exists.",
|
|
||||||
name));
|
|
||||||
}
|
|
||||||
#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
|
|
||||||
#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
|
|
||||||
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
|
||||||
|
|
||||||
newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
|
newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
|
||||||
|
|
||||||
dbContext.Users.Add(newUser);
|
dbContext.Users.Add(newUser);
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
_users.Add(newUser.Id, newUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
|
await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
|
||||||
@@ -288,84 +245,62 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task DeleteUserAsync(Guid userId)
|
public async Task DeleteUserAsync(Guid userId)
|
||||||
{
|
{
|
||||||
User? user;
|
if (!_users.TryGetValue(userId, out var user))
|
||||||
using (await _userLock.LockAsync(userId).ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
throw new ResourceNotFoundException(nameof(userId));
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
user = await dbContext.Users
|
|
||||||
.Include(u => u.Permissions)
|
|
||||||
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (user is null)
|
|
||||||
{
|
|
||||||
throw new ResourceNotFoundException(nameof(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
var userCount = await dbContext.Users.CountAsync().ConfigureAwait(false);
|
|
||||||
if (userCount == 1)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"The user '{0}' cannot be deleted because there must be at least one user in the system.",
|
|
||||||
user.Username));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.HasPermission(PermissionKind.IsAdministrator)
|
|
||||||
&& await dbContext.Users
|
|
||||||
.CountAsync(i => i.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value))
|
|
||||||
.ConfigureAwait(false) == 1)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
|
|
||||||
user.Username),
|
|
||||||
nameof(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
dbContext.Users.Remove(user);
|
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_users.Count == 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"The user '{0}' cannot be deleted because there must be at least one user in the system.",
|
||||||
|
user.Username));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.HasPermission(PermissionKind.IsAdministrator)
|
||||||
|
&& Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
|
||||||
|
user.Username),
|
||||||
|
nameof(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
dbContext.Users.Attach(user);
|
||||||
|
dbContext.Users.Remove(user);
|
||||||
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_users.Remove(userId);
|
||||||
|
|
||||||
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
|
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public Task ResetPassword(Guid userId)
|
public Task ResetPassword(User user)
|
||||||
{
|
{
|
||||||
return ChangePassword(userId, string.Empty);
|
return ChangePassword(user, string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task ChangePassword(Guid userId, string newPassword)
|
public async Task ChangePassword(User user, string newPassword)
|
||||||
{
|
{
|
||||||
User dbUser = null!;
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
using (await _userLock.LockAsync(userId).ConfigureAwait(false))
|
if (user.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword))
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword));
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
dbUser = await UserQuery(dbContext)
|
|
||||||
.AsTracking()
|
|
||||||
.FirstOrDefaultAsync(u => u.Id == userId)
|
|
||||||
.ConfigureAwait(false)
|
|
||||||
?? throw new ResourceNotFoundException(nameof(userId));
|
|
||||||
|
|
||||||
if (dbUser.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword));
|
|
||||||
}
|
|
||||||
|
|
||||||
await GetAuthenticationProvider(dbUser).ChangePassword(dbUser, newPassword).ConfigureAwait(false);
|
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(dbUser)).ConfigureAwait(false);
|
await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
|
||||||
|
await UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -468,114 +403,102 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
throw new ArgumentNullException(nameof(username));
|
throw new ArgumentNullException(nameof(username));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool success;
|
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
|
||||||
var user = GetUserByName(username);
|
var authResult = await AuthenticateLocalUser(username, password, user)
|
||||||
using (await _userLock.LockAsync(user?.Id ?? Guid.Empty).ConfigureAwait(false))
|
.ConfigureAwait(false);
|
||||||
|
var authenticationProvider = authResult.AuthenticationProvider;
|
||||||
|
var success = authResult.Success;
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
{
|
{
|
||||||
// Reload the user now that we hold the lock so the RowVersion is current.
|
string updatedUsername = authResult.Username;
|
||||||
// GetUserByName uses AsNoTracking and the snapshot may be stale if another
|
|
||||||
// write (e.g. a concurrent login) incremented RowVersion after our initial load.
|
if (success
|
||||||
if (user is not null)
|
&& authenticationProvider is not null
|
||||||
|
&& authenticationProvider is not DefaultAuthenticationProvider)
|
||||||
{
|
{
|
||||||
user = GetUserById(user.Id) ?? user;
|
// Trust the username returned by the authentication provider
|
||||||
}
|
username = updatedUsername;
|
||||||
|
|
||||||
var authResult = await AuthenticateLocalUser(username, password, user)
|
// Search the database for the user again
|
||||||
.ConfigureAwait(false);
|
// the authentication provider might have created it
|
||||||
var authenticationProvider = authResult.AuthenticationProvider;
|
user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
|
||||||
success = authResult.Success;
|
|
||||||
|
|
||||||
if (user is null)
|
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user is not null)
|
||||||
{
|
|
||||||
string updatedUsername = authResult.Username;
|
|
||||||
|
|
||||||
if (success
|
|
||||||
&& authenticationProvider is not null
|
|
||||||
&& authenticationProvider is not DefaultAuthenticationProvider)
|
|
||||||
{
|
{
|
||||||
// Trust the username returned by the authentication provider
|
await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
|
||||||
username = updatedUsername;
|
|
||||||
|
|
||||||
// Search the database for the user again
|
|
||||||
// the authentication provider might have created it
|
|
||||||
user = GetUserByName(username);
|
|
||||||
|
|
||||||
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy && user is not null)
|
|
||||||
{
|
|
||||||
await UpdatePolicyAsync(user.Id, hasNewUserPolicy.GetNewUserPolicy()).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (success && user is not null && authenticationProvider is not null)
|
if (success && user is not null && authenticationProvider is not null)
|
||||||
|
{
|
||||||
|
var providerId = authenticationProvider.GetType().FullName;
|
||||||
|
|
||||||
|
if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var providerId = authenticationProvider.GetType().FullName;
|
user.AuthenticationProviderId = providerId;
|
||||||
|
await UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (providerId is not null && !string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
|
if (user is null)
|
||||||
{
|
{
|
||||||
user.AuthenticationProviderId = providerId;
|
_logger.LogInformation(
|
||||||
await UpdateUserInternalAsync(user).ConfigureAwait(false);
|
"Authentication request for {UserName} has been denied (IP: {IP}).",
|
||||||
}
|
username,
|
||||||
|
remoteEndPoint);
|
||||||
|
throw new AuthenticationException("Invalid username or password entered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.HasPermission(PermissionKind.IsDisabled))
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).",
|
||||||
|
username,
|
||||||
|
remoteEndPoint);
|
||||||
|
throw new SecurityException(
|
||||||
|
$"The {user.Username} account is currently disabled. Please consult with your administrator.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
|
||||||
|
!_networkManager.IsInLocalNetwork(remoteEndPoint))
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).",
|
||||||
|
username,
|
||||||
|
remoteEndPoint);
|
||||||
|
throw new SecurityException("Forbidden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.IsParentalScheduleAllowed())
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).",
|
||||||
|
username,
|
||||||
|
remoteEndPoint);
|
||||||
|
throw new SecurityException("User is not allowed access at this time.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update LastActivityDate and LastLoginDate, then save
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
if (isUserSession)
|
||||||
|
{
|
||||||
|
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user is null)
|
user.InvalidLoginAttemptCount = 0;
|
||||||
{
|
await UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
_logger.LogInformation(
|
_logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
|
||||||
"Authentication request for {UserName} has been denied (IP: {IP}).",
|
}
|
||||||
username,
|
else
|
||||||
remoteEndPoint);
|
{
|
||||||
throw new AuthenticationException("Invalid username or password entered.");
|
await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
|
||||||
}
|
_logger.LogInformation(
|
||||||
|
"Authentication request for {UserName} has been denied (IP: {IP}).",
|
||||||
if (user.HasPermission(PermissionKind.IsDisabled))
|
user.Username,
|
||||||
{
|
remoteEndPoint);
|
||||||
_logger.LogInformation(
|
|
||||||
"Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).",
|
|
||||||
username,
|
|
||||||
remoteEndPoint);
|
|
||||||
throw new SecurityException(
|
|
||||||
$"The {user.Username} account is currently disabled. Please consult with your administrator.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
|
|
||||||
!_networkManager.IsInLocalNetwork(remoteEndPoint))
|
|
||||||
{
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).",
|
|
||||||
username,
|
|
||||||
remoteEndPoint);
|
|
||||||
throw new SecurityException("Forbidden.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.IsParentalScheduleAllowed())
|
|
||||||
{
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).",
|
|
||||||
username,
|
|
||||||
remoteEndPoint);
|
|
||||||
throw new SecurityException("User is not allowed access at this time.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update LastActivityDate and LastLoginDate, then save
|
|
||||||
if (success)
|
|
||||||
{
|
|
||||||
if (isUserSession)
|
|
||||||
{
|
|
||||||
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.InvalidLoginAttemptCount = 0;
|
|
||||||
await UpdateUserInternalAsync(user).ConfigureAwait(false);
|
|
||||||
_logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false);
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Authentication request for {UserName} has been denied (IP: {IP}).",
|
|
||||||
user.Username,
|
|
||||||
remoteEndPoint);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return success ? user : null;
|
return success ? user : null;
|
||||||
@@ -619,22 +542,22 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
|
||||||
|
if (_users.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultName = Environment.UserName;
|
||||||
|
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
|
||||||
|
{
|
||||||
|
defaultName = "MyJellyfinUser";
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
|
||||||
|
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultName = Environment.UserName;
|
|
||||||
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
|
|
||||||
{
|
|
||||||
defaultName = "MyJellyfinUser";
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
|
|
||||||
|
|
||||||
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
|
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
|
||||||
newUser.SetPermission(PermissionKind.IsAdministrator, true);
|
newUser.SetPermission(PermissionKind.IsAdministrator, true);
|
||||||
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
|
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
|
||||||
@@ -642,6 +565,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
|
|
||||||
dbContext.Users.Add(newUser);
|
dbContext.Users.Add(newUser);
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
|
_users.Add(newUser.Id, newUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,120 +602,122 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
|
public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
|
||||||
{
|
{
|
||||||
using (await _userLock.LockAsync(userId).ConfigureAwait(false))
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var user = dbContext.Users
|
||||||
await using (dbContext.ConfigureAwait(false))
|
.Include(u => u.Permissions)
|
||||||
|
.Include(u => u.Preferences)
|
||||||
|
.Include(u => u.AccessSchedules)
|
||||||
|
.Include(u => u.ProfileImage)
|
||||||
|
.FirstOrDefault(u => u.Id.Equals(userId))
|
||||||
|
?? throw new ArgumentException("No user exists with given Id!");
|
||||||
|
|
||||||
|
user.SubtitleMode = config.SubtitleMode;
|
||||||
|
user.HidePlayedInLatest = config.HidePlayedInLatest;
|
||||||
|
user.EnableLocalPassword = config.EnableLocalPassword;
|
||||||
|
user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
|
||||||
|
user.DisplayCollectionsView = config.DisplayCollectionsView;
|
||||||
|
user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
|
||||||
|
user.AudioLanguagePreference = config.AudioLanguagePreference;
|
||||||
|
user.RememberAudioSelections = config.RememberAudioSelections;
|
||||||
|
user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
|
||||||
|
user.RememberSubtitleSelections = config.RememberSubtitleSelections;
|
||||||
|
user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
|
||||||
|
|
||||||
|
// Only set cast receiver id if it is passed in and it exists in the server config.
|
||||||
|
if (!string.IsNullOrEmpty(config.CastReceiverId)
|
||||||
|
&& _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, config.CastReceiverId, StringComparison.Ordinal)))
|
||||||
{
|
{
|
||||||
var user = UserQuery(dbContext)
|
user.CastReceiverId = config.CastReceiverId;
|
||||||
.AsTracking()
|
|
||||||
.FirstOrDefault(u => u.Id.Equals(userId))
|
|
||||||
?? throw new ArgumentException("No user exists with given Id!");
|
|
||||||
|
|
||||||
user.SubtitleMode = config.SubtitleMode;
|
|
||||||
user.HidePlayedInLatest = config.HidePlayedInLatest;
|
|
||||||
user.EnableLocalPassword = config.EnableLocalPassword;
|
|
||||||
user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
|
|
||||||
user.DisplayCollectionsView = config.DisplayCollectionsView;
|
|
||||||
user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
|
|
||||||
user.AudioLanguagePreference = config.AudioLanguagePreference;
|
|
||||||
user.RememberAudioSelections = config.RememberAudioSelections;
|
|
||||||
user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
|
|
||||||
user.RememberSubtitleSelections = config.RememberSubtitleSelections;
|
|
||||||
user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
|
|
||||||
|
|
||||||
// Only set cast receiver id if it is passed in and it exists in the server config.
|
|
||||||
if (!string.IsNullOrEmpty(config.CastReceiverId)
|
|
||||||
&& _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, config.CastReceiverId, StringComparison.Ordinal)))
|
|
||||||
{
|
|
||||||
user.CastReceiverId = config.CastReceiverId;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
|
|
||||||
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
|
|
||||||
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
|
|
||||||
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
|
|
||||||
|
|
||||||
dbContext.Update(user);
|
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
|
||||||
|
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
|
||||||
|
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
|
||||||
|
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
|
||||||
|
|
||||||
|
dbContext.Update(user);
|
||||||
|
_users[user.Id] = user;
|
||||||
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
|
public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
|
||||||
{
|
{
|
||||||
using (await _userLock.LockAsync(userId).ConfigureAwait(false))
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var user = dbContext.Users
|
||||||
await using (dbContext.ConfigureAwait(false))
|
.Include(u => u.Permissions)
|
||||||
|
.Include(u => u.Preferences)
|
||||||
|
.Include(u => u.AccessSchedules)
|
||||||
|
.Include(u => u.ProfileImage)
|
||||||
|
.FirstOrDefault(u => u.Id.Equals(userId))
|
||||||
|
?? throw new ArgumentException("No user exists with given Id!");
|
||||||
|
|
||||||
|
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
|
||||||
|
int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
|
||||||
{
|
{
|
||||||
var user = UserQuery(dbContext)
|
-1 => null,
|
||||||
.AsTracking()
|
0 => 3,
|
||||||
.FirstOrDefault(u => u.Id.Equals(userId))
|
_ => policy.LoginAttemptsBeforeLockout
|
||||||
?? throw new ArgumentException("No user exists with given Id!");
|
};
|
||||||
|
|
||||||
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
|
user.MaxParentalRatingScore = policy.MaxParentalRating;
|
||||||
int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
|
user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
|
||||||
{
|
user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
|
||||||
-1 => null,
|
user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
|
||||||
0 => 3,
|
user.AuthenticationProviderId = policy.AuthenticationProviderId;
|
||||||
_ => policy.LoginAttemptsBeforeLockout
|
user.PasswordResetProviderId = policy.PasswordResetProviderId;
|
||||||
};
|
user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
|
||||||
|
user.LoginAttemptsBeforeLockout = maxLoginAttempts;
|
||||||
|
user.MaxActiveSessions = policy.MaxActiveSessions;
|
||||||
|
user.SyncPlayAccess = policy.SyncPlayAccess;
|
||||||
|
user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
|
||||||
|
user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
|
||||||
|
user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
|
||||||
|
user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
|
||||||
|
user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
|
||||||
|
user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
|
||||||
|
user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
|
||||||
|
user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
|
||||||
|
user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
|
||||||
|
user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
|
||||||
|
user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
|
||||||
|
user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
|
||||||
|
user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
|
||||||
|
user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
|
||||||
|
user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
|
||||||
|
user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
|
||||||
|
user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
|
||||||
|
user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
|
||||||
|
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
|
||||||
|
user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
|
||||||
|
user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
|
||||||
|
user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
|
||||||
|
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
|
||||||
|
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
|
||||||
|
|
||||||
user.MaxParentalRatingScore = policy.MaxParentalRating;
|
user.AccessSchedules.Clear();
|
||||||
user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
|
foreach (var policyAccessSchedule in policy.AccessSchedules)
|
||||||
user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
|
{
|
||||||
user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
|
user.AccessSchedules.Add(policyAccessSchedule);
|
||||||
user.AuthenticationProviderId = policy.AuthenticationProviderId;
|
|
||||||
user.PasswordResetProviderId = policy.PasswordResetProviderId;
|
|
||||||
user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
|
|
||||||
user.LoginAttemptsBeforeLockout = maxLoginAttempts;
|
|
||||||
user.MaxActiveSessions = policy.MaxActiveSessions;
|
|
||||||
user.SyncPlayAccess = policy.SyncPlayAccess;
|
|
||||||
user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
|
|
||||||
user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
|
|
||||||
user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
|
|
||||||
user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
|
|
||||||
user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
|
|
||||||
user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
|
|
||||||
user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
|
|
||||||
user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
|
|
||||||
user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
|
|
||||||
user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
|
|
||||||
user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
|
|
||||||
user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
|
|
||||||
user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
|
|
||||||
user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
|
|
||||||
user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
|
|
||||||
user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
|
|
||||||
user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
|
|
||||||
user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
|
|
||||||
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
|
|
||||||
user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
|
|
||||||
user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
|
|
||||||
user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
|
|
||||||
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
|
|
||||||
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
|
|
||||||
|
|
||||||
user.AccessSchedules.Clear();
|
|
||||||
foreach (var policyAccessSchedule in policy.AccessSchedules)
|
|
||||||
{
|
|
||||||
user.AccessSchedules.Add(policyAccessSchedule);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fix this at some point
|
|
||||||
user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
|
|
||||||
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
|
|
||||||
user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
|
|
||||||
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
|
|
||||||
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
|
|
||||||
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
|
|
||||||
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
|
|
||||||
|
|
||||||
dbContext.Update(user);
|
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: fix this at some point
|
||||||
|
user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
|
||||||
|
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
|
||||||
|
user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
|
||||||
|
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
|
||||||
|
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
|
||||||
|
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
|
||||||
|
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
|
||||||
|
|
||||||
|
dbContext.Update(user);
|
||||||
|
_users[user.Id] = user;
|
||||||
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,17 +729,15 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (await _userLock.LockAsync(user.Id).ConfigureAwait(false))
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
dbContext.Remove(user.ProfileImage);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
{
|
|
||||||
dbContext.Remove(user.ProfileImage);
|
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
user.ProfileImage = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.ProfileImage = null;
|
||||||
|
_users[user.Id] = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void ThrowIfInvalidUsername(string name)
|
internal static void ThrowIfInvalidUsername(string name)
|
||||||
@@ -959,42 +883,15 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
user.InvalidLoginAttemptCount);
|
user.InvalidLoginAttemptCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
await UpdateUserInternalAsync(user).ConfigureAwait(false);
|
await UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateUserInternalAsync(User user)
|
|
||||||
{
|
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
|
||||||
await using (dbContext.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
|
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
|
||||||
{
|
{
|
||||||
dbContext.Users.Attach(user);
|
dbContext.Users.Attach(user);
|
||||||
dbContext.Entry(user).State = EntityState.Modified;
|
dbContext.Entry(user).State = EntityState.Modified;
|
||||||
|
_users[user.Id] = user;
|
||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disposes all members of this class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing">Defines if the class has been cleaned up by a dispose or finalizer.</param>
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_userLock.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Server.ServerSetupApp;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Model.Globalization;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Jellyfin.Server.Migrations.Routines;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Migration to fix broken library subtitle download languages.
|
|
||||||
/// </summary>
|
|
||||||
[JellyfinMigration("2026-02-06T20:00:00", nameof(FixLibrarySubtitleDownloadLanguages))]
|
|
||||||
internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine
|
|
||||||
{
|
|
||||||
private readonly ILocalizationManager _localizationManager;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="FixLibrarySubtitleDownloadLanguages"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizationManager">The Localization manager.</param>
|
|
||||||
/// <param name="startupLogger">The startup logger for Startup UI integration.</param>
|
|
||||||
/// <param name="libraryManager">The Library manager.</param>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
public FixLibrarySubtitleDownloadLanguages(
|
|
||||||
ILocalizationManager localizationManager,
|
|
||||||
IStartupLogger<FixLibrarySubtitleDownloadLanguages> startupLogger,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
ILogger<FixLibrarySubtitleDownloadLanguages> logger)
|
|
||||||
{
|
|
||||||
_localizationManager = localizationManager;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_logger = startupLogger.With(logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task PerformAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Starting to fix library subtitle download languages.");
|
|
||||||
|
|
||||||
var virtualFolders = _libraryManager.GetVirtualFolders(false);
|
|
||||||
|
|
||||||
foreach (var virtualFolder in virtualFolders)
|
|
||||||
{
|
|
||||||
var options = virtualFolder.LibraryOptions;
|
|
||||||
if (options?.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some virtual folders don't have a proper item id.
|
|
||||||
if (!Guid.TryParse(virtualFolder.ItemId, out var folderId))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId);
|
|
||||||
if (collectionFolder is null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Could not find collection folder for virtual folder '{LibraryName}' with id '{FolderId}'. Skipping.", virtualFolder.Name, folderId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fixedLanguages = new List<string>();
|
|
||||||
|
|
||||||
foreach (var language in options.SubtitleDownloadLanguages)
|
|
||||||
{
|
|
||||||
var foundLanguage = _localizationManager.FindLanguageInfo(language)?.ThreeLetterISOLanguageName;
|
|
||||||
if (foundLanguage is not null)
|
|
||||||
{
|
|
||||||
// Converted ISO 639-2/B to T (ger to deu)
|
|
||||||
if (!string.Equals(foundLanguage, language, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Converted '{Language}' to '{ResolvedLanguage}' in library '{LibraryName}'.", language, foundLanguage, virtualFolder.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fixedLanguages.Contains(foundLanguage, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Language '{Language}' already exists for library '{LibraryName}'. Skipping duplicate.", foundLanguage, virtualFolder.Name);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fixedLanguages.Add(foundLanguage);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Could not resolve language '{Language}' in library '{LibraryName}'. Skipping.", language, virtualFolder.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
options.SubtitleDownloadLanguages = [.. fixedLanguages];
|
|
||||||
collectionFolder.UpdateLibraryOptions(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Library subtitle download languages fixed.");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -464,16 +464,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
|||||||
|
|
||||||
SqliteConnection.ClearAllPools();
|
SqliteConnection.ClearAllPools();
|
||||||
|
|
||||||
using (var checkpointConnection = new SqliteConnection($"Filename={libraryDbPath}"))
|
|
||||||
{
|
|
||||||
checkpointConnection.Open();
|
|
||||||
using var cmd = checkpointConnection.CreateCommand();
|
|
||||||
cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);";
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
SqliteConnection.ClearAllPools();
|
|
||||||
|
|
||||||
_logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
|
_logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
|
||||||
File.Move(libraryDbPath, libraryDbPath + ".old", true);
|
File.Move(libraryDbPath, libraryDbPath + ".old", true);
|
||||||
}
|
}
|
||||||
@@ -1173,9 +1163,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
|||||||
Item = null!,
|
Item = null!,
|
||||||
ProviderId = e[0],
|
ProviderId = e[0],
|
||||||
ProviderValue = string.Join('|', e.Skip(1))
|
ProviderValue = string.Join('|', e.Skip(1))
|
||||||
})
|
}).ToArray();
|
||||||
.DistinctBy(e => e.ProviderId)
|
|
||||||
.ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reader.TryGetString(index++, out var imageInfos))
|
if (reader.TryGetString(index++, out var imageInfos))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Common</PackageId>
|
<PackageId>Jellyfin.Common</PackageId>
|
||||||
<VersionPrefix>10.11.9</VersionPrefix>
|
<VersionPrefix>10.11.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1172,18 +1172,11 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
info.Video3DFormat = video.Video3DFormat;
|
info.Video3DFormat = video.Video3DFormat;
|
||||||
info.Timestamp = video.Timestamp;
|
info.Timestamp = video.Timestamp;
|
||||||
|
|
||||||
if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath))
|
if (video.IsShortcut)
|
||||||
{
|
{
|
||||||
var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath);
|
info.IsRemote = true;
|
||||||
|
info.Path = video.ShortcutPath;
|
||||||
// Only allow remote shortcut paths — local file paths in .strm files
|
info.Protocol = MediaSourceManager.GetPathProtocol(info.Path);
|
||||||
// could be used to read arbitrary files from the server.
|
|
||||||
if (shortcutProtocol != MediaProtocol.File)
|
|
||||||
{
|
|
||||||
info.IsRemote = true;
|
|
||||||
info.Path = video.ShortcutPath;
|
|
||||||
info.Protocol = shortcutProtocol;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(info.Container))
|
if (string.IsNullOrEmpty(info.Container))
|
||||||
@@ -2060,9 +2053,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
|
public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||||
=> await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false);
|
=> await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
|
|
||||||
await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates that images within the item are still on the filesystem.
|
/// Validates that images within the item are still on the filesystem.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -452,7 +452,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
// That's all the new and changed ones - now see if any have been removed and need cleanup
|
// That's all the new and changed ones - now see if any have been removed and need cleanup
|
||||||
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
|
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
|
||||||
var shouldRemove = !IsRoot || allowRemoveRoot;
|
var shouldRemove = !IsRoot || allowRemoveRoot;
|
||||||
var actuallyRemoved = new List<BaseItem>();
|
|
||||||
// If it's an AggregateFolder, don't remove
|
// If it's an AggregateFolder, don't remove
|
||||||
if (shouldRemove && itemsRemoved.Count > 0)
|
if (shouldRemove && itemsRemoved.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -468,7 +467,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
{
|
{
|
||||||
Logger.LogDebug("Removed item: {Path}", item.Path);
|
Logger.LogDebug("Removed item: {Path}", item.Path);
|
||||||
|
|
||||||
actuallyRemoved.Add(item);
|
|
||||||
item.SetParent(null);
|
item.SetParent(null);
|
||||||
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
|
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
|
||||||
}
|
}
|
||||||
@@ -479,20 +477,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
{
|
{
|
||||||
LibraryManager.CreateItems(newItems, this, cancellationToken);
|
LibraryManager.CreateItems(newItems, this, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// After removing items, reattach any detached user data to remaining children
|
|
||||||
// that share the same user data keys (eg. same episode replaced with a new file).
|
|
||||||
if (actuallyRemoved.Count > 0)
|
|
||||||
{
|
|
||||||
var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet();
|
|
||||||
foreach (var child in validChildren)
|
|
||||||
{
|
|
||||||
if (child.GetUserDataKeys().Any(removedKeys.Contains))
|
|
||||||
{
|
|
||||||
await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -201,17 +201,12 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
|
|
||||||
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
if (series is null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
|
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetEpisodes()
|
public List<BaseItem> GetEpisodes()
|
||||||
{
|
{
|
||||||
return GetEpisodes(Series, null, null, new DtoOptions(true), true);
|
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
query.AncestorWithPresentationUniqueKey = null;
|
query.AncestorWithPresentationUniqueKey = null;
|
||||||
query.SeriesPresentationUniqueKey = seriesKey;
|
query.SeriesPresentationUniqueKey = seriesKey;
|
||||||
query.IncludeItemTypes = new[] { BaseItemKind.Season };
|
query.IncludeItemTypes = new[] { BaseItemKind.Season };
|
||||||
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
|
query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) };
|
||||||
|
|
||||||
if (user is not null && !user.DisplayMissingEpisodes)
|
if (user is not null && !user.DisplayMissingEpisodes)
|
||||||
{
|
{
|
||||||
@@ -247,10 +247,6 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
|
|
||||||
query.AncestorWithPresentationUniqueKey = null;
|
query.AncestorWithPresentationUniqueKey = null;
|
||||||
query.SeriesPresentationUniqueKey = seriesKey;
|
query.SeriesPresentationUniqueKey = seriesKey;
|
||||||
if (query.OrderBy.Count == 0)
|
|
||||||
{
|
|
||||||
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.IncludeItemTypes.Length == 0)
|
if (query.IncludeItemTypes.Length == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -281,14 +281,6 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <returns>Returns a Task that can be awaited.</returns>
|
/// <returns>Returns a Task that can be awaited.</returns>
|
||||||
Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
|
Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reattaches the user data to the item.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The item.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>A task that represents the asynchronous reattachment operation.</returns>
|
|
||||||
Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the item.
|
/// Retrieves the item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -660,12 +652,5 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// This exists so plugins can trigger a library scan.
|
/// This exists so plugins can trigger a library scan.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
void QueueLibraryScan();
|
void QueueLibraryScan();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add mblink file for a media path.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="virtualFolderPath">The path to the virtualfolder.</param>
|
|
||||||
/// <param name="pathInfo">The new virtualfolder.</param>
|
|
||||||
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the users.
|
/// Gets the users.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The users.</returns>
|
/// <value>The users.</value>
|
||||||
IEnumerable<User> GetUsers();
|
IEnumerable<User> Users { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the user ids.
|
/// Gets the user ids.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The users ids.</returns>
|
/// <value>The users ids.</value>
|
||||||
IEnumerable<Guid> GetUsersIds();
|
IEnumerable<Guid> UsersIds { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the user manager and ensures that a user exists.
|
/// Initializes the user manager and ensures that a user exists.
|
||||||
@@ -47,12 +47,6 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <exception cref="ArgumentException"><c>id</c> is an empty Guid.</exception>
|
/// <exception cref="ArgumentException"><c>id</c> is an empty Guid.</exception>
|
||||||
User? GetUserById(Guid id);
|
User? GetUserById(Guid id);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the first available user.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The first user, or <c>null</c> if no users exist.</returns>
|
|
||||||
User? GetFirstUser();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name of the user by.
|
/// Gets the name of the user by.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -63,13 +57,12 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Renames the user.
|
/// Renames the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The UserId to change.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <param name="oldName">The old Username.</param>
|
|
||||||
/// <param name="newName">The new name.</param>
|
/// <param name="newName">The new name.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
/// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
|
/// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
|
||||||
/// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
|
/// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
|
||||||
Task RenameUser(Guid userId, string oldName, string newName);
|
Task RenameUser(User user, string newName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the user.
|
/// Updates the user.
|
||||||
@@ -99,17 +92,17 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets the password.
|
/// Resets the password.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The users Id.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task ResetPassword(Guid userId);
|
Task ResetPassword(User user);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Changes the password.
|
/// Changes the password.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The users id.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <param name="newPassword">New password to use.</param>
|
/// <param name="newPassword">New password to use.</param>
|
||||||
/// <returns>Awaitable task.</returns>
|
/// <returns>Awaitable task.</returns>
|
||||||
Task ChangePassword(Guid userId, string newPassword);
|
Task ChangePassword(User user, string newPassword);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the user dto.
|
/// Gets the user dto.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Controller</PackageId>
|
<PackageId>Jellyfin.Controller</PackageId>
|
||||||
<VersionPrefix>10.11.9</VersionPrefix>
|
<VersionPrefix>10.11.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -33,18 +33,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
public partial class EncodingHelper
|
public partial class EncodingHelper
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The codec validation regex string.
|
/// The codec validation regex.
|
||||||
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
|
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
|
||||||
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
||||||
/// This should matches all common valid codecs.
|
/// This should matches all common valid codecs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The level validation regex string.
|
/// The level validation regex.
|
||||||
/// This regular expression matches strings representing a double.
|
/// This regular expression matches strings representing a double.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LevelValidationRegexStr = @"-?[0-9]+(?:\.[0-9]+)?";
|
public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
|
||||||
|
|
||||||
private const string _defaultMjpegEncoder = "mjpeg";
|
private const string _defaultMjpegEncoder = "mjpeg";
|
||||||
|
|
||||||
@@ -85,7 +85,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
|
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
|
||||||
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
|
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
|
||||||
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
|
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
|
||||||
private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0);
|
|
||||||
|
private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly string[] _videoProfilesH264 =
|
private static readonly string[] _videoProfilesH264 =
|
||||||
[
|
[
|
||||||
@@ -179,15 +180,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
RemoveHdr10Plus,
|
RemoveHdr10Plus,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The codec validation regex.
|
|
||||||
/// This regular expression matches strings that consist of alphanumeric characters, hyphens,
|
|
||||||
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
|
|
||||||
/// This should matches all common valid codecs.
|
|
||||||
/// </summary>
|
|
||||||
[GeneratedRegex(@"^[a-zA-Z0-9\-\._,|]{0,40}$")]
|
|
||||||
public static partial Regex ContainerValidationRegex();
|
|
||||||
|
|
||||||
[GeneratedRegex(@"\s+")]
|
[GeneratedRegex(@"\s+")]
|
||||||
private static partial Regex WhiteSpaceRegex();
|
private static partial Regex WhiteSpaceRegex();
|
||||||
|
|
||||||
@@ -413,9 +405,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
|
|
||||||
return state.VideoStream.VideoRange == VideoRange.HDR
|
return state.VideoStream.VideoRange == VideoRange.HDR
|
||||||
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|
&& IsDoviWithHdr10Bl(state.VideoStream);
|
||||||
|| IsHdr10Plus(state.VideoStream)
|
|
||||||
|| IsDoviWithHdr10Bl(state.VideoStream));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||||
@@ -430,10 +420,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding.
|
// Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding.
|
||||||
// All other HDR formats working.
|
// All other HDR formats working.
|
||||||
return state.VideoStream.VideoRange == VideoRange.HDR
|
return state.VideoStream.VideoRange == VideoRange.HDR
|
||||||
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|
&& (IsDoviWithHdr10Bl(state.VideoStream)
|
||||||
|| IsHdr10Plus(state.VideoStream)
|
|| state.VideoStream.VideoRangeType is VideoRangeType.HLG);
|
||||||
|| IsDoviWithHdr10Bl(state.VideoStream)
|
|
||||||
|| state.VideoStream.VideoRangeType == VideoRangeType.HLG);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsVideoStreamHevcRext(EncodingJobInfo state)
|
private bool IsVideoStreamHevcRext(EncodingJobInfo state)
|
||||||
@@ -488,7 +476,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return GetMjpegEncoder(state, encodingOptions);
|
return GetMjpegEncoder(state, encodingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ContainerValidationRegex().IsMatch(codec))
|
if (_containerValidationRegex.IsMatch(codec))
|
||||||
{
|
{
|
||||||
return codec.ToLowerInvariant();
|
return codec.ToLowerInvariant();
|
||||||
}
|
}
|
||||||
@@ -529,7 +517,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
public static string GetInputFormat(string container)
|
public static string GetInputFormat(string container)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(container) || !ContainerValidationRegex().IsMatch(container))
|
if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -747,7 +735,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
var codec = state.OutputAudioCodec;
|
var codec = state.OutputAudioCodec;
|
||||||
|
|
||||||
if (!ContainerValidationRegex().IsMatch(codec))
|
if (!_containerValidationRegex.IsMatch(codec))
|
||||||
{
|
{
|
||||||
codec = "aac";
|
codec = "aac";
|
||||||
}
|
}
|
||||||
@@ -1279,20 +1267,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use analyzeduration also for subtitle streams to improve resolution detection with streams inside MKS files
|
|
||||||
var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state);
|
|
||||||
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
|
||||||
{
|
|
||||||
arg.Append(' ').Append(analyzeDurationArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply probesize, too, if configured
|
|
||||||
var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg();
|
|
||||||
if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument))
|
|
||||||
{
|
|
||||||
arg.Append(' ').Append(ffmpegProbeSizeArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also seek the external subtitles stream.
|
// Also seek the external subtitles stream.
|
||||||
var seekSubParam = GetFastSeekCommandLineParameter(state, options, segmentContainer);
|
var seekSubParam = GetFastSeekCommandLineParameter(state, options, segmentContainer);
|
||||||
if (!string.IsNullOrEmpty(seekSubParam))
|
if (!string.IsNullOrEmpty(seekSubParam))
|
||||||
@@ -1578,15 +1552,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
int bitrate = state.OutputVideoBitrate.Value;
|
int bitrate = state.OutputVideoBitrate.Value;
|
||||||
|
|
||||||
// Bit rate under 1000k is not allowed in h264_qsv.
|
// Bit rate under 1000k is not allowed in h264_qsv
|
||||||
if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
bitrate = Math.Max(bitrate, 1000);
|
bitrate = Math.Max(bitrate, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently use the same buffer size for all non-QSV encoders.
|
// Currently use the same buffer size for all encoders
|
||||||
// Use long arithmetic to prevent int32 overflow for very high bitrate values.
|
int bufsize = bitrate * 2;
|
||||||
int bufsize = (int)Math.Min((long)bitrate * 2, int.MaxValue);
|
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -1614,33 +1587,16 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
mbbrcOpt = " -mbbrc 1";
|
mbbrcOpt = " -mbbrc 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some less powerful H.264 HW decoders require strict CPB size
|
|
||||||
// So bufsize optimizations should not be applied to them
|
|
||||||
int factor = 2;
|
|
||||||
var codec = state.ActualOutputVideoCodec;
|
|
||||||
var level = state.GetRequestedLevel(codec);
|
|
||||||
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& double.TryParse(level, CultureInfo.InvariantCulture, out double requestedLevel)
|
|
||||||
&& requestedLevel < 51)
|
|
||||||
{
|
|
||||||
factor = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation
|
// Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation
|
||||||
// Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes
|
// Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes
|
||||||
// Use long arithmetic and clamp to int.MaxValue to prevent int32 overflow
|
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {bitrate + 1} -rc_init_occupancy {bitrate * 2} -bufsize {bitrate * 4}");
|
||||||
// (e.g. bitrate * 4 wraps to a negative value for bitrates above ~537 million)
|
|
||||||
int qsvMaxrate = (int)Math.Min((long)bitrate + 1, int.MaxValue);
|
|
||||||
int qsvInitOcc = (int)Math.Min((long)bitrate * 1 * factor, int.MaxValue);
|
|
||||||
int qsvBufsize = (int)Math.Min((long)bitrate * 2 * factor, int.MaxValue);
|
|
||||||
|
|
||||||
return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {qsvMaxrate} -rc_init_occupancy {qsvInitOcc} -bufsize {qsvBufsize}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Override the too high default qmin 18 in transcoding preset in legacy h26x_amf
|
// Override the too high default qmin 18 in transcoding preset
|
||||||
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
|
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1812,40 +1768,38 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
|
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
|
||||||
{
|
{
|
||||||
if (!double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
|
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
|
||||||
{
|
{
|
||||||
return null;
|
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// Transcode to level 5.3 (15) and lower for maximum compatibility.
|
|
||||||
// https://en.wikipedia.org/wiki/AV1#Levels
|
|
||||||
if (requestLevel < 0 || requestLevel >= 15)
|
|
||||||
{
|
{
|
||||||
return "15";
|
// Transcode to level 5.3 (15) and lower for maximum compatibility.
|
||||||
|
// https://en.wikipedia.org/wiki/AV1#Levels
|
||||||
|
if (requestLevel < 0 || requestLevel >= 15)
|
||||||
|
{
|
||||||
|
return "15";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||||
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||||
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// Transcode to level 5.0 and lower for maximum compatibility.
|
|
||||||
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
|
|
||||||
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
|
||||||
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
|
|
||||||
if (requestLevel < 0 || requestLevel >= 150)
|
|
||||||
{
|
{
|
||||||
return "150";
|
// Transcode to level 5.0 and lower for maximum compatibility.
|
||||||
|
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
|
||||||
|
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
||||||
|
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
|
||||||
|
if (requestLevel < 0 || requestLevel >= 150)
|
||||||
|
{
|
||||||
|
return "150";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||||
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// Transcode to level 5.1 and lower for maximum compatibility.
|
|
||||||
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
|
|
||||||
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
|
|
||||||
if (requestLevel < 0 || requestLevel >= 51)
|
|
||||||
{
|
{
|
||||||
return "51";
|
// Transcode to level 5.1 and lower for maximum compatibility.
|
||||||
|
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
|
||||||
|
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
|
||||||
|
if (requestLevel < 0 || requestLevel >= 51)
|
||||||
|
{
|
||||||
|
return "51";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2235,10 +2189,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var level = NormalizeTranscodingLevel(state, state.GetRequestedLevel(targetVideoCodec));
|
var level = state.GetRequestedLevel(targetVideoCodec);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(level))
|
if (!string.IsNullOrEmpty(level))
|
||||||
{
|
{
|
||||||
|
level = NormalizeTranscodingLevel(state, level);
|
||||||
|
|
||||||
// libx264, QSV, AMF can adjust the given level to match the output.
|
// libx264, QSV, AMF can adjust the given level to match the output.
|
||||||
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -6403,19 +6359,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format
|
|
||||||
if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& ((videoStream.Profile?.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) ?? false)
|
|
||||||
|| (videoStream.Profile?.Contains("4:4:4", StringComparison.OrdinalIgnoreCase) ?? false)))
|
|
||||||
{
|
|
||||||
// VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
|
|
||||||
if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
|
|
||||||
&& RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var decoder = hardwareAccelerationType switch
|
var decoder = hardwareAccelerationType switch
|
||||||
{
|
{
|
||||||
HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth),
|
HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth),
|
||||||
@@ -7160,8 +7103,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetFfmpegAnalyzeDurationArg(EncodingJobInfo state)
|
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
|
||||||
{
|
{
|
||||||
|
var inputModifier = string.Empty;
|
||||||
var analyzeDurationArgument = string.Empty;
|
var analyzeDurationArgument = string.Empty;
|
||||||
|
|
||||||
// Apply -analyzeduration as per the environment variable,
|
// Apply -analyzeduration as per the environment variable,
|
||||||
@@ -7177,26 +7121,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
|
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
return analyzeDurationArgument;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetFfmpegProbesizeArg()
|
|
||||||
{
|
|
||||||
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ffmpegProbeSize))
|
|
||||||
{
|
|
||||||
return $"-probesize {ffmpegProbeSize}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
|
|
||||||
{
|
|
||||||
var inputModifier = string.Empty;
|
|
||||||
var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
||||||
{
|
{
|
||||||
inputModifier += " " + analyzeDurationArgument;
|
inputModifier += " " + analyzeDurationArgument;
|
||||||
@@ -7205,11 +7129,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
inputModifier = inputModifier.Trim();
|
inputModifier = inputModifier.Trim();
|
||||||
|
|
||||||
// Apply -probesize if configured
|
// Apply -probesize if configured
|
||||||
var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg();
|
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument))
|
if (!string.IsNullOrEmpty(ffmpegProbeSize))
|
||||||
{
|
{
|
||||||
inputModifier += " " + ffmpegProbeSizeArgument;
|
inputModifier += $" -probesize {ffmpegProbeSize}";
|
||||||
}
|
}
|
||||||
|
|
||||||
var userAgentParam = GetUserAgentParam(state);
|
var userAgentParam = GetUserAgentParam(state);
|
||||||
@@ -7249,10 +7173,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
|
inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
int readrate = 0;
|
|
||||||
if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp)
|
if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp)
|
||||||
{
|
{
|
||||||
readrate = 1;
|
|
||||||
inputModifier += " -re";
|
inputModifier += " -re";
|
||||||
}
|
}
|
||||||
else if (encodingOptions.EnableSegmentDeletion
|
else if (encodingOptions.EnableSegmentDeletion
|
||||||
@@ -7263,15 +7185,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
// Set an input read rate limit 10x for using SegmentDeletion with stream-copy
|
// Set an input read rate limit 10x for using SegmentDeletion with stream-copy
|
||||||
// to prevent ffmpeg from exiting prematurely (due to fast drive)
|
// to prevent ffmpeg from exiting prematurely (due to fast drive)
|
||||||
readrate = 10;
|
inputModifier += " -readrate 10";
|
||||||
inputModifier += $" -readrate {readrate}";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a larger catchup value to revert to the old behavior,
|
|
||||||
// otherwise, remuxing might stall due to this new option
|
|
||||||
if (readrate > 0 && _mediaEncoder.EncoderVersion >= _minFFmpegReadrateCatchupOption)
|
|
||||||
{
|
|
||||||
inputModifier += $" -readrate_catchup {readrate * 100}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var flags = new List<string>();
|
var flags = new List<string>();
|
||||||
|
|||||||
@@ -35,14 +35,6 @@ public interface IItemRepository
|
|||||||
|
|
||||||
void SaveImages(BaseItem item);
|
void SaveImages(BaseItem item);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reattaches the user data to the item.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The item.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>A task that represents the asynchronous reattachment operation.</returns>
|
|
||||||
Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the item.
|
/// Retrieves the item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -350,12 +350,5 @@ namespace MediaBrowser.Controller.Session
|
|||||||
/// <param name="sessionIdOrPlaySessionId">The session id or playsession id.</param>
|
/// <param name="sessionIdOrPlaySessionId">The session id or playsession id.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId);
|
Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the dto for session info.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sessionInfo">The session info.</param>
|
|
||||||
/// <returns><see cref="SessionInfoDto"/> of the session.</returns>
|
|
||||||
SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -693,7 +693,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
[GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
[GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||||
private static partial Regex CodecRegex();
|
private static partial Regex CodecRegex();
|
||||||
|
|
||||||
[GeneratedRegex("^\\s\\S{2,3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
[GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||||
private static partial Regex FilterRegex();
|
private static partial Regex FilterRegex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,12 +299,9 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
// Handle WebM
|
// Handle WebM
|
||||||
else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Limit WebM to supported stream types and codecs.
|
// Limit WebM to supported codecs
|
||||||
// FFprobe can report "matroska,webm" for Matroska-like containers, so only keep "webm" if all streams are WebM-compatible.
|
if (mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||||
// Any stream that is not video nor audio is not supported in WebM and should disqualify the webm container probe result.
|
|| (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))))
|
||||||
if (mediaStreams.Any(stream => stream.Type is not MediaStreamType.Video and not MediaStreamType.Audio)
|
|
||||||
|| mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))
|
|
||||||
|| (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))))
|
|
||||||
{
|
{
|
||||||
splitFormat[i] = string.Empty;
|
splitFormat[i] = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -856,12 +853,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
}
|
}
|
||||||
|
|
||||||
// http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe
|
// http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe
|
||||||
if (string.IsNullOrEmpty(streamInfo.SampleAspectRatio)
|
if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
|
||||||
&& string.IsNullOrEmpty(streamInfo.DisplayAspectRatio))
|
|
||||||
{
|
|
||||||
stream.IsAnamorphic = false;
|
|
||||||
}
|
|
||||||
else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
stream.IsAnamorphic = false;
|
stream.IsAnamorphic = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
{
|
{
|
||||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
if (!File.Exists(outputPath))
|
||||||
{
|
{
|
||||||
await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -423,22 +423,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
else if (!File.Exists(outputPath))
|
||||||
{
|
{
|
||||||
failed = true;
|
failed = true;
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Deleting converted subtitle due to failure: {Path}", outputPath);
|
|
||||||
_fileSystem.DeleteFile(outputPath);
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failed)
|
if (failed)
|
||||||
@@ -512,7 +499,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
|
|
||||||
var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
|
var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (File.Exists(outputPath) && _fileSystem.GetFileInfo(outputPath).Length > 0)
|
if (File.Exists(outputPath))
|
||||||
{
|
{
|
||||||
releaser.Dispose();
|
releaser.Dispose();
|
||||||
continue;
|
continue;
|
||||||
@@ -569,7 +556,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
var outputPaths = new List<string>();
|
var outputPaths = new List<string>();
|
||||||
var args = string.Format(
|
var args = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"-i {0}",
|
"-i {0} -copyts",
|
||||||
inputPath);
|
inputPath);
|
||||||
|
|
||||||
foreach (var subtitleStream in subtitleStreams)
|
foreach (var subtitleStream in subtitleStreams)
|
||||||
@@ -594,7 +581,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
outputPaths.Add(outputPath);
|
outputPaths.Add(outputPath);
|
||||||
args += string.Format(
|
args += string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
" -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"",
|
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
|
||||||
streamIndex,
|
streamIndex,
|
||||||
outputCodec,
|
outputCodec,
|
||||||
outputPath);
|
outputPath);
|
||||||
@@ -613,7 +600,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
var outputPaths = new List<string>();
|
var outputPaths = new List<string>();
|
||||||
var args = string.Format(
|
var args = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"-i {0}",
|
"-i {0} -copyts",
|
||||||
inputPath);
|
inputPath);
|
||||||
|
|
||||||
foreach (var subtitleStream in subtitleStreams)
|
foreach (var subtitleStream in subtitleStreams)
|
||||||
@@ -639,7 +626,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
outputPaths.Add(outputPath);
|
outputPaths.Add(outputPath);
|
||||||
args += string.Format(
|
args += string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
" -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"",
|
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
|
||||||
streamIndex,
|
streamIndex,
|
||||||
outputCodec,
|
outputCodec,
|
||||||
outputPath);
|
outputPath);
|
||||||
@@ -726,24 +713,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
{
|
{
|
||||||
foreach (var outputPath in outputPaths)
|
foreach (var outputPath in outputPaths)
|
||||||
{
|
{
|
||||||
if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
if (!File.Exists(outputPath))
|
||||||
{
|
{
|
||||||
_logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
|
_logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
|
||||||
failed = true;
|
failed = true;
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
|
|
||||||
_fileSystem.DeleteFile(outputPath);
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,7 +755,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
{
|
{
|
||||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
if (!File.Exists(outputPath))
|
||||||
{
|
{
|
||||||
var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
|
var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
|
||||||
|
|
||||||
@@ -884,22 +857,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
|
else if (!File.Exists(outputPath))
|
||||||
{
|
{
|
||||||
failed = true;
|
failed = true;
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
|
|
||||||
_fileSystem.DeleteFile(outputPath);
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failed)
|
if (failed)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Model</PackageId>
|
<PackageId>Jellyfin.Model</PackageId>
|
||||||
<VersionPrefix>10.11.9</VersionPrefix>
|
<VersionPrefix>10.11.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
if (isFirstRefresh)
|
if (isFirstRefresh)
|
||||||
{
|
{
|
||||||
await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false);
|
await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next run metadata providers
|
// Next run metadata providers
|
||||||
@@ -247,7 +247,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false);
|
await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateType;
|
return updateType;
|
||||||
@@ -275,14 +275,9 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken)
|
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
|
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
|
||||||
if (reattachUserData)
|
|
||||||
{
|
|
||||||
await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.Item.SupportsPeople && result.People is not null)
|
if (result.Item.SupportsPeople && result.People is not null)
|
||||||
{
|
{
|
||||||
var baseItem = result.Item;
|
var baseItem = result.Item;
|
||||||
|
|||||||
@@ -262,28 +262,9 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
|
|
||||||
private void FetchShortcutInfo(BaseItem item)
|
private void FetchShortcutInfo(BaseItem item)
|
||||||
{
|
{
|
||||||
var shortcutPath = File.ReadAllLines(item.Path)
|
item.ShortcutPath = File.ReadAllLines(item.Path)
|
||||||
.Select(NormalizeStrmLine)
|
.Select(NormalizeStrmLine)
|
||||||
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#'));
|
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#'));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(shortcutPath))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only allow remote URLs in .strm files to prevent local file access
|
|
||||||
if (Uri.TryCreate(shortcutPath, UriKind.Absolute, out var uri)
|
|
||||||
&& (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
item.ShortcutPath = shortcutPath;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Ignoring invalid or non-remote .strm path in {File}: {Path}", item.Path, shortcutPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.Path).ToArray();
|
targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replaceData || targetItem.Shares.Count == 0)
|
if (replaceData || targetItem.Shares.Count == 0)
|
||||||
|
|||||||
@@ -303,7 +303,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
|||||||
CrewMember = crewMember,
|
CrewMember = crewMember,
|
||||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||||
})
|
})
|
||||||
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
|
.Where(entry =>
|
||||||
|
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||||
|
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (config.HideMissingCrewMembers)
|
if (config.HideMissingCrewMembers)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -275,7 +275,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
CrewMember = crewMember,
|
CrewMember = crewMember,
|
||||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||||
})
|
})
|
||||||
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
|
.Where(entry =>
|
||||||
|
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||||
|
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (config.HideMissingCrewMembers)
|
if (config.HideMissingCrewMembers)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
CrewMember = crewMember,
|
CrewMember = crewMember,
|
||||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||||
})
|
})
|
||||||
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
|
.Where(entry =>
|
||||||
|
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||||
|
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (config.HideMissingCrewMembers)
|
if (config.HideMissingCrewMembers)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -367,7 +367,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
CrewMember = crewMember,
|
CrewMember = crewMember,
|
||||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||||
})
|
})
|
||||||
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
|
.Where(entry =>
|
||||||
|
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||||
|
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (config.HideMissingCrewMembers)
|
if (config.HideMissingCrewMembers)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -518,10 +518,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use "original" as default size if size is null or empty to prevent malformed URLs
|
return _tmDbClient.GetImageUrl(size, path, true).ToString();
|
||||||
var imageSize = string.IsNullOrEmpty(size) ? "original" : size;
|
|
||||||
|
|
||||||
return _tmDbClient.GetImageUrl(imageSize, path, true).ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -69,20 +69,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
|||||||
/// <returns>The Jellyfin person type.</returns>
|
/// <returns>The Jellyfin person type.</returns>
|
||||||
public static PersonKind MapCrewToPersonType(Crew crew)
|
public static PersonKind MapCrewToPersonType(Crew crew)
|
||||||
{
|
{
|
||||||
if (crew.Department.Equals("directing", StringComparison.OrdinalIgnoreCase)
|
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
|
||||||
&& crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase))
|
&& crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return PersonKind.Director;
|
return PersonKind.Director;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
|
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
|
||||||
&& crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase))
|
&& crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return PersonKind.Producer;
|
return PersonKind.Producer;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)
|
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase))
|
||||||
&& (crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase) || crew.Job.Equals("screenplay", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
{
|
||||||
return PersonKind.Writer;
|
return PersonKind.Writer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Naming.Common;
|
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -33,7 +32,6 @@ namespace MediaBrowser.Providers.Subtitles
|
|||||||
private readonly ILibraryMonitor _monitor;
|
private readonly ILibraryMonitor _monitor;
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
private readonly HashSet<string> _allowedSubtitleFormats;
|
|
||||||
|
|
||||||
private readonly ISubtitleProvider[] _subtitleProviders;
|
private readonly ISubtitleProvider[] _subtitleProviders;
|
||||||
|
|
||||||
@@ -43,8 +41,7 @@ namespace MediaBrowser.Providers.Subtitles
|
|||||||
ILibraryMonitor monitor,
|
ILibraryMonitor monitor,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
ILocalizationManager localizationManager,
|
ILocalizationManager localizationManager,
|
||||||
IEnumerable<ISubtitleProvider> subtitleProviders,
|
IEnumerable<ISubtitleProvider> subtitleProviders)
|
||||||
NamingOptions namingOptions)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
@@ -54,9 +51,6 @@ namespace MediaBrowser.Providers.Subtitles
|
|||||||
_subtitleProviders = subtitleProviders
|
_subtitleProviders = subtitleProviders
|
||||||
.OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
|
.OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
_allowedSubtitleFormats = new HashSet<string>(
|
|
||||||
namingOptions.SubtitleFileExtensions.Select(e => e.TrimStart('.')),
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -177,12 +171,6 @@ namespace MediaBrowser.Providers.Subtitles
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task UploadSubtitle(Video video, SubtitleResponse response)
|
public Task UploadSubtitle(Video video, SubtitleResponse response)
|
||||||
{
|
{
|
||||||
var format = response.Format;
|
|
||||||
if (string.IsNullOrEmpty(format) || !_allowedSubtitleFormats.Contains(format))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Unsupported subtitle format: '{format}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
|
var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
|
||||||
return TrySaveSubtitle(video, libraryOptions, response);
|
return TrySaveSubtitle(video, libraryOptions, response);
|
||||||
}
|
}
|
||||||
@@ -205,13 +193,7 @@ namespace MediaBrowser.Providers.Subtitles
|
|||||||
}
|
}
|
||||||
|
|
||||||
var savePaths = new List<string>();
|
var savePaths = new List<string>();
|
||||||
var language = response.Language.ToLowerInvariant();
|
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
|
||||||
if (language.AsSpan().IndexOfAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) >= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Language contains invalid characters.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + language;
|
|
||||||
|
|
||||||
if (response.IsForced)
|
if (response.IsForced)
|
||||||
{
|
{
|
||||||
@@ -239,22 +221,15 @@ namespace MediaBrowser.Providers.Subtitles
|
|||||||
|
|
||||||
private async Task TrySaveToFiles(Stream stream, List<string> savePaths, Video video, string extension)
|
private async Task TrySaveToFiles(Stream stream, List<string> savePaths, Video video, string extension)
|
||||||
{
|
{
|
||||||
if (!_allowedSubtitleFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Invalid subtitle format: {extension}");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Exception>? exs = null;
|
List<Exception>? exs = null;
|
||||||
|
|
||||||
foreach (var savePath in savePaths)
|
foreach (var savePath in savePaths)
|
||||||
{
|
{
|
||||||
var path = Path.GetFullPath(savePath + "." + extension);
|
var path = savePath + "." + extension;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var containingFolder = video.ContainingFolderPath + Path.DirectorySeparatorChar;
|
if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)
|
||||||
var metadataFolder = video.GetInternalMetadataPath() + Path.DirectorySeparatorChar;
|
|| path.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
|
||||||
if (path.StartsWith(containingFolder, StringComparison.Ordinal)
|
|
||||||
|| path.StartsWith(metadataFolder, StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
var fileExists = File.Exists(path);
|
var fileExists = File.Exists(path);
|
||||||
var counter = 0;
|
var counter = 0;
|
||||||
|
|||||||
@@ -547,7 +547,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
|||||||
writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
|
writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection))
|
if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection))
|
||||||
{
|
{
|
||||||
writer.WriteElementString("collectionnumber", tmdbCollection);
|
writer.WriteElementString("collectionnumber", tmdbCollection);
|
||||||
writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());
|
writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.11.9")]
|
[assembly: AssemblyVersion("10.11.4")]
|
||||||
[assembly: AssemblyFileVersion("10.11.9")]
|
[assembly: AssemblyFileVersion("10.11.4")]
|
||||||
|
|||||||
@@ -268,11 +268,6 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
|
|||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (DbUpdateConcurrencyException)
|
|
||||||
{
|
|
||||||
// a concurrency exception is supposed to be always handled by the invoker of the method, logging it here is only causing log bloat.
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.LogError(e, "Error trying to save changes.");
|
logger.LogError(e, "Error trying to save changes.");
|
||||||
@@ -294,11 +289,6 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
|
|||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (DbUpdateConcurrencyException)
|
|
||||||
{
|
|
||||||
// a concurrency exception is supposed to be always handled by the invoker of the method, logging it here is only causing log bloat.
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.LogError(e, "Error trying to save changes.");
|
logger.LogError(e, "Error trying to save changes.");
|
||||||
|
|||||||
@@ -24,29 +24,61 @@ public class SkiaEncoder : IImageEncoder
|
|||||||
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||||
private readonly ILogger<SkiaEncoder> _logger;
|
private readonly ILogger<SkiaEncoder> _logger;
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
private static readonly SKTypeface?[] _typefaces = InitializeTypefaces();
|
private static readonly SKImageFilter _imageFilter;
|
||||||
private static readonly SKImageFilter _imageFilter = SKImageFilter.CreateMatrixConvolution(
|
private static readonly SKTypeface[] _typefaces;
|
||||||
new SKSizeI(3, 3),
|
|
||||||
[
|
|
||||||
0, -.1f, 0,
|
|
||||||
-.1f, 1.4f, -.1f,
|
|
||||||
0, -.1f, 0
|
|
||||||
],
|
|
||||||
1f,
|
|
||||||
0f,
|
|
||||||
new SKPointI(1, 1),
|
|
||||||
SKShaderTileMode.Clamp,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default sampling options, equivalent to old high quality filter settings when upscaling.
|
/// The default sampling options, equivalent to old high quality filter settings when upscaling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly SKSamplingOptions UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
|
public static readonly SKSamplingOptions UpscaleSamplingOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling.
|
/// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly SKSamplingOptions DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
|
public static readonly SKSamplingOptions DefaultSamplingOptions;
|
||||||
|
|
||||||
|
#pragma warning disable CA1810
|
||||||
|
static SkiaEncoder()
|
||||||
|
#pragma warning restore CA1810
|
||||||
|
{
|
||||||
|
var kernel = new[]
|
||||||
|
{
|
||||||
|
0, -.1f, 0,
|
||||||
|
-.1f, 1.4f, -.1f,
|
||||||
|
0, -.1f, 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
var kernelSize = new SKSizeI(3, 3);
|
||||||
|
var kernelOffset = new SKPointI(1, 1);
|
||||||
|
_imageFilter = SKImageFilter.CreateMatrixConvolution(
|
||||||
|
kernelSize,
|
||||||
|
kernel,
|
||||||
|
1f,
|
||||||
|
0f,
|
||||||
|
kernelOffset,
|
||||||
|
SKShaderTileMode.Clamp,
|
||||||
|
true);
|
||||||
|
|
||||||
|
// Initialize the list of typefaces
|
||||||
|
// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
|
||||||
|
// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F)
|
||||||
|
_typefaces =
|
||||||
|
[
|
||||||
|
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '鸡'), // CJK Simplified Chinese
|
||||||
|
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '雞'), // CJK Traditional Chinese
|
||||||
|
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ノ'), // CJK Japanese
|
||||||
|
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '각'), // CJK Korean
|
||||||
|
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the 💩emoji
|
||||||
|
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ז'), // Hebrew
|
||||||
|
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic
|
||||||
|
SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font
|
||||||
|
];
|
||||||
|
|
||||||
|
// use cubic for upscaling
|
||||||
|
UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
|
||||||
|
// use bilinear for everything else
|
||||||
|
DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
|
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
|
||||||
@@ -100,7 +132,7 @@ public class SkiaEncoder : IImageEncoder
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the default typeface to use.
|
/// Gets the default typeface to use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static SKTypeface? DefaultTypeFace => _typefaces.Last();
|
public static SKTypeface DefaultTypeFace => _typefaces.Last();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if the native lib is available.
|
/// Check if the native lib is available.
|
||||||
@@ -120,40 +152,6 @@ public class SkiaEncoder : IImageEncoder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize the list of typefaces
|
|
||||||
/// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
|
|
||||||
/// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F).
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The list of typefaces.</returns>
|
|
||||||
private static SKTypeface?[] InitializeTypefaces()
|
|
||||||
{
|
|
||||||
int[] chars = [
|
|
||||||
'鸡', // CJK Simplified Chinese
|
|
||||||
'雞', // CJK Traditional Chinese
|
|
||||||
'ノ', // CJK Japanese
|
|
||||||
'각', // CJK Korean
|
|
||||||
128169, // Emojis, 128169 is the Pile of Poo (💩) emoji
|
|
||||||
'ז', // Hebrew
|
|
||||||
'ي' // Arabic
|
|
||||||
];
|
|
||||||
var fonts = new List<SKTypeface>(chars.Length + 1);
|
|
||||||
foreach (var ch in chars)
|
|
||||||
{
|
|
||||||
var font = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, ch);
|
|
||||||
if (font is not null)
|
|
||||||
{
|
|
||||||
fonts.Add(font);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default font
|
|
||||||
fonts.Add(SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright)
|
|
||||||
?? SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'a'));
|
|
||||||
|
|
||||||
return fonts.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
|
/// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -811,7 +809,7 @@ public class SkiaEncoder : IImageEncoder
|
|||||||
{
|
{
|
||||||
foreach (var typeface in _typefaces)
|
foreach (var typeface in _typefaces)
|
||||||
{
|
{
|
||||||
if (typeface is not null && typeface.ContainsGlyphs(c))
|
if (typeface.ContainsGlyphs(c))
|
||||||
{
|
{
|
||||||
return typeface;
|
return typeface;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Extensions</PackageId>
|
<PackageId>Jellyfin.Extensions</PackageId>
|
||||||
<VersionPrefix>10.11.9</VersionPrefix>
|
<VersionPrefix>10.11.4</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -156,13 +156,6 @@ namespace Jellyfin.LiveTv.IO
|
|||||||
if (mediaSource.ReadAtNativeFramerate)
|
if (mediaSource.ReadAtNativeFramerate)
|
||||||
{
|
{
|
||||||
inputModifier += " -re";
|
inputModifier += " -re";
|
||||||
|
|
||||||
// Set a larger catchup value to revert to the old behavior,
|
|
||||||
// otherwise, remuxing might stall due to this new option
|
|
||||||
if (_mediaEncoder.EncoderVersion >= new Version(8, 0))
|
|
||||||
{
|
|
||||||
inputModifier += " -readrate_catchup 100";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaSource.RequiresLooping)
|
if (mediaSource.RequiresLooping)
|
||||||
|
|||||||
@@ -1204,7 +1204,7 @@ namespace Jellyfin.LiveTv
|
|||||||
{
|
{
|
||||||
Services = services,
|
Services = services,
|
||||||
IsEnabled = services.Length > 0,
|
IsEnabled = services.Length > 0,
|
||||||
EnabledUsers = _userManager.GetUsers()
|
EnabledUsers = _userManager.Users
|
||||||
.Where(IsLiveTvEnabled)
|
.Where(IsLiveTvEnabled)
|
||||||
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
|
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
|
||||||
.ToArray()
|
.ToArray()
|
||||||
@@ -1220,7 +1220,7 @@ namespace Jellyfin.LiveTv
|
|||||||
|
|
||||||
public IEnumerable<User> GetEnabledUsers()
|
public IEnumerable<User> GetEnabledUsers()
|
||||||
{
|
{
|
||||||
return _userManager.GetUsers()
|
return _userManager.Users
|
||||||
.Where(IsLiveTvEnabled);
|
.Where(IsLiveTvEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ namespace Jellyfin.LiveTv.Recordings
|
|||||||
|
|
||||||
private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
|
private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
|
||||||
{
|
{
|
||||||
var users = _userManager.GetUsers()
|
var users = _userManager.Users
|
||||||
.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess))
|
.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess))
|
||||||
.Select(i => i.Id)
|
.Select(i => i.Id)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|||||||
@@ -93,13 +93,6 @@ namespace Jellyfin.LiveTv.TunerHosts
|
|||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
|
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
|
||||||
{
|
{
|
||||||
if (!IsValidChannelUrl(trimmedLine))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Skipping M3U channel entry with non-HTTP path: {Path}", trimmedLine);
|
|
||||||
extInf = string.Empty;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine);
|
var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine);
|
||||||
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
@@ -254,16 +247,6 @@ namespace Jellyfin.LiveTv.TunerHosts
|
|||||||
return numberString;
|
return numberString;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidChannelUrl(string url)
|
|
||||||
{
|
|
||||||
return Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
|
||||||
&& (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(uri.Scheme, "udp", StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsValidChannelNumber(string numberString)
|
private static bool IsValidChannelNumber(string numberString)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(numberString)
|
if (string.IsNullOrWhiteSpace(numberString)
|
||||||
|
|||||||
@@ -195,18 +195,6 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
|
|||||||
Assert.False(res.MediaStreams[0].IsAVC);
|
Assert.False(res.MediaStreams[0].IsAVC);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetMediaInfo_WebM_Like_Mkv()
|
|
||||||
{
|
|
||||||
var bytes = File.ReadAllBytes("Test Data/Probing/video_web_like_mkv_with_subtitle.json");
|
|
||||||
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
|
|
||||||
|
|
||||||
MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File);
|
|
||||||
|
|
||||||
Assert.Equal("mkv", res.Container);
|
|
||||||
Assert.Equal(3, res.MediaStreams.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetMediaInfo_ProgressiveVideoNoFieldOrder_Success()
|
public void GetMediaInfo_ProgressiveVideoNoFieldOrder_Success()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
{
|
|
||||||
"streams": [
|
|
||||||
{
|
|
||||||
"index": 0,
|
|
||||||
"codec_name": "vp8",
|
|
||||||
"codec_long_name": "On2 VP8",
|
|
||||||
"profile": "1",
|
|
||||||
"codec_type": "video",
|
|
||||||
"codec_tag_string": "[0][0][0][0]",
|
|
||||||
"codec_tag": "0x0000",
|
|
||||||
"width": 540,
|
|
||||||
"height": 360,
|
|
||||||
"coded_width": 540,
|
|
||||||
"coded_height": 360,
|
|
||||||
"closed_captions": 0,
|
|
||||||
"film_grain": 0,
|
|
||||||
"has_b_frames": 0,
|
|
||||||
"sample_aspect_ratio": "1:1",
|
|
||||||
"display_aspect_ratio": "3:2",
|
|
||||||
"pix_fmt": "yuv420p",
|
|
||||||
"level": -99,
|
|
||||||
"field_order": "progressive",
|
|
||||||
"refs": 1,
|
|
||||||
"r_frame_rate": "2997/125",
|
|
||||||
"avg_frame_rate": "2997/125",
|
|
||||||
"time_base": "1/1000",
|
|
||||||
"start_pts": 0,
|
|
||||||
"start_time": "0.000000",
|
|
||||||
"disposition": {
|
|
||||||
"default": 1,
|
|
||||||
"dub": 0,
|
|
||||||
"original": 0,
|
|
||||||
"comment": 0,
|
|
||||||
"lyrics": 0,
|
|
||||||
"karaoke": 0,
|
|
||||||
"forced": 0,
|
|
||||||
"hearing_impaired": 0,
|
|
||||||
"visual_impaired": 0,
|
|
||||||
"clean_effects": 0,
|
|
||||||
"attached_pic": 0,
|
|
||||||
"timed_thumbnails": 0,
|
|
||||||
"captions": 0,
|
|
||||||
"descriptions": 0,
|
|
||||||
"metadata": 0,
|
|
||||||
"dependent": 0,
|
|
||||||
"still_image": 0
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"language": "eng"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"index": 1,
|
|
||||||
"codec_name": "vorbis",
|
|
||||||
"codec_long_name": "Vorbis",
|
|
||||||
"codec_type": "audio",
|
|
||||||
"codec_tag_string": "[0][0][0][0]",
|
|
||||||
"codec_tag": "0x0000",
|
|
||||||
"sample_fmt": "fltp",
|
|
||||||
"sample_rate": "44100",
|
|
||||||
"channels": 2,
|
|
||||||
"channel_layout": "stereo",
|
|
||||||
"bits_per_sample": 0,
|
|
||||||
"r_frame_rate": "0/0",
|
|
||||||
"avg_frame_rate": "0/0",
|
|
||||||
"time_base": "1/1000",
|
|
||||||
"start_pts": 0,
|
|
||||||
"start_time": "0.000000",
|
|
||||||
"duration": "117.707000",
|
|
||||||
"bit_rate": "127998",
|
|
||||||
"disposition": {
|
|
||||||
"default": 1,
|
|
||||||
"dub": 0,
|
|
||||||
"original": 0,
|
|
||||||
"comment": 0,
|
|
||||||
"lyrics": 0,
|
|
||||||
"karaoke": 0,
|
|
||||||
"forced": 0,
|
|
||||||
"hearing_impaired": 0,
|
|
||||||
"visual_impaired": 0,
|
|
||||||
"clean_effects": 0,
|
|
||||||
"attached_pic": 0,
|
|
||||||
"timed_thumbnails": 0,
|
|
||||||
"captions": 0,
|
|
||||||
"descriptions": 0,
|
|
||||||
"metadata": 0,
|
|
||||||
"dependent": 0,
|
|
||||||
"still_image": 0
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"language": "eng"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"index": 2,
|
|
||||||
"codec_name": "subrip",
|
|
||||||
"codec_long_name": "SubRip subtitle",
|
|
||||||
"codec_type": "subtitle",
|
|
||||||
"codec_tag_string": "[0][0][0][0]",
|
|
||||||
"codec_tag": "0x0000",
|
|
||||||
"disposition": {
|
|
||||||
"default": 0,
|
|
||||||
"dub": 0,
|
|
||||||
"original": 0,
|
|
||||||
"comment": 0,
|
|
||||||
"lyrics": 0,
|
|
||||||
"karaoke": 0,
|
|
||||||
"forced": 0,
|
|
||||||
"hearing_impaired": 0,
|
|
||||||
"visual_impaired": 0,
|
|
||||||
"clean_effects": 0,
|
|
||||||
"attached_pic": 0,
|
|
||||||
"timed_thumbnails": 0,
|
|
||||||
"captions": 0,
|
|
||||||
"descriptions": 0,
|
|
||||||
"metadata": 0,
|
|
||||||
"dependent": 0,
|
|
||||||
"still_image": 0
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"language": "eng"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"format": {
|
|
||||||
"filename": "sample.mkv",
|
|
||||||
"nb_streams": 3,
|
|
||||||
"nb_programs": 0,
|
|
||||||
"format_name": "matroska,webm",
|
|
||||||
"format_long_name": "Matroska / WebM",
|
|
||||||
"start_time": "0.000000",
|
|
||||||
"duration": "117.700914",
|
|
||||||
"size": "8566268",
|
|
||||||
"bit_rate": "582239",
|
|
||||||
"probe_score": 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -203,25 +203,6 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
|||||||
Assert.Null(localizationManager.GetRatingScore(value));
|
Assert.Null(localizationManager.GetRatingScore(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("TV-MA", "DE", 17, 1)] // US-only rating, DE country code
|
|
||||||
[InlineData("PG-13", "FR", 13, 0)] // US-only rating, FR country code
|
|
||||||
[InlineData("R", "JP", 17, 0)] // US-only rating, JP country code
|
|
||||||
public async Task GetRatingScore_FallbackPrioritizesUS_Success(string rating, string countryCode, int expectedScore, int? expectedSubScore)
|
|
||||||
{
|
|
||||||
var localizationManager = Setup(new ServerConfiguration()
|
|
||||||
{
|
|
||||||
MetadataCountryCode = countryCode
|
|
||||||
});
|
|
||||||
await localizationManager.LoadAll();
|
|
||||||
|
|
||||||
var score = localizationManager.GetRatingScore(rating);
|
|
||||||
|
|
||||||
Assert.NotNull(score);
|
|
||||||
Assert.Equal(expectedScore, score.Score);
|
|
||||||
Assert.Equal(expectedSubScore, score.SubScore);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Default", "Default")]
|
[InlineData("Default", "Default")]
|
||||||
[InlineData("HeaderLiveTV", "Live TV")]
|
[InlineData("HeaderLiveTV", "Live TV")]
|
||||||
|
|||||||
Reference in New Issue
Block a user