From e1190d15d6ca0b7cad2b4991e524b35ecca35949 Mon Sep 17 00:00:00 2001
From: herby2212 <12448284+herby2212@users.noreply.github.com>
Date: Mon, 1 May 2023 20:11:22 +0200
Subject: option to disable and configure inactive session threshold
---
MediaBrowser.Model/Configuration/ServerConfiguration.cs | 6 ++++++
1 file changed, 6 insertions(+)
(limited to 'MediaBrowser.Model')
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 07f02d187..f619f384c 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -157,6 +157,12 @@ namespace MediaBrowser.Model.Configuration
/// The remaining time in minutes.
public int MaxAudiobookResume { get; set; } = 5;
+ ///
+ /// Gets or sets the threshold in minutes after a inactive session gets closed automatically.
+ ///
+ /// The close inactive session threshold in minutes.
+ public int InactiveSessionThreshold { get; set; } = 10;
+
///
/// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
/// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
--
cgit v1.2.3
From ace89e45976a65a365a3d9d7a2ed737a61d584d4 Mon Sep 17 00:00:00 2001
From: herby2212 <12448284+herby2212@users.noreply.github.com>
Date: Sun, 14 May 2023 15:05:03 +0200
Subject: fix formatting and update summary
---
Emby.Server.Implementations/Session/SessionManager.cs | 9 +++++----
MediaBrowser.Model/Configuration/ServerConfiguration.cs | 3 ++-
2 files changed, 7 insertions(+), 5 deletions(-)
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 056c3e4b6..14a62626c 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -404,7 +404,7 @@ namespace Emby.Server.Implementations.Session
session.LastPlaybackCheckIn = DateTime.UtcNow;
}
- if (info.IsPaused && session.LastPausedDate.HasValue == false)
+ if (info.IsPaused && session.LastPausedDate is null)
{
session.LastPausedDate = DateTime.UtcNow;
}
@@ -656,9 +656,10 @@ namespace Emby.Server.Implementations.Session
private async void CheckForInactiveSteams(object state)
{
var pausedSessions = Sessions.Where(i =>
- (i.NowPlayingItem is not null) &&
- (i.PlayState.IsPaused == true) &&
- (i.LastPausedDate is not null)).ToList();
+ i.NowPlayingItem is not null
+ && i.PlayState.IsPaused
+ && i.LastPausedDate is not null)
+ .ToList();
if (pausedSessions.Count > 0)
{
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index f619f384c..40dcf50b7 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -159,8 +159,9 @@ namespace MediaBrowser.Model.Configuration
///
/// Gets or sets the threshold in minutes after a inactive session gets closed automatically.
+ /// If set to 0 the check for inactive sessions gets disabled.
///
- /// The close inactive session threshold in minutes.
+ /// The close inactive session threshold in minutes. 0 to disable.
public int InactiveSessionThreshold { get; set; } = 10;
///
--
cgit v1.2.3
From ca7d1a13000ad948eebbfdeb40542312f3e37d3e Mon Sep 17 00:00:00 2001
From: nicknsy <20588554+nicknsy@users.noreply.github.com>
Date: Wed, 22 Feb 2023 00:08:35 -0800
Subject: Trickplay generation, manager, storage
---
Emby.Server.Implementations/ApplicationHost.cs | 3 +
.../Data/SqliteItemRepository.cs | 123 +++++++
Emby.Server.Implementations/Dto/DtoService.cs | 5 +
.../MediaEncoding/EncodingHelper.cs | 35 ++
.../MediaEncoding/IMediaEncoder.cs | 27 ++
.../Persistence/IItemRepository.cs | 21 ++
.../Trickplay/ITrickplayManager.cs | 54 +++
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 173 ++++++++++
.../Configuration/EncodingOptions.cs | 12 +
MediaBrowser.Model/Dto/BaseItemDto.cs | 6 +
MediaBrowser.Model/Entities/TrickplayTilesInfo.cs | 50 +++
MediaBrowser.Model/Querying/ItemFields.cs | 5 +
.../MediaBrowser.Providers.csproj | 1 +
.../Trickplay/TrickplayImagesTask.cs | 116 +++++++
.../Trickplay/TrickplayManager.cs | 363 +++++++++++++++++++++
.../Trickplay/TrickplayProvider.cs | 109 +++++++
16 files changed, 1103 insertions(+)
create mode 100644 MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
create mode 100644 MediaBrowser.Model/Entities/TrickplayTilesInfo.cs
create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayManager.cs
create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 7969577bc..1e0bb0cd6 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -78,6 +78,7 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
@@ -96,6 +97,7 @@ using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles;
+using MediaBrowser.Providers.Trickplay;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -591,6 +593,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index ca8f605a0..8ec24522b 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -48,6 +48,7 @@ namespace Emby.Server.Implementations.Data
{
private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2";
+ private const string TrickplayTableName = "Trickplay";
private const string SaveItemCommandText =
@"replace into TypedBaseItems
@@ -383,6 +384,8 @@ namespace Emby.Server.Implementations.Data
"create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
+ "create table if not exists " + TrickplayTableName + " (ItemId GUID, Width INT NOT NULL, Height INT NOT NULL, TileWidth INT NOT NULL, TileHeight INT NOT NULL, TileCount INT NOT NULL, Interval INT NOT NULL, Bandwidth INT NOT NULL, PRIMARY KEY (ItemId, Width))",
+
CreateMediaStreamsTableCommand,
CreateMediaAttachmentsTableCommand,
@@ -2135,6 +2138,126 @@ namespace Emby.Server.Implementations.Data
}
}
+ ///
+ public Dictionary GetTilesResolutions(Guid itemId)
+ {
+ CheckDisposed();
+
+ var tilesResolutions = new Dictionary();
+ using (var connection = GetConnection(true))
+ {
+ using (var statement = PrepareStatement(connection, "select Width,Height,TileWidth,TileHeight,TileCount,Interval,Bandwidth from " + TrickplayTableName + " where ItemId = @ItemId order by Width asc"))
+ {
+ statement.TryBind("@ItemId", itemId);
+
+ foreach (var row in statement.ExecuteQuery())
+ {
+ TrickplayTilesInfo tilesInfo = GetTrickplayTilesInfo(row);
+ tilesResolutions[tilesInfo.Width] = tilesInfo;
+ }
+ }
+ }
+
+ return tilesResolutions;
+ }
+
+ ///
+ public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo)
+ {
+ CheckDisposed();
+
+ ArgumentNullException.ThrowIfNull(tilesInfo);
+
+ var idBlob = itemId.ToByteArray();
+ using (var connection = GetConnection(false))
+ {
+ connection.RunInTransaction(
+ db =>
+ {
+ // Delete old tiles info
+ db.Execute("delete from " + TrickplayTableName + " where ItemId=@ItemId and Width=@Width", idBlob, tilesInfo.Width);
+ db.Execute(
+ "insert into " + TrickplayTableName + " values (@ItemId, @Width, @Height, @TileWidth, @TileHeight, @TileCount, @Interval, @Bandwidth)",
+ idBlob,
+ tilesInfo.Width,
+ tilesInfo.Height,
+ tilesInfo.TileWidth,
+ tilesInfo.TileHeight,
+ tilesInfo.TileCount,
+ tilesInfo.Interval,
+ tilesInfo.Bandwidth);
+ },
+ TransactionMode);
+ }
+ }
+
+ ///
+ public Dictionary> GetTrickplayManifest(BaseItem item)
+ {
+ CheckDisposed();
+
+ var trickplayManifest = new Dictionary>();
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ var mediaSourceId = Guid.Parse(mediaSource.Id);
+ var tilesResolutions = GetTilesResolutions(mediaSourceId);
+
+ if (tilesResolutions.Count > 0)
+ {
+ trickplayManifest[mediaSourceId] = tilesResolutions;
+ }
+ }
+
+ return trickplayManifest;
+ }
+
+ ///
+ /// Gets the trickplay tiles info.
+ ///
+ /// The reader.
+ /// TrickplayTilesInfo.
+ private TrickplayTilesInfo GetTrickplayTilesInfo(IReadOnlyList reader)
+ {
+ var tilesInfo = new TrickplayTilesInfo();
+
+ if (reader.TryGetInt32(0, out var width))
+ {
+ tilesInfo.Width = width;
+ }
+
+ if (reader.TryGetInt32(1, out var height))
+ {
+ tilesInfo.Height = height;
+ }
+
+ if (reader.TryGetInt32(2, out var tileWidth))
+ {
+ tilesInfo.TileWidth = tileWidth;
+ }
+
+ if (reader.TryGetInt32(3, out var tileHeight))
+ {
+ tilesInfo.TileHeight = tileHeight;
+ }
+
+ if (reader.TryGetInt32(4, out var tileCount))
+ {
+ tilesInfo.TileCount = tileCount;
+ }
+
+ if (reader.TryGetInt32(5, out var interval))
+ {
+ tilesInfo.Interval = interval;
+ }
+
+ if (reader.TryGetInt32(6, out var bandwidth))
+ {
+ tilesInfo.Bandwidth = bandwidth;
+ }
+
+ return tilesInfo;
+ }
+
private static bool EnableJoinUserData(InternalItemsQuery query)
{
if (query.User is null)
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 7a6ed2cb8..10352b6ff 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1058,6 +1058,11 @@ namespace Emby.Server.Implementations.Dto
dto.Chapters = _itemRepo.GetChapters(item);
}
+ if (options.ContainsField(ItemFields.Trickplay))
+ {
+ dto.Trickplay = _itemRepo.GetTrickplayManifest(item);
+ }
+
if (video.ExtraType.HasValue)
{
dto.ExtraType = video.ExtraType.Value.ToString();
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index b155d674d..0889a90f4 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -149,6 +149,36 @@ namespace MediaBrowser.Controller.MediaEncoding
return defaultEncoder;
}
+ private string GetMjpegEncoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
+ var defaultEncoder = "mjpeg";
+
+ if (state.VideoType == VideoType.VideoFile)
+ {
+ var hwType = encodingOptions.HardwareAccelerationType;
+
+ var codecMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "vaapi", defaultEncoder + "_vaapi" },
+ { "qsv", defaultEncoder + "_qsv" }
+ };
+
+ if (!string.IsNullOrEmpty(hwType)
+ && encodingOptions.EnableHardwareEncoding
+ && codecMap.ContainsKey(hwType))
+ {
+ var preferredEncoder = codecMap[hwType];
+
+ if (_mediaEncoder.SupportsEncoder(preferredEncoder))
+ {
+ return preferredEncoder;
+ }
+ }
+ }
+
+ return defaultEncoder;
+ }
+
private bool IsVaapiSupported(EncodingJobInfo state)
{
// vaapi will throw an error with this input
@@ -277,6 +307,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetH264Encoder(state, encodingOptions);
}
+ if (string.Equals(codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetMjpegEncoder(state, encodingOptions);
+ }
+
if (string.Equals(codec, "vp8", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
{
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index f830b9f29..aa9faa936 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -137,6 +138,32 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Location of video image.
Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken);
+ ///
+ /// Extracts the video images on interval.
+ ///
+ /// Input file.
+ /// Video container type.
+ /// Media source information.
+ /// Media stream information.
+ /// The interval.
+ /// The maximum width.
+ /// Allow for hardware acceleration.
+ /// Allow for hardware encoding. allowHwAccel must also be true.
+ /// EncodingHelper instance.
+ /// The cancellation token.
+ /// Directory where images where extracted. A given image made before another will always be named with a lower number.
+ Task ExtractVideoImagesOnIntervalAccelerated(
+ string inputFile,
+ string container,
+ MediaSourceInfo mediaSource,
+ MediaStream imageStream,
+ TimeSpan interval,
+ int maxWidth,
+ bool allowHwAccel,
+ bool allowHwEncode,
+ EncodingHelper encodingHelper,
+ CancellationToken cancellationToken);
+
///
/// Gets the media info.
///
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 2c52b2b45..11eb4932c 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -61,6 +61,27 @@ namespace MediaBrowser.Controller.Persistence
/// The list of chapters to save.
void SaveChapters(Guid id, IReadOnlyList chapters);
+ ///
+ /// Get available trickplay resolutions and corresponding info.
+ ///
+ /// The item.
+ /// Map of width resolutions to trickplay tiles info.
+ Dictionary GetTilesResolutions(Guid itemId);
+
+ ///
+ /// Saves trickplay tiles info.
+ ///
+ /// The item.
+ /// The trickplay tiles info.
+ void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo);
+
+ ///
+ /// Gets trickplay data for an item.
+ ///
+ /// The item.
+ /// A map of media source id to a map of tile width to tile info.
+ Dictionary> GetTrickplayManifest(BaseItem item);
+
///
/// Gets the media streams.
///
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
new file mode 100644
index 000000000..bae458f98
--- /dev/null
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Trickplay
+{
+ ///
+ /// Interface ITrickplayManager.
+ ///
+ public interface ITrickplayManager
+ {
+ ///
+ /// Generate or replace trickplay data.
+ ///
+ /// The video.
+ /// Whether or not existing data should be replaced.
+ /// CancellationToken to use for operation.
+ /// Task.
+ Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken);
+
+ ///
+ /// Get available trickplay resolutions and corresponding info.
+ ///
+ /// The item.
+ /// Map of width resolutions to trickplay tiles info.
+ Dictionary GetTilesResolutions(Guid itemId);
+
+ ///
+ /// Saves trickplay tiles info.
+ ///
+ /// The item.
+ /// The trickplay tiles info.
+ void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo);
+
+ ///
+ /// Gets the trickplay manifest.
+ ///
+ /// The item.
+ /// A map of media source id to a map of tile width to tile info.
+ Dictionary> GetTrickplayManifest(BaseItem item);
+
+ ///
+ /// Gets the path to a trickplay tiles image.
+ ///
+ /// The item.
+ /// The width of a single tile.
+ /// The tile grid's index.
+ /// The absolute path.
+ string GetTrickplayTilePath(BaseItem item, int width, int index);
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 4e63d205c..7f8ec03fa 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -21,6 +21,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Probing;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -28,8 +29,10 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
+using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using static Nikse.SubtitleEdit.Core.Common.IfoParser;
namespace MediaBrowser.MediaEncoding.Encoder
{
@@ -775,6 +778,176 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
///
+ public Task ExtractVideoImagesOnIntervalAccelerated(
+ string inputFile,
+ string container,
+ MediaSourceInfo mediaSource,
+ MediaStream imageStream,
+ TimeSpan interval,
+ int maxWidth,
+ bool allowHwAccel,
+ bool allowHwEncode,
+ EncodingHelper encodingHelper,
+ CancellationToken cancellationToken)
+ {
+ var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
+
+ // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
+ // Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
+ if (!allowHwAccel)
+ {
+ options.EnableHardwareEncoding = false;
+ options.HardwareAccelerationType = string.Empty;
+ options.EnableTonemapping = false;
+ }
+
+ var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth };
+ var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
+ {
+ IsVideoRequest = true, // must be true for InputVideoHwaccelArgs to return non-empty value
+ MediaSource = mediaSource,
+ VideoStream = imageStream,
+ BaseRequest = baseRequest, // GetVideoProcessingFilterParam errors if null
+ MediaPath = inputFile,
+ OutputVideoCodec = "mjpeg"
+ };
+ var vidEncoder = options.AllowMjpegEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec;
+
+ // Get input and filter arguments
+ var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
+ if (string.IsNullOrWhiteSpace(inputArg))
+ {
+ throw new InvalidOperationException("EncodingHelper returned empty input arguments.");
+ }
+
+ if (!allowHwAccel)
+ {
+ inputArg = "-threads " + _threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled
+ }
+
+ var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim();
+ if (string.IsNullOrWhiteSpace(filterParam) || filterParam.IndexOf("\"", StringComparison.Ordinal) == -1)
+ {
+ throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
+ }
+
+ return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, _threads, cancellationToken);
+ }
+
+ private async Task ExtractVideoImagesOnIntervalInternal(
+ string inputArg,
+ string filterParam,
+ TimeSpan interval,
+ string vidEncoder,
+ int outputThreads,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(inputArg))
+ {
+ throw new InvalidOperationException("Empty or invalid input argument.");
+ }
+
+ // Output arguments
+ string fps = "fps=1/" + interval.TotalSeconds.ToString(CultureInfo.InvariantCulture);
+ if (string.IsNullOrWhiteSpace(filterParam))
+ {
+ filterParam = "-vf \"" + fps + "\"";
+ }
+ else
+ {
+ filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ",");
+ }
+
+ var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(targetDirectory);
+ var outputPath = Path.Combine(targetDirectory, "%08d.jpg");
+
+ // Final command arguments
+ var args = string.Format(
+ CultureInfo.InvariantCulture,
+ "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} -f {4} \"{5}\"",
+ inputArg,
+ filterParam,
+ outputThreads,
+ vidEncoder,
+ "image2",
+ outputPath);
+
+ // Start ffmpeg process
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = _ffmpegPath,
+ Arguments = args,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false,
+ },
+ EnableRaisingEvents = true
+ };
+
+ var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
+ _logger.LogDebug("{ProcessDescription}", processDescription);
+
+ using (var processWrapper = new ProcessWrapper(process, this))
+ {
+ bool ranToCompletion = false;
+
+ await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ StartProcess(processWrapper);
+
+ // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
+ // but we still need to detect if the process hangs.
+ // Making the assumption that as long as new jpegs are showing up, everything is good.
+
+ bool isResponsive = true;
+ int lastCount = 0;
+
+ while (isResponsive)
+ {
+ if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false))
+ {
+ ranToCompletion = true;
+ break;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var jpegCount = _fileSystem.GetFilePaths(targetDirectory)
+ .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase));
+
+ isResponsive = jpegCount > lastCount;
+ lastCount = jpegCount;
+ }
+
+ if (!ranToCompletion)
+ {
+ _logger.LogInformation("Killing ffmpeg extraction process due to inactivity.");
+ StopProcess(processWrapper, 1000);
+ }
+ }
+ finally
+ {
+ _thumbnailResourcePool.Release();
+ }
+
+ var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
+
+ if (exitCode == -1)
+ {
+ _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
+
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription));
+ }
+
+ return targetDirectory;
+ }
+ }
+
public string GetTimeParameter(long ticks)
{
var time = TimeSpan.FromTicks(ticks);
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index a53be0fee..e1d9e00b7 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -48,7 +48,9 @@ public class EncodingOptions
EnableIntelLowPowerH264HwEncoder = false;
EnableIntelLowPowerHevcHwEncoder = false;
EnableHardwareEncoding = true;
+ EnableTrickplayHwAccel = false;
AllowHevcEncoding = false;
+ AllowMjpegEncoding = false;
EnableSubtitleExtraction = true;
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
HardwareDecodingCodecs = new string[] { "h264", "vc1" };
@@ -244,11 +246,21 @@ public class EncodingOptions
///
public bool EnableHardwareEncoding { get; set; }
+ ///
+ /// Gets or sets a value indicating whether hardware acceleration is enabled for trickplay generation.
+ ///
+ public bool EnableTrickplayHwAccel { get; set; }
+
///
/// Gets or sets a value indicating whether HEVC encoding is enabled.
///
public bool AllowHevcEncoding { get; set; }
+ ///
+ /// Gets or sets a value indicating whether MJPEG encoding is enabled.
+ ///
+ public bool AllowMjpegEncoding { get; set; }
+
///
/// Gets or sets a value indicating whether subtitle extraction is enabled.
///
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index 8fab1ca6d..ab424c6f5 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -568,6 +568,12 @@ namespace MediaBrowser.Model.Dto
/// The chapters.
public List Chapters { get; set; }
+ ///
+ /// Gets or sets the trickplay manifest.
+ ///
+ /// The trickplay manifest.
+ public Dictionary> Trickplay { get; set; }
+
///
/// Gets or sets the type of the location.
///
diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs
new file mode 100644
index 000000000..84b6b0322
--- /dev/null
+++ b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs
@@ -0,0 +1,50 @@
+namespace MediaBrowser.Model.Entities
+{
+ ///
+ /// Class TrickplayTilesInfo.
+ ///
+ public class TrickplayTilesInfo
+ {
+ ///
+ /// Gets or sets width of an individual tile.
+ ///
+ /// The width.
+ public int Width { get; set; }
+
+ ///
+ /// Gets or sets height of an individual tile.
+ ///
+ /// The height.
+ public int Height { get; set; }
+
+ ///
+ /// Gets or sets amount of tiles per row.
+ ///
+ /// The tile grid's width.
+ public int TileWidth { get; set; }
+
+ ///
+ /// Gets or sets amount of tiles per column.
+ ///
+ /// The tile grid's height.
+ public int TileHeight { get; set; }
+
+ ///
+ /// Gets or sets total amount of non-black tiles.
+ ///
+ /// The tile count.
+ public int TileCount { get; set; }
+
+ ///
+ /// Gets or sets interval in milliseconds between each trickplay tile.
+ ///
+ /// The interval.
+ public int Interval { get; set; }
+
+ ///
+ /// Gets or sets peak bandwith usage in bits per second.
+ ///
+ /// The bandwidth.
+ public int Bandwidth { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs
index 6fa1d778a..242a1c6e9 100644
--- a/MediaBrowser.Model/Querying/ItemFields.cs
+++ b/MediaBrowser.Model/Querying/ItemFields.cs
@@ -34,6 +34,11 @@ namespace MediaBrowser.Model.Querying
///
Chapters,
+ ///
+ /// The trickplay manifest.
+ ///
+ Trickplay,
+
ChildCount,
///
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 6a40833d7..c836c8ed5 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -22,6 +22,7 @@
+
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
new file mode 100644
index 000000000..3d1450a90
--- /dev/null
+++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace MediaBrowser.Providers.Trickplay
+{
+ ///
+ /// Class TrickplayImagesTask.
+ ///
+ public class TrickplayImagesTask : IScheduledTask
+ {
+ private readonly ILogger _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly ITrickplayManager _trickplayManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// The library manager.
+ /// The localization manager.
+ /// The configuration manager.
+ /// The trickplay manager.
+ public TrickplayImagesTask(
+ ILogger logger,
+ ILibraryManager libraryManager,
+ ILocalizationManager localization,
+ IServerConfigurationManager configurationManager,
+ ITrickplayManager trickplayManager)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _localization = localization;
+ _configurationManager = configurationManager;
+ _trickplayManager = trickplayManager;
+ }
+
+ ///
+ public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages");
+
+ ///
+ public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription");
+
+ ///
+ public string Key => "RefreshTrickplayImages";
+
+ ///
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ ///
+ public IEnumerable GetDefaultTriggers()
+ {
+ return new[]
+ {
+ new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfo.TriggerDaily,
+ TimeOfDayTicks = TimeSpan.FromHours(3).Ticks,
+ MaxRuntimeTicks = TimeSpan.FromHours(5).Ticks
+ }
+ };
+ }
+
+ ///
+ public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
+ {
+ // TODO: libraryoptions dont run on libraries with trickplay disabled
+ var items = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ MediaTypes = new[] { MediaType.Video },
+ IsVirtualItem = false,
+ IsFolder = false,
+ Recursive = false
+ }).OfType
public bool EnableHardwareEncoding { get; set; }
- ///
- /// Gets or sets a value indicating whether hardware acceleration is enabled for trickplay generation.
- ///
- public bool EnableTrickplayHwAccel { get; set; }
-
///
/// Gets or sets a value indicating whether HEVC encoding is enabled.
///
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index df6829946..7718f822b 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -36,6 +36,10 @@ namespace MediaBrowser.Model.Configuration
public bool ExtractChapterImagesDuringLibraryScan { get; set; }
+ public bool EnableTrickplayImageExtraction { get; set; }
+
+ public bool ExtractTrickplayImagesDuringLibraryScan { get; set; }
+
public MediaPathInfo[] PathInfos { get; set; }
public bool SaveLocalMetadata { get; set; }
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
index 62180804f..4b4514897 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Trickplay;
@@ -26,6 +27,7 @@ namespace MediaBrowser.Providers.Trickplay
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly EncodingHelper _encodingHelper;
+ private readonly ILibraryManager _libraryManager;
private static readonly SemaphoreSlim _resourcePool = new(1, 1);
@@ -37,18 +39,21 @@ namespace MediaBrowser.Providers.Trickplay
/// The media encoder.
/// The file systen.
/// The encoding helper.
+ /// The library manager.
public TrickplayManager(
ILogger logger,
IItemRepository itemRepo,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
- EncodingHelper encodingHelper)
+ EncodingHelper encodingHelper,
+ ILibraryManager libraryManager)
{
_logger = logger;
_itemRepo = itemRepo;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
_encodingHelper = encodingHelper;
+ _libraryManager = libraryManager;
}
///
@@ -287,11 +292,15 @@ namespace MediaBrowser.Providers.Trickplay
return false;
}
- /* TODO config options
+ if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
+ {
+ return false;
+ }
+
var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (libraryOptions is not null)
{
- if (!libraryOptions.EnableChapterImageExtraction)
+ if (!libraryOptions.EnableTrickplayImageExtraction)
{
return false;
}
@@ -300,12 +309,6 @@ namespace MediaBrowser.Providers.Trickplay
{
return false;
}
- */
-
- if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
- {
- return false;
- }
// Can't extract images if there are no video streams
return video.GetMediaStreams().Count > 0;
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
index be66dea8a..2b3879ca3 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
@@ -27,6 +27,7 @@ namespace MediaBrowser.Providers.Trickplay
private readonly ILogger _logger;
private readonly IServerConfigurationManager _configurationManager;
private readonly ITrickplayManager _trickplayManager;
+ private readonly ILibraryManager _libraryManager;
///
/// Initializes a new instance of the class.
@@ -34,21 +35,24 @@ namespace MediaBrowser.Providers.Trickplay
/// The logger.
/// The configuration manager.
/// The trickplay manager.
+ /// The library manager.
public TrickplayProvider(
ILogger logger,
IServerConfigurationManager configurationManager,
- ITrickplayManager trickplayManager)
+ ITrickplayManager trickplayManager,
+ ILibraryManager libraryManager)
{
_logger = logger;
_configurationManager = configurationManager;
_trickplayManager = trickplayManager;
+ _libraryManager = libraryManager;
}
///
- public string Name => "Trickplay Preview";
+ public string Name => "Trickplay Provider";
///
- public int Order => 1000;
+ public int Order => 100;
///
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
@@ -95,11 +99,24 @@ namespace MediaBrowser.Providers.Trickplay
return FetchInternal(item, options, cancellationToken);
}
- private async Task FetchInternal(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ private async Task FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
- // TODO: implement all config options -->
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
+ bool replace = options.ReplaceAllImages;
+
+ if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false))
+ {
+ _logger.LogDebug("exit refresh: automated - {0} enable scan - {1}", options.IsAutomated, enableDuringScan.GetValueOrDefault(false));
+ return ItemUpdateType.None;
+ }
+
// TODO: this is always blocking for metadata collection, make non-blocking option
- await _trickplayManager.RefreshTrickplayData(item, options.ReplaceAllImages, cancellationToken).ConfigureAwait(false);
+ if (true)
+ {
+ _logger.LogDebug("called refresh");
+ await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false);
+ }
// The core doesn't need to trigger any save operations over this
return ItemUpdateType.None;
--
cgit v1.2.3
From 6c649a7e723454e94303d95d178e91b820ba6b50 Mon Sep 17 00:00:00 2001
From: nicknsy <20588554+nicknsy@users.noreply.github.com>
Date: Thu, 23 Feb 2023 17:58:34 -0800
Subject: Options
---
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 ++-
.../Configuration/TrickplayOptions.cs | 61 ++++++++++++++++++++++
.../Configuration/TrickplayScanBehavior.cs | 18 +++++++
3 files changed, 84 insertions(+), 1 deletion(-)
create mode 100644 MediaBrowser.Model/Configuration/TrickplayOptions.cs
create mode 100644 MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs
(limited to 'MediaBrowser.Model')
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 7f8ec03fa..9b58f83b4 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -853,10 +853,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
filterParam = "-vf \"" + fps + "\"";
}
- else
+ else if (filterParam.IndexOf("\"", StringComparison.Ordinal) != -1)
{
filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ",");
}
+ else
+ {
+ filterParam += fps + ",";
+ }
var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(targetDirectory);
diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
new file mode 100644
index 000000000..d527baaa4
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace MediaBrowser.Model.Configuration
+{
+ ///
+ /// Class TrickplayOptions.
+ ///
+ public class TrickplayOptions
+ {
+ ///
+ /// Gets or sets a value indicating whether or not to use HW acceleration.
+ ///
+ public bool EnableHwAcceleration { get; set; } = false;
+
+ ///
+ /// Gets or sets the behavior used by trickplay provider on library scan/update.
+ ///
+ public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking;
+
+ ///
+ /// Gets or sets the process priority for the ffmpeg process.
+ ///
+ public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal;
+
+ ///
+ /// Gets or sets the interval, in ms, between each new trickplay image.
+ ///
+ public int Interval { get; set; } = 10000;
+
+ ///
+ /// Gets or sets the target width resolutions, in px, to generates preview images for.
+ ///
+ public HashSet WidthResolutions { get; set; } = new HashSet { 320 };
+
+ ///
+ /// Gets or sets number of tile images to allow in X dimension.
+ ///
+ public int TileWidth { get; set; } = 10;
+
+ ///
+ /// Gets or sets number of tile images to allow in Y dimension.
+ ///
+ public int TileHeight { get; set; } = 10;
+
+ ///
+ /// Gets or sets the ffmpeg output quality level.
+ ///
+ public int Qscale { get; set; } = 10;
+
+ ///
+ /// Gets or sets the jpeg quality to use for image tiles.
+ ///
+ public int JpegQuality { get; set; } = 90;
+
+ ///
+ /// Gets or sets the number of threads to be used by ffmpeg.
+ ///
+ public int ProcessThreads { get; set; } = 0;
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs
new file mode 100644
index 000000000..799794176
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs
@@ -0,0 +1,18 @@
+namespace MediaBrowser.Model.Configuration
+{
+ ///
+ /// Enum TrickplayScanBehavior.
+ ///
+ public enum TrickplayScanBehavior
+ {
+ ///
+ /// Starts generation, only return once complete.
+ ///
+ Blocking,
+
+ ///
+ /// Start generation, return immediately.
+ ///
+ NonBlocking
+ }
+}
--
cgit v1.2.3
From 6744e712d3a4fd6f800e5499c90b247787e48cb6 Mon Sep 17 00:00:00 2001
From: nicknsy <20588554+nicknsy@users.noreply.github.com>
Date: Sat, 25 Feb 2023 15:59:46 -0800
Subject: Use config values
---
.../MediaEncoding/IMediaEncoder.cs | 13 ++++--
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 37 +++++++++++-----
.../Configuration/ServerConfiguration.cs | 2 +
.../Trickplay/TrickplayImagesTask.cs | 12 ++++--
.../Trickplay/TrickplayManager.cs | 49 +++++++++++++++-------
.../Trickplay/TrickplayProvider.cs | 16 ++++---
6 files changed, 89 insertions(+), 40 deletions(-)
(limited to 'MediaBrowser.Model')
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index aa9faa936..f5e3d03cb 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Configuration;
@@ -145,10 +146,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Video container type.
/// Media source information.
/// Media stream information.
- /// The interval.
/// The maximum width.
+ /// The interval.
/// Allow for hardware acceleration.
- /// Allow for hardware encoding. allowHwAccel must also be true.
+ /// The input/output thread count for ffmpeg.
+ /// The qscale value for ffmpeg.
+ /// The process priority for the ffmpeg process.
/// EncodingHelper instance.
/// The cancellation token.
/// Directory where images where extracted. A given image made before another will always be named with a lower number.
@@ -157,10 +160,12 @@ namespace MediaBrowser.Controller.MediaEncoding
string container,
MediaSourceInfo mediaSource,
MediaStream imageStream,
- TimeSpan interval,
int maxWidth,
+ TimeSpan interval,
bool allowHwAccel,
- bool allowHwEncode,
+ int? threads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
EncodingHelper encodingHelper,
CancellationToken cancellationToken);
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 9b58f83b4..11f42c3f9 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -783,14 +783,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
string container,
MediaSourceInfo mediaSource,
MediaStream imageStream,
- TimeSpan interval,
int maxWidth,
+ TimeSpan interval,
bool allowHwAccel,
- bool allowHwEncode,
+ int? threads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
EncodingHelper encodingHelper,
CancellationToken cancellationToken)
{
var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
+ threads = threads ?? _threads;
// A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
// Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
@@ -822,7 +825,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (!allowHwAccel)
{
- inputArg = "-threads " + _threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled
+ inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled
}
var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim();
@@ -831,7 +834,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
}
- return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, _threads, cancellationToken);
+ return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, threads, qualityScale, priority, cancellationToken);
}
private async Task ExtractVideoImagesOnIntervalInternal(
@@ -839,7 +842,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
string filterParam,
TimeSpan interval,
string vidEncoder,
- int outputThreads,
+ int? outputThreads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(inputArg))
@@ -857,10 +862,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ",");
}
- else
- {
- filterParam += fps + ",";
- }
var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(targetDirectory);
@@ -869,11 +870,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Final command arguments
var args = string.Format(
CultureInfo.InvariantCulture,
- "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} -f {4} \"{5}\"",
+ "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}-f {5} \"{6}\"",
inputArg,
filterParam,
- outputThreads,
+ outputThreads.GetValueOrDefault(_threads),
vidEncoder,
+ qualityScale.HasValue ? "-qscale:v " + qualityScale.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty,
"image2",
outputPath);
@@ -904,6 +906,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
StartProcess(processWrapper);
+ // Set process priority
+ if (priority.HasValue)
+ {
+ try
+ {
+ processWrapper.Process.PriorityClass = priority.Value;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", priority.Value, processDescription);
+ }
+ }
+
// Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
// but we still need to detect if the process hangs.
// Making the assumption that as long as new jpegs are showing up, everything is good.
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 78a310f0b..097eff295 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -264,5 +264,7 @@ namespace MediaBrowser.Model.Configuration
///
/// The limit for parallel image encoding.
public int ParallelImageEncodingLimit { get; set; }
+
+ public TrickplayOptions TrickplayOptions { get; set; } = new TrickplayOptions();
}
}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
index 87ac145d7..a364926c0 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
@@ -22,7 +22,6 @@ namespace MediaBrowser.Providers.Trickplay
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
- private readonly IServerConfigurationManager _configurationManager;
private readonly ITrickplayManager _trickplayManager;
///
@@ -31,19 +30,16 @@ namespace MediaBrowser.Providers.Trickplay
/// The logger.
/// The library manager.
/// The localization manager.
- /// The configuration manager.
/// The trickplay manager.
public TrickplayImagesTask(
ILogger logger,
ILibraryManager libraryManager,
ILocalizationManager localization,
- IServerConfigurationManager configurationManager,
ITrickplayManager trickplayManager)
{
_libraryManager = libraryManager;
_logger = logger;
_localization = localization;
- _configurationManager = configurationManager;
_trickplayManager = trickplayManager;
}
@@ -77,6 +73,14 @@ namespace MediaBrowser.Providers.Trickplay
public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
{
// TODO: libraryoptions dont run on libraries with trickplay disabled
+ /* will this still get all sub-items? should recursive be true?
+ * from chapterimagestask
+ * DtoOptions = new DtoOptions(false)
+ {
+ EnableImages = false
+ },
+ SourceTypes = new SourceType[] { SourceType.Library },
+ */
var items = _libraryManager.GetItemList(new InternalItemsQuery
{
MediaTypes = new[] { MediaType.Video },
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
index cb916dfdb..ed2c11281 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
@@ -5,11 +5,13 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@@ -28,6 +30,7 @@ namespace MediaBrowser.Providers.Trickplay
private readonly IFileSystem _fileSystem;
private readonly EncodingHelper _encodingHelper;
private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
private static readonly SemaphoreSlim _resourcePool = new(1, 1);
@@ -40,13 +43,15 @@ namespace MediaBrowser.Providers.Trickplay
/// The file systen.
/// The encoding helper.
/// The library manager.
+ /// The server configuration manager.
public TrickplayManager(
ILogger logger,
IItemRepository itemRepo,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
EncodingHelper encodingHelper,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ IServerConfigurationManager config)
{
_logger = logger;
_itemRepo = itemRepo;
@@ -54,6 +59,7 @@ namespace MediaBrowser.Providers.Trickplay
_fileSystem = fileSystem;
_encodingHelper = encodingHelper;
_libraryManager = libraryManager;
+ _config = config;
}
///
@@ -61,16 +67,27 @@ namespace MediaBrowser.Providers.Trickplay
{
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
- foreach (var width in new int[] { 320 } /*todo conf*/)
+ var options = _config.Configuration.TrickplayOptions;
+ foreach (var width in options.WidthResolutions)
{
cancellationToken.ThrowIfCancellationRequested();
- await RefreshTrickplayData(video, replace, width, 10000/*todo conf*/, 10/*todo conf*/, 10/*todo conf*/, true/*todo conf*/, true/*todo conf*/, cancellationToken).ConfigureAwait(false);
+ await RefreshTrickplayDataInternal(
+ video,
+ replace,
+ width,
+ options,
+ cancellationToken).ConfigureAwait(false);
}
}
- private async Task RefreshTrickplayData(Video video, bool replace, int width, int interval, int tileWidth, int tileHeight, bool doHwAccel, bool doHwEncode, CancellationToken cancellationToken)
+ private async Task RefreshTrickplayDataInternal(
+ Video video,
+ bool replace,
+ int width,
+ TrickplayOptions options,
+ CancellationToken cancellationToken)
{
- if (!CanGenerateTrickplay(video, interval))
+ if (!CanGenerateTrickplay(video, options.Interval))
{
return;
}
@@ -108,10 +125,12 @@ namespace MediaBrowser.Providers.Trickplay
container,
mediaSource,
mediaStream,
- TimeSpan.FromMilliseconds(interval),
width,
- doHwAccel,
- doHwEncode,
+ TimeSpan.FromMilliseconds(options.Interval),
+ options.EnableHwAcceleration,
+ options.ProcessThreads,
+ options.Qscale,
+ options.ProcessPriority,
_encodingHelper,
cancellationToken).ConfigureAwait(false);
@@ -127,7 +146,7 @@ namespace MediaBrowser.Providers.Trickplay
// Create tiles
var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N"));
- var tilesInfo = CreateTiles(images, width, interval, tileWidth, tileHeight, 100/* todo _config.JpegQuality*/, tilesTempDir, outputDir);
+ var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir);
// Save tiles info
try
@@ -166,7 +185,7 @@ namespace MediaBrowser.Providers.Trickplay
}
}
- private TrickplayTilesInfo CreateTiles(List images, int width, int interval, int tileWidth, int tileHeight, int quality, string workDir, string outputDir)
+ private TrickplayTilesInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir)
{
if (images.Count == 0)
{
@@ -178,9 +197,9 @@ namespace MediaBrowser.Providers.Trickplay
var tilesInfo = new TrickplayTilesInfo
{
Width = width,
- Interval = interval,
- TileWidth = tileWidth,
- TileHeight = tileHeight,
+ Interval = options.Interval,
+ TileWidth = options.TileWidth,
+ TileHeight = options.TileHeight,
TileCount = 0,
Bandwidth = 0
};
@@ -244,7 +263,7 @@ namespace MediaBrowser.Providers.Trickplay
var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg");
using (var stream = File.OpenWrite(tileGridPath))
{
- tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, quality);
+ tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality);
}
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000));
@@ -351,7 +370,7 @@ namespace MediaBrowser.Providers.Trickplay
{
Directory.Move(source, destination);
}
- catch (System.IO.IOException)
+ catch (IOException)
{
// Cross device move requires a copy
Directory.CreateDirectory(destination);
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
index e4bd9e3c2..e29646725 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
@@ -7,6 +7,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Trickplay
@@ -25,7 +26,7 @@ namespace MediaBrowser.Providers.Trickplay
IForcedProvider
{
private readonly ILogger _logger;
- private readonly IServerConfigurationManager _configurationManager;
+ private readonly IServerConfigurationManager _config;
private readonly ITrickplayManager _trickplayManager;
private readonly ILibraryManager _libraryManager;
@@ -33,17 +34,17 @@ namespace MediaBrowser.Providers.Trickplay
/// Initializes a new instance of the class.
///
/// The logger.
- /// The configuration manager.
+ /// The configuration manager.
/// The trickplay manager.
/// The library manager.
public TrickplayProvider(
ILogger logger,
- IServerConfigurationManager configurationManager,
+ IServerConfigurationManager config,
ITrickplayManager trickplayManager,
ILibraryManager libraryManager)
{
_logger = logger;
- _configurationManager = configurationManager;
+ _config = config;
_trickplayManager = trickplayManager;
_libraryManager = libraryManager;
}
@@ -110,11 +111,14 @@ namespace MediaBrowser.Providers.Trickplay
return ItemUpdateType.None;
}
- // TODO: this is always blocking for metadata collection, make non-blocking option
- if (true)
+ if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking)
{
await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false);
}
+ else
+ {
+ _ = _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false);
+ }
// The core doesn't need to trigger any save operations over this
return ItemUpdateType.None;
--
cgit v1.2.3
From d84370a6f72111b544f1bbcaa8f3b0339148b5f8 Mon Sep 17 00:00:00 2001
From: nicknsy <20588554+nicknsy@users.noreply.github.com>
Date: Sat, 25 Feb 2023 20:36:38 -0800
Subject: Make trickplay response ids have no dashes
---
Emby.Server.Implementations/Dto/DtoService.cs | 4 +++-
MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 10352b6ff..1687fa442 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1060,7 +1060,9 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Trickplay))
{
- dto.Trickplay = _itemRepo.GetTrickplayManifest(item);
+ // To stay consistent with other fields, this must go from a Guid to a non-dashed string.
+ // This does not seem to occur automatically to dictionaries like it does with other Guid fields.
+ dto.Trickplay = _itemRepo.GetTrickplayManifest(item).ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value);
}
if (video.ExtraType.HasValue)
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index ab424c6f5..3db9cb08b 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -572,7 +572,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the trickplay manifest.
///
/// The trickplay manifest.
- public Dictionary> Trickplay { get; set; }
+ public Dictionary> Trickplay { get; set; }
///
/// Gets or sets the type of the location.
--
cgit v1.2.3
From 2e2085a212312282e20dcb6578503b6daff9b72e Mon Sep 17 00:00:00 2001
From: nicknsy <20588554+nicknsy@users.noreply.github.com>
Date: Sat, 4 Mar 2023 16:19:09 -0800
Subject: HashSet datatype was causing default values to always be added on
server start
---
MediaBrowser.Model/Configuration/TrickplayOptions.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'MediaBrowser.Model')
diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
index d527baaa4..f85259a14 100644
--- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs
+++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
@@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Configuration
///
/// Gets or sets the target width resolutions, in px, to generates preview images for.
///
- public HashSet WidthResolutions { get; set; } = new HashSet { 320 };
+ public int[] WidthResolutions { get; set; } = new[] { 320 };
///
/// Gets or sets number of tile images to allow in X dimension.
@@ -51,7 +51,7 @@ namespace MediaBrowser.Model.Configuration
///
/// Gets or sets the jpeg quality to use for image tiles.
///
- public int JpegQuality { get; set; } = 90;
+ public int JpegQuality { get; set; } = 80;
///
/// Gets or sets the number of threads to be used by ffmpeg.
--
cgit v1.2.3
From b89bf5d735de144495b3dc9acbcf70c17fb24c15 Mon Sep 17 00:00:00 2001
From: Nick <20588554+nicknsy@users.noreply.github.com>
Date: Tue, 28 Mar 2023 13:09:34 -0700
Subject: Change defaults
---
MediaBrowser.Model/Configuration/TrickplayOptions.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'MediaBrowser.Model')
diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
index f85259a14..d89e5f590 100644
--- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs
+++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
@@ -46,12 +46,12 @@ namespace MediaBrowser.Model.Configuration
///
/// Gets or sets the ffmpeg output quality level.
///
- public int Qscale { get; set; } = 10;
+ public int Qscale { get; set; } = 4;
///
/// Gets or sets the jpeg quality to use for image tiles.
///
- public int JpegQuality { get; set; } = 80;
+ public int JpegQuality { get; set; } = 90;
///
/// Gets or sets the number of threads to be used by ffmpeg.
--
cgit v1.2.3
From 33770322282326304b4b8073f583d6fed2354c0b Mon Sep 17 00:00:00 2001
From: Nick <20588554+nicknsy@users.noreply.github.com>
Date: Mon, 1 May 2023 12:51:05 -0700
Subject: crobibero styling, format, code suggestions
---
.../MediaEncoding/EncodingHelper.cs | 27 +-
.../Trickplay/ITrickplayManager.cs | 77 ++-
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +-
.../Configuration/TrickplayOptions.cs | 109 ++--
.../Configuration/TrickplayScanBehavior.cs | 25 +-
MediaBrowser.Model/Entities/TrickplayTilesInfo.cs | 79 ++-
.../Trickplay/TrickplayImagesTask.cs | 147 +++---
.../Trickplay/TrickplayManager.cs | 569 ++++++++++-----------
.../Trickplay/TrickplayProvider.cs | 181 ++++---
9 files changed, 602 insertions(+), 614 deletions(-)
(limited to 'MediaBrowser.Model')
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index bcdf2934a..01b6e31e9 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -90,6 +90,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "truehd", 6 },
};
+ private static readonly string _defaultMjpegEncoder = "mjpeg";
+ private static readonly Dictionary _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase)
+ {
+ { "vaapi", _defaultMjpegEncoder + "_vaapi" },
+ { "qsv", _defaultMjpegEncoder + "_qsv" }
+ };
+
public static readonly string[] LosslessAudioCodecs = new string[]
{
"alac",
@@ -151,32 +158,20 @@ namespace MediaBrowser.Controller.MediaEncoding
private string GetMjpegEncoder(EncodingJobInfo state, EncodingOptions encodingOptions)
{
- var defaultEncoder = "mjpeg";
-
if (state.VideoType == VideoType.VideoFile)
{
var hwType = encodingOptions.HardwareAccelerationType;
- var codecMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
- {
- { "vaapi", defaultEncoder + "_vaapi" },
- { "qsv", defaultEncoder + "_qsv" }
- };
-
if (!string.IsNullOrEmpty(hwType)
&& encodingOptions.EnableHardwareEncoding
- && codecMap.ContainsKey(hwType))
+ && _mjpegCodecMap.TryGetValue(hwType, out var preferredEncoder)
+ && _mediaEncoder.SupportsEncoder(preferredEncoder))
{
- var preferredEncoder = codecMap[hwType];
-
- if (_mediaEncoder.SupportsEncoder(preferredEncoder))
- {
- return preferredEncoder;
- }
+ return preferredEncoder;
}
}
- return defaultEncoder;
+ return _defaultMjpegEncoder;
}
private bool IsVaapiSupported(EncodingJobInfo state)
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
index bae458f98..8e82c57d4 100644
--- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -5,50 +5,49 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Controller.Trickplay
+namespace MediaBrowser.Controller.Trickplay;
+
+///
+/// Interface ITrickplayManager.
+///
+public interface ITrickplayManager
{
///
- /// Interface ITrickplayManager.
+ /// Generate or replace trickplay data.
///
- public interface ITrickplayManager
- {
- ///
- /// Generate or replace trickplay data.
- ///
- /// The video.
- /// Whether or not existing data should be replaced.
- /// CancellationToken to use for operation.
- /// Task.
- Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken);
+ /// The video.
+ /// Whether or not existing data should be replaced.
+ /// CancellationToken to use for operation.
+ /// Task.
+ Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken);
- ///
- /// Get available trickplay resolutions and corresponding info.
- ///
- /// The item.
- /// Map of width resolutions to trickplay tiles info.
- Dictionary GetTilesResolutions(Guid itemId);
+ ///
+ /// Get available trickplay resolutions and corresponding info.
+ ///
+ /// The item.
+ /// Map of width resolutions to trickplay tiles info.
+ Dictionary GetTilesResolutions(Guid itemId);
- ///
- /// Saves trickplay tiles info.
- ///
- /// The item.
- /// The trickplay tiles info.
- void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo);
+ ///
+ /// Saves trickplay tiles info.
+ ///
+ /// The item.
+ /// The trickplay tiles info.
+ void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo);
- ///
- /// Gets the trickplay manifest.
- ///
- /// The item.
- /// A map of media source id to a map of tile width to tile info.
- Dictionary> GetTrickplayManifest(BaseItem item);
+ ///
+ /// Gets the trickplay manifest.
+ ///
+ /// The item.
+ /// A map of media source id to a map of tile width to tile info.
+ Dictionary> GetTrickplayManifest(BaseItem item);
- ///
- /// Gets the path to a trickplay tiles image.
- ///
- /// The item.
- /// The width of a single tile.
- /// The tile grid's index.
- /// The absolute path.
- string GetTrickplayTilePath(BaseItem item, int width, int index);
- }
+ ///
+ /// Gets the path to a trickplay tiles image.
+ ///
+ /// The item.
+ /// The width of a single tile.
+ /// The tile grid's index.
+ /// The absolute path.
+ string GetTrickplayTilePath(BaseItem item, int width, int index);
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 4692bf504..000831fe2 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -793,7 +793,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
CancellationToken cancellationToken)
{
var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
- threads = threads ?? _threads;
+ threads ??= _threads;
// A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
// Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
index d89e5f590..1fff1a5ed 100644
--- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs
+++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
@@ -1,61 +1,60 @@
using System.Collections.Generic;
using System.Diagnostics;
-namespace MediaBrowser.Model.Configuration
+namespace MediaBrowser.Model.Configuration;
+
+///
+/// Class TrickplayOptions.
+///
+public class TrickplayOptions
{
///
- /// Class TrickplayOptions.
- ///
- public class TrickplayOptions
- {
- ///
- /// Gets or sets a value indicating whether or not to use HW acceleration.
- ///
- public bool EnableHwAcceleration { get; set; } = false;
-
- ///
- /// Gets or sets the behavior used by trickplay provider on library scan/update.
- ///
- public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking;
-
- ///
- /// Gets or sets the process priority for the ffmpeg process.
- ///
- public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal;
-
- ///
- /// Gets or sets the interval, in ms, between each new trickplay image.
- ///
- public int Interval { get; set; } = 10000;
-
- ///
- /// Gets or sets the target width resolutions, in px, to generates preview images for.
- ///
- public int[] WidthResolutions { get; set; } = new[] { 320 };
-
- ///
- /// Gets or sets number of tile images to allow in X dimension.
- ///
- public int TileWidth { get; set; } = 10;
-
- ///
- /// Gets or sets number of tile images to allow in Y dimension.
- ///
- public int TileHeight { get; set; } = 10;
-
- ///
- /// Gets or sets the ffmpeg output quality level.
- ///
- public int Qscale { get; set; } = 4;
-
- ///
- /// Gets or sets the jpeg quality to use for image tiles.
- ///
- public int JpegQuality { get; set; } = 90;
-
- ///
- /// Gets or sets the number of threads to be used by ffmpeg.
- ///
- public int ProcessThreads { get; set; } = 0;
- }
+ /// Gets or sets a value indicating whether or not to use HW acceleration.
+ ///
+ public bool EnableHwAcceleration { get; set; } = false;
+
+ ///
+ /// Gets or sets the behavior used by trickplay provider on library scan/update.
+ ///
+ public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking;
+
+ ///
+ /// Gets or sets the process priority for the ffmpeg process.
+ ///
+ public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal;
+
+ ///
+ /// Gets or sets the interval, in ms, between each new trickplay image.
+ ///
+ public int Interval { get; set; } = 10000;
+
+ ///
+ /// Gets or sets the target width resolutions, in px, to generates preview images for.
+ ///
+ public int[] WidthResolutions { get; set; } = new[] { 320 };
+
+ ///
+ /// Gets or sets number of tile images to allow in X dimension.
+ ///
+ public int TileWidth { get; set; } = 10;
+
+ ///
+ /// Gets or sets number of tile images to allow in Y dimension.
+ ///
+ public int TileHeight { get; set; } = 10;
+
+ ///
+ /// Gets or sets the ffmpeg output quality level.
+ ///
+ public int Qscale { get; set; } = 4;
+
+ ///
+ /// Gets or sets the jpeg quality to use for image tiles.
+ ///
+ public int JpegQuality { get; set; } = 90;
+
+ ///
+ /// Gets or sets the number of threads to be used by ffmpeg.
+ ///
+ public int ProcessThreads { get; set; } = 0;
}
diff --git a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs
index 799794176..d0db53218 100644
--- a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs
+++ b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs
@@ -1,18 +1,17 @@
-namespace MediaBrowser.Model.Configuration
+namespace MediaBrowser.Model.Configuration;
+
+///
+/// Enum TrickplayScanBehavior.
+///
+public enum TrickplayScanBehavior
{
///
- /// Enum TrickplayScanBehavior.
+ /// Starts generation, only return once complete.
///
- public enum TrickplayScanBehavior
- {
- ///
- /// Starts generation, only return once complete.
- ///
- Blocking,
+ Blocking,
- ///
- /// Start generation, return immediately.
- ///
- NonBlocking
- }
+ ///
+ /// Start generation, return immediately.
+ ///
+ NonBlocking
}
diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs
index 84b6b0322..86d37787f 100644
--- a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs
+++ b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs
@@ -1,50 +1,49 @@
-namespace MediaBrowser.Model.Entities
+namespace MediaBrowser.Model.Entities;
+
+///
+/// Class TrickplayTilesInfo.
+///
+public class TrickplayTilesInfo
{
///
- /// Class TrickplayTilesInfo.
+ /// Gets or sets width of an individual tile.
///
- public class TrickplayTilesInfo
- {
- ///
- /// Gets or sets width of an individual tile.
- ///
- /// The width.
- public int Width { get; set; }
+ /// The width.
+ public int Width { get; set; }
- ///
- /// Gets or sets height of an individual tile.
- ///
- /// The height.
- public int Height { get; set; }
+ ///
+ /// Gets or sets height of an individual tile.
+ ///
+ /// The height.
+ public int Height { get; set; }
- ///
- /// Gets or sets amount of tiles per row.
- ///
- /// The tile grid's width.
- public int TileWidth { get; set; }
+ ///
+ /// Gets or sets amount of tiles per row.
+ ///
+ /// The tile grid's width.
+ public int TileWidth { get; set; }
- ///
- /// Gets or sets amount of tiles per column.
- ///
- /// The tile grid's height.
- public int TileHeight { get; set; }
+ ///
+ /// Gets or sets amount of tiles per column.
+ ///
+ /// The tile grid's height.
+ public int TileHeight { get; set; }
- ///
- /// Gets or sets total amount of non-black tiles.
- ///
- /// The tile count.
- public int TileCount { get; set; }
+ ///
+ /// Gets or sets total amount of non-black tiles.
+ ///
+ /// The tile count.
+ public int TileCount { get; set; }
- ///
- /// Gets or sets interval in milliseconds between each trickplay tile.
- ///
- /// The interval.
- public int Interval { get; set; }
+ ///
+ /// Gets or sets interval in milliseconds between each trickplay tile.
+ ///
+ /// The interval.
+ public int Interval { get; set; }
- ///
- /// Gets or sets peak bandwith usage in bits per second.
- ///
- /// The bandwidth.
- public int Bandwidth { get; set; }
- }
+ ///
+ /// Gets or sets peak bandwith usage in bits per second.
+ ///
+ /// The bandwidth.
+ public int Bandwidth { get; set; }
}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
index 8d0d9d5a3..f32557cd1 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
@@ -12,98 +12,97 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Trickplay
+namespace MediaBrowser.Providers.Trickplay;
+
+///
+/// Class TrickplayImagesTask.
+///
+public class TrickplayImagesTask : IScheduledTask
{
+ private readonly ILogger _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly ITrickplayManager _trickplayManager;
+
///
- /// Class TrickplayImagesTask.
+ /// Initializes a new instance of the class.
///
- public class TrickplayImagesTask : IScheduledTask
+ /// The logger.
+ /// The library manager.
+ /// The localization manager.
+ /// The trickplay manager.
+ public TrickplayImagesTask(
+ ILogger logger,
+ ILibraryManager libraryManager,
+ ILocalizationManager localization,
+ ITrickplayManager trickplayManager)
{
- private readonly ILogger _logger;
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localization;
- private readonly ITrickplayManager _trickplayManager;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The library manager.
- /// The localization manager.
- /// The trickplay manager.
- public TrickplayImagesTask(
- ILogger logger,
- ILibraryManager libraryManager,
- ILocalizationManager localization,
- ITrickplayManager trickplayManager)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _localization = localization;
- _trickplayManager = trickplayManager;
- }
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _localization = localization;
+ _trickplayManager = trickplayManager;
+ }
- ///
- public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages");
+ ///
+ public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages");
- ///
- public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription");
+ ///
+ public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription");
- ///
- public string Key => "RefreshTrickplayImages";
+ ///
+ public string Key => "RefreshTrickplayImages";
- ///
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+ ///
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
- ///
- public IEnumerable GetDefaultTriggers()
+ ///
+ public IEnumerable GetDefaultTriggers()
+ {
+ return new[]
{
- return new[]
+ new TaskTriggerInfo
{
- new TaskTriggerInfo
- {
- Type = TaskTriggerInfo.TriggerDaily,
- TimeOfDayTicks = TimeSpan.FromHours(3).Ticks
- }
- };
- }
+ Type = TaskTriggerInfo.TriggerDaily,
+ TimeOfDayTicks = TimeSpan.FromHours(3).Ticks
+ }
+ };
+ }
- ///
- public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
+ ///
+ public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
+ {
+ var items = _libraryManager.GetItemList(new InternalItemsQuery
{
- var items = _libraryManager.GetItemList(new InternalItemsQuery
- {
- MediaTypes = new[] { MediaType.Video },
- IsVirtualItem = false,
- IsFolder = false,
- Recursive = true
- }).OfType().ToList();
+ MediaTypes = new[] { MediaType.Video },
+ IsVirtualItem = false,
+ IsFolder = false,
+ Recursive = true
+ }).OfType().ToList();
- var numComplete = 0;
+ var numComplete = 0;
- foreach (var item in items)
+ foreach (var item in items)
+ {
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await _trickplayManager.RefreshTrickplayDataAsync(item, false, cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
{
- try
- {
- cancellationToken.ThrowIfCancellationRequested();
- await _trickplayManager.RefreshTrickplayData(item, false, cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- break;
- }
- catch (Exception ex)
- {
- _logger.LogError("Error creating trickplay files for {ItemName}: {Msg}", item.Name, ex);
- }
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("Error creating trickplay files for {ItemName}: {Msg}", item.Name, ex);
+ }
- numComplete++;
- double percent = numComplete;
- percent /= items.Count;
- percent *= 100;
+ numComplete++;
+ double percent = numComplete;
+ percent /= items.Count;
+ percent *= 100;
- progress.Report(percent);
- }
+ progress.Report(percent);
}
}
}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
index ed2c11281..9b8eb8150 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
@@ -17,370 +17,369 @@ using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using SkiaSharp;
-namespace MediaBrowser.Providers.Trickplay
+namespace MediaBrowser.Providers.Trickplay;
+
+///
+/// ITrickplayManager implementation.
+///
+public class TrickplayManager : ITrickplayManager
{
+ private readonly ILogger _logger;
+ private readonly IItemRepository _itemRepo;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
+
+ private static readonly SemaphoreSlim _resourcePool = new(1, 1);
+
///
- /// ITrickplayManager implementation.
+ /// Initializes a new instance of the class.
///
- public class TrickplayManager : ITrickplayManager
+ /// The logger.
+ /// The item repository.
+ /// The media encoder.
+ /// The file systen.
+ /// The encoding helper.
+ /// The library manager.
+ /// The server configuration manager.
+ public TrickplayManager(
+ ILogger logger,
+ IItemRepository itemRepo,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ EncodingHelper encodingHelper,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager config)
{
- private readonly ILogger _logger;
- private readonly IItemRepository _itemRepo;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly EncodingHelper _encodingHelper;
- private readonly ILibraryManager _libraryManager;
- private readonly IServerConfigurationManager _config;
-
- private static readonly SemaphoreSlim _resourcePool = new(1, 1);
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The item repository.
- /// The media encoder.
- /// The file systen.
- /// The encoding helper.
- /// The library manager.
- /// The server configuration manager.
- public TrickplayManager(
- ILogger logger,
- IItemRepository itemRepo,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- EncodingHelper encodingHelper,
- ILibraryManager libraryManager,
- IServerConfigurationManager config)
+ _logger = logger;
+ _itemRepo = itemRepo;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _encodingHelper = encodingHelper;
+ _libraryManager = libraryManager;
+ _config = config;
+ }
+
+ ///
+ public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken)
+ {
+ _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
+
+ var options = _config.Configuration.TrickplayOptions;
+ foreach (var width in options.WidthResolutions)
{
- _logger = logger;
- _itemRepo = itemRepo;
- _mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _encodingHelper = encodingHelper;
- _libraryManager = libraryManager;
- _config = config;
+ cancellationToken.ThrowIfCancellationRequested();
+ await RefreshTrickplayDataInternal(
+ video,
+ replace,
+ width,
+ options,
+ cancellationToken).ConfigureAwait(false);
}
+ }
- ///
- public async Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken)
+ private async Task RefreshTrickplayDataInternal(
+ Video video,
+ bool replace,
+ int width,
+ TrickplayOptions options,
+ CancellationToken cancellationToken)
+ {
+ if (!CanGenerateTrickplay(video, options.Interval))
{
- _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
-
- var options = _config.Configuration.TrickplayOptions;
- foreach (var width in options.WidthResolutions)
- {
- cancellationToken.ThrowIfCancellationRequested();
- await RefreshTrickplayDataInternal(
- video,
- replace,
- width,
- options,
- cancellationToken).ConfigureAwait(false);
- }
+ return;
}
- private async Task RefreshTrickplayDataInternal(
- Video video,
- bool replace,
- int width,
- TrickplayOptions options,
- CancellationToken cancellationToken)
+ var imgTempDir = string.Empty;
+ var outputDir = GetTrickplayDirectory(video, width);
+
+ try
{
- if (!CanGenerateTrickplay(video, options.Interval))
+ await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width))
{
+ _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
return;
}
- var imgTempDir = string.Empty;
- var outputDir = GetTrickplayDirectory(video, width);
+ // Extract images
+ // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
+ var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
- try
+ if (mediaSource is null)
{
- await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width))
- {
- _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
- return;
- }
-
- // Extract images
- // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
- var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
-
- if (mediaSource is null)
- {
- _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
- return;
- }
+ _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
+ return;
+ }
- var mediaPath = mediaSource.Path;
- var mediaStream = mediaSource.VideoStream;
- var container = mediaSource.Container;
-
- _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
- imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
- mediaPath,
- container,
- mediaSource,
- mediaStream,
- width,
- TimeSpan.FromMilliseconds(options.Interval),
- options.EnableHwAcceleration,
- options.ProcessThreads,
- options.Qscale,
- options.ProcessPriority,
- _encodingHelper,
- cancellationToken).ConfigureAwait(false);
-
- if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
- {
- throw new InvalidOperationException("Null or invalid directory from media encoder.");
- }
+ var mediaPath = mediaSource.Path;
+ var mediaStream = mediaSource.VideoStream;
+ var container = mediaSource.Container;
+
+ _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
+ imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
+ mediaPath,
+ container,
+ mediaSource,
+ mediaStream,
+ width,
+ TimeSpan.FromMilliseconds(options.Interval),
+ options.EnableHwAcceleration,
+ options.ProcessThreads,
+ options.Qscale,
+ options.ProcessPriority,
+ _encodingHelper,
+ cancellationToken).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
+ {
+ throw new InvalidOperationException("Null or invalid directory from media encoder.");
+ }
- var images = _fileSystem.GetFiles(imgTempDir, new string[] { ".jpg" }, false, false)
- .Where(img => string.Equals(img.Extension, ".jpg", StringComparison.Ordinal))
- .OrderBy(i => i.FullName)
- .ToList();
+ var images = _fileSystem.GetFiles(imgTempDir, new string[] { ".jpg" }, false, false)
+ .Where(img => string.Equals(img.Extension, ".jpg", StringComparison.Ordinal))
+ .OrderBy(i => i.FullName)
+ .ToList();
- // Create tiles
- var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N"));
- var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir);
+ // Create tiles
+ var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N"));
+ var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir);
- // Save tiles info
- try
+ // Save tiles info
+ try
+ {
+ if (tilesInfo is not null)
{
- if (tilesInfo is not null)
- {
- SaveTilesInfo(video.Id, tilesInfo);
- _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
- }
- else
- {
- throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
- }
+ SaveTilesInfo(video.Id, tilesInfo);
+ _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
}
- catch (Exception ex)
+ else
{
- _logger.LogError(ex, "Error while saving trickplay tiles info.");
-
- // Make sure no files stay in metadata folders on failure
- // if tiles info wasn't saved.
- Directory.Delete(outputDir, true);
+ throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
}
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error creating trickplay images.");
- }
- finally
- {
- _resourcePool.Release();
+ _logger.LogError(ex, "Error while saving trickplay tiles info.");
- if (!string.IsNullOrEmpty(imgTempDir))
- {
- Directory.Delete(imgTempDir, true);
- }
+ // Make sure no files stay in metadata folders on failure
+ // if tiles info wasn't saved.
+ Directory.Delete(outputDir, true);
}
}
-
- private TrickplayTilesInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir)
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating trickplay images.");
+ }
+ finally
{
- if (images.Count == 0)
+ _resourcePool.Release();
+
+ if (!string.IsNullOrEmpty(imgTempDir))
{
- throw new InvalidOperationException("Can't create trickplay from 0 images.");
+ Directory.Delete(imgTempDir, true);
}
+ }
+ }
- Directory.CreateDirectory(workDir);
+ private TrickplayTilesInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir)
+ {
+ if (images.Count == 0)
+ {
+ throw new InvalidOperationException("Can't create trickplay from 0 images.");
+ }
- var tilesInfo = new TrickplayTilesInfo
- {
- Width = width,
- Interval = options.Interval,
- TileWidth = options.TileWidth,
- TileHeight = options.TileHeight,
- TileCount = 0,
- Bandwidth = 0
- };
-
- var firstImg = SKBitmap.Decode(images[0].FullName);
- if (firstImg == null)
- {
- throw new InvalidDataException("Could not decode image data.");
- }
+ Directory.CreateDirectory(workDir);
- tilesInfo.Height = firstImg.Height;
- if (tilesInfo.Width != firstImg.Width)
- {
- throw new InvalidOperationException("Image width does not match config width.");
- }
+ var tilesInfo = new TrickplayTilesInfo
+ {
+ Width = width,
+ Interval = options.Interval,
+ TileWidth = options.TileWidth,
+ TileHeight = options.TileHeight,
+ TileCount = 0,
+ Bandwidth = 0
+ };
+
+ var firstImg = SKBitmap.Decode(images[0].FullName);
+ if (firstImg == null)
+ {
+ throw new InvalidDataException("Could not decode image data.");
+ }
- /*
- * Generate grids of trickplay image tiles
- */
- var imgNo = 0;
- var i = 0;
- while (i < images.Count)
- {
- var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight);
+ tilesInfo.Height = firstImg.Height;
+ if (tilesInfo.Width != firstImg.Width)
+ {
+ throw new InvalidOperationException("Image width does not match config width.");
+ }
+
+ /*
+ * Generate grids of trickplay image tiles
+ */
+ var imgNo = 0;
+ var i = 0;
+ while (i < images.Count)
+ {
+ var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight);
- using (var canvas = new SKCanvas(tileGrid))
+ using (var canvas = new SKCanvas(tileGrid))
+ {
+ for (var y = 0; y < tilesInfo.TileHeight; y++)
{
- for (var y = 0; y < tilesInfo.TileHeight; y++)
+ for (var x = 0; x < tilesInfo.TileWidth; x++)
{
- for (var x = 0; x < tilesInfo.TileWidth; x++)
+ if (i >= images.Count)
{
- if (i >= images.Count)
- {
- break;
- }
-
- var img = SKBitmap.Decode(images[i].FullName);
- if (img == null)
- {
- throw new InvalidDataException("Could not decode image data.");
- }
-
- if (tilesInfo.Width != img.Width)
- {
- throw new InvalidOperationException("Image width does not match config width.");
- }
-
- if (tilesInfo.Height != img.Height)
- {
- throw new InvalidOperationException("Image height does not match first image height.");
- }
-
- canvas.DrawBitmap(img, x * tilesInfo.Width, y * tilesInfo.Height);
- tilesInfo.TileCount++;
- i++;
+ break;
}
- }
- }
- // Output each tile grid to singular file
- var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg");
- using (var stream = File.OpenWrite(tileGridPath))
- {
- tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality);
- }
+ var img = SKBitmap.Decode(images[i].FullName);
+ if (img == null)
+ {
+ throw new InvalidDataException("Could not decode image data.");
+ }
+
+ if (tilesInfo.Width != img.Width)
+ {
+ throw new InvalidOperationException("Image width does not match config width.");
+ }
- var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000));
- tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate);
+ if (tilesInfo.Height != img.Height)
+ {
+ throw new InvalidOperationException("Image height does not match first image height.");
+ }
- imgNo++;
+ canvas.DrawBitmap(img, x * tilesInfo.Width, y * tilesInfo.Height);
+ tilesInfo.TileCount++;
+ i++;
+ }
+ }
}
- /*
- * Move trickplay tiles to output directory
- */
- Directory.CreateDirectory(outputDir);
-
- // Replace existing tile grids if they already exist
- if (Directory.Exists(outputDir))
+ // Output each tile grid to singular file
+ var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg");
+ using (var stream = File.OpenWrite(tileGridPath))
{
- Directory.Delete(outputDir, true);
+ tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality);
}
- MoveDirectory(workDir, outputDir);
+ var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000));
+ tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate);
- return tilesInfo;
+ imgNo++;
}
- private bool CanGenerateTrickplay(Video video, int interval)
- {
- var videoType = video.VideoType;
- if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay)
- {
- return false;
- }
-
- if (video.IsPlaceHolder)
- {
- return false;
- }
+ /*
+ * Move trickplay tiles to output directory
+ */
+ Directory.CreateDirectory(outputDir);
- if (video.IsShortcut)
- {
- return false;
- }
+ // Replace existing tile grids if they already exist
+ if (Directory.Exists(outputDir))
+ {
+ Directory.Delete(outputDir, true);
+ }
- if (!video.IsCompleteMedia)
- {
- return false;
- }
+ MoveDirectory(workDir, outputDir);
- if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
- {
- return false;
- }
-
- var libraryOptions = _libraryManager.GetLibraryOptions(video);
- if (libraryOptions is not null)
- {
- if (!libraryOptions.EnableTrickplayImageExtraction)
- {
- return false;
- }
- }
- else
- {
- return false;
- }
+ return tilesInfo;
+ }
- // Can't extract images if there are no video streams
- return video.GetMediaStreams().Count > 0;
+ private bool CanGenerateTrickplay(Video video, int interval)
+ {
+ var videoType = video.VideoType;
+ if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay)
+ {
+ return false;
}
- ///
- public Dictionary GetTilesResolutions(Guid itemId)
+ if (video.IsPlaceHolder)
{
- return _itemRepo.GetTilesResolutions(itemId);
+ return false;
}
- ///
- public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo)
+ if (video.IsShortcut)
{
- _itemRepo.SaveTilesInfo(itemId, tilesInfo);
+ return false;
}
- ///
- public Dictionary> GetTrickplayManifest(BaseItem item)
+ if (!video.IsCompleteMedia)
{
- return _itemRepo.GetTrickplayManifest(item);
+ return false;
}
- ///
- public string GetTrickplayTilePath(BaseItem item, int width, int index)
+ if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
{
- return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
+ return false;
}
- private string GetTrickplayDirectory(BaseItem item, int? width = null)
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ if (libraryOptions is not null)
{
- var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
-
- return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+ if (!libraryOptions.EnableTrickplayImageExtraction)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ return false;
}
- private void MoveDirectory(string source, string destination)
+ // Can't extract images if there are no video streams
+ return video.GetMediaStreams().Count > 0;
+ }
+
+ ///
+ public Dictionary GetTilesResolutions(Guid itemId)
+ {
+ return _itemRepo.GetTilesResolutions(itemId);
+ }
+
+ ///
+ public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo)
+ {
+ _itemRepo.SaveTilesInfo(itemId, tilesInfo);
+ }
+
+ ///
+ public Dictionary> GetTrickplayManifest(BaseItem item)
+ {
+ return _itemRepo.GetTrickplayManifest(item);
+ }
+
+ ///
+ public string GetTrickplayTilePath(BaseItem item, int width, int index)
+ {
+ return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
+ }
+
+ private string GetTrickplayDirectory(BaseItem item, int? width = null)
+ {
+ var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+ return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+ }
+
+ private void MoveDirectory(string source, string destination)
+ {
+ try
{
- try
+ Directory.Move(source, destination);
+ }
+ catch (IOException)
+ {
+ // Cross device move requires a copy
+ Directory.CreateDirectory(destination);
+ foreach (string file in Directory.GetFiles(source))
{
- Directory.Move(source, destination);
+ File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
}
- catch (IOException)
- {
- // Cross device move requires a copy
- Directory.CreateDirectory(destination);
- foreach (string file in Directory.GetFiles(source))
- {
- File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
- }
- Directory.Delete(source, true);
- }
+ Directory.Delete(source, true);
}
}
}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
index e29646725..d467c480e 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
@@ -10,118 +10,117 @@ using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Configuration;
using Microsoft.Extensions.Logging;
-namespace MediaBrowser.Providers.Trickplay
+namespace MediaBrowser.Providers.Trickplay;
+
+///
+/// Class TrickplayProvider. Provides images and metadata for trickplay
+/// scrubbing previews.
+///
+public class TrickplayProvider : ICustomMetadataProvider,
+ ICustomMetadataProvider,
+ ICustomMetadataProvider,
+ ICustomMetadataProvider,
+ ICustomMetadataProvider,
+ IHasItemChangeMonitor,
+ IHasOrder,
+ IForcedProvider
{
+ private readonly ILogger _logger;
+ private readonly IServerConfigurationManager _config;
+ private readonly ITrickplayManager _trickplayManager;
+ private readonly ILibraryManager _libraryManager;
+
///
- /// Class TrickplayProvider. Provides images and metadata for trickplay
- /// scrubbing previews.
+ /// Initializes a new instance of the class.
///
- public class TrickplayProvider : ICustomMetadataProvider,
- ICustomMetadataProvider,
- ICustomMetadataProvider,
- ICustomMetadataProvider,
- ICustomMetadataProvider,
- IHasItemChangeMonitor,
- IHasOrder,
- IForcedProvider
+ /// The logger.
+ /// The configuration manager.
+ /// The trickplay manager.
+ /// The library manager.
+ public TrickplayProvider(
+ ILogger logger,
+ IServerConfigurationManager config,
+ ITrickplayManager trickplayManager,
+ ILibraryManager libraryManager)
{
- private readonly ILogger _logger;
- private readonly IServerConfigurationManager _config;
- private readonly ITrickplayManager _trickplayManager;
- private readonly ILibraryManager _libraryManager;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The configuration manager.
- /// The trickplay manager.
- /// The library manager.
- public TrickplayProvider(
- ILogger logger,
- IServerConfigurationManager config,
- ITrickplayManager trickplayManager,
- ILibraryManager libraryManager)
- {
- _logger = logger;
- _config = config;
- _trickplayManager = trickplayManager;
- _libraryManager = libraryManager;
- }
+ _logger = logger;
+ _config = config;
+ _trickplayManager = trickplayManager;
+ _libraryManager = libraryManager;
+ }
- ///
- public string Name => "Trickplay Provider";
+ ///
+ public string Name => "Trickplay Provider";
- ///
- public int Order => 100;
+ ///
+ public int Order => 100;
- ///
- public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ ///
+ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ {
+ if (item.IsFileProtocol)
{
- if (item.IsFileProtocol)
+ var file = directoryService.GetFile(item.Path);
+ if (file is not null && item.DateModified != file.LastWriteTimeUtc)
{
- var file = directoryService.GetFile(item.Path);
- if (file is not null && item.DateModified != file.LastWriteTimeUtc)
- {
- return true;
- }
+ return true;
}
-
- return false;
}
- ///
- public Task FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
- {
- return FetchInternal(item, options, cancellationToken);
- }
+ return false;
+ }
- ///
- public Task FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
- {
- return FetchInternal(item, options, cancellationToken);
- }
+ ///
+ public Task FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
- ///
- public Task FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
- {
- return FetchInternal(item, options, cancellationToken);
- }
+ ///
+ public Task FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ ///
+ public Task FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
- ///
- public Task FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ ///
+ public Task FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ ///
+ public Task FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ private async Task FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
+ bool replace = options.ReplaceAllImages;
+
+ if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false))
{
- return FetchInternal(item, options, cancellationToken);
+ return ItemUpdateType.None;
}
- ///
- public Task FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking)
{
- return FetchInternal(item, options, cancellationToken);
+ await _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false);
}
-
- private async Task FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ else
{
- var libraryOptions = _libraryManager.GetLibraryOptions(video);
- bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
- bool replace = options.ReplaceAllImages;
-
- if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false))
- {
- return ItemUpdateType.None;
- }
-
- if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking)
- {
- await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- _ = _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false);
- }
-
- // The core doesn't need to trigger any save operations over this
- return ItemUpdateType.None;
+ _ = _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false);
}
+
+ // The core doesn't need to trigger any save operations over this
+ return ItemUpdateType.None;
}
}
--
cgit v1.2.3
From 07e6804f7a892514487334066a24661b395aab3d Mon Sep 17 00:00:00 2001
From: Nick <20588554+nicknsy@users.noreply.github.com>
Date: Wed, 17 May 2023 22:46:30 -0700
Subject: Change default threads to 1
---
MediaBrowser.Model/Configuration/TrickplayOptions.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'MediaBrowser.Model')
diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
index 1fff1a5ed..92c16ee84 100644
--- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs
+++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
@@ -56,5 +56,5 @@ public class TrickplayOptions
///
/// Gets or sets the number of threads to be used by ffmpeg.
///
- public int ProcessThreads { get; set; } = 0;
+ public int ProcessThreads { get; set; } = 1;
}
--
cgit v1.2.3
From ab20ceaad65b2e72fe6e823aa6086e2c6ac36844 Mon Sep 17 00:00:00 2001
From: Nick <20588554+nicknsy@users.noreply.github.com>
Date: Mon, 26 Jun 2023 17:40:10 -0700
Subject: Migrate to trickplay table to EF. Rename vars/methods/members to have
consistent use of tile and thumbnail
---
Emby.Server.Implementations/ApplicationHost.cs | 3 -
.../Data/SqliteItemRepository.cs | 124 ----
Emby.Server.Implementations/Dto/DtoService.cs | 10 +-
Jellyfin.Api/Controllers/TrickplayController.cs | 19 +-
Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 18 +-
Jellyfin.Data/Entities/TrickplayInfo.cs | 75 +++
.../JellyfinDbContext.cs | 5 +
.../20230626233818_AddTrickplayInfos.Designer.cs | 681 +++++++++++++++++++++
.../Migrations/20230626233818_AddTrickplayInfos.cs | 40 ++
.../Migrations/JellyfinDbModelSnapshot.cs | 35 +-
.../TrickplayInfoConfiguration.cs | 18 +
.../Trickplay/TrickplayManager.cs | 468 ++++++++++++++
Jellyfin.Server/CoreAppHost.cs | 3 +
MediaBrowser.Controller/Drawing/IImageEncoder.cs | 13 +-
.../Persistence/IItemRepository.cs | 21 -
.../Trickplay/ITrickplayManager.cs | 30 +-
MediaBrowser.Model/Dto/BaseItemDto.cs | 3 +-
MediaBrowser.Model/Entities/TrickplayTilesInfo.cs | 49 --
.../Trickplay/TrickplayManager.cs | 425 -------------
src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +-
src/Jellyfin.Drawing/NullImageEncoder.cs | 2 +-
21 files changed, 1375 insertions(+), 669 deletions(-)
create mode 100644 Jellyfin.Data/Entities/TrickplayInfo.cs
create mode 100644 Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
create mode 100644 Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
create mode 100644 Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
delete mode 100644 MediaBrowser.Model/Entities/TrickplayTilesInfo.cs
delete mode 100644 MediaBrowser.Providers/Trickplay/TrickplayManager.cs
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 1e0bb0cd6..7969577bc 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -78,7 +78,6 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.SyncPlay;
-using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
@@ -97,7 +96,6 @@ using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles;
-using MediaBrowser.Providers.Trickplay;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -593,7 +591,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 8ec24522b..d1fbea95a 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -26,7 +26,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
@@ -48,7 +47,6 @@ namespace Emby.Server.Implementations.Data
{
private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2";
- private const string TrickplayTableName = "Trickplay";
private const string SaveItemCommandText =
@"replace into TypedBaseItems
@@ -384,8 +382,6 @@ namespace Emby.Server.Implementations.Data
"create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
- "create table if not exists " + TrickplayTableName + " (ItemId GUID, Width INT NOT NULL, Height INT NOT NULL, TileWidth INT NOT NULL, TileHeight INT NOT NULL, TileCount INT NOT NULL, Interval INT NOT NULL, Bandwidth INT NOT NULL, PRIMARY KEY (ItemId, Width))",
-
CreateMediaStreamsTableCommand,
CreateMediaAttachmentsTableCommand,
@@ -2138,126 +2134,6 @@ namespace Emby.Server.Implementations.Data
}
}
- ///
- public Dictionary GetTilesResolutions(Guid itemId)
- {
- CheckDisposed();
-
- var tilesResolutions = new Dictionary();
- using (var connection = GetConnection(true))
- {
- using (var statement = PrepareStatement(connection, "select Width,Height,TileWidth,TileHeight,TileCount,Interval,Bandwidth from " + TrickplayTableName + " where ItemId = @ItemId order by Width asc"))
- {
- statement.TryBind("@ItemId", itemId);
-
- foreach (var row in statement.ExecuteQuery())
- {
- TrickplayTilesInfo tilesInfo = GetTrickplayTilesInfo(row);
- tilesResolutions[tilesInfo.Width] = tilesInfo;
- }
- }
- }
-
- return tilesResolutions;
- }
-
- ///
- public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo)
- {
- CheckDisposed();
-
- ArgumentNullException.ThrowIfNull(tilesInfo);
-
- var idBlob = itemId.ToByteArray();
- using (var connection = GetConnection(false))
- {
- connection.RunInTransaction(
- db =>
- {
- // Delete old tiles info
- db.Execute("delete from " + TrickplayTableName + " where ItemId=@ItemId and Width=@Width", idBlob, tilesInfo.Width);
- db.Execute(
- "insert into " + TrickplayTableName + " values (@ItemId, @Width, @Height, @TileWidth, @TileHeight, @TileCount, @Interval, @Bandwidth)",
- idBlob,
- tilesInfo.Width,
- tilesInfo.Height,
- tilesInfo.TileWidth,
- tilesInfo.TileHeight,
- tilesInfo.TileCount,
- tilesInfo.Interval,
- tilesInfo.Bandwidth);
- },
- TransactionMode);
- }
- }
-
- ///
- public Dictionary> GetTrickplayManifest(BaseItem item)
- {
- CheckDisposed();
-
- var trickplayManifest = new Dictionary>();
- foreach (var mediaSource in item.GetMediaSources(false))
- {
- var mediaSourceId = Guid.Parse(mediaSource.Id);
- var tilesResolutions = GetTilesResolutions(mediaSourceId);
-
- if (tilesResolutions.Count > 0)
- {
- trickplayManifest[mediaSourceId] = tilesResolutions;
- }
- }
-
- return trickplayManifest;
- }
-
- ///
- /// Gets the trickplay tiles info.
- ///
- /// The reader.
- /// TrickplayTilesInfo.
- private TrickplayTilesInfo GetTrickplayTilesInfo(IReadOnlyList reader)
- {
- var tilesInfo = new TrickplayTilesInfo();
-
- if (reader.TryGetInt32(0, out var width))
- {
- tilesInfo.Width = width;
- }
-
- if (reader.TryGetInt32(1, out var height))
- {
- tilesInfo.Height = height;
- }
-
- if (reader.TryGetInt32(2, out var tileWidth))
- {
- tilesInfo.TileWidth = tileWidth;
- }
-
- if (reader.TryGetInt32(3, out var tileHeight))
- {
- tilesInfo.TileHeight = tileHeight;
- }
-
- if (reader.TryGetInt32(4, out var tileCount))
- {
- tilesInfo.TileCount = tileCount;
- }
-
- if (reader.TryGetInt32(5, out var interval))
- {
- tilesInfo.Interval = interval;
- }
-
- if (reader.TryGetInt32(6, out var bandwidth))
- {
- tilesInfo.Bandwidth = bandwidth;
- }
-
- return tilesInfo;
- }
-
private static bool EnableJoinUserData(InternalItemsQuery query)
{
if (query.User is null)
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 1687fa442..933b95df0 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
@@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy _livetvManagerFactory;
private readonly ILyricManager _lyricManager;
+ private readonly ITrickplayManager _trickplayManager;
public DtoService(
ILogger logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy livetvManagerFactory,
- ILyricManager lyricManager)
+ ILyricManager lyricManager,
+ ITrickplayManager trickplayManager)
{
_logger = logger;
_libraryManager = libraryManager;
@@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_lyricManager = lyricManager;
+ _trickplayManager = trickplayManager;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -1060,9 +1064,11 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Trickplay))
{
+ var manifest = _trickplayManager.GetTrickplayManifest(item).ConfigureAwait(false).GetAwaiter().GetResult();
+
// To stay consistent with other fields, this must go from a Guid to a non-dashed string.
// This does not seem to occur automatically to dictionaries like it does with other Guid fields.
- dto.Trickplay = _itemRepo.GetTrickplayManifest(item).ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value);
+ dto.Trickplay = manifest.ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value);
}
if (video.ExtraType.HasValue)
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
index 36464d726..e4f8f076e 100644
--- a/Jellyfin.Api/Controllers/TrickplayController.cs
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -2,6 +2,7 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using System.Text;
+using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using MediaBrowser.Controller.Library;
@@ -42,18 +43,18 @@ public class TrickplayController : BaseJellyfinApiController
/// The item id.
/// The width of a single tile.
/// The media version id, if using an alternate version.
- /// Tiles stream returned.
- /// A containing the trickplay tiles file.
+ /// Tiles playlist returned.
+ /// A containing the trickplay playlist file.
[HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesPlaylistFile]
- public ActionResult GetTrickplayHlsPlaylist(
+ public async Task GetTrickplayHlsPlaylist(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int width,
[FromQuery] Guid? mediaSourceId)
{
- string? playlist = _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken());
+ string? playlist = await _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()).ConfigureAwait(false);
if (string.IsNullOrEmpty(playlist))
{
@@ -64,20 +65,20 @@ public class TrickplayController : BaseJellyfinApiController
}
///
- /// Gets a trickplay tile grid image.
+ /// Gets a trickplay tile image.
///
/// The item id.
/// The width of a single tile.
- /// The index of the desired tile grid.
+ /// The index of the desired tile.
/// The media version id, if using an alternate version.
- /// Tiles image returned.
- /// Tiles image not found at specified index.
+ /// Tile image returned.
+ /// Tile image not found at specified index.
/// A containing the trickplay tiles image.
[HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
- public ActionResult GetTrickplayGridImage(
+ public ActionResult GetTrickplayTileImage(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int width,
[FromRoute, Required] int index,
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index b1657aeae..bd3091055 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -308,8 +308,8 @@ public class DynamicHlsHelper
if (!isLiveStream && (state.VideoRequest?.EnableTrickplay).GetValueOrDefault(false))
{
var sourceId = Guid.Parse(state.Request.MediaSourceId);
- var tilesResolutions = _trickplayManager.GetTilesResolutions(sourceId);
- AddTrickplay(state, tilesResolutions, builder, _httpContextAccessor.HttpContext.User);
+ var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
+ AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
}
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
@@ -544,17 +544,17 @@ public class DynamicHlsHelper
/// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
///
/// StreamState of the current stream.
- /// Dictionary of widths to corresponding tiles info.
+ /// Dictionary of widths to corresponding tiles info.
/// StringBuilder to append the field to.
/// Http user context.
- private void AddTrickplay(StreamState state, Dictionary tilesResolutions, StringBuilder builder, ClaimsPrincipal user)
+ private void AddTrickplay(StreamState state, Dictionary trickplayResolutions, StringBuilder builder, ClaimsPrincipal user)
{
const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\"";
- foreach (var resolution in tilesResolutions)
+ foreach (var resolution in trickplayResolutions)
{
var width = resolution.Key;
- var tilesInfo = resolution.Value;
+ var trickplayInfo = resolution.Value;
var url = string.Format(
CultureInfo.InvariantCulture,
@@ -566,9 +566,9 @@ public class DynamicHlsHelper
var line = string.Format(
CultureInfo.InvariantCulture,
playlistFormat,
- tilesInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
- tilesInfo.Width.ToString(CultureInfo.InvariantCulture),
- tilesInfo.Height.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
url);
builder.AppendLine(line);
diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Data/Entities/TrickplayInfo.cs
new file mode 100644
index 000000000..64e7da1b5
--- /dev/null
+++ b/Jellyfin.Data/Entities/TrickplayInfo.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Data.Entities;
+
+///
+/// An entity representing the metadata for a group of trickplay tiles.
+///
+public class TrickplayInfo
+{
+ ///
+ /// Gets or sets the id of the associated item.
+ ///
+ ///
+ /// Required.
+ ///
+ [JsonIgnore]
+ public Guid ItemId { get; set; }
+
+ ///
+ /// Gets or sets width of an individual thumbnail.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Width { get; set; }
+
+ ///
+ /// Gets or sets height of an individual thumbnail.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Height { get; set; }
+
+ ///
+ /// Gets or sets amount of thumbnails per row.
+ ///
+ ///
+ /// Required.
+ ///
+ public int TileWidth { get; set; }
+
+ ///
+ /// Gets or sets amount of thumbnails per column.
+ ///
+ ///
+ /// Required.
+ ///
+ public int TileHeight { get; set; }
+
+ ///
+ /// Gets or sets total amount of non-black thumbnails.
+ ///
+ ///
+ /// Required.
+ ///
+ public int ThumbnailCount { get; set; }
+
+ ///
+ /// Gets or sets interval in milliseconds between each trickplay thumbnail.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Interval { get; set; }
+
+ ///
+ /// Gets or sets peak bandwith usage in bits per second.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Bandwidth { get; set; }
+}
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
index 0d91707e3..ea99af004 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext
///
public DbSet Users => Set();
+ ///
+ /// Gets the containing the trickplay metadata.
+ ///
+ public DbSet TrickplayInfos => Set();
+
/*public DbSet Artwork => Set();
public DbSet Books => Set();
diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
new file mode 100644
index 000000000..28baf1992
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
@@ -0,0 +1,681 @@
+//
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20230626233818_AddTrickplayInfos")]
+ partial class AddTrickplayInfos
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
new file mode 100644
index 000000000..76b12de08
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
@@ -0,0 +1,40 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ ///
+ public partial class AddTrickplayInfos : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "TrickplayInfos",
+ columns: table => new
+ {
+ ItemId = table.Column(type: "TEXT", nullable: false),
+ Width = table.Column(type: "INTEGER", nullable: false),
+ Height = table.Column(type: "INTEGER", nullable: false),
+ TileWidth = table.Column(type: "INTEGER", nullable: false),
+ TileHeight = table.Column(type: "INTEGER", nullable: false),
+ ThumbnailCount = table.Column(type: "INTEGER", nullable: false),
+ Interval = table.Column(type: "INTEGER", nullable: false),
+ Bandwidth = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width });
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "TrickplayInfos");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index d23508096..3c06e1cfc 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("DeviceOptions");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property("Id")
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
new file mode 100644
index 000000000..dc1c17e5e
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
@@ -0,0 +1,18 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ ///
+ /// FluentAPI configuration for the TrickplayInfo entity.
+ ///
+ public class TrickplayInfoConfiguration : IEntityTypeConfiguration
+ {
+ ///
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(info => new { info.ItemId, info.Width });
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
new file mode 100644
index 000000000..34b27e21a
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -0,0 +1,468 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Trickplay;
+
+///
+/// ITrickplayManager implementation.
+///
+public class TrickplayManager : ITrickplayManager
+{
+ private readonly ILogger _logger;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
+ private readonly IImageEncoder _imageEncoder;
+ private readonly IDbContextFactory _dbProvider;
+
+ private static readonly SemaphoreSlim _resourcePool = new(1, 1);
+ private static readonly string[] _trickplayImgExtensions = { ".jpg" };
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// The media encoder.
+ /// The file systen.
+ /// The encoding helper.
+ /// The library manager.
+ /// The server configuration manager.
+ /// The image encoder.
+ /// The database provider.
+ public TrickplayManager(
+ ILogger logger,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ EncodingHelper encodingHelper,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager config,
+ IImageEncoder imageEncoder,
+ IDbContextFactory dbProvider)
+ {
+ _logger = logger;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _encodingHelper = encodingHelper;
+ _libraryManager = libraryManager;
+ _config = config;
+ _imageEncoder = imageEncoder;
+ _dbProvider = dbProvider;
+ }
+
+ ///
+ public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken)
+ {
+ _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
+
+ var options = _config.Configuration.TrickplayOptions;
+ foreach (var width in options.WidthResolutions)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await RefreshTrickplayDataInternal(
+ video,
+ replace,
+ width,
+ options,
+ cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task RefreshTrickplayDataInternal(
+ Video video,
+ bool replace,
+ int width,
+ TrickplayOptions options,
+ CancellationToken cancellationToken)
+ {
+ if (!CanGenerateTrickplay(video, options.Interval))
+ {
+ return;
+ }
+
+ var imgTempDir = string.Empty;
+ var outputDir = GetTrickplayDirectory(video, width);
+
+ await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
+ {
+ _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
+ return;
+ }
+
+ // Extract images
+ // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
+ var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
+
+ if (mediaSource is null)
+ {
+ _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
+ return;
+ }
+
+ var mediaPath = mediaSource.Path;
+ var mediaStream = mediaSource.VideoStream;
+ var container = mediaSource.Container;
+
+ _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
+ imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
+ mediaPath,
+ container,
+ mediaSource,
+ mediaStream,
+ width,
+ TimeSpan.FromMilliseconds(options.Interval),
+ options.EnableHwAcceleration,
+ options.ProcessThreads,
+ options.Qscale,
+ options.ProcessPriority,
+ _encodingHelper,
+ cancellationToken).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
+ {
+ throw new InvalidOperationException("Null or invalid directory from media encoder.");
+ }
+
+ var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
+ .Select(i => i.FullName)
+ .OrderBy(i => i)
+ .ToList();
+
+ // Create tiles
+ var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N"));
+ var trickplayInfo = CreateTiles(images, width, options, tilesTempDir, outputDir);
+
+ // Save tiles info
+ try
+ {
+ if (trickplayInfo is not null)
+ {
+ trickplayInfo.ItemId = video.Id;
+ await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
+
+ _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+ }
+ else
+ {
+ throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while saving trickplay tiles info.");
+
+ // Make sure no files stay in metadata folders on failure
+ // if tiles info wasn't saved.
+ Directory.Delete(outputDir, true);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating trickplay images.");
+ }
+ finally
+ {
+ _resourcePool.Release();
+
+ if (!string.IsNullOrEmpty(imgTempDir))
+ {
+ Directory.Delete(imgTempDir, true);
+ }
+ }
+ }
+
+ private TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir)
+ {
+ if (images.Count == 0)
+ {
+ throw new ArgumentException("Can't create trickplay from 0 images.");
+ }
+
+ Directory.CreateDirectory(workDir);
+
+ var trickplayInfo = new TrickplayInfo
+ {
+ Width = width,
+ Interval = options.Interval,
+ TileWidth = options.TileWidth,
+ TileHeight = options.TileHeight,
+ ThumbnailCount = images.Count,
+ // Set during image generation
+ Height = 0,
+ Bandwidth = 0
+ };
+
+ /*
+ * Generate trickplay tiles from sets of thumbnails
+ */
+ var imageOptions = new ImageCollageOptions
+ {
+ Width = trickplayInfo.TileWidth,
+ Height = trickplayInfo.TileHeight
+ };
+
+ var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+ var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
+
+ for (int i = 0; i < requiredTiles; i++)
+ {
+ // Set output/input paths
+ var tilePath = Path.Combine(workDir, $"{i}.jpg");
+
+ imageOptions.OutputPath = tilePath;
+ imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(thumbnailsPerTile).ToList();
+
+ // Generate image and use returned height for tiles info
+ var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
+ if (trickplayInfo.Height == 0)
+ {
+ trickplayInfo.Height = height;
+ }
+
+ // Update bitrate
+ var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
+ trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
+ }
+
+ /*
+ * Move trickplay tiles to output directory
+ */
+ Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
+
+ // Replace existing tiles if they already exist
+ if (Directory.Exists(outputDir))
+ {
+ Directory.Delete(outputDir, true);
+ }
+
+ MoveDirectory(workDir, outputDir);
+
+ return trickplayInfo;
+ }
+
+ private bool CanGenerateTrickplay(Video video, int interval)
+ {
+ var videoType = video.VideoType;
+ if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay)
+ {
+ return false;
+ }
+
+ if (video.IsPlaceHolder)
+ {
+ return false;
+ }
+
+ if (video.IsShortcut)
+ {
+ return false;
+ }
+
+ if (!video.IsCompleteMedia)
+ {
+ return false;
+ }
+
+ if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
+ {
+ return false;
+ }
+
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction)
+ {
+ return false;
+ }
+
+ // Can't extract images if there are no video streams
+ return video.GetMediaStreams().Count > 0;
+ }
+
+ ///
+ public async Task> GetTrickplayResolutions(Guid itemId)
+ {
+ var trickplayResolutions = new Dictionary();
+
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var trickplayInfos = await dbContext.TrickplayInfos
+ .AsNoTracking()
+ .Where(i => i.ItemId.Equals(itemId))
+ .ToListAsync()
+ .ConfigureAwait(false);
+
+ foreach (var info in trickplayInfos)
+ {
+ trickplayResolutions[info.Width] = info;
+ }
+ }
+
+ return trickplayResolutions;
+ }
+
+ ///
+ public async Task SaveTrickplayInfo(TrickplayInfo info)
+ {
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var oldInfo = await dbContext.TrickplayInfos.FindAsync(info.ItemId, info.Width).ConfigureAwait(false);
+ if (oldInfo is not null)
+ {
+ dbContext.TrickplayInfos.Remove(oldInfo);
+ }
+
+ dbContext.Add(info);
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+ }
+
+ ///
+ public async Task>> GetTrickplayManifest(BaseItem item)
+ {
+ var trickplayManifest = new Dictionary>();
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ var mediaSourceId = Guid.Parse(mediaSource.Id);
+ var trickplayResolutions = await GetTrickplayResolutions(mediaSourceId).ConfigureAwait(false);
+
+ if (trickplayResolutions.Count > 0)
+ {
+ trickplayManifest[mediaSourceId] = trickplayResolutions;
+ }
+ }
+
+ return trickplayManifest;
+ }
+
+ ///
+ public string GetTrickplayTilePath(BaseItem item, int width, int index)
+ {
+ return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
+ }
+
+ ///
+ public async Task GetHlsPlaylist(Guid itemId, int width, string? apiKey)
+ {
+ var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false);
+ if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo))
+ {
+ var builder = new StringBuilder(128);
+
+ if (trickplayInfo.ThumbnailCount > 0)
+ {
+ const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
+ const string decimalFormat = "{0:0.###}";
+
+ var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}";
+ var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}";
+ var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+ var thumbnailDuration = trickplayInfo.Interval / 1000d;
+ var infDuration = thumbnailDuration * thumbnailsPerTile;
+ var tileCount = (int)Math.Ceiling((decimal)trickplayInfo.ThumbnailCount / thumbnailsPerTile);
+
+ builder
+ .AppendLine("#EXTM3U")
+ .Append("#EXT-X-TARGETDURATION:")
+ .AppendLine(tileCount.ToString(CultureInfo.InvariantCulture))
+ .AppendLine("#EXT-X-VERSION:7")
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
+ .AppendLine("#EXT-X-IMAGES-ONLY");
+
+ for (int i = 0; i < tileCount; i++)
+ {
+ // All tiles prior to the last must contain full amount of thumbnails (no black).
+ if (i == tileCount - 1)
+ {
+ thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile);
+ infDuration = thumbnailDuration * thumbnailsPerTile;
+ }
+
+ // EXTINF
+ builder
+ .Append("#EXTINF:")
+ .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration)
+ .AppendLine(",");
+
+ // EXT-X-TILES
+ builder
+ .Append("#EXT-X-TILES:RESOLUTION=")
+ .Append(resolution)
+ .Append(",LAYOUT=")
+ .Append(layout)
+ .Append(",DURATION=")
+ .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration)
+ .AppendLine();
+
+ // URL
+ builder
+ .AppendFormat(
+ CultureInfo.InvariantCulture,
+ urlFormat,
+ width.ToString(CultureInfo.InvariantCulture),
+ i.ToString(CultureInfo.InvariantCulture),
+ itemId.ToString("N"),
+ apiKey)
+ .AppendLine();
+ }
+
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return builder.ToString();
+ }
+ }
+
+ return null;
+ }
+
+ private string GetTrickplayDirectory(BaseItem item, int? width = null)
+ {
+ var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+ return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+ }
+
+ private void MoveDirectory(string source, string destination)
+ {
+ try
+ {
+ Directory.Move(source, destination);
+ }
+ catch (IOException)
+ {
+ // Cross device move requires a copy
+ Directory.CreateDirectory(destination);
+ foreach (string file in Directory.GetFiles(source))
+ {
+ File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
+ }
+
+ Directory.Delete(source, true);
+ }
+ }
+}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 939376dd8..18d924aa8 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Devices;
using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Security;
+using Jellyfin.Server.Implementations.Trickplay;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager;
@@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Activity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -77,6 +79,7 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton();
serviceCollection.AddScoped();
serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
// TODO search the assemblies instead of adding them manually?
serviceCollection.AddSingleton();
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index 42c680761..c7bfbdb53 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Drawing;
namespace MediaBrowser.Controller.Drawing
@@ -84,13 +83,13 @@ namespace MediaBrowser.Controller.Drawing
void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops);
///
- /// Creates a new jpeg trickplay grid image.
+ /// Creates a new trickplay tile image.
///
- /// The options to use when creating the image. Width and Height are a quantity of tiles in this case, not pixels.
+ /// The options to use when creating the image. Width and Height are a quantity of thumbnails in this case, not pixels.
/// The image encode quality.
- /// The width of a single trickplay image.
- /// Optional height of a single trickplay image, if it is known.
- /// Height of single decoded trickplay image.
- int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight);
+ /// The width of a single trickplay thumbnail.
+ /// Optional height of a single trickplay thumbnail, if it is known.
+ /// Height of single decoded trickplay thumbnail.
+ int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight);
}
}
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 11eb4932c..2c52b2b45 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -61,27 +61,6 @@ namespace MediaBrowser.Controller.Persistence
/// The list of chapters to save.
void SaveChapters(Guid id, IReadOnlyList chapters);
- ///
- /// Get available trickplay resolutions and corresponding info.
- ///
- /// The item.
- /// Map of width resolutions to trickplay tiles info.
- Dictionary GetTilesResolutions(Guid itemId);
-
- ///
- /// Saves trickplay tiles info.
- ///
- /// The item.
- /// The trickplay tiles info.
- void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo);
-
- ///
- /// Gets trickplay data for an item.
- ///
- /// The item.
- /// A map of media source id to a map of tile width to tile info.
- Dictionary> GetTrickplayManifest(BaseItem item);
-
///
/// Gets the media streams.
///
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
index 8d36fc3ff..0a1e780b2 100644
--- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -2,8 +2,8 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Trickplay;
@@ -13,7 +13,7 @@ namespace MediaBrowser.Controller.Trickplay;
public interface ITrickplayManager
{
///
- /// Generate or replace trickplay data.
+ /// Generates new trickplay images and metadata.
///
/// The video.
/// Whether or not existing data should be replaced.
@@ -26,28 +26,28 @@ public interface ITrickplayManager
///
/// The item.
/// Map of width resolutions to trickplay tiles info.
- Dictionary GetTilesResolutions(Guid itemId);
+ Task> GetTrickplayResolutions(Guid itemId);
///
- /// Saves trickplay tiles info.
+ /// Saves trickplay info.
///
- /// The item.
- /// The trickplay tiles info.
- void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo);
+ /// The trickplay info.
+ /// Task.
+ Task SaveTrickplayInfo(TrickplayInfo info);
///
- /// Gets the trickplay manifest.
+ /// Gets all trickplay infos for all media streams of an item.
///
/// The item.
- /// A map of media source id to a map of tile width to tile info.
- Dictionary> GetTrickplayManifest(BaseItem item);
+ /// A map of media source id to a map of tile width to trickplay info.
+ Task>> GetTrickplayManifest(BaseItem item);
///
- /// Gets the path to a trickplay tiles image.
+ /// Gets the path to a trickplay tile image.
///
/// The item.
- /// The width of a single tile.
- /// The tile grid's index.
+ /// The width of a single thumbnail.
+ /// The tile's index.
/// The absolute path.
string GetTrickplayTilePath(BaseItem item, int width, int index);
@@ -55,8 +55,8 @@ public interface ITrickplayManager
/// Gets the trickplay HLS playlist.
///
/// The item.
- /// The width of a single tile.
+ /// The width of a single thumbnail.
/// Optional api key of the requesting user.
/// The text content of the .m3u8 playlist.
- string? GetHlsPlaylist(Guid itemId, int width, string? apiKey);
+ Task GetHlsPlaylist(Guid itemId, int width, string? apiKey);
}
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index 3db9cb08b..287966dd0 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
@@ -572,7 +573,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the trickplay manifest.
///
/// The trickplay manifest.
- public Dictionary> Trickplay { get; set; }
+ public Dictionary> Trickplay { get; set; }
///
/// Gets or sets the type of the location.
diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs
deleted file mode 100644
index 86d37787f..000000000
--- a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-namespace MediaBrowser.Model.Entities;
-
-///
-/// Class TrickplayTilesInfo.
-///
-public class TrickplayTilesInfo
-{
- ///
- /// Gets or sets width of an individual tile.
- ///
- /// The width.
- public int Width { get; set; }
-
- ///
- /// Gets or sets height of an individual tile.
- ///
- /// The height.
- public int Height { get; set; }
-
- ///
- /// Gets or sets amount of tiles per row.
- ///
- /// The tile grid's width.
- public int TileWidth { get; set; }
-
- ///
- /// Gets or sets amount of tiles per column.
- ///
- /// The tile grid's height.
- public int TileHeight { get; set; }
-
- ///
- /// Gets or sets total amount of non-black tiles.
- ///
- /// The tile count.
- public int TileCount { get; set; }
-
- ///
- /// Gets or sets interval in milliseconds between each trickplay tile.
- ///
- /// The interval.
- public int Interval { get; set; }
-
- ///
- /// Gets or sets peak bandwith usage in bits per second.
- ///
- /// The bandwidth.
- public int Bandwidth { get; set; }
-}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
deleted file mode 100644
index 3ea7c00d0..000000000
--- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
+++ /dev/null
@@ -1,425 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Trickplay;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.Trickplay;
-
-///
-/// ITrickplayManager implementation.
-///
-public class TrickplayManager : ITrickplayManager
-{
- private readonly ILogger _logger;
- private readonly IItemRepository _itemRepo;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly EncodingHelper _encodingHelper;
- private readonly ILibraryManager _libraryManager;
- private readonly IServerConfigurationManager _config;
- private readonly IImageEncoder _imageEncoder;
-
- private static readonly SemaphoreSlim _resourcePool = new(1, 1);
- private static readonly string[] _trickplayImgExtensions = { ".jpg" };
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The item repository.
- /// The media encoder.
- /// The file systen.
- /// The encoding helper.
- /// The library manager.
- /// The server configuration manager.
- /// The image encoder.
- public TrickplayManager(
- ILogger logger,
- IItemRepository itemRepo,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- EncodingHelper encodingHelper,
- ILibraryManager libraryManager,
- IServerConfigurationManager config,
- IImageEncoder imageEncoder)
- {
- _logger = logger;
- _itemRepo = itemRepo;
- _mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _encodingHelper = encodingHelper;
- _libraryManager = libraryManager;
- _config = config;
- _imageEncoder = imageEncoder;
- }
-
- ///
- public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken)
- {
- _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
-
- var options = _config.Configuration.TrickplayOptions;
- foreach (var width in options.WidthResolutions)
- {
- cancellationToken.ThrowIfCancellationRequested();
- await RefreshTrickplayDataInternal(
- video,
- replace,
- width,
- options,
- cancellationToken).ConfigureAwait(false);
- }
- }
-
- private async Task RefreshTrickplayDataInternal(
- Video video,
- bool replace,
- int width,
- TrickplayOptions options,
- CancellationToken cancellationToken)
- {
- if (!CanGenerateTrickplay(video, options.Interval))
- {
- return;
- }
-
- var imgTempDir = string.Empty;
- var outputDir = GetTrickplayDirectory(video, width);
-
- await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
- {
- if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width))
- {
- _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
- return;
- }
-
- // Extract images
- // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
- var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
-
- if (mediaSource is null)
- {
- _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
- return;
- }
-
- var mediaPath = mediaSource.Path;
- var mediaStream = mediaSource.VideoStream;
- var container = mediaSource.Container;
-
- _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
- imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
- mediaPath,
- container,
- mediaSource,
- mediaStream,
- width,
- TimeSpan.FromMilliseconds(options.Interval),
- options.EnableHwAcceleration,
- options.ProcessThreads,
- options.Qscale,
- options.ProcessPriority,
- _encodingHelper,
- cancellationToken).ConfigureAwait(false);
-
- if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
- {
- throw new InvalidOperationException("Null or invalid directory from media encoder.");
- }
-
- var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
- .Select(i => i.FullName)
- .OrderBy(i => i)
- .ToList();
-
- // Create tiles
- var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N"));
- var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir);
-
- // Save tiles info
- try
- {
- if (tilesInfo is not null)
- {
- SaveTilesInfo(video.Id, tilesInfo);
- _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
- }
- else
- {
- throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error while saving trickplay tiles info.");
-
- // Make sure no files stay in metadata folders on failure
- // if tiles info wasn't saved.
- Directory.Delete(outputDir, true);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error creating trickplay images.");
- }
- finally
- {
- _resourcePool.Release();
-
- if (!string.IsNullOrEmpty(imgTempDir))
- {
- Directory.Delete(imgTempDir, true);
- }
- }
- }
-
- private TrickplayTilesInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir)
- {
- if (images.Count == 0)
- {
- throw new ArgumentException("Can't create trickplay from 0 images.");
- }
-
- Directory.CreateDirectory(workDir);
-
- var tilesInfo = new TrickplayTilesInfo
- {
- Width = width,
- Interval = options.Interval,
- TileWidth = options.TileWidth,
- TileHeight = options.TileHeight,
- TileCount = images.Count,
- // Set during image generation
- Height = 0,
- Bandwidth = 0
- };
-
- /*
- * Generate trickplay tile grids from sets of images
- */
- var imageOptions = new ImageCollageOptions
- {
- Width = tilesInfo.TileWidth,
- Height = tilesInfo.TileHeight
- };
-
- var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
- var requiredTileGrids = (int)Math.Ceiling((double)images.Count / tilesPerGrid);
-
- for (int i = 0; i < requiredTileGrids; i++)
- {
- // Set output/input paths
- var tileGridPath = Path.Combine(workDir, $"{i}.jpg");
-
- imageOptions.OutputPath = tileGridPath;
- imageOptions.InputPaths = images.Skip(i * tilesPerGrid).Take(tilesPerGrid).ToList();
-
- // Generate image and use returned height for tiles info
- var height = _imageEncoder.CreateTrickplayGrid(imageOptions, options.JpegQuality, tilesInfo.Width, tilesInfo.Height != 0 ? tilesInfo.Height : null);
- if (tilesInfo.Height == 0)
- {
- tilesInfo.Height = height;
- }
-
- // Update bitrate
- var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000));
- tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate);
- }
-
- /*
- * Move trickplay tiles to output directory
- */
- Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
-
- // Replace existing tile grids if they already exist
- if (Directory.Exists(outputDir))
- {
- Directory.Delete(outputDir, true);
- }
-
- MoveDirectory(workDir, outputDir);
-
- return tilesInfo;
- }
-
- private bool CanGenerateTrickplay(Video video, int interval)
- {
- var videoType = video.VideoType;
- if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay)
- {
- return false;
- }
-
- if (video.IsPlaceHolder)
- {
- return false;
- }
-
- if (video.IsShortcut)
- {
- return false;
- }
-
- if (!video.IsCompleteMedia)
- {
- return false;
- }
-
- if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
- {
- return false;
- }
-
- var libraryOptions = _libraryManager.GetLibraryOptions(video);
- if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction)
- {
- return false;
- }
-
- // Can't extract images if there are no video streams
- return video.GetMediaStreams().Count > 0;
- }
-
- ///
- public Dictionary GetTilesResolutions(Guid itemId)
- {
- return _itemRepo.GetTilesResolutions(itemId);
- }
-
- ///
- public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo)
- {
- _itemRepo.SaveTilesInfo(itemId, tilesInfo);
- }
-
- ///
- public Dictionary> GetTrickplayManifest(BaseItem item)
- {
- return _itemRepo.GetTrickplayManifest(item);
- }
-
- ///
- public string GetTrickplayTilePath(BaseItem item, int width, int index)
- {
- return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
- }
-
- ///
- public string? GetHlsPlaylist(Guid itemId, int width, string? apiKey)
- {
- var tilesResolutions = GetTilesResolutions(itemId);
- if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo))
- {
- var builder = new StringBuilder(128);
-
- if (tilesInfo.TileCount > 0)
- {
- const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
- const string decimalFormat = "{0:0.###}";
-
- var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}";
- var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}";
- var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
- var tileDuration = tilesInfo.Interval / 1000d;
- var infDuration = tileDuration * tilesPerGrid;
- var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid);
-
- builder
- .AppendLine("#EXTM3U")
- .Append("#EXT-X-TARGETDURATION:")
- .AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture))
- .AppendLine("#EXT-X-VERSION:7")
- .AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
- .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
- .AppendLine("#EXT-X-IMAGES-ONLY");
-
- for (int i = 0; i < tileGridCount; i++)
- {
- // All tile grids before the last one must contain full amount of tiles.
- // The final grid will be 0 < count <= maxTiles
- if (i == tileGridCount - 1)
- {
- tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid);
- infDuration = tileDuration * tilesPerGrid;
- }
-
- // EXTINF
- builder
- .Append("#EXTINF:")
- .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration)
- .AppendLine(",");
-
- // EXT-X-TILES
- builder
- .Append("#EXT-X-TILES:RESOLUTION=")
- .Append(resolution)
- .Append(",LAYOUT=")
- .Append(layout)
- .Append(",DURATION=")
- .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration)
- .AppendLine();
-
- // URL
- builder
- .AppendFormat(
- CultureInfo.InvariantCulture,
- urlFormat,
- width.ToString(CultureInfo.InvariantCulture),
- i.ToString(CultureInfo.InvariantCulture),
- itemId.ToString("N"),
- apiKey)
- .AppendLine();
- }
-
- builder.AppendLine("#EXT-X-ENDLIST");
- return builder.ToString();
- }
- }
-
- return null;
- }
-
- private string GetTrickplayDirectory(BaseItem item, int? width = null)
- {
- var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
-
- return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
- }
-
- private void MoveDirectory(string source, string destination)
- {
- try
- {
- Directory.Move(source, destination);
- }
- catch (IOException)
- {
- // Cross device move requires a copy
- Directory.CreateDirectory(destination);
- foreach (string file in Directory.GetFiles(source))
- {
- File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
- }
-
- Directory.Delete(source, true);
- }
- }
-}
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 2facf0f37..b387c437b 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -531,7 +531,7 @@ public class SkiaEncoder : IImageEncoder
}
///
- public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
+ public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
{
var paths = options.InputPaths;
var tileWidth = options.Width;
diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs
index 15345e1bc..1495661c1 100644
--- a/src/Jellyfin.Drawing/NullImageEncoder.cs
+++ b/src/Jellyfin.Drawing/NullImageEncoder.cs
@@ -50,7 +50,7 @@ public class NullImageEncoder : IImageEncoder
}
///
- public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
+ public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
{
throw new NotImplementedException();
}
--
cgit v1.2.3
From fc619337481e3bb0f686aee06d9c5630926baaef Mon Sep 17 00:00:00 2001
From: Nick <20588554+nicknsy@users.noreply.github.com>
Date: Mon, 26 Jun 2023 22:04:39 -0700
Subject: Minor code changes
---
Emby.Server.Implementations/Dto/DtoService.cs | 2 +-
MediaBrowser.Model/Configuration/ServerConfiguration.cs | 4 ++++
2 files changed, 5 insertions(+), 1 deletion(-)
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 933b95df0..19ae39662 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1064,7 +1064,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Trickplay))
{
- var manifest = _trickplayManager.GetTrickplayManifest(item).ConfigureAwait(false).GetAwaiter().GetResult();
+ var manifest = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
// To stay consistent with other fields, this must go from a Guid to a non-dashed string.
// This does not seem to occur automatically to dictionaries like it does with other Guid fields.
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 097eff295..18f107503 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -265,6 +265,10 @@ namespace MediaBrowser.Model.Configuration
/// The limit for parallel image encoding.
public int ParallelImageEncodingLimit { get; set; }
+ ///
+ /// Gets or sets the trickplay options.
+ ///
+ /// The trickplay options.
public TrickplayOptions TrickplayOptions { get; set; } = new TrickplayOptions();
}
}
--
cgit v1.2.3
From ba7e3bfd82aeee3c74a99fa1f4ca94deca8b4dbe Mon Sep 17 00:00:00 2001
From: Cody Robibero
Date: Sat, 23 Sep 2023 10:59:13 -0600
Subject: Migrate to customizable cast receiver config
---
.config/dotnet-tools.json | 12 +
Emby.Server.Implementations/ApplicationHost.cs | 3 +-
Jellyfin.Data/Entities/User.cs | 6 +
.../20230923170422_UserCastReceiver.Designer.cs | 654 +++++++++++++++++++++
.../Migrations/20230923170422_UserCastReceiver.cs | 29 +
.../Migrations/JellyfinDbModelSnapshot.cs | 6 +-
.../Users/UserManager.cs | 16 +-
Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +-
.../Migrations/Routines/AddDefaultCastReceivers.cs | 55 ++
.../Configuration/ServerConfiguration.cs | 497 ++++++++--------
.../Configuration/UserConfiguration.cs | 5 +
.../System/CastReceiverApplication.cs | 17 +
MediaBrowser.Model/System/SystemInfo.cs | 6 +
13 files changed, 1058 insertions(+), 251 deletions(-)
create mode 100644 .config/dotnet-tools.json
create mode 100644 Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
create mode 100644 Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs
create mode 100644 Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
create mode 100644 MediaBrowser.Model/System/CastReceiverApplication.cs
(limited to 'MediaBrowser.Model')
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 000000000..96be437d5
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-ef": {
+ "version": "7.0.11",
+ "commands": [
+ "dotnet-ef"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index f8208af70..5f8d275b6 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -968,7 +968,8 @@ namespace Emby.Server.Implementations
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
- PackageName = _startupOptions.PackageName
+ PackageName = _startupOptions.PackageName,
+ CastReceiverApplications = ConfigurationManager.Configuration.CastReceiverApplications
};
}
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 58ddaaf83..5c3e0338d 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -288,6 +288,12 @@ namespace Jellyfin.Data.Entities
///
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
+ ///
+ /// Gets or sets the cast receiver id.
+ ///
+ [StringLength(32)]
+ public string? CastReceiverId { get; set; }
+
///
[ConcurrencyCheck]
public uint RowVersion { get; private set; }
diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
new file mode 100644
index 000000000..2884d4256
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
@@ -0,0 +1,654 @@
+//
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20230923170422_UserCastReceiver")]
+ partial class UserCastReceiver
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs
new file mode 100644
index 000000000..f06410c15
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ ///
+ public partial class UserCastReceiver : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "CastReceiverId",
+ table: "Users",
+ type: "TEXT",
+ maxLength: 32,
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "CastReceiverId",
+ table: "Users");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index d23508096..6cd4594f6 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -457,6 +457,10 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasMaxLength(255)
.HasColumnType("TEXT");
+ b.Property("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
b.Property("DisplayCollectionsView")
.HasColumnType("INTEGER");
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 5010751dd..2a38fcecc 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -15,6 +15,7 @@ using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
@@ -45,6 +46,7 @@ namespace Jellyfin.Server.Implementations.Users
private readonly InvalidAuthProvider _invalidAuthProvider;
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDictionary _users;
@@ -58,6 +60,7 @@ namespace Jellyfin.Server.Implementations.Users
/// The application host.
/// The image processor.
/// The logger.
+ /// The system config manager.
public UserManager(
IDbContextFactory dbProvider,
IEventManager eventManager,
@@ -65,7 +68,8 @@ namespace Jellyfin.Server.Implementations.Users
INetworkManager networkManager,
IApplicationHost appHost,
IImageProcessor imageProcessor,
- ILogger logger)
+ ILogger logger,
+ IServerConfigurationManager serverConfigurationManager)
{
_dbProvider = dbProvider;
_eventManager = eventManager;
@@ -74,6 +78,7 @@ namespace Jellyfin.Server.Implementations.Users
_appHost = appHost;
_imageProcessor = imageProcessor;
_logger = logger;
+ _serverConfigurationManager = serverConfigurationManager;
_passwordResetProviders = appHost.GetExports();
_authenticationProviders = appHost.GetExports();
@@ -320,7 +325,10 @@ namespace Jellyfin.Server.Implementations.Users
OrderedViews = user.GetPreferenceValues(PreferenceKind.OrderedViews),
GroupedFolders = user.GetPreferenceValues(PreferenceKind.GroupedFolders),
MyMediaExcludes = user.GetPreferenceValues(PreferenceKind.MyMediaExcludes),
- LatestItemsExcludes = user.GetPreferenceValues(PreferenceKind.LatestItemExcludes)
+ LatestItemsExcludes = user.GetPreferenceValues(PreferenceKind.LatestItemExcludes),
+ CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
+ ? _serverConfigurationManager.Configuration.CastReceiverApplications.FirstOrDefault()?.Id
+ : user.CastReceiverId
},
Policy = new UserPolicy
{
@@ -608,6 +616,10 @@ namespace Jellyfin.Server.Implementations.Users
user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
user.RememberSubtitleSelections = config.RememberSubtitleSelections;
user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
+ if (!string.IsNullOrEmpty(config.CastReceiverId))
+ {
+ user.CastReceiverId = config.CastReceiverId;
+ }
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 2db0b77cd..757b56a49 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -42,7 +42,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.RemoveDownloadImagesInAdvance),
typeof(Routines.MigrateAuthenticationDb),
typeof(Routines.FixPlaylistOwner),
- typeof(Routines.MigrateRatingLevels)
+ typeof(Routines.MigrateRatingLevels),
+ typeof(Routines.AddDefaultCastReceivers)
};
///
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
new file mode 100644
index 000000000..75a6a6176
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
@@ -0,0 +1,55 @@
+using System;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.System;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+///
+/// Migration to add the default cast receivers to the system config.
+///
+public class AddDefaultCastReceivers : IMigrationRoutine
+{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ public AddDefaultCastReceivers(IServerConfigurationManager serverConfigurationManager)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ ///
+ public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8");
+
+ ///
+ public string Name => "AddDefaultCastReceivers";
+
+ ///
+ public bool PerformOnNewInstall => true;
+
+ ///
+ public void Perform()
+ {
+ // Only add if receiver list is empty.
+ if (_serverConfigurationManager.Configuration.CastReceiverApplications.Length == 0)
+ {
+ _serverConfigurationManager.Configuration.CastReceiverApplications = new CastReceiverApplication[]
+ {
+ new()
+ {
+ Id = "F007D354",
+ Name = "Stable"
+ },
+ new()
+ {
+ Id = "6F511C87",
+ Name = "Unstable"
+ }
+ };
+
+ _serverConfigurationManager.SaveConfiguration();
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 78a310f0b..1342ffb48 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -4,265 +4,270 @@
using System;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.System;
using MediaBrowser.Model.Updates;
-namespace MediaBrowser.Model.Configuration
+namespace MediaBrowser.Model.Configuration;
+
+///
+/// Represents the server configuration.
+///
+public class ServerConfiguration : BaseApplicationConfiguration
{
///
- /// Represents the server configuration.
+ /// Initializes a new instance of the class.
///
- public class ServerConfiguration : BaseApplicationConfiguration
+ public ServerConfiguration()
{
- ///
- /// Initializes a new instance of the class.
- ///
- public ServerConfiguration()
+ MetadataOptions = new[]
{
- MetadataOptions = new[]
+ new MetadataOptions()
+ {
+ ItemType = "Book"
+ },
+ new MetadataOptions()
+ {
+ ItemType = "Movie"
+ },
+ new MetadataOptions
+ {
+ ItemType = "MusicVideo",
+ DisabledMetadataFetchers = new[] { "The Open Movie Database" },
+ DisabledImageFetchers = new[] { "The Open Movie Database" }
+ },
+ new MetadataOptions
+ {
+ ItemType = "Series",
+ },
+ new MetadataOptions
+ {
+ ItemType = "MusicAlbum",
+ DisabledMetadataFetchers = new[] { "TheAudioDB" }
+ },
+ new MetadataOptions
+ {
+ ItemType = "MusicArtist",
+ DisabledMetadataFetchers = new[] { "TheAudioDB" }
+ },
+ new MetadataOptions
{
- new MetadataOptions()
- {
- ItemType = "Book"
- },
- new MetadataOptions()
- {
- ItemType = "Movie"
- },
- new MetadataOptions
- {
- ItemType = "MusicVideo",
- DisabledMetadataFetchers = new[] { "The Open Movie Database" },
- DisabledImageFetchers = new[] { "The Open Movie Database" }
- },
- new MetadataOptions
- {
- ItemType = "Series",
- },
- new MetadataOptions
- {
- ItemType = "MusicAlbum",
- DisabledMetadataFetchers = new[] { "TheAudioDB" }
- },
- new MetadataOptions
- {
- ItemType = "MusicArtist",
- DisabledMetadataFetchers = new[] { "TheAudioDB" }
- },
- new MetadataOptions
- {
- ItemType = "BoxSet"
- },
- new MetadataOptions
- {
- ItemType = "Season",
- },
- new MetadataOptions
- {
- ItemType = "Episode",
- }
- };
- }
-
- ///
- /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
- ///
- public bool EnableMetrics { get; set; } = false;
-
- public bool EnableNormalizedItemByNameIds { get; set; } = true;
-
- ///
- /// Gets or sets a value indicating whether this instance is port authorized.
- ///
- /// true if this instance is port authorized; otherwise, false.
- public bool IsPortAuthorized { get; set; }
-
- ///
- /// Gets or sets a value indicating whether quick connect is available for use on this server.
- ///
- public bool QuickConnectAvailable { get; set; } = true;
-
- ///
- /// Gets or sets a value indicating whether [enable case sensitive item ids].
- ///
- /// true if [enable case sensitive item ids]; otherwise, false.
- public bool EnableCaseSensitiveItemIds { get; set; } = true;
-
- public bool DisableLiveTvChannelUserDataName { get; set; } = true;
-
- ///
- /// Gets or sets the metadata path.
- ///
- /// The metadata path.
- public string MetadataPath { get; set; } = string.Empty;
-
- public string MetadataNetworkPath { get; set; } = string.Empty;
-
- ///
- /// Gets or sets the preferred metadata language.
- ///
- /// The preferred metadata language.
- public string PreferredMetadataLanguage { get; set; } = "en";
-
- ///
- /// Gets or sets the metadata country code.
- ///
- /// The metadata country code.
- public string MetadataCountryCode { get; set; } = "US";
-
- ///
- /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name.
- ///
- /// The sort replace characters.
- public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" };
-
- ///
- /// Gets or sets characters to be removed from strings to create a sort name.
- ///
- /// The sort remove characters.
- public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" };
-
- ///
- /// Gets or sets words to be removed from strings to create a sort name.
- ///
- /// The sort remove words.
- public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" };
-
- ///
- /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.
- ///
- /// The min resume PCT.
- public int MinResumePct { get; set; } = 5;
-
- ///
- /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
- ///
- /// The max resume PCT.
- public int MaxResumePct { get; set; } = 90;
-
- ///
- /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..
- ///
- /// The min resume duration seconds.
- public int MinResumeDurationSeconds { get; set; } = 300;
-
- ///
- /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated.
- ///
- /// The min resume in minutes.
- public int MinAudiobookResume { get; set; } = 5;
-
- ///
- /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
- ///
- /// The remaining time in minutes.
- public int MaxAudiobookResume { get; set; } = 5;
-
- ///
- /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
- /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
- /// different directories and files.
- ///
- /// The file watcher delay.
- public int LibraryMonitorDelay { get; set; } = 60;
-
- ///
- /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification.
- ///
- /// The library update duration.
- public int LibraryUpdateDuration { get; set; } = 30;
-
- ///
- /// Gets or sets the image saving convention.
- ///
- /// The image saving convention.
- public ImageSavingConvention ImageSavingConvention { get; set; }
-
- public MetadataOptions[] MetadataOptions { get; set; }
-
- public bool SkipDeserializationForBasicTypes { get; set; } = true;
-
- public string ServerName { get; set; } = string.Empty;
-
- public string UICulture { get; set; } = "en-US";
-
- public bool SaveMetadataHidden { get; set; } = false;
-
- public NameValuePair[] ContentTypes { get; set; } = Array.Empty();
-
- public int RemoteClientBitrateLimit { get; set; }
-
- public bool EnableFolderView { get; set; } = false;
-
- public bool EnableGroupingIntoCollections { get; set; } = false;
-
- public bool DisplaySpecialsWithinSeasons { get; set; } = true;
-
- public string[] CodecsUsed { get; set; } = Array.Empty();
-
- public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty();
-
- public bool EnableExternalContentInSuggestions { get; set; } = true;
-
- public int ImageExtractionTimeoutMs { get; set; }
-
- public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty();
-
- ///
- /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
- ///
- public bool EnableSlowResponseWarning { get; set; } = true;
-
- ///
- /// Gets or sets the threshold for the slow response time warning in ms.
- ///
- public long SlowResponseThresholdMs { get; set; } = 500;
-
- ///
- /// Gets or sets the cors hosts.
- ///
- public string[] CorsHosts { get; set; } = new[] { "*" };
-
- ///
- /// Gets or sets the number of days we should retain activity logs.
- ///
- public int? ActivityLogRetentionDays { get; set; } = 30;
+ ItemType = "BoxSet"
+ },
+ new MetadataOptions
+ {
+ ItemType = "Season",
+ },
+ new MetadataOptions
+ {
+ ItemType = "Episode",
+ }
+ };
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
+ ///
+ public bool EnableMetrics { get; set; } = false;
+
+ public bool EnableNormalizedItemByNameIds { get; set; } = true;
- ///
- /// Gets or sets the how the library scan fans out.
- ///
- public int LibraryScanFanoutConcurrency { get; set; }
+ ///
+ /// Gets or sets a value indicating whether this instance is port authorized.
+ ///
+ /// true if this instance is port authorized; otherwise, false.
+ public bool IsPortAuthorized { get; set; }
- ///
- /// Gets or sets the how many metadata refreshes can run concurrently.
- ///
- public int LibraryMetadataRefreshConcurrency { get; set; }
+ ///
+ /// Gets or sets a value indicating whether quick connect is available for use on this server.
+ ///
+ public bool QuickConnectAvailable { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether [enable case sensitive item ids].
+ ///
+ /// true if [enable case sensitive item ids]; otherwise, false.
+ public bool EnableCaseSensitiveItemIds { get; set; } = true;
- ///
- /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
- ///
- public bool RemoveOldPlugins { get; set; }
+ public bool DisableLiveTvChannelUserDataName { get; set; } = true;
- ///
- /// Gets or sets a value indicating whether clients should be allowed to upload logs.
- ///
- public bool AllowClientLogUpload { get; set; } = true;
+ ///
+ /// Gets or sets the metadata path.
+ ///
+ /// The metadata path.
+ public string MetadataPath { get; set; } = string.Empty;
- ///
- /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
- ///
- /// The dummy chapters duration.
- public int DummyChapterDuration { get; set; }
+ public string MetadataNetworkPath { get; set; } = string.Empty;
- ///
- /// Gets or sets the chapter image resolution.
- ///
- /// The chapter image resolution.
- public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource;
+ ///
+ /// Gets or sets the preferred metadata language.
+ ///
+ /// The preferred metadata language.
+ public string PreferredMetadataLanguage { get; set; } = "en";
- ///
- /// Gets or sets the limit for parallel image encoding.
- ///
- /// The limit for parallel image encoding.
- public int ParallelImageEncodingLimit { get; set; }
- }
+ ///
+ /// Gets or sets the metadata country code.
+ ///
+ /// The metadata country code.
+ public string MetadataCountryCode { get; set; } = "US";
+
+ ///
+ /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name.
+ ///
+ /// The sort replace characters.
+ public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" };
+
+ ///
+ /// Gets or sets characters to be removed from strings to create a sort name.
+ ///
+ /// The sort remove characters.
+ public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" };
+
+ ///
+ /// Gets or sets words to be removed from strings to create a sort name.
+ ///
+ /// The sort remove words.
+ public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" };
+
+ ///
+ /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.
+ ///
+ /// The min resume PCT.
+ public int MinResumePct { get; set; } = 5;
+
+ ///
+ /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
+ ///
+ /// The max resume PCT.
+ public int MaxResumePct { get; set; } = 90;
+
+ ///
+ /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..
+ ///
+ /// The min resume duration seconds.
+ public int MinResumeDurationSeconds { get; set; } = 300;
+
+ ///
+ /// Gets or sets the minimum minutes of a book that must be played in order for playstate to be updated.
+ ///
+ /// The min resume in minutes.
+ public int MinAudiobookResume { get; set; } = 5;
+
+ ///
+ /// Gets or sets the remaining minutes of a book that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
+ ///
+ /// The remaining time in minutes.
+ public int MaxAudiobookResume { get; set; } = 5;
+
+ ///
+ /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
+ /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
+ /// different directories and files.
+ ///
+ /// The file watcher delay.
+ public int LibraryMonitorDelay { get; set; } = 60;
+
+ ///
+ /// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification.
+ ///
+ /// The library update duration.
+ public int LibraryUpdateDuration { get; set; } = 30;
+
+ ///
+ /// Gets or sets the image saving convention.
+ ///
+ /// The image saving convention.
+ public ImageSavingConvention ImageSavingConvention { get; set; }
+
+ public MetadataOptions[] MetadataOptions { get; set; }
+
+ public bool SkipDeserializationForBasicTypes { get; set; } = true;
+
+ public string ServerName { get; set; } = string.Empty;
+
+ public string UICulture { get; set; } = "en-US";
+
+ public bool SaveMetadataHidden { get; set; } = false;
+
+ public NameValuePair[] ContentTypes { get; set; } = Array.Empty();
+
+ public int RemoteClientBitrateLimit { get; set; }
+
+ public bool EnableFolderView { get; set; } = false;
+
+ public bool EnableGroupingIntoCollections { get; set; } = false;
+
+ public bool DisplaySpecialsWithinSeasons { get; set; } = true;
+
+ public string[] CodecsUsed { get; set; } = Array.Empty();
+
+ public RepositoryInfo[] PluginRepositories { get; set; } = Array.Empty();
+
+ public bool EnableExternalContentInSuggestions { get; set; } = true;
+
+ public int ImageExtractionTimeoutMs { get; set; }
+
+ public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty();
+
+ ///
+ /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
+ ///
+ public bool EnableSlowResponseWarning { get; set; } = true;
+
+ ///
+ /// Gets or sets the threshold for the slow response time warning in ms.
+ ///
+ public long SlowResponseThresholdMs { get; set; } = 500;
+
+ ///
+ /// Gets or sets the cors hosts.
+ ///
+ public string[] CorsHosts { get; set; } = new[] { "*" };
+
+ ///
+ /// Gets or sets the number of days we should retain activity logs.
+ ///
+ public int? ActivityLogRetentionDays { get; set; } = 30;
+
+ ///
+ /// Gets or sets the how the library scan fans out.
+ ///
+ public int LibraryScanFanoutConcurrency { get; set; }
+
+ ///
+ /// Gets or sets the how many metadata refreshes can run concurrently.
+ ///
+ public int LibraryMetadataRefreshConcurrency { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
+ ///
+ public bool RemoveOldPlugins { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether clients should be allowed to upload logs.
+ ///
+ public bool AllowClientLogUpload { get; set; } = true;
+
+ ///
+ /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
+ ///
+ /// The dummy chapters duration.
+ public int DummyChapterDuration { get; set; }
+
+ ///
+ /// Gets or sets the chapter image resolution.
+ ///
+ /// The chapter image resolution.
+ public ImageResolution ChapterImageResolution { get; set; } = ImageResolution.MatchSource;
+
+ ///
+ /// Gets or sets the limit for parallel image encoding.
+ ///
+ /// The limit for parallel image encoding.
+ public int ParallelImageEncodingLimit { get; set; }
+
+ ///
+ /// Gets or sets the list of cast receiver applications.
+ ///
+ public CastReceiverApplication[] CastReceiverApplications { get; set; } = Array.Empty();
}
diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs
index 94f354660..b477f2593 100644
--- a/MediaBrowser.Model/Configuration/UserConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs
@@ -69,5 +69,10 @@ namespace MediaBrowser.Model.Configuration
public bool RememberSubtitleSelections { get; set; }
public bool EnableNextEpisodeAutoPlay { get; set; }
+
+ ///
+ /// Gets or sets the id of the selected cast receiver.
+ ///
+ public string? CastReceiverId { get; set; }
}
}
diff --git a/MediaBrowser.Model/System/CastReceiverApplication.cs b/MediaBrowser.Model/System/CastReceiverApplication.cs
new file mode 100644
index 000000000..6a49a5cac
--- /dev/null
+++ b/MediaBrowser.Model/System/CastReceiverApplication.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.System;
+
+///
+/// The cast receiver application model.
+///
+public class CastReceiverApplication
+{
+ ///
+ /// Gets or sets the cast receiver application id.
+ ///
+ public required string Id { get; set; }
+
+ ///
+ /// Gets or sets the cast receiver application name.
+ ///
+ public required string Name { get; set; }
+}
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index bd0099af7..d9544b40f 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
using System.Runtime.InteropServices;
using MediaBrowser.Model.Updates;
@@ -128,6 +129,11 @@ namespace MediaBrowser.Model.System
/// The transcode path.
public string TranscodingTempPath { get; set; }
+ ///
+ /// Gets or sets the list of cast receiver applications.
+ ///
+ public IReadOnlyList CastReceiverApplications { get; set; }
+
///
/// Gets or sets a value indicating whether this instance has update available.
///
--
cgit v1.2.3
From 47254d6a2236e079af3cc8c2e37c77d9d1479235 Mon Sep 17 00:00:00 2001
From: Stepan Goremykin
Date: Sun, 8 Oct 2023 00:46:15 +0200
Subject: Remove conditional access when it is known to be not null
---
Emby.Server.Implementations/Net/SocketFactory.cs | 6 +++---
Jellyfin.Api/Controllers/LibraryController.cs | 6 +++---
MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +-
3 files changed, 7 insertions(+), 7 deletions(-)
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 3091398b7..8478fbc4a 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Net
}
catch
{
- socket?.Dispose();
+ socket.Dispose();
throw;
}
@@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Net
}
catch
{
- socket?.Dispose();
+ socket.Dispose();
throw;
}
@@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.Net
}
catch
{
- socket?.Dispose();
+ socket.Dispose();
throw;
}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 46c0a8d52..21941ff94 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -294,8 +294,8 @@ public class LibraryController : BaseJellyfinApiController
return new AllThemeMediaResult
{
- ThemeSongsResult = themeSongs?.Value,
- ThemeVideosResult = themeVideos?.Value,
+ ThemeSongsResult = themeSongs.Value,
+ ThemeVideosResult = themeVideos.Value,
SoundtrackSongsResult = new ThemeMediaResult()
};
}
@@ -490,7 +490,7 @@ public class LibraryController : BaseJellyfinApiController
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
- parent = parent?.GetParent();
+ parent = parent.GetParent();
}
return baseItemDtos;
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 889e2494a..666e78795 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -1313,7 +1313,7 @@ namespace MediaBrowser.Model.Dlna
var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream));
var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions);
- if (audioStream?.IsExternal == true)
+ if (audioStream.IsExternal == true)
{
audioStreamFailureReasons |= TranscodeReason.AudioIsExternal;
}
--
cgit v1.2.3
From c707baed8366cd44ea80713cf93e5f92971815bf Mon Sep 17 00:00:00 2001
From: Bond_009
Date: Thu, 5 Oct 2023 23:57:11 +0200
Subject: Jellyfin.Drawing minor improvements
Reduce duplicate/dead code
---
MediaBrowser.Controller/Drawing/IImageProcessor.cs | 11 -----
.../Drawing/ImageProcessingOptions.cs | 3 +-
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 10 +---
.../Drawing/ImageFormatExtensions.cs | 17 +++++++
.../MediaInfo/EmbeddedImageProvider.cs | 6 ---
src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 14 +-----
src/Jellyfin.Drawing/ImageProcessor.cs | 56 +---------------------
.../Drawing/ImageFormatExtensionsTests.cs | 13 +++++
8 files changed, 37 insertions(+), 93 deletions(-)
(limited to 'MediaBrowser.Model')
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index cdc3d52b9..0d1e2a5a0 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
@@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing
string? GetImageCacheTag(User user);
- ///
- /// Processes the image.
- ///
- /// The options.
- /// To stream.
- /// Task.
- Task ProcessImage(ImageProcessingOptions options, Stream toStream);
-
///
/// Processes the image.
///
@@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing
/// The options.
/// The library name to draw onto the collage.
void CreateImageCollage(ImageCollageOptions options, string? libraryName);
-
- bool SupportsTransparency(string path);
}
}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
index 7912c5e87..953cfe698 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
@@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing
private bool IsFormatSupported(string originalImagePath)
{
var ext = Path.GetExtension(originalImagePath);
- return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase));
+ ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase);
+ return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase));
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 4bff19665..26f47a18f 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -650,15 +650,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
ArgumentException.ThrowIfNullOrEmpty(inputPath);
- var outputExtension = targetFormat switch
- {
- ImageFormat.Bmp => ".bmp",
- ImageFormat.Gif => ".gif",
- ImageFormat.Jpg => ".jpg",
- ImageFormat.Png => ".png",
- ImageFormat.Webp => ".webp",
- _ => ".jpg"
- };
+ var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
index 68a5c2534..1bb24112e 100644
--- a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
+++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
@@ -24,4 +24,21 @@ public static class ImageFormatExtensions
ImageFormat.Webp => "image/webp",
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
};
+
+ ///
+ /// Returns the correct extension for this .
+ ///
+ /// This .
+ /// The is an invalid enumeration value.
+ /// The correct extension for this .
+ public static string GetExtension(this ImageFormat format)
+ => format switch
+ {
+ ImageFormat.Bmp => ".bmp",
+ ImageFormat.Gif => ".gif",
+ ImageFormat.Jpg => ".jpg",
+ ImageFormat.Png => ".png",
+ ImageFormat.Webp => ".webp",
+ _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
+ };
}
diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
index c24f4e2fc..0bfee07fd 100644
--- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
@@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo
? Path.GetExtension(attachmentStream.FileName)
: MimeTypes.ToExtension(attachmentStream.MimeType);
- if (string.IsNullOrEmpty(extension))
- {
- extension = ".jpg";
- }
-
ImageFormat format = extension switch
{
".bmp" => ImageFormat.Bmp,
".gif" => ImageFormat.Gif,
- ".jpg" => ImageFormat.Jpg,
".png" => ImageFormat.Png,
".webp" => ImageFormat.Webp,
_ => ImageFormat.Jpg
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 416f60217..03f90da8e 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder
{
if (!orientation.HasValue)
{
- return SKEncodedOrigin.TopLeft;
+ return SKEncodedOrigin.Default;
}
- return orientation.Value switch
- {
- ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
- ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
- ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
- ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
- ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
- ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
- ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
- _ => SKEncodedOrigin.TopLeft
- };
+ return (SKEncodedOrigin)orientation.Value;
}
///
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 4f16e294b..65a8f4e83 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
///
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
- ///
- public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
- {
- var file = await ProcessImage(options).ConfigureAwait(false);
- using var fileStream = AsyncFile.OpenRead(file.Path);
- await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
- }
-
///
public IReadOnlyCollection GetSupportedImageOutputFormats()
=> _imageEncoder.SupportedOutputFormats;
- ///
- public bool SupportsTransparency(string path)
- => _transparentImageTypes.Contains(Path.GetExtension(path));
-
///
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
{
@@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
}
}
- return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
+ return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
}
catch (Exception ex)
{
@@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return ImageFormat.Jpg;
}
- private string GetMimeType(ImageFormat format, string path)
- => format switch
- {
- ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
- ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
- ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
- ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
- ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
- _ => MimeTypes.GetMimeType(path)
- };
-
///
/// Gets the cache file path based on a set of parameters.
///
@@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
filename.Append(",v=");
filename.Append(Version);
- return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
+ return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension());
}
///
@@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return Task.FromResult((originalImagePath, dateModified));
}
- // TODO _mediaEncoder.ConvertImage is not implemented
- // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
- // {
- // try
- // {
- // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
- //
- // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
- // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
- //
- // var file = _fileSystem.GetFileInfo(outputPath);
- // if (!file.Exists)
- // {
- // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
- // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
- // }
- // else
- // {
- // dateModified = file.LastWriteTimeUtc;
- // }
- //
- // originalImagePath = outputPath;
- // }
- // catch (Exception ex)
- // {
- // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
- // }
- // }
-
return Task.FromResult((originalImagePath, dateModified));
}
diff --git a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
index a5bdb42d8..198ad5a27 100644
--- a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
+++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
@@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests
[InlineData((ImageFormat)5)]
public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
=> Assert.Throws(() => format.GetMimeType());
+
+ [Theory]
+ [MemberData(nameof(GetAllImageFormats))]
+ public static void GetExtension_Valid_Valid(ImageFormat format)
+ => Assert.Null(Record.Exception(() => format.GetExtension()));
+
+ [Theory]
+ [InlineData((ImageFormat)int.MinValue)]
+ [InlineData((ImageFormat)int.MaxValue)]
+ [InlineData((ImageFormat)(-1))]
+ [InlineData((ImageFormat)5)]
+ public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
+ => Assert.Throws(() => format.GetExtension());
}
--
cgit v1.2.3
From 2920611ffc206d845563637c4a865bf3f02d1374 Mon Sep 17 00:00:00 2001
From: Cody Robibero
Date: Sat, 13 May 2023 12:44:31 -0600
Subject: Convert string MediaType to enum MediaType
---
Emby.Dlna/ContentDirectory/ControlHandler.cs | 2 +-
Emby.Dlna/Didl/DidlBuilder.cs | 24 ++++++++--------
Emby.Dlna/PlayTo/PlayToController.cs | 7 +++--
Emby.Dlna/PlayTo/uBaseObject.cs | 13 +++++----
.../Data/SqliteItemRepository.cs | 16 ++++-------
.../Library/MediaSourceManager.cs | 15 +++++-----
.../Library/Resolvers/PlaylistResolver.cs | 1 +
.../Library/UserViewManager.cs | 2 +-
.../Library/Validators/CollectionPostScanTask.cs | 2 +-
.../Playlists/PlaylistManager.cs | 15 +++++-----
.../ScheduledTasks/Tasks/ChapterImagesTask.cs | 1 +
Jellyfin.Api/Controllers/ArtistsController.cs | 4 +--
Jellyfin.Api/Controllers/FilterController.cs | 2 +-
Jellyfin.Api/Controllers/ItemsController.cs | 6 ++--
Jellyfin.Api/Controllers/PlaylistsController.cs | 3 +-
Jellyfin.Api/Controllers/SearchController.cs | 2 +-
Jellyfin.Api/Controllers/SessionController.cs | 2 +-
Jellyfin.Api/Controllers/SuggestionsController.cs | 2 +-
Jellyfin.Api/Controllers/TrailersController.cs | 2 +-
Jellyfin.Api/Controllers/YearsController.cs | 6 ++--
Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +-
Jellyfin.Api/Helpers/StreamingHelpers.cs | 3 +-
.../Models/PlaylistDtos/CreatePlaylistDto.cs | 3 +-
.../Models/SessionDtos/ClientCapabilitiesDto.cs | 3 +-
Jellyfin.Data/Enums/MediaType.cs | 32 ++++++++++++++++++++++
.../Consumers/Session/PlaybackStartLogger.cs | 7 +++--
.../Events/Consumers/Session/PlaybackStopLogger.cs | 7 +++--
MediaBrowser.Controller/Entities/Audio/Audio.cs | 2 +-
MediaBrowser.Controller/Entities/BaseItem.cs | 2 +-
MediaBrowser.Controller/Entities/Book.cs | 2 +-
.../Entities/InternalItemsQuery.cs | 4 +--
MediaBrowser.Controller/Entities/Photo.cs | 3 +-
.../Entities/UserViewBuilder.cs | 2 +-
MediaBrowser.Controller/Entities/Video.cs | 3 +-
MediaBrowser.Controller/LiveTv/LiveTvChannel.cs | 2 +-
MediaBrowser.Controller/Playlists/Playlist.cs | 14 +++++-----
MediaBrowser.Controller/Session/SessionInfo.cs | 5 ++--
.../Parsers/PlaylistXmlParser.cs | 8 +++++-
.../Savers/PlaylistXmlSaver.cs | 5 ++--
MediaBrowser.Model/Dlna/DeviceProfile.cs | 8 ++++--
MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +-
MediaBrowser.Model/Entities/MediaType.cs | 28 -------------------
.../Playlists/PlaylistCreationRequest.cs | 3 +-
MediaBrowser.Model/Search/SearchHint.cs | 4 +--
MediaBrowser.Model/Search/SearchQuery.cs | 4 +--
MediaBrowser.Model/Session/ClientCapabilities.cs | 5 ++--
.../MediaInfo/SubtitleScheduledTask.cs | 2 +-
47 files changed, 159 insertions(+), 133 deletions(-)
create mode 100644 Jellyfin.Data/Enums/MediaType.cs
delete mode 100644 MediaBrowser.Model/Entities/MediaType.cs
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index abd594a3a..12cd7e2ef 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -494,7 +494,7 @@ namespace Emby.Dlna.ContentDirectory
{
var folder = (Folder)item;
- string[] mediaTypes = Array.Empty();
+ MediaType[] mediaTypes = Array.Empty();
bool? isFolder = null;
switch (search.SearchType)
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 5ed982876..9f152df13 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -174,13 +174,14 @@ namespace Emby.Dlna.Didl
if (item is IHasMediaSources)
{
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ switch (item.MediaType)
{
- AddAudioResource(writer, item, deviceId, filter, streamInfo);
- }
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
- {
- AddVideoResource(writer, item, deviceId, filter, streamInfo);
+ case MediaType.Audio:
+ AddAudioResource(writer, item, deviceId, filter, streamInfo);
+ break;
+ case MediaType.Video:
+ AddVideoResource(writer, item, deviceId, filter, streamInfo);
+ break;
}
}
@@ -821,15 +822,15 @@ namespace Emby.Dlna.Didl
writer.WriteString(classType ?? "object.container.storageFolder");
}
- else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Audio)
{
writer.WriteString("object.item.audioItem.musicTrack");
}
- else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Photo)
{
writer.WriteString("object.item.imageItem.photo");
}
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Video)
{
if (!_profile.RequiresPlainVideoItems && item is Movie)
{
@@ -1006,8 +1007,7 @@ namespace Emby.Dlna.Didl
if (!_profile.EnableAlbumArtInDidl)
{
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- || string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video)
{
if (!stubType.HasValue)
{
@@ -1016,7 +1016,7 @@ namespace Emby.Dlna.Didl
}
}
- if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
+ if (!_profile.EnableSingleAlbumArtLimit || item.MediaType == MediaType.Photo)
{
AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG");
AddImageResElement(item, writer, 1024, 768, "jpg", "JPEG_MED");
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index b1ad15cdc..9e3d1d4d6 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.Didl;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@@ -577,7 +578,7 @@ namespace Emby.Dlna.PlayTo
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
{
- if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Video)
{
return new PlaylistItem
{
@@ -597,7 +598,7 @@ namespace Emby.Dlna.PlayTo
};
}
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Audio)
{
return new PlaylistItem
{
@@ -615,7 +616,7 @@ namespace Emby.Dlna.PlayTo
};
}
- if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Photo)
{
return PlaylistItemFactory.Create((Photo)item, profile);
}
diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs
index 2e0f2063b..a8f451405 100644
--- a/Emby.Dlna/PlayTo/uBaseObject.cs
+++ b/Emby.Dlna/PlayTo/uBaseObject.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
namespace Emby.Dlna.PlayTo
{
@@ -33,19 +34,19 @@ namespace Emby.Dlna.PlayTo
{
var classType = UpnpClass ?? string.Empty;
- if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Audio, StringComparison.Ordinal) != -1)
+ if (classType.Contains("Audio", StringComparison.Ordinal))
{
- return MediaBrowser.Model.Entities.MediaType.Audio;
+ return "Audio";
}
- if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
+ if (classType.Contains("Video", StringComparison.Ordinal))
{
- return MediaBrowser.Model.Entities.MediaType.Video;
+ return "Video";
}
- if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
+ if (classType.Contains("image", StringComparison.Ordinal))
{
- return MediaBrowser.Model.Entities.MediaType.Photo;
+ return "Photo";
}
return null;
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 77cf4089b..798521d22 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -722,7 +722,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@IsLocked", item.IsLocked);
saveItemStatement.TryBind("@Name", item.Name);
saveItemStatement.TryBind("@OfficialRating", item.OfficialRating);
- saveItemStatement.TryBind("@MediaType", item.MediaType);
+ saveItemStatement.TryBind("@MediaType", item.MediaType.ToString());
saveItemStatement.TryBind("@Overview", item.Overview);
saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber);
saveItemStatement.TryBind("@PremiereDate", item.PremiereDate);
@@ -3109,11 +3109,6 @@ namespace Emby.Server.Implementations.Data
return true;
}
- private bool IsValidMediaType(string value)
- {
- return IsAlphaNumeric(value);
- }
-
private bool IsValidPersonType(string value)
{
return IsAlphaNumeric(value);
@@ -4124,15 +4119,14 @@ namespace Emby.Server.Implementations.Data
}
}
- var queryMediaTypes = query.MediaTypes.Where(IsValidMediaType).ToArray();
- if (queryMediaTypes.Length == 1)
+ if (query.MediaTypes.Length == 1)
{
whereClauses.Add("MediaType=@MediaTypes");
- statement?.TryBind("@MediaTypes", queryMediaTypes[0]);
+ statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString());
}
- else if (queryMediaTypes.Length > 1)
+ else if (query.MediaTypes.Length > 1)
{
- var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
+ var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'"));
whereClauses.Add("MediaType in (" + val + ")");
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index c9a26a30f..3d0f9f02c 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -11,6 +11,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using EasyCaching.Core.Configurations;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
@@ -186,11 +187,11 @@ namespace Emby.Server.Implementations.Library
{
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Audio)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Video)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
@@ -334,11 +335,11 @@ namespace Emby.Server.Implementations.Library
{
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Audio)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Video)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
@@ -417,9 +418,9 @@ namespace Emby.Server.Implementations.Library
public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
{
// Item would only be null if the app didn't supply ItemId as part of the live stream open request
- var mediaType = item is null ? MediaType.Video : item.MediaType;
+ var mediaType = item?.MediaType ?? MediaType.Video;
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Video)
{
var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item);
@@ -428,7 +429,7 @@ namespace Emby.Server.Implementations.Library
SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
}
- else if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ else if (mediaType == MediaType.Audio)
{
var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index 5d569009d..8e34a42b3 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 2c3dc1857..0343db68b 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -317,7 +317,7 @@ namespace Emby.Server.Implementations.Library
}
}
- var mediaTypes = new List();
+ var mediaTypes = new List();
if (includeItemTypes.Length == 0)
{
diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
index df45793c3..89f64ee4f 100644
--- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
- MediaTypes = new string[] { MediaType.Video },
+ MediaTypes = new[] { MediaType.Video },
IncludeItemTypes = new[] { BaseItemKind.Movie },
IsVirtualItem = false,
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 649c49924..c783f2b01 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -10,6 +10,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -74,7 +75,7 @@ namespace Emby.Server.Implementations.Playlists
throw new ArgumentException(nameof(parentFolder));
}
- if (string.IsNullOrEmpty(options.MediaType))
+ if (options.MediaType is null || options.MediaType == MediaType.Unknown)
{
foreach (var itemId in options.ItemIdList)
{
@@ -84,7 +85,7 @@ namespace Emby.Server.Implementations.Playlists
throw new ArgumentException("No item exists with the supplied Id");
}
- if (!string.IsNullOrEmpty(item.MediaType))
+ if (item.MediaType == MediaType.Unknown)
{
options.MediaType = item.MediaType;
}
@@ -102,20 +103,20 @@ namespace Emby.Server.Implementations.Playlists
{
options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
.Select(i => i.MediaType)
- .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ .FirstOrDefault(i => i != MediaType.Unknown);
}
}
- if (!string.IsNullOrEmpty(options.MediaType))
+ if (options.MediaType is null || options.MediaType == MediaType.Unknown)
{
break;
}
}
}
- if (string.IsNullOrEmpty(options.MediaType))
+ if (options.MediaType is null || options.MediaType == MediaType.Unknown)
{
- options.MediaType = "Audio";
+ options.MediaType = MediaType.Audio;
}
var user = _userManager.GetUserById(options.UserId);
@@ -168,7 +169,7 @@ namespace Emby.Server.Implementations.Playlists
return path;
}
- private List GetPlaylistItems(IEnumerable itemIds, string playlistMediaType, User user, DtoOptions options)
+ private List GetPlaylistItems(IEnumerable itemIds, MediaType playlistMediaType, User user, DtoOptions options)
{
var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i is not null);
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 6ad6c4cbd..1d72ceae9 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dto;
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index c9d2f67f9..1c994ac8b 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -95,7 +95,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -299,7 +299,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index d51a5325f..baeb8b81a 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -50,7 +50,7 @@ public class FilterController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 80128536d..884613afa 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -195,7 +195,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] bool? isPlayed,
@@ -652,7 +652,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] bool? isPlayed,
@@ -812,7 +812,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 8d2a738d4..c4c89ccde 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.PlaylistDtos;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
@@ -75,7 +76,7 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery, ParameterObsolete] string? name,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList ids,
[FromQuery, ParameterObsolete] Guid? userId,
- [FromQuery, ParameterObsolete] string? mediaType,
+ [FromQuery, ParameterObsolete] MediaType? mediaType,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
{
if (ids.Count == 0)
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 387b3ea5a..5b4594165 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -86,7 +86,7 @@ public class SearchController : BaseJellyfinApiController
[FromQuery, Required] string searchTerm,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery] Guid? parentId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index e93456de6..e20cf034d 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -393,7 +393,7 @@ public class SessionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task PostCapabilities(
[FromQuery] string? id,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 5b808f257..675757fc5 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -56,7 +56,7 @@ public class SuggestionsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult> GetSuggestions(
[FromRoute, Required] Guid userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index b5b640620..9f1e0e837 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -160,7 +160,7 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] bool? isPlayed,
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index 74370db50..200ecea09 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -76,7 +76,7 @@ public class YearsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
@@ -191,7 +191,7 @@ public class YearsController : BaseJellyfinApiController
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
- private bool FilterItem(BaseItem f, IReadOnlyCollection excludeItemTypes, IReadOnlyCollection includeItemTypes, IReadOnlyCollection mediaTypes)
+ private bool FilterItem(BaseItem f, IReadOnlyCollection excludeItemTypes, IReadOnlyCollection includeItemTypes, IReadOnlyCollection mediaTypes)
{
var baseItemKind = f.GetBaseItemKind();
// Exclude item types
@@ -207,7 +207,7 @@ public class YearsController : BaseJellyfinApiController
}
// Include MediaTypes
- if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType))
{
return false;
}
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index a36028cfe..321987ca7 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -243,7 +243,7 @@ public class MediaInfoHelper
}
// Beginning of Playback Determination
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+ var streamInfo = item.MediaType == MediaType.Audio
? streamBuilder.GetOptimalAudioStream(options)
: streamBuilder.GetOptimalVideoStream(options);
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index a653c5795..ed2358dd8 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -128,7 +129,7 @@ public static class StreamingHelpers
var item = libraryManager.GetItemById(streamingRequest.Id);
- state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
+ state.IsInputVideo = item.MediaType == MediaType.Video;
MediaSourceInfo? mediaSource = null;
if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index 1fba32c5b..bdc488871 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
namespace Jellyfin.Api.Models.PlaylistDtos;
@@ -29,5 +30,5 @@ public class CreatePlaylistDto
///
/// Gets or sets the media type.
///
- public string? MediaType { get; set; }
+ public MediaType? MediaType { get; set; }
}
diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
index b88be33b2..b021771a0 100644
--- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
+++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Session;
@@ -16,7 +17,7 @@ public class ClientCapabilitiesDto
/// Gets or sets the list of playable media types.
///
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList PlayableMediaTypes { get; set; } = Array.Empty();
+ public IReadOnlyList PlayableMediaTypes { get; set; } = Array.Empty();
///
/// Gets or sets the list of supported commands.
diff --git a/Jellyfin.Data/Enums/MediaType.cs b/Jellyfin.Data/Enums/MediaType.cs
new file mode 100644
index 000000000..b014ff366
--- /dev/null
+++ b/Jellyfin.Data/Enums/MediaType.cs
@@ -0,0 +1,32 @@
+namespace Jellyfin.Data.Enums;
+
+///
+/// Media types.
+///
+public enum MediaType
+{
+ ///
+ /// Unknown media type.
+ ///
+ Unknown = 0,
+
+ ///
+ /// Video media.
+ ///
+ Video = 1,
+
+ ///
+ /// Audio media.
+ ///
+ Audio = 2,
+
+ ///
+ /// Photo media.
+ ///
+ Photo = 3,
+
+ ///
+ /// Book media.
+ ///
+ Book = 4
+}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
index 27726a57a..8a33383e3 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
@@ -89,14 +90,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
return name;
}
- private static string GetPlaybackNotificationType(string mediaType)
+ private static string GetPlaybackNotificationType(MediaType mediaType)
{
- if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Audio)
{
return NotificationType.AudioPlayback.ToString();
}
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Video)
{
return NotificationType.VideoPlayback.ToString();
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
index 6b16477aa..4c2effc2e 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
@@ -97,14 +98,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
return name;
}
- private static string? GetPlaybackStoppedNotificationType(string mediaType)
+ private static string? GetPlaybackStoppedNotificationType(MediaType mediaType)
{
- if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Audio)
{
return NotificationType.AudioPlaybackStopped.ToString();
}
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Video)
{
return NotificationType.VideoPlaybackStopped.ToString();
}
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
index c7216a320..243d2f04f 100644
--- a/MediaBrowser.Controller/Entities/Audio/Audio.cs
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -63,7 +63,7 @@ namespace MediaBrowser.Controller.Entities.Audio
///
/// The type of the media.
[JsonIgnore]
- public override string MediaType => Model.Entities.MediaType.Audio;
+ public override MediaType MediaType => MediaType.Audio;
public override double GetDefaultPrimaryImageAspectRatio()
{
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 9f3e8eec9..f83065a01 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -422,7 +422,7 @@ namespace MediaBrowser.Controller.Entities
///
/// The type of the media.
[JsonIgnore]
- public virtual string MediaType => null;
+ public virtual MediaType MediaType => MediaType.Unknown;
[JsonIgnore]
public virtual string[] PhysicalLocations
diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs
index d75beb06d..63f9180fd 100644
--- a/MediaBrowser.Controller/Entities/Book.cs
+++ b/MediaBrowser.Controller/Entities/Book.cs
@@ -18,7 +18,7 @@ namespace MediaBrowser.Controller.Entities
}
[JsonIgnore]
- public override string MediaType => Model.Entities.MediaType.Book;
+ public override MediaType MediaType => MediaType.Book;
public override bool SupportsPlayedStatus => true;
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index a51299284..2de974349 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -36,7 +36,7 @@ namespace MediaBrowser.Controller.Entities
ImageTypes = Array.Empty();
IncludeItemTypes = Array.Empty();
ItemIds = Array.Empty();
- MediaTypes = Array.Empty();
+ MediaTypes = Array.Empty();
MinSimilarityScore = 20;
OfficialRatings = Array.Empty();
OrderBy = Array.Empty<(string, SortOrder)>();
@@ -86,7 +86,7 @@ namespace MediaBrowser.Controller.Entities
public bool? IncludeItemsByName { get; set; }
- public string[] MediaTypes { get; set; }
+ public MediaType[] MediaTypes { get; set; }
public BaseItemKind[] IncludeItemTypes { get; set; }
diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs
index ba6ce189a..cb9feacd3 100644
--- a/MediaBrowser.Controller/Entities/Photo.cs
+++ b/MediaBrowser.Controller/Entities/Photo.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing;
namespace MediaBrowser.Controller.Entities
@@ -13,7 +14,7 @@ namespace MediaBrowser.Controller.Entities
public override bool SupportsLocalMetadata => false;
[JsonIgnore]
- public override string MediaType => Model.Entities.MediaType.Photo;
+ public override MediaType MediaType => MediaType.Photo;
[JsonIgnore]
public override Folder LatestItemsIndexContainer => AlbumEntity;
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index c276ab463..5f187e64e 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -476,7 +476,7 @@ namespace MediaBrowser.Controller.Entities
public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager)
{
- if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType))
{
return false;
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 9f685b7e2..be2eb4d28 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -9,6 +9,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@@ -256,7 +257,7 @@ namespace MediaBrowser.Controller.Entities
///
/// The type of the media.
[JsonIgnore]
- public override string MediaType => Model.Entities.MediaType.Video;
+ public override MediaType MediaType => MediaType.Video;
public override List GetUserDataKeys()
{
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index f11e3c8f6..3c2cf8e3d 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.LiveTv
public override LocationType LocationType => LocationType.Remote;
[JsonIgnore]
- public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
+ public override MediaType MediaType => ChannelType == ChannelType.Radio ? MediaType.Audio : MediaType.Video;
[JsonIgnore]
public bool IsMovie { get; set; }
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 498df5ab0..ca032e7f6 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -69,7 +69,7 @@ namespace MediaBrowser.Controller.Playlists
public override bool SupportsInheritedParentImages => false;
[JsonIgnore]
- public override bool SupportsPlayedStatus => string.Equals(MediaType, "Video", StringComparison.OrdinalIgnoreCase);
+ public override bool SupportsPlayedStatus => MediaType == Jellyfin.Data.Enums.MediaType.Video;
[JsonIgnore]
public override bool AlwaysScanInternalMetadataPath => true;
@@ -80,10 +80,10 @@ namespace MediaBrowser.Controller.Playlists
[JsonIgnore]
public override bool IsPreSorted => true;
- public string PlaylistMediaType { get; set; }
+ public MediaType PlaylistMediaType { get; set; }
[JsonIgnore]
- public override string MediaType => PlaylistMediaType;
+ public override MediaType MediaType => PlaylistMediaType;
[JsonIgnore]
private bool IsSharedItem
@@ -107,9 +107,9 @@ namespace MediaBrowser.Controller.Playlists
return System.IO.Path.HasExtension(path) && !Directory.Exists(path);
}
- public void SetMediaType(string value)
+ public void SetMediaType(MediaType? value)
{
- PlaylistMediaType = value;
+ PlaylistMediaType = value ?? MediaType.Unknown;
}
public override double GetDefaultPrimaryImageAspectRatio()
@@ -167,7 +167,7 @@ namespace MediaBrowser.Controller.Playlists
return base.GetChildren(user, true, query);
}
- public static List GetPlaylistItems(string playlistMediaType, IEnumerable inputItems, User user, DtoOptions options)
+ public static List GetPlaylistItems(MediaType playlistMediaType, IEnumerable inputItems, User user, DtoOptions options)
{
if (user is not null)
{
@@ -185,7 +185,7 @@ namespace MediaBrowser.Controller.Playlists
return list;
}
- private static IEnumerable GetPlaylistItems(BaseItem item, User user, string mediaType, DtoOptions options)
+ private static IEnumerable GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options)
{
if (item is MusicGenre musicGenre)
{
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 25bf23d61..07ad529b4 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session;
@@ -60,13 +61,13 @@ namespace MediaBrowser.Controller.Session
/// Gets the playable media types.
///
/// The playable media types.
- public IReadOnlyList PlayableMediaTypes
+ public IReadOnlyList PlayableMediaTypes
{
get
{
if (Capabilities is null)
{
- return Array.Empty();
+ return Array.Empty();
}
return Capabilities.PlayableMediaTypes;
diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
index 879a3616b..e0277870d 100644
--- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
@@ -1,5 +1,7 @@
+using System;
using System.Collections.Generic;
using System.Xml;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Playlists;
@@ -31,7 +33,11 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "PlaylistMediaType":
- item.PlaylistMediaType = reader.ReadNormalizedString();
+ if (Enum.TryParse(reader.ReadNormalizedString(), out var mediaType))
+ {
+ item.PlaylistMediaType = mediaType;
+ }
+
break;
case "PlaylistItems":
diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
index 847add07f..3f018cae9 100644
--- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
@@ -1,6 +1,7 @@
using System.IO;
using System.Threading.Tasks;
using System.Xml;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -48,12 +49,12 @@ namespace MediaBrowser.LocalMetadata.Savers
{
var game = (Playlist)item;
- if (string.IsNullOrEmpty(game.PlaylistMediaType))
+ if (game.PlaylistMediaType == MediaType.Unknown)
{
return Task.CompletedTask;
}
- return writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType);
+ return writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType.ToString());
}
///
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index 07bb002ea..71d0896a7 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -1,6 +1,7 @@
#pragma warning disable CA1819 // Properties should not return arrays
using System;
using System.ComponentModel;
+using System.Linq;
using System.Xml.Serialization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -227,9 +228,12 @@ namespace MediaBrowser.Model.Dlna
/// The GetSupportedMediaTypes.
///
/// The .
- public string[] GetSupportedMediaTypes()
+ public MediaType[] GetSupportedMediaTypes()
{
- return ContainerProfile.SplitValue(SupportedMediaTypes);
+ return ContainerProfile.SplitValue(SupportedMediaTypes)
+ .Select(m => Enum.TryParse(m, out var parsed) ? parsed : MediaType.Unknown)
+ .Where(m => m != MediaType.Unknown)
+ .ToArray();
}
///
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index 8fab1ca6d..edbf2d9d4 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -584,7 +584,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the type of the media.
///
/// The type of the media.
- public string MediaType { get; set; }
+ public MediaType MediaType { get; set; }
///
/// Gets or sets the end date.
diff --git a/MediaBrowser.Model/Entities/MediaType.cs b/MediaBrowser.Model/Entities/MediaType.cs
deleted file mode 100644
index dd2ae810b..000000000
--- a/MediaBrowser.Model/Entities/MediaType.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace MediaBrowser.Model.Entities
-{
- ///
- /// Class MediaType.
- ///
- public static class MediaType
- {
- ///
- /// The video.
- ///
- public const string Video = "Video";
-
- ///
- /// The audio.
- ///
- public const string Audio = "Audio";
-
- ///
- /// The photo.
- ///
- public const string Photo = "Photo";
-
- ///
- /// The book.
- ///
- public const string Book = "Book";
- }
-}
diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
index 847269716..62d496d04 100644
--- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
+++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Playlists;
@@ -22,7 +23,7 @@ public class PlaylistCreationRequest
///
/// Gets or sets the media type.
///
- public string? MediaType { get; set; }
+ public MediaType? MediaType { get; set; }
///
/// Gets or sets the user id.
diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs
index 3fa7f3d56..fd911dbed 100644
--- a/MediaBrowser.Model/Search/SearchHint.cs
+++ b/MediaBrowser.Model/Search/SearchHint.cs
@@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Search
{
Name = string.Empty;
MatchedTerm = string.Empty;
- MediaType = string.Empty;
+ MediaType = Jellyfin.Data.Enums.MediaType.Unknown;
Artists = Array.Empty();
}
@@ -115,7 +115,7 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the type of the media.
///
/// The type of the media.
- public string MediaType { get; set; }
+ public MediaType MediaType { get; set; }
///
/// Gets or sets the start date.
diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs
index 1caed827f..b91fd8657 100644
--- a/MediaBrowser.Model/Search/SearchQuery.cs
+++ b/MediaBrowser.Model/Search/SearchQuery.cs
@@ -16,7 +16,7 @@ namespace MediaBrowser.Model.Search
IncludePeople = true;
IncludeStudios = true;
- MediaTypes = Array.Empty();
+ MediaTypes = Array.Empty();
IncludeItemTypes = Array.Empty();
ExcludeItemTypes = Array.Empty();
}
@@ -55,7 +55,7 @@ namespace MediaBrowser.Model.Search
public bool IncludeArtists { get; set; }
- public string[] MediaTypes { get; set; }
+ public MediaType[] MediaTypes { get; set; }
public BaseItemKind[] IncludeItemTypes { get; set; }
diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs
index d692906c6..7fefce9cd 100644
--- a/MediaBrowser.Model/Session/ClientCapabilities.cs
+++ b/MediaBrowser.Model/Session/ClientCapabilities.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dlna;
namespace MediaBrowser.Model.Session
@@ -11,12 +12,12 @@ namespace MediaBrowser.Model.Session
{
public ClientCapabilities()
{
- PlayableMediaTypes = Array.Empty();
+ PlayableMediaTypes = Array.Empty();
SupportedCommands = Array.Empty();
SupportsPersistentIdentifier = true;
}
- public IReadOnlyList PlayableMediaTypes { get; set; }
+ public IReadOnlyList PlayableMediaTypes { get; set; }
public IReadOnlyList SupportedCommands { get; set; }
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index f21939d2a..6eb75891a 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -97,7 +97,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
var query = new InternalItemsQuery
{
- MediaTypes = new string[] { MediaType.Video },
+ MediaTypes = new[] { MediaType.Video },
IsVirtualItem = false,
IncludeItemTypes = types,
DtoOptions = new DtoOptions(true),
--
cgit v1.2.3
From dc27d8f9cd3b6cf280ba8e5e2520db387f651fb4 Mon Sep 17 00:00:00 2001
From: Tim Eisele
Date: Wed, 11 Oct 2023 00:02:37 +0200
Subject: Refactor URI overrides (#10051)
---
Emby.Dlna/Main/DlnaEntryPoint.cs | 9 +-
Emby.Server.Implementations/ApplicationHost.cs | 6 +-
.../EntryPoints/UdpServerEntryPoint.cs | 29 ++-
Emby.Server.Implementations/Net/SocketFactory.cs | 33 ++-
Emby.Server.Implementations/Udp/UdpServer.cs | 11 +-
.../Extensions/NetworkExtensions.cs | 13 +-
Jellyfin.Networking/Manager/NetworkManager.cs | 244 +++++++++++++--------
.../Extensions/ApiServiceCollectionExtensions.cs | 2 +-
.../Extensions/WebHostBuilderExtensions.cs | 3 +-
MediaBrowser.Model/Net/IPData.cs | 5 +
.../Net/PublishedServerUriOverride.cs | 42 ++++
RSSDP/SsdpCommunicationsServer.cs | 124 +++++------
RSSDP/SsdpDeviceLocator.cs | 42 ++--
RSSDP/SsdpDevicePublisher.cs | 89 ++++----
.../NetworkExtensionsTests.cs | 1 -
.../NetworkManagerTests.cs | 10 +-
.../Jellyfin.Networking.Tests/NetworkParseTests.cs | 39 ++--
tests/Jellyfin.Server.Tests/ParseNetworkTests.cs | 5 +-
18 files changed, 431 insertions(+), 276 deletions(-)
create mode 100644 MediaBrowser.Model/Net/PublishedServerUriOverride.cs
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 1a4bec398..83b0ef316 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -201,9 +201,10 @@ namespace Emby.Dlna.Main
{
if (_communicationsServer is null)
{
- var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
-
- _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
+ _communicationsServer = new SsdpCommunicationsServer(
+ _socketFactory,
+ _networkManager,
+ _logger)
{
IsShared = true
};
@@ -213,7 +214,7 @@ namespace Emby.Dlna.Main
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error starting ssdp handlers");
+ _logger.LogError(ex, "Error starting SSDP handlers");
}
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 3253ea026..c247178ee 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -450,7 +450,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.AddParts(GetExports());
- NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger());
+ NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger());
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
@@ -913,7 +913,7 @@ namespace Emby.Server.Implementations
///
public string GetSmartApiUrl(HttpRequest request)
{
- // Return the host in the HTTP request as the API url
+ // Return the host in the HTTP request as the API URL if not configured otherwise
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
{
int? requestPort = request.Host.Port;
@@ -948,7 +948,7 @@ namespace Emby.Server.Implementations
public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
{
// With an empty source, the port will be null
- var smart = NetManager.GetBindAddress(ipAddress, out _, true);
+ var smart = NetManager.GetBindAddress(ipAddress, out _, false);
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart, scheme, port);
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 54191649d..2c03f9ffd 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
{
///
- /// Class UdpServerEntryPoint.
+ /// Class responsible for registering all UDP broadcast endpoints and their handlers.
///
public sealed class UdpServerEntryPoint : IServerEntryPoint
{
@@ -35,7 +35,6 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IConfiguration _config;
private readonly IConfigurationManager _configurationManager;
private readonly INetworkManager _networkManager;
- private readonly bool _enableMultiSocketBinding;
///
/// The UDP server.
@@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints
_configurationManager = configurationManager;
_networkManager = networkManager;
_udpServers = new List();
- _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
}
///
@@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
- if (_enableMultiSocketBinding)
+ // Linux needs to bind to the broadcast addresses to get broadcast traffic
+ // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
+ if (OperatingSystem.IsLinux())
{
- // Add global broadcast socket
+ // Add global broadcast listener
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server);
- // Add bind address specific broadcast sockets
+ // Add bind address specific broadcast listeners
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
foreach (var intf in validInterfaces)
@@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints
}
else
{
- var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber);
- server.Start(_cancellationTokenSource.Token);
- _udpServers.Add(server);
+ // Add bind address specific broadcast listeners
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+ foreach (var intf in validInterfaces)
+ {
+ var intfAddress = intf.Address;
+ _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
+
+ var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
+ server.Start(_cancellationTokenSource.Token);
+ _udpServers.Add(server);
+ }
}
}
catch (SocketException ex)
@@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (_disposed)
{
- throw new ObjectDisposedException(this.GetType().Name);
+ throw new ObjectDisposedException(GetType().Name);
}
}
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 51e92953d..505984cfb 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -1,12 +1,15 @@
-#pragma warning disable CS1591
-
using System;
+using System.Linq;
using System.Net;
+using System.Net.NetworkInformation;
using System.Net.Sockets;
using MediaBrowser.Model.Net;
namespace Emby.Server.Implementations.Net
{
+ ///
+ /// Factory class to create different kinds of sockets.
+ ///
public class SocketFactory : ISocketFactory
{
///
@@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net
///
public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
{
- ArgumentNullException.ThrowIfNull(bindInterface.Address);
+ var interfaceAddress = bindInterface.Address;
+ ArgumentNullException.ThrowIfNull(interfaceAddress);
if (localPort < 0)
{
@@ -49,7 +53,7 @@ namespace Emby.Server.Implementations.Net
try
{
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- socket.Bind(new IPEndPoint(bindInterface.Address, localPort));
+ socket.Bind(new IPEndPoint(interfaceAddress, localPort));
return socket;
}
@@ -82,16 +86,25 @@ namespace Emby.Server.Implementations.Net
try
{
- var interfaceIndex = bindInterface.Index;
- var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex);
-
socket.MulticastLoopback = false;
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
- socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped);
- socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
- socket.Bind(new IPEndPoint(multicastAddress, localPort));
+
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
+ socket.Bind(new IPEndPoint(multicastAddress, localPort));
+ }
+ else
+ {
+ // Only create socket if interface supports multicast
+ var interfaceIndex = bindInterface.Index;
+ var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
+
+ socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
+ socket.Bind(new IPEndPoint(bindIPAddress, localPort));
+ }
return socket;
}
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index a3bbd6df0..10376ed76 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp
_endpoint = new IPEndPoint(bindAddress, port);
- _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
+ {
+ MulticastLoopback = false,
+ };
_udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
@@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp
try
{
+ _logger.LogDebug("Sending AutoDiscovery response");
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
}
catch (SocketException ex)
@@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp
{
try
{
- var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false);
+ var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
+ var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
{
@@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp
}
catch (OperationCanceledException)
{
- // Don't throw
+ _logger.LogDebug("Broadcast socket operation cancelled");
}
}
}
diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs
index e45fa3bcb..2d921a482 100644
--- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs
+++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs
@@ -231,12 +231,12 @@ public static partial class NetworkExtensions
}
else if (address.AddressFamily == AddressFamily.InterNetwork)
{
- result = new IPNetwork(address, Network.MinimumIPv4PrefixSize);
+ result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize);
return true;
}
else if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
- result = new IPNetwork(address, Network.MinimumIPv6PrefixSize);
+ result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize);
return true;
}
}
@@ -284,12 +284,15 @@ public static partial class NetworkExtensions
if (hosts.Count <= 2)
{
+ var firstPart = hosts[0];
+
// Is hostname or hostname:port
- if (FqdnGeneratedRegex().IsMatch(hosts[0]))
+ if (FqdnGeneratedRegex().IsMatch(firstPart))
{
try
{
- addresses = Dns.GetHostAddresses(hosts[0]);
+ // .NET automatically filters only supported returned addresses based on OS support.
+ addresses = Dns.GetHostAddresses(firstPart);
return true;
}
catch (SocketException)
@@ -299,7 +302,7 @@ public static partial class NetworkExtensions
}
// Is an IPv4 or IPv4:port
- if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address))
+ if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address))
{
if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
|| ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index f20e28526..9c59500d7 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -15,7 +15,9 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Jellyfin.Networking.Manager
{
@@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager
private readonly IConfigurationManager _configurationManager;
+ private readonly IConfiguration _startupConfig;
+
private readonly object _networkEventLock;
///
/// Holds the published server URLs and the IPs to use them on.
///
- private IReadOnlyDictionary _publishedServerUrls;
+ private IReadOnlyList _publishedServerUrls;
private IReadOnlyList _remoteAddressFilter;
@@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager
///
/// Initializes a new instance of the class.
///
- /// IServerConfigurationManager instance.
+ /// The instance.
+ /// The instance holding startup parameters.
/// Logger to use for messages.
#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
- public NetworkManager(IConfigurationManager configurationManager, ILogger logger)
+ public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger logger)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(configurationManager);
_logger = logger;
_configurationManager = configurationManager;
+ _startupConfig = startupConfig;
_initLock = new();
_interfaces = new List();
_macAddresses = new List();
- _publishedServerUrls = new Dictionary();
+ _publishedServerUrls = new List();
_networkEventLock = new object();
_remoteAddressFilter = new List();
@@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager
///
/// Gets the Published server override list.
///
- public IReadOnlyDictionary PublishedServerUrls => _publishedServerUrls;
+ public IReadOnlyList PublishedServerUrls => _publishedServerUrls;
///
public void Dispose()
@@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager
{
if (!_eventfire)
{
- _logger.LogDebug("Network Address Change Event.");
// As network events tend to fire one after the other only fire once every second.
_eventfire = true;
OnNetworkChange();
@@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager
}
else
{
- InitialiseInterfaces();
- InitialiseLan(networkConfig);
+ InitializeInterfaces();
+ InitializeLan(networkConfig);
EnforceBindSettings(networkConfig);
}
+ PrintNetworkInformation(networkConfig);
NetworkChanged?.Invoke(this, EventArgs.Empty);
}
finally
@@ -210,7 +216,7 @@ namespace Jellyfin.Networking.Manager
/// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
/// Generate a list of all active mac addresses that aren't loopback addresses.
///
- private void InitialiseInterfaces()
+ private void InitializeInterfaces()
{
lock (_initLock)
{
@@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager
try
{
var nics = NetworkInterface.GetAllNetworkInterfaces()
- .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up);
+ .Where(i => i.OperationalStatus == OperationalStatus.Up);
foreach (NetworkInterface adapter in nics)
{
@@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager
{
if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
{
- var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
- interfaceObject.Index = ipProperties.GetIPv4Properties().Index;
- interfaceObject.Name = adapter.Name;
+ var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+ {
+ Index = ipProperties.GetIPv4Properties().Index,
+ Name = adapter.Name,
+ SupportsMulticast = adapter.SupportsMulticast
+ };
interfaces.Add(interfaceObject);
}
else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
{
- var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
- interfaceObject.Index = ipProperties.GetIPv6Properties().Index;
- interfaceObject.Name = adapter.Name;
+ var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+ {
+ Index = ipProperties.GetIPv6Properties().Index,
+ Name = adapter.Name,
+ SupportsMulticast = adapter.SupportsMulticast
+ };
interfaces.Add(interfaceObject);
}
}
}
-#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
-#pragma warning restore CA1031 // Do not catch general exception types
{
// Ignore error, and attempt to continue.
_logger.LogError(ex, "Error encountered parsing interfaces.");
}
}
}
-#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
-#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Error obtaining interfaces.");
}
@@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager
{
_logger.LogWarning("No interface information available. Using loopback interface(s).");
- if (IsIPv4Enabled && !IsIPv6Enabled)
+ if (IsIPv4Enabled)
{
- interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo"));
+ interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
}
- if (!IsIPv4Enabled && IsIPv6Enabled)
+ if (IsIPv6Enabled)
{
- interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo"));
+ interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
}
}
@@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager
}
///
- /// Initialises internal LAN cache.
+ /// Initializes internal LAN cache.
///
- private void InitialiseLan(NetworkConfiguration config)
+ private void InitializeLan(NetworkConfiguration config)
{
lock (_initLock)
{
@@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager
_excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
? excludedSubnets
: new List();
-
- _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
- _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
- _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
}
}
@@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager
.ToHashSet();
interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
- if (bindAddresses.Contains(IPAddress.Loopback))
+ if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
{
interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
}
- if (bindAddresses.Contains(IPAddress.IPv6Loopback))
+ if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
{
interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
}
@@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager
interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
}
- _logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
_interfaces = interfaces;
}
}
///
- /// Initialises the remote address values.
+ /// Initializes the remote address values.
///
- private void InitialiseRemote(NetworkConfiguration config)
+ private void InitializeRemote(NetworkConfiguration config)
{
lock (_initLock)
{
@@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager
/// format is subnet=ipaddress|host|uri
/// when subnet = 0.0.0.0, any external address matches.
///
- private void InitialiseOverrides(NetworkConfiguration config)
+ private void InitializeOverrides(NetworkConfiguration config)
{
lock (_initLock)
{
- var publishedServerUrls = new Dictionary();
- var overrides = config.PublishedServerUriBySubnet;
+ var publishedServerUrls = new List();
+
+ // Prefer startup configuration.
+ var startupOverrideKey = _startupConfig[AddressOverrideKey];
+ if (!string.IsNullOrEmpty(startupOverrideKey))
+ {
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ startupOverrideKey,
+ true,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ startupOverrideKey,
+ true,
+ true));
+ _publishedServerUrls = publishedServerUrls;
+ return;
+ }
+ var overrides = config.PublishedServerUriBySubnet;
foreach (var entry in overrides)
{
var parts = entry.Split('=');
@@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager
var identifier = parts[0];
if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
{
- publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement;
+ // Drop any other overrides in case an "all" override exists
+ publishedServerUrls.Clear();
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ replacement,
+ true,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ replacement,
+ true,
+ true));
+ break;
}
else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
{
- publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement;
- publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ replacement,
+ false,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ replacement,
+ false,
+ true));
}
else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
{
foreach (var lan in _lanSubnets)
{
var lanPrefix = lan.Prefix;
- publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
+ replacement,
+ true,
+ false));
}
}
else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
{
var data = new IPData(result.Prefix, result);
- publishedServerUrls[data] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ data,
+ replacement,
+ true,
+ true));
}
else if (TryParseInterface(identifier, out var ifaces))
{
foreach (var iface in ifaces)
{
- publishedServerUrls[iface] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ iface,
+ replacement,
+ true,
+ true));
}
}
else
@@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager
}
///
- /// Reloads all settings and re-initialises the instance.
+ /// Reloads all settings and re-Initializes the instance.
///
/// The to use.
public void UpdateSettings(object configuration)
@@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager
var config = (NetworkConfiguration)configuration;
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
- InitialiseLan(config);
- InitialiseRemote(config);
+ InitializeLan(config);
+ InitializeRemote(config);
if (string.IsNullOrEmpty(MockNetworkSettings))
{
- InitialiseInterfaces();
+ InitializeInterfaces();
}
else // Used in testing only.
{
@@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager
var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
{
- var data = new IPData(address, subnet, parts[2]);
- data.Index = index;
+ var data = new IPData(address, subnet, parts[2])
+ {
+ Index = index
+ };
interfaces.Add(data);
}
}
@@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager
}
EnforceBindSettings(config);
- InitialiseOverrides(config);
+ InitializeOverrides(config);
+
+ PrintNetworkInformation(config, false);
}
///
@@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager
///
public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false)
{
- if (_interfaces.Count != 0)
+ if (_interfaces.Count > 0 || individualInterfaces)
{
return _interfaces;
}
// No bind address and no exclusions, so listen on all interfaces.
var result = new List();
-
- if (individualInterfaces)
- {
- result.AddRange(_interfaces);
- return result;
- }
-
if (IsIPv4Enabled && IsIPv6Enabled)
{
// Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
@@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager
bindPreference = string.Empty;
int? port = null;
- var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any)
- || x.Key.Address.Equals(IPAddress.IPv6Any)
- || x.Key.Subnet.Contains(source))
- .DistinctBy(x => x.Key)
- .OrderBy(x => x.Key.Address.Equals(IPAddress.Any)
- || x.Key.Address.Equals(IPAddress.IPv6Any))
+ // Only consider subnets including the source IP, prefering specific overrides
+ List validPublishedServerUrls;
+ if (!isInExternalSubnet)
+ {
+ // Only use matching internal subnets
+ // Prefer more specific (bigger subnet prefix) overrides
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
+ .OrderByDescending(x => x.Data.Subnet.PrefixLength)
+ .ToList();
+ }
+ else
+ {
+ // Only use matching external subnets
+ // Prefer more specific (bigger subnet prefix) overrides
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
+ .OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList();
+ }
- // Check for user override.
foreach (var data in validPublishedServerUrls)
{
- if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any)))
- {
- // External.
- bindPreference = data.Value;
- break;
- }
-
- // Get address interface.
- var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address));
+ // Get interface matching override subnet
+ var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
if (intf?.Address is not null)
{
- // Match IP address.
- bindPreference = data.Value;
+ // If matching interface is found, use override
+ bindPreference = data.OverrideUri;
break;
}
}
@@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager
return false;
}
- // Has it got a port defined?
+ // Handle override specifying port
var parts = bindPreference.Split(':');
if (parts.Length > 1)
{
@@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager
{
bindPreference = parts[0];
port = p;
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
+ return true;
}
}
- if (port is not null)
- {
- _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
- }
- else
- {
- _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
- }
-
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
return true;
}
@@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager
_logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
return true;
}
+
+ private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
+ {
+ var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
+ if (_logger.IsEnabled(logLevel))
+ {
+ _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
+ _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
+ _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
+ }
+ }
}
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 89dbbdd2f..cb1680558 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -282,7 +282,7 @@ namespace Jellyfin.Server.Extensions
AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
}
}
- else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses))
+ else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6))
{
foreach (var address in addresses)
{
diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
index 3cb791b57..c9d5b54de 100644
--- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
@@ -3,7 +3,6 @@ using System.IO;
using System.Net;
using Jellyfin.Server.Helpers;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
@@ -36,7 +35,7 @@ public static class WebHostBuilderExtensions
return builder
.UseKestrel((builderContext, options) =>
{
- var addresses = appHost.NetManager.GetAllBindInterfaces();
+ var addresses = appHost.NetManager.GetAllBindInterfaces(true);
bool flagged = false;
foreach (var netAdd in addresses)
diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs
index 985b16c6e..e9fcd6797 100644
--- a/MediaBrowser.Model/Net/IPData.cs
+++ b/MediaBrowser.Model/Net/IPData.cs
@@ -47,6 +47,11 @@ public class IPData
///
public int Index { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the network supports multicast.
+ ///
+ public bool SupportsMulticast { get; set; } = false;
+
///
/// Gets or sets the interface name.
///
diff --git a/MediaBrowser.Model/Net/PublishedServerUriOverride.cs b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs
new file mode 100644
index 000000000..476d1ba38
--- /dev/null
+++ b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs
@@ -0,0 +1,42 @@
+namespace MediaBrowser.Model.Net;
+
+///
+/// Class holding information for a published server URI override.
+///
+public class PublishedServerUriOverride
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ /// The override.
+ /// A value indicating whether the override is for internal requests.
+ /// A value indicating whether the override is for external requests.
+ public PublishedServerUriOverride(IPData data, string overrideUri, bool internalOverride, bool externalOverride)
+ {
+ Data = data;
+ OverrideUri = overrideUri;
+ IsInternalOverride = internalOverride;
+ IsExternalOverride = externalOverride;
+ }
+
+ ///
+ /// Gets or sets the object's IP address.
+ ///
+ public IPData Data { get; set; }
+
+ ///
+ /// Gets or sets the override URI.
+ ///
+ public string OverrideUri { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the override should be applied to internal requests.
+ ///
+ public bool IsInternalOverride { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the override should be applied to external requests.
+ ///
+ public bool IsExternalOverride { get; set; }
+}
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index 0dce6c3bf..a3f30c174 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -32,10 +32,10 @@ namespace Rssdp.Infrastructure
* port to use, we will default to 0 which allows the underlying system to auto-assign a free port.
*/
- private object _BroadcastListenSocketSynchroniser = new object();
+ private object _BroadcastListenSocketSynchroniser = new();
private List _MulticastListenSockets;
- private object _SendSocketSynchroniser = new object();
+ private object _SendSocketSynchroniser = new();
private List _sendSockets;
private HttpRequestParser _RequestParser;
@@ -48,7 +48,6 @@ namespace Rssdp.Infrastructure
private int _MulticastTtl;
private bool _IsShared;
- private readonly bool _enableMultiSocketBinding;
///
/// Raised when a HTTPU request message is received by a socket (unicast or multicast).
@@ -64,9 +63,11 @@ namespace Rssdp.Infrastructure
/// Minimum constructor.
///
/// The argument is null.
- public SsdpCommunicationsServer(ISocketFactory socketFactory,
- INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
- : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
+ public SsdpCommunicationsServer(
+ ISocketFactory socketFactory,
+ INetworkManager networkManager,
+ ILogger logger)
+ : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger)
{
}
@@ -76,7 +77,12 @@ namespace Rssdp.Infrastructure
///
/// The argument is null.
/// The argument is less than or equal to zero.
- public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
+ public SsdpCommunicationsServer(
+ ISocketFactory socketFactory,
+ int localPort,
+ int multicastTimeToLive,
+ INetworkManager networkManager,
+ ILogger logger)
{
if (socketFactory is null)
{
@@ -88,19 +94,18 @@ namespace Rssdp.Infrastructure
throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero.");
}
- _BroadcastListenSocketSynchroniser = new object();
- _SendSocketSynchroniser = new object();
+ _BroadcastListenSocketSynchroniser = new();
+ _SendSocketSynchroniser = new();
_LocalPort = localPort;
_SocketFactory = socketFactory;
- _RequestParser = new HttpRequestParser();
- _ResponseParser = new HttpResponseParser();
+ _RequestParser = new();
+ _ResponseParser = new();
_MulticastTtl = multicastTimeToLive;
_networkManager = networkManager;
_logger = logger;
- _enableMultiSocketBinding = enableMultiSocketBinding;
}
///
@@ -335,7 +340,7 @@ namespace Rssdp.Infrastructure
{
sockets = sockets.ToList();
- var tasks = sockets.Where(s => (fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address)))
+ var tasks = sockets.Where(s => fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))
.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
return Task.WhenAll(tasks);
}
@@ -347,33 +352,26 @@ namespace Rssdp.Infrastructure
{
var sockets = new List();
var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress);
- if (_enableMultiSocketBinding)
- {
- // IPv6 is currently unsupported
- var validInterfaces = _networkManager.GetInternalBindAddresses()
- .Where(x => x.Address is not null)
- .Where(x => x.AddressFamily == AddressFamily.InterNetwork)
- .DistinctBy(x => x.Index);
- foreach (var intf in validInterfaces)
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses()
+ .Where(x => x.Address is not null)
+ .Where(x => x.SupportsMulticast)
+ .Where(x => x.AddressFamily == AddressFamily.InterNetwork)
+ .DistinctBy(x => x.Index);
+
+ foreach (var intf in validInterfaces)
+ {
+ try
{
- try
- {
- var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort);
- _ = ListenToSocketInternal(socket);
- sockets.Add(socket);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in CreateMulticastSocketsAndListen. IP address: {0}", intf.Address);
- }
+ var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort);
+ _ = ListenToSocketInternal(socket);
+ sockets.Add(socket);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to create SSDP UDP multicast socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
}
- }
- else
- {
- var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, new IPData(IPAddress.Any, null), _MulticastTtl, SsdpConstants.MulticastPort);
- _ = ListenToSocketInternal(socket);
- sockets.Add(socket);
}
return sockets;
@@ -382,34 +380,32 @@ namespace Rssdp.Infrastructure
private List CreateSendSockets()
{
var sockets = new List();
- if (_enableMultiSocketBinding)
+
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses()
+ .Where(x => x.Address is not null)
+ .Where(x => x.SupportsMulticast)
+ .Where(x => x.AddressFamily == AddressFamily.InterNetwork);
+
+ if (OperatingSystem.IsMacOS())
{
- // IPv6 is currently unsupported
- var validInterfaces = _networkManager.GetInternalBindAddresses()
- .Where(x => x.Address is not null)
- .Where(x => x.AddressFamily == AddressFamily.InterNetwork);
+ // Manually remove loopback on macOS due to https://github.com/dotnet/runtime/issues/24340
+ validInterfaces = validInterfaces.Where(x => !x.Address.Equals(IPAddress.Loopback));
+ }
- foreach (var intf in validInterfaces)
+ foreach (var intf in validInterfaces)
+ {
+ try
{
- try
- {
- var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort);
- _ = ListenToSocketInternal(socket);
- sockets.Add(socket);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in CreateSsdpUdpSocket. IPAddress: {0}", intf.Address);
- }
+ var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort);
+ _ = ListenToSocketInternal(socket);
+ sockets.Add(socket);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to create SSDP UDP sender socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
}
}
- else
- {
- var socket = _SocketFactory.CreateSsdpUdpSocket(new IPData(IPAddress.Any, null), _LocalPort);
- _ = ListenToSocketInternal(socket);
- sockets.Add(socket);
- }
-
return sockets;
}
@@ -423,7 +419,7 @@ namespace Rssdp.Infrastructure
{
try
{
- var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false);;
+ var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false);;
if (result.ReceivedBytes > 0)
{
@@ -431,7 +427,7 @@ namespace Rssdp.Infrastructure
var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface);
ProcessMessage(
- UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes),
+ Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes),
remoteEndpoint,
localEndpointAdapter.Address);
}
@@ -511,7 +507,7 @@ namespace Rssdp.Infrastructure
return;
}
- var handlers = this.RequestReceived;
+ var handlers = RequestReceived;
if (handlers is not null)
{
handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress));
@@ -520,7 +516,7 @@ namespace Rssdp.Infrastructure
private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress)
{
- var handlers = this.ResponseReceived;
+ var handlers = ResponseReceived;
if (handlers is not null)
{
handlers(this, new ResponseReceivedEventArgs(data, endPoint)
diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs
index 59f4c5070..82b09c4b4 100644
--- a/RSSDP/SsdpDeviceLocator.cs
+++ b/RSSDP/SsdpDeviceLocator.cs
@@ -17,7 +17,7 @@ namespace Rssdp.Infrastructure
private ISsdpCommunicationsServer _CommunicationsServer;
private Timer _BroadcastTimer;
- private object _timerLock = new object();
+ private object _timerLock = new();
private string _OSName;
@@ -221,12 +221,12 @@ namespace Rssdp.Infrastructure
///
protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress)
{
- if (this.IsDisposed)
+ if (IsDisposed)
{
return;
}
- var handlers = this.DeviceAvailable;
+ var handlers = DeviceAvailable;
if (handlers is not null)
{
handlers(this, new DeviceAvailableEventArgs(device, isNewDevice)
@@ -244,12 +244,12 @@ namespace Rssdp.Infrastructure
///
protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired)
{
- if (this.IsDisposed)
+ if (IsDisposed)
{
return;
}
- var handlers = this.DeviceUnavailable;
+ var handlers = DeviceUnavailable;
if (handlers is not null)
{
handlers(this, new DeviceUnavailableEventArgs(device, expired));
@@ -291,8 +291,8 @@ namespace Rssdp.Infrastructure
_CommunicationsServer = null;
if (commsServer is not null)
{
- commsServer.ResponseReceived -= this.CommsServer_ResponseReceived;
- commsServer.RequestReceived -= this.CommsServer_RequestReceived;
+ commsServer.ResponseReceived -= CommsServer_ResponseReceived;
+ commsServer.RequestReceived -= CommsServer_RequestReceived;
}
}
}
@@ -341,7 +341,7 @@ namespace Rssdp.Infrastructure
var values = new Dictionary(StringComparer.OrdinalIgnoreCase);
- values["HOST"] = "239.255.255.250:1900";
+ values["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort);
values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
values["MAN"] = "\"ssdp:discover\"";
@@ -382,17 +382,17 @@ namespace Rssdp.Infrastructure
private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress)
{
- if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
+ if (string.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
{
return;
}
var notificationType = GetFirstHeaderStringValue("NTS", message);
- if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
+ if (string.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
{
ProcessAliveNotification(message, IPAddress);
}
- else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
+ else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
{
ProcessByeByeNotification(message);
}
@@ -420,7 +420,7 @@ namespace Rssdp.Infrastructure
private void ProcessByeByeNotification(HttpRequestMessage message)
{
var notficationType = GetFirstHeaderStringValue("NT", message);
- if (!String.IsNullOrEmpty(notficationType))
+ if (!string.IsNullOrEmpty(notficationType))
{
var usn = GetFirstHeaderStringValue("USN", message);
@@ -447,10 +447,9 @@ namespace Rssdp.Infrastructure
private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message)
{
string retVal = null;
- IEnumerable values;
if (message.Headers.Contains(headerName))
{
- message.Headers.TryGetValues(headerName, out values);
+ message.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
retVal = values.FirstOrDefault();
@@ -463,10 +462,9 @@ namespace Rssdp.Infrastructure
private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message)
{
string retVal = null;
- IEnumerable values;
if (message.Headers.Contains(headerName))
{
- message.Headers.TryGetValues(headerName, out values);
+ message.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
retVal = values.FirstOrDefault();
@@ -479,10 +477,9 @@ namespace Rssdp.Infrastructure
private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request)
{
string value = null;
- IEnumerable values;
if (request.Headers.Contains(headerName))
{
- request.Headers.TryGetValues(headerName, out values);
+ request.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
value = values.FirstOrDefault();
@@ -496,10 +493,9 @@ namespace Rssdp.Infrastructure
private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response)
{
string value = null;
- IEnumerable values;
if (response.Headers.Contains(headerName))
{
- response.Headers.TryGetValues(headerName, out values);
+ response.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
value = values.FirstOrDefault();
@@ -529,7 +525,7 @@ namespace Rssdp.Infrastructure
foreach (var device in expiredDevices)
{
- if (this.IsDisposed)
+ if (IsDisposed)
{
return;
}
@@ -543,7 +539,7 @@ namespace Rssdp.Infrastructure
// problems.
foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct())
{
- if (this.IsDisposed)
+ if (IsDisposed)
{
return;
}
@@ -560,7 +556,7 @@ namespace Rssdp.Infrastructure
existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn);
foreach (var existingDevice in existingDevices)
{
- if (this.IsDisposed)
+ if (IsDisposed)
{
return true;
}
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
index 950e6fec8..65ae658a4 100644
--- a/RSSDP/SsdpDevicePublisher.cs
+++ b/RSSDP/SsdpDevicePublisher.cs
@@ -206,9 +206,9 @@ namespace Rssdp.Infrastructure
IPAddress receivedOnlocalIPAddress,
CancellationToken cancellationToken)
{
- if (String.IsNullOrEmpty(searchTarget))
+ if (string.IsNullOrEmpty(searchTarget))
{
- WriteTrace(String.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
+ WriteTrace(string.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
return;
}
@@ -232,7 +232,7 @@ namespace Rssdp.Infrastructure
// return;
}
- if (!Int32.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0)
+ if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0)
{
return;
}
@@ -243,27 +243,27 @@ namespace Rssdp.Infrastructure
}
// Do not block synchronously as that may tie up a threadpool thread for several seconds.
- Task.Delay(_Random.Next(16, (maxWaitInterval * 1000))).ContinueWith((parentTask) =>
+ Task.Delay(_Random.Next(16, maxWaitInterval * 1000)).ContinueWith((parentTask) =>
{
// Copying devices to local array here to avoid threading issues/enumerator exceptions.
IEnumerable devices = null;
lock (_Devices)
{
- if (String.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)
+ if (string.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)
{
devices = GetAllDevicesAsFlatEnumerable().ToArray();
}
- else if (String.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (this.SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0))
+ else if (string.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0))
{
devices = _Devices.ToArray();
}
else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase))
{
- devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray();
+ devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray();
}
else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase))
{
- devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray();
+ devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray();
}
}
@@ -299,7 +299,7 @@ namespace Rssdp.Infrastructure
if (isRootDevice)
{
SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
- if (this.SupportPnpRootDevice)
+ if (SupportPnpRootDevice)
{
SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
}
@@ -312,7 +312,7 @@ namespace Rssdp.Infrastructure
private string GetUsn(string udn, string fullDeviceType)
{
- return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
+ return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
}
private async void SendSearchResponse(
@@ -326,16 +326,17 @@ namespace Rssdp.Infrastructure
const string header = "HTTP/1.1 200 OK";
var rootDevice = device.ToRootDevice();
- var values = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- values["EXT"] = "";
- values["DATE"] = DateTime.UtcNow.ToString("r");
- values["HOST"] = "239.255.255.250:1900";
- values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
- values["ST"] = searchTarget;
- values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
- values["USN"] = uniqueServiceName;
- values["LOCATION"] = rootDevice.Location.ToString();
+ var values = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["EXT"] = "",
+ ["DATE"] = DateTime.UtcNow.ToString("r"),
+ ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
+ ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds,
+ ["ST"] = searchTarget,
+ ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
+ ["USN"] = uniqueServiceName,
+ ["LOCATION"] = rootDevice.Location.ToString()
+ };
var message = BuildMessage(header, values);
@@ -439,7 +440,7 @@ namespace Rssdp.Infrastructure
if (isRoot)
{
SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken);
- if (this.SupportPnpRootDevice)
+ if (SupportPnpRootDevice)
{
SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken);
}
@@ -460,17 +461,18 @@ namespace Rssdp.Infrastructure
const string header = "NOTIFY * HTTP/1.1";
- var values = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- // If needed later for non-server devices, these headers will need to be dynamic
- values["HOST"] = "239.255.255.250:1900";
- values["DATE"] = DateTime.UtcNow.ToString("r");
- values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
- values["LOCATION"] = rootDevice.Location.ToString();
- values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
- values["NTS"] = "ssdp:alive";
- values["NT"] = notificationType;
- values["USN"] = uniqueServiceName;
+ var values = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ // If needed later for non-server devices, these headers will need to be dynamic
+ ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
+ ["DATE"] = DateTime.UtcNow.ToString("r"),
+ ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds,
+ ["LOCATION"] = rootDevice.Location.ToString(),
+ ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
+ ["NTS"] = "ssdp:alive",
+ ["NT"] = notificationType,
+ ["USN"] = uniqueServiceName
+ };
var message = BuildMessage(header, values);
@@ -485,7 +487,7 @@ namespace Rssdp.Infrastructure
if (isRoot)
{
tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken));
- if (this.SupportPnpRootDevice)
+ if (SupportPnpRootDevice)
{
tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken));
}
@@ -506,20 +508,21 @@ namespace Rssdp.Infrastructure
{
const string header = "NOTIFY * HTTP/1.1";
- var values = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- // If needed later for non-server devices, these headers will need to be dynamic
- values["HOST"] = "239.255.255.250:1900";
- values["DATE"] = DateTime.UtcNow.ToString("r");
- values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
- values["NTS"] = "ssdp:byebye";
- values["NT"] = notificationType;
- values["USN"] = uniqueServiceName;
+ var values = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ // If needed later for non-server devices, these headers will need to be dynamic
+ ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
+ ["DATE"] = DateTime.UtcNow.ToString("r"),
+ ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
+ ["NTS"] = "ssdp:byebye",
+ ["NT"] = notificationType,
+ ["USN"] = uniqueServiceName
+ };
var message = BuildMessage(header, values);
var sendCount = IsDisposed ? 1 : 3;
- WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
+ WriteTrace(string.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
}
diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
index 2ff7c7de4..072e0a8c5 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
@@ -16,7 +16,6 @@ namespace Jellyfin.Networking.Tests
[InlineData("127.0.0.1:123")]
[InlineData("localhost")]
[InlineData("localhost:1345")]
- [InlineData("www.google.co.uk")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
index 0b07a3c53..2302f90b8 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
@@ -1,7 +1,9 @@
using System.Net;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
using Xunit;
namespace Jellyfin.Networking.Tests
@@ -28,7 +30,8 @@ namespace Jellyfin.Networking.Tests
LocalNetworkSubnets = network.Split(',')
};
- using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger());
+ var startupConf = new Mock();
+ using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger());
Assert.True(networkManager.IsInLocalNetwork(ip));
}
@@ -56,9 +59,10 @@ namespace Jellyfin.Networking.Tests
LocalNetworkSubnets = network.Split(',')
};
- using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger());
+ var startupConf = new Mock();
+ using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger());
- Assert.False(nm.IsInLocalNetwork(ip));
+ Assert.False(networkManager.IsInLocalNetwork(ip));
}
}
}
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index 731cbbafb..022b8a3d0 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -7,6 +7,7 @@ using Jellyfin.Networking.Extensions;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Net;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@@ -54,7 +55,8 @@ namespace Jellyfin.Networking.Tests
};
NetworkManager.MockNetworkSettings = interfaces;
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+ var startupConf = new Mock();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger());
NetworkManager.MockNetworkSettings = string.Empty;
Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]");
@@ -200,7 +202,8 @@ namespace Jellyfin.Networking.Tests
};
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+ var startupConf = new Mock();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger());
NetworkManager.MockNetworkSettings = string.Empty;
// Check to see if DNS resolution is working. If not, skip test.
@@ -229,24 +232,24 @@ namespace Jellyfin.Networking.Tests
[InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")]
// User on external network, we're bound internal and external - so result is override.
- [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
+ [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "all=http://helloworld.com", "http://helloworld.com")]
// User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override.
- [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")]
+ [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "external=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")]
// User on internal network, no binding specified - so result is the 1st internal.
- [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
+ [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "external=http://helloworld.com", "eth16")]
// User on external network, internal binding only - so assumption is a proxy forward, return external override.
- [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
+ [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "external=http://helloworld.com", "http://helloworld.com")]
// User on external network, no binding - so result is the 1st external which is overriden.
- [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
+ [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "external=http://helloworld.com", "http://helloworld.com")]
- // User assumed to be internal, no binding - so result is the 1st internal.
- [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
+ // User assumed to be internal, no binding - so result is the 1st matching interface.
+ [InlineData("", "192.168.1.0/24", "", false, "all=http://helloworld.com", "eth16")]
- // User is internal, no binding - so result is the 1st internal, which is then overridden.
+ // User is internal, no binding - so result is the 1st internal interface, which is then overridden.
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")]
public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result)
{
@@ -264,7 +267,8 @@ namespace Jellyfin.Networking.Tests
};
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+ var startupConf = new Mock();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger());
NetworkManager.MockNetworkSettings = string.Empty;
if (nm.TryParseInterface(result, out IReadOnlyList? resultObj) && resultObj is not null)
@@ -293,7 +297,9 @@ namespace Jellyfin.Networking.Tests
RemoteIPFilter = addresses.Split(','),
IsRemoteIPFilterBlacklist = false
};
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+
+ var startupConf = new Mock();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger());
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied);
}
@@ -314,7 +320,8 @@ namespace Jellyfin.Networking.Tests
IsRemoteIPFilterBlacklist = true
};
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+ var startupConf = new Mock();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger());
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied);
}
@@ -334,7 +341,8 @@ namespace Jellyfin.Networking.Tests
};
NetworkManager.MockNetworkSettings = interfaces;
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+ var startupConf = new Mock();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger());
var interfaceToUse = nm.GetBindAddress(string.Empty, out _);
@@ -358,7 +366,8 @@ namespace Jellyfin.Networking.Tests
};
NetworkManager.MockNetworkSettings = interfaces;
- using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+ var startupConf = new Mock();
+ using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger());
var interfaceToUse = nm.GetBindAddress(source, out _);
diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
index 49516cccc..288102037 100644
--- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
+++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
@@ -7,6 +7,7 @@ using Jellyfin.Server.Extensions;
using MediaBrowser.Common.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@@ -119,8 +120,8 @@ namespace Jellyfin.Server.Tests
EnableIPv6 = true,
EnableIPv4 = true,
};
-
- return new NetworkManager(GetMockConfig(conf), new NullLogger());
+ var startupConf = new Mock();
+ return new NetworkManager(GetMockConfig(conf), startupConf.Object, new NullLogger());
}
}
}
--
cgit v1.2.3
From 26571a8c51b9670f198e58175463e1d3db5441ee Mon Sep 17 00:00:00 2001
From: Bond-009
Date: Wed, 11 Oct 2023 01:51:15 +0200
Subject: Deprecate CanLaunchWebBrowser (#10381)
It's been a while since I removed this feature from server not sure why it's
in the api anyway. The macOS and Windows app have this functionality
---
Emby.Server.Implementations/SystemManager.cs | 5 -----
MediaBrowser.Model/System/SystemInfo.cs | 3 ++-
2 files changed, 2 insertions(+), 6 deletions(-)
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs
index 5e9c424e9..af66b62e3 100644
--- a/Emby.Server.Implementations/SystemManager.cs
+++ b/Emby.Server.Implementations/SystemManager.cs
@@ -46,10 +46,6 @@ public class SystemManager : ISystemManager
_installationManager = installationManager;
}
- private bool CanLaunchWebBrowser => Environment.UserInteractive
- && !_startupOptions.IsService
- && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
-
///
public SystemInfo GetSystemInfo(HttpRequest request)
{
@@ -67,7 +63,6 @@ public class SystemManager : ISystemManager
ItemsByNamePath = _applicationPaths.InternalMetadataPath,
InternalMetadataPath = _applicationPaths.InternalMetadataPath,
CachePath = _applicationPaths.CachePath,
- CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = _configurationManager.GetTranscodePath(),
ServerName = _applicationHost.FriendlyName,
LocalAddress = _applicationHost.GetSmartApiUrl(request),
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index bd0099af7..502fc38ab 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -84,7 +84,8 @@ namespace MediaBrowser.Model.System
[Obsolete("This is always true")]
public bool CanSelfRestart { get; set; } = true;
- public bool CanLaunchWebBrowser { get; set; }
+ [Obsolete("This is always false")]
+ public bool CanLaunchWebBrowser { get; set; } = false;
///
/// Gets or sets the program data path.
--
cgit v1.2.3
From 9f259aa404a4bf4185f22ece9a13be2a7ae346df Mon Sep 17 00:00:00 2001
From: chinkara
Date: Sun, 15 Oct 2023 15:53:53 +0200
Subject: add EnableSubtitleManagement permission
---
Jellyfin.Data/Entities/User.cs | 1 +
Jellyfin.Data/Enums/PermissionKind.cs | 7 ++++++-
Jellyfin.Server.Implementations/Users/UserManager.cs | 2 ++
MediaBrowser.Model/Users/UserPolicy.cs | 8 ++++++++
4 files changed, 17 insertions(+), 1 deletion(-)
(limited to 'MediaBrowser.Model')
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 58ddaaf83..8e66a3287 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -499,6 +499,7 @@ namespace Jellyfin.Data.Entities
Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false));
+ Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false));
}
///
diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs
index 40280b95e..6644f0151 100644
--- a/Jellyfin.Data/Enums/PermissionKind.cs
+++ b/Jellyfin.Data/Enums/PermissionKind.cs
@@ -113,6 +113,11 @@ namespace Jellyfin.Data.Enums
///
/// Whether the user can create, modify and delete collections.
///
- EnableCollectionManagement = 21
+ EnableCollectionManagement = 21,
+
+ ///
+ /// Whether the user can edit subtitles.
+ ///
+ EnableSubtitleManagement = 22
}
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 94ac4798c..59d655c82 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -349,6 +349,7 @@ namespace Jellyfin.Server.Implementations.Users
ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
+ EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
@@ -666,6 +667,7 @@ namespace Jellyfin.Server.Implementations.Users
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.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 8354c60ef..f5aff07db 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -15,6 +15,7 @@ namespace MediaBrowser.Model.Users
{
IsHidden = true;
EnableCollectionManagement = false;
+ EnableSubtitleManagement = false;
EnableContentDeletion = false;
EnableContentDeletionFromFolders = Array.Empty();
@@ -83,6 +84,13 @@ namespace MediaBrowser.Model.Users
[DefaultValue(false)]
public bool EnableCollectionManagement { get; set; }
+ ///
+ /// Gets or sets a value indicating whether this instance can manage subtitles.
+ ///
+ /// true if this instance is allowed; otherwise, false.
+ [DefaultValue(false)]
+ public bool EnableSubtitleManagement { get; set; }
+
///
/// Gets or sets a value indicating whether this instance is disabled.
///
--
cgit v1.2.3
From 6c5a4a93ccdb7c6ca8b26c36505d0954a7c79300 Mon Sep 17 00:00:00 2001
From: herby2212 <12448284+herby2212@users.noreply.github.com>
Date: Sat, 21 Oct 2023 11:49:01 +0200
Subject: fix indentation after merge
---
.../Configuration/ServerConfiguration.cs | 28 +++++++++++-----------
1 file changed, 14 insertions(+), 14 deletions(-)
(limited to 'MediaBrowser.Model')
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 2fb5fbd0b..fe92251e9 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -158,20 +158,20 @@ public class ServerConfiguration : BaseApplicationConfiguration
/// The remaining time in minutes.
public int MaxAudiobookResume { get; set; } = 5;
- ///
- /// Gets or sets the threshold in minutes after a inactive session gets closed automatically.
- /// If set to 0 the check for inactive sessions gets disabled.
- ///
- /// The close inactive session threshold in minutes. 0 to disable.
- public int InactiveSessionThreshold { get; set; } = 10;
-
- ///
- /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
- /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
- /// different directories and files.
- ///
- /// The file watcher delay.
- public int LibraryMonitorDelay { get; set; } = 60;
+ ///
+ /// Gets or sets the threshold in minutes after a inactive session gets closed automatically.
+ /// If set to 0 the check for inactive sessions gets disabled.
+ ///
+ /// The close inactive session threshold in minutes. 0 to disable.
+ public int InactiveSessionThreshold { get; set; } = 10;
+
+ ///
+ /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
+ /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
+ /// different directories and files.
+ ///
+ /// The file watcher delay.
+ public int LibraryMonitorDelay { get; set; } = 60;
///
/// Gets or sets the duration in seconds that we will wait after a library updated event before executing the library changed notification.
--
cgit v1.2.3
From a9ef103c95a7460031879726f4afda3013ca6619 Mon Sep 17 00:00:00 2001
From: Bond_009
Date: Sun, 5 Nov 2023 02:01:14 +0100
Subject: Add IDisposableAnalyzers to more projects
---
Emby.Dlna/Emby.Dlna.csproj | 6 +++++-
Emby.Dlna/PlayTo/Device.cs | 5 ++---
Emby.Dlna/PlayTo/PlayToController.cs | 13 ++++++-------
Emby.Naming/Emby.Naming.csproj | 6 +++++-
Emby.Photos/Emby.Photos.csproj | 8 ++++++--
.../Emby.Server.Implementations.csproj | 7 ++++++-
Jellyfin.Api/Jellyfin.Api.csproj | 6 +++++-
.../ActivityLogWebSocketListener.cs | 5 ++++-
.../ScheduledTasksWebSocketListener.cs | 7 +++++--
.../SessionInfoWebSocketListener.cs | 17 ++++++++++-------
Jellyfin.Data/Jellyfin.Data.csproj | 6 +++++-
Jellyfin.Networking/Jellyfin.Networking.csproj | 6 +++++-
.../Jellyfin.Server.Implementations.csproj | 2 +-
Jellyfin.Server/Jellyfin.Server.csproj | 6 +++++-
MediaBrowser.Common/MediaBrowser.Common.csproj | 6 +++++-
.../MediaBrowser.Controller.csproj | 6 +++++-
.../MediaBrowser.LocalMetadata.csproj | 6 +++++-
.../MediaBrowser.MediaEncoding.csproj | 6 +++++-
.../Subtitles/SubtitleEncoder.cs | 8 +++-----
MediaBrowser.Model/MediaBrowser.Model.csproj | 7 ++++++-
MediaBrowser.Providers/Manager/ProviderManager.cs | 10 +++++-----
.../MediaBrowser.Providers.csproj | 6 +++++-
.../MediaBrowser.XbmcMetadata.csproj | 4 ++++
src/Directory.Build.props | 21 +++++++++++++++++++++
.../Jellyfin.Drawing.Skia.csproj | 15 ---------------
src/Jellyfin.Drawing/Jellyfin.Drawing.csproj | 15 ---------------
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 16 ----------------
.../Jellyfin.MediaEncoding.Hls.csproj | 15 ---------------
.../Jellyfin.MediaEncoding.Keyframes.csproj | 15 ---------------
29 files changed, 134 insertions(+), 122 deletions(-)
create mode 100644 src/Directory.Build.props
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index aca239964..efbef0564 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -26,8 +26,12 @@
false
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index bb9b8b0fd..18fa19650 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -1248,11 +1248,10 @@ namespace Emby.Dlna.PlayTo
if (disposing)
{
_timer?.Dispose();
+ _timer = null;
+ Properties = null!;
}
- _timer = null;
- Properties = null!;
-
_disposed = true;
}
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index b1ad15cdc..df02fe631 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -683,16 +683,15 @@ namespace Emby.Dlna.PlayTo
if (disposing)
{
+ _device.PlaybackStart -= OnDevicePlaybackStart;
+ _device.PlaybackProgress -= OnDevicePlaybackProgress;
+ _device.PlaybackStopped -= OnDevicePlaybackStopped;
+ _device.MediaChanged -= OnDeviceMediaChanged;
+ _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
+ _device.OnDeviceUnavailable = null;
_device.Dispose();
}
- _device.PlaybackStart -= OnDevicePlaybackStart;
- _device.PlaybackProgress -= OnDevicePlaybackProgress;
- _device.PlaybackStopped -= OnDevicePlaybackStopped;
- _device.MediaChanged -= OnDeviceMediaChanged;
- _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
- _device.OnDeviceUnavailable = null;
-
_disposed = true;
}
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index f3973dad9..bc7548189 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -45,8 +45,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 0f97a0686..5a04bbe49 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -24,14 +24,18 @@
true
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
-
+
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 80263c139..b48e389ac 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -49,8 +49,13 @@
false
-
+
+
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 7ac231885..03dd97367 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -24,8 +24,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 5b90d65d8..ba228cb00 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -53,7 +53,10 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener
protected override void Dispose(bool dispose)
{
- _activityManager.EntryCreated -= OnEntryCreated;
+ if (dispose)
+ {
+ _activityManager.EntryCreated -= OnEntryCreated;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index a9df2d671..37c108d5a 100644
--- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
@@ -58,8 +58,11 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener
protected override void Dispose(bool dispose)
{
- _taskManager.TaskExecuting -= OnTaskExecuting;
- _taskManager.TaskCompleted -= OnTaskCompleted;
+ if (dispose)
+ {
+ _taskManager.TaskExecuting -= OnTaskExecuting;
+ _taskManager.TaskCompleted -= OnTaskCompleted;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index b403ff46d..3c2b86142 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -57,13 +57,16 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener
protected override void Dispose(bool dispose)
{
- _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
- _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
- _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
- _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
- _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
- _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
- _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+ if (dispose)
+ {
+ _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+ _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
+ _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+ _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
+ _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
+ _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 1bc5d8bf9..847853ca0 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -27,8 +27,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj
index 4cff5927f..43d08c37a 100644
--- a/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -9,8 +9,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index fa6adb022..df1d5a3e1 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -6,7 +6,7 @@
true
-
+
all
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 62abb8935..5479d2296 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -22,8 +22,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 7015d991f..7d0d7a173 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -43,8 +43,12 @@
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 69c0d26b6..f9468f6cd 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -49,8 +49,12 @@
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index 71cdea529..a39bc238a 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -20,8 +20,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index a0624fe76..1f39e88cd 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -29,8 +29,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 21fa4468e..8eea773d8 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -135,19 +135,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subtitleStream = mediaSource.MediaStreams
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
- var subtitle = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
+ var (stream, inputFormat) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
.ConfigureAwait(false);
- var inputFormat = subtitle.Format;
-
// Return the original if the same format is being requested
// Character encoding was already handled in GetSubtitleStream
if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase))
{
- return subtitle.Stream;
+ return stream;
}
- using (var stream = subtitle.Stream)
+ using (stream)
{
return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken);
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 58ba83a35..75c5bc6f0 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -48,8 +48,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
@@ -58,6 +62,7 @@
+
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index d0bb34d52..4ba884418 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -1096,13 +1096,13 @@ namespace MediaBrowser.Providers.Manager
return;
}
- if (!_disposeCancellationTokenSource.IsCancellationRequested)
- {
- _disposeCancellationTokenSource.Cancel();
- }
-
if (disposing)
{
+ if (!_disposeCancellationTokenSource.IsCancellationRequested)
+ {
+ _disposeCancellationTokenSource.Cancel();
+ }
+
_disposeCancellationTokenSource.Dispose();
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 7ef70f4b0..8471f6fa1 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -33,8 +33,12 @@
../jellyfin.ruleset
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 807234915..d7e34fd22 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -22,6 +22,10 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 000000000..ac2726ed5
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 09e103c6b..3c417f8ff 100644
--- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -31,19 +31,4 @@
-
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
-
-
-
-
diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index 2a5e24a44..d7ef6f8e7 100644
--- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -21,19 +21,4 @@
-
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
-
-
-
-
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 36ae55ed2..997df6dbe 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -27,24 +27,8 @@
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
-
-
-
-
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index b792e7ec6..76dde1cf6 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -5,21 +5,6 @@
true
-
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
-
-
-
-
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index 09b1f8faa..0d91a447b 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -9,21 +9,6 @@
-
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
-
-
-
-
--
cgit v1.2.3
From c7a94d48ae019f41d5f06340bca7efe0788ad5ad Mon Sep 17 00:00:00 2001
From: Cody Robibero
Date: Thu, 9 Nov 2023 14:00:13 -0700
Subject: Convert ItemSortBy to enum (#9765)
* Convert ItemSortBy to enum
* Rename Unknown to Default
---
Emby.Dlna/ContentDirectory/ControlHandler.cs | 18 +-
.../Data/SqliteItemRepository.cs | 205 +++++----------------
.../Images/BaseFolderImageProvider.cs | 2 +-
.../Library/LibraryManager.cs | 8 +-
.../LiveTv/LiveTvManager.cs | 2 +-
.../Sorting/AiredEpisodeOrderComparer.cs | 3 +-
.../Sorting/AlbumArtistComparer.cs | 3 +-
.../Sorting/AlbumComparer.cs | 3 +-
.../Sorting/ArtistComparer.cs | 3 +-
.../Sorting/CommunityRatingComparer.cs | 3 +-
.../Sorting/CriticRatingComparer.cs | 3 +-
.../Sorting/DateCreatedComparer.cs | 3 +-
.../Sorting/DateLastMediaAddedComparer.cs | 3 +-
.../Sorting/DatePlayedComparer.cs | 3 +-
.../Sorting/IndexNumberComparer.cs | 3 +-
.../Sorting/IsFavoriteOrLikeComparer.cs | 3 +-
.../Sorting/IsFolderComparer.cs | 3 +-
.../Sorting/IsPlayedComparer.cs | 3 +-
.../Sorting/IsUnplayedComparer.cs | 3 +-
.../Sorting/NameComparer.cs | 3 +-
.../Sorting/OfficialRatingComparer.cs | 3 +-
.../Sorting/ParentIndexNumberComparer.cs | 3 +-
.../Sorting/PlayCountComparer.cs | 3 +-
.../Sorting/PremiereDateComparer.cs | 3 +-
.../Sorting/ProductionYearComparer.cs | 3 +-
.../Sorting/RandomComparer.cs | 3 +-
.../Sorting/RuntimeComparer.cs | 3 +-
.../Sorting/SeriesSortNameComparer.cs | 3 +-
.../Sorting/SortNameComparer.cs | 3 +-
.../Sorting/StartDateComparer.cs | 3 +-
.../Sorting/StudioComparer.cs | 3 +-
Jellyfin.Api/Controllers/ArtistsController.cs | 4 +-
Jellyfin.Api/Controllers/ChannelsController.cs | 2 +-
Jellyfin.Api/Controllers/GenresController.cs | 2 +-
Jellyfin.Api/Controllers/ItemsController.cs | 4 +-
Jellyfin.Api/Controllers/LiveTvController.cs | 4 +-
Jellyfin.Api/Controllers/MusicGenresController.cs | 2 +-
Jellyfin.Api/Controllers/TrailersController.cs | 2 +-
Jellyfin.Api/Controllers/TvShowsController.cs | 4 +-
Jellyfin.Api/Controllers/YearsController.cs | 2 +-
Jellyfin.Api/Helpers/RequestHelpers.cs | 6 +-
Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs | 2 +-
Jellyfin.Data/Enums/ItemSortBy.cs | 167 +++++++++++++++++
.../Entities/IHasDisplayOrder.cs | 2 +
.../Entities/InternalItemsQuery.cs | 4 +-
MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 8 +-
MediaBrowser.Controller/Library/ILibraryManager.cs | 4 +-
MediaBrowser.Controller/Providers/EpisodeInfo.cs | 1 +
.../Sorting/IBaseItemComparer.cs | 6 +-
MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs | 1 +
MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs | 4 +-
MediaBrowser.Model/Querying/ItemSortBy.cs | 163 ----------------
MediaBrowser.Providers/Manager/MetadataService.cs | 1 +
.../Plugins/Tmdb/TmdbClientManager.cs | 1 +
.../Helpers/RequestHelpersTests.cs | 36 ++--
55 files changed, 329 insertions(+), 416 deletions(-)
create mode 100644 Jellyfin.Data/Enums/ItemSortBy.cs
delete mode 100644 MediaBrowser.Model/Querying/ItemSortBy.cs
(limited to 'MediaBrowser.Model')
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index abd594a3a..e685d252e 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -917,7 +917,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult GetGenres(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var genresResult = _libraryManager.GetGenres(query);
@@ -933,7 +933,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult GetMusicGenres(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var genresResult = _libraryManager.GetMusicGenres(query);
@@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var artists = _libraryManager.GetAlbumArtists(query);
@@ -965,7 +965,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult GetMusicArtists(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var artists = _libraryManager.GetArtists(query);
return ToResult(query.StartIndex, artists);
@@ -980,7 +980,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult GetFavoriteArtists(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
query.IsFavorite = true;
var artists = _libraryManager.GetArtists(query);
@@ -1011,7 +1011,7 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private QueryResult GetNextUp(BaseItem parent, InternalItemsQuery query)
{
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
@@ -1036,7 +1036,7 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private QueryResult GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
{
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
@@ -1203,9 +1203,9 @@ namespace Emby.Dlna.ContentDirectory
///
/// The .
/// True if pre-sorted.
- private static (string SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
+ private static (ItemSortBy SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
{
- return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
+ return isPreSorted ? Array.Empty<(ItemSortBy, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
}
///
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index e519364c2..fadd4f2f3 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -2042,7 +2042,7 @@ namespace Emby.Server.Implementations.Data
return false;
}
- var sortingFields = new HashSet(query.OrderBy.Select(i => i.OrderBy), StringComparer.OrdinalIgnoreCase);
+ var sortingFields = new HashSet(query.OrderBy.Select(i => i.OrderBy));
return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked)
|| sortingFields.Contains(ItemSortBy.IsPlayed)
@@ -2832,20 +2832,20 @@ namespace Emby.Server.Implementations.Data
if (hasSimilar || hasSearch)
{
- List<(string, SortOrder)> prepend = new List<(string, SortOrder)>(4);
+ List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4);
if (hasSearch)
{
- prepend.Add(("SearchScore", SortOrder.Descending));
+ prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending));
prepend.Add((ItemSortBy.SortName, SortOrder.Ascending));
}
if (hasSimilar)
{
- prepend.Add(("SimilarityScore", SortOrder.Descending));
+ prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending));
prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
}
- var arr = new (string, SortOrder)[prepend.Count + orderBy.Count];
+ var arr = new (ItemSortBy, SortOrder)[prepend.Count + orderBy.Count];
prepend.CopyTo(arr, 0);
orderBy.CopyTo(arr, prepend.Count);
orderBy = query.OrderBy = arr;
@@ -2863,166 +2863,43 @@ namespace Emby.Server.Implementations.Data
}));
}
- private string MapOrderByField(string name, InternalItemsQuery query)
+ private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
{
- if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase))
- {
- // TODO
- return "SortName";
- }
-
- if (string.Equals(name, ItemSortBy.Runtime, StringComparison.OrdinalIgnoreCase))
- {
- return "RuntimeTicks";
- }
-
- if (string.Equals(name, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
- {
- return "RANDOM()";
- }
-
- if (string.Equals(name, ItemSortBy.DatePlayed, StringComparison.OrdinalIgnoreCase))
- {
- if (query.GroupBySeriesPresentationUniqueKey)
- {
- return "MAX(LastPlayedDate)";
- }
-
- return "LastPlayedDate";
- }
-
- if (string.Equals(name, ItemSortBy.PlayCount, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.PlayCount;
- }
-
- if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase))
- {
- return "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )";
- }
-
- if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.IsFolder;
- }
-
- if (string.Equals(name, ItemSortBy.IsPlayed, StringComparison.OrdinalIgnoreCase))
- {
- return "played";
- }
-
- if (string.Equals(name, ItemSortBy.IsUnplayed, StringComparison.OrdinalIgnoreCase))
- {
- return "played";
- }
-
- if (string.Equals(name, ItemSortBy.DateLastContentAdded, StringComparison.OrdinalIgnoreCase))
- {
- return "DateLastMediaAdded";
- }
-
- if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase))
- {
- return "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)";
- }
-
- if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase))
- {
- return "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)";
- }
-
- if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase))
- {
- return "InheritedParentalRatingValue";
- }
-
- if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase))
- {
- return "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)";
- }
-
- if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase))
- {
- return "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)";
- }
-
- if (string.Equals(name, ItemSortBy.SeriesSortName, StringComparison.OrdinalIgnoreCase))
- {
- return "SeriesName";
- }
-
- if (string.Equals(name, ItemSortBy.AiredEpisodeOrder, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.AiredEpisodeOrder;
- }
-
- if (string.Equals(name, ItemSortBy.Album, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.Album;
- }
-
- if (string.Equals(name, ItemSortBy.DateCreated, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.DateCreated;
- }
-
- if (string.Equals(name, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.PremiereDate;
- }
-
- if (string.Equals(name, ItemSortBy.StartDate, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.StartDate;
- }
-
- if (string.Equals(name, ItemSortBy.Name, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.Name;
- }
-
- if (string.Equals(name, ItemSortBy.CommunityRating, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.CommunityRating;
- }
-
- if (string.Equals(name, ItemSortBy.ProductionYear, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.ProductionYear;
- }
-
- if (string.Equals(name, ItemSortBy.CriticRating, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.CriticRating;
- }
-
- if (string.Equals(name, ItemSortBy.VideoBitRate, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.VideoBitRate;
- }
-
- if (string.Equals(name, ItemSortBy.ParentIndexNumber, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.ParentIndexNumber;
- }
-
- if (string.Equals(name, ItemSortBy.IndexNumber, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.IndexNumber;
- }
-
- if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.SimilarityScore;
- }
-
- if (string.Equals(name, ItemSortBy.SearchScore, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.SearchScore;
- }
-
- // Unknown SortBy, just sort by the SortName.
- return ItemSortBy.SortName;
+ return sortBy switch
+ {
+ ItemSortBy.AirTime => "SortName", // TODO
+ ItemSortBy.Runtime => "RuntimeTicks",
+ ItemSortBy.Random => "RANDOM()",
+ ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)",
+ ItemSortBy.DatePlayed => "LastPlayedDate",
+ ItemSortBy.PlayCount => "PlayCount",
+ ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )",
+ ItemSortBy.IsFolder => "IsFolder",
+ ItemSortBy.IsPlayed => "played",
+ ItemSortBy.IsUnplayed => "played",
+ ItemSortBy.DateLastContentAdded => "DateLastMediaAdded",
+ ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)",
+ ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)",
+ ItemSortBy.OfficialRating => "InheritedParentalRatingValue",
+ ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)",
+ ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
+ ItemSortBy.SeriesSortName => "SeriesName",
+ ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
+ ItemSortBy.Album => "Album",
+ ItemSortBy.DateCreated => "DateCreated",
+ ItemSortBy.PremiereDate => "PremiereDate",
+ ItemSortBy.StartDate => "StartDate",
+ ItemSortBy.Name => "Name",
+ ItemSortBy.CommunityRating => "CommunityRating",
+ ItemSortBy.ProductionYear => "ProductionYear",
+ ItemSortBy.CriticRating => "CriticRating",
+ ItemSortBy.VideoBitRate => "VideoBitRate",
+ ItemSortBy.ParentIndexNumber => "ParentIndexNumber",
+ ItemSortBy.IndexNumber => "IndexNumber",
+ ItemSortBy.SimilarityScore => "SimilarityScore",
+ ItemSortBy.SearchScore => "SearchScore",
+ _ => "SortName"
+ };
}
public List GetItemIdsList(InternalItemsQuery query)
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
index 539d4a63a..04d90af3c 100644
--- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Images
Recursive = true,
DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary },
- OrderBy = new (string, SortOrder)[]
+ OrderBy = new (ItemSortBy, SortOrder)[]
{
(ItemSortBy.IsFolder, SortOrder.Ascending),
(ItemSortBy.SortName, SortOrder.Ascending)
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 4f0983564..5c76e77be 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1678,7 +1678,7 @@ namespace Emby.Server.Implementations.Library
/// The sort by.
/// The sort order.
/// IEnumerable{BaseItem}.
- public IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder)
+ public IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder)
{
var isFirst = true;
@@ -1701,7 +1701,7 @@ namespace Emby.Server.Implementations.Library
return orderedItems ?? items;
}
- public IEnumerable Sort(IEnumerable items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy)
+ public IEnumerable Sort(IEnumerable items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
{
var isFirst = true;
@@ -1736,9 +1736,9 @@ namespace Emby.Server.Implementations.Library
/// The name.
/// The user.
/// IBaseItemComparer.
- private IBaseItemComparer GetComparer(string name, User user)
+ private IBaseItemComparer GetComparer(ItemSortBy name, User user)
{
- var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase));
+ var comparer = Comparers.FirstOrDefault(c => name == c.Type);
// If it requires a user, create a new one, and assign the user
if (comparer is IUserBaseItemComparer)
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index ee039ff0f..dd427c736 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -207,7 +207,7 @@ namespace Emby.Server.Implementations.LiveTv
orderBy.Insert(0, (ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
}
- if (!internalQuery.OrderBy.Any(i => string.Equals(i.OrderBy, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)))
+ if (internalQuery.OrderBy.All(i => i.OrderBy != ItemSortBy.SortName))
{
orderBy.Add((ItemSortBy.SortName, SortOrder.Ascending));
}
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 964004ecc..6d13c6d57 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Sorting;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.AiredEpisodeOrder;
+ public ItemSortBy Type => ItemSortBy.AiredEpisodeOrder;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index 67a9fbd3b..65c8599e7 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
@@ -16,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.AlbumArtist;
+ public ItemSortBy Type => ItemSortBy.AlbumArtist;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
index 4bed0fca1..e07113655 100644
--- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
@@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.Album;
+ public ItemSortBy Type => ItemSortBy.Album;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
index a8bb55e2b..f99977e5c 100644
--- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
@@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting
public class ArtistComparer : IBaseItemComparer
{
///
- public string Name => ItemSortBy.Artist;
+ public ItemSortBy Type => ItemSortBy.Artist;
///
public int Compare(BaseItem? x, BaseItem? y)
diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
index 5cb11ab46..9e02ea2ae 100644
--- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.CommunityRating;
+ public ItemSortBy Type => ItemSortBy.CommunityRating;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
index ba1835e4f..d4a8d4689 100644
--- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.CriticRating;
+ public ItemSortBy Type => ItemSortBy.CriticRating;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
index 6133aaccc..b86b4432f 100644
--- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.DateCreated;
+ public ItemSortBy Type => ItemSortBy.DateCreated;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index b1cb123ce..e1c26d012 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -3,6 +3,7 @@
using System;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -34,7 +35,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.DateLastContentAdded;
+ public ItemSortBy Type => ItemSortBy.DateLastContentAdded;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 453d817c7..d668c17bf 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -2,6 +2,7 @@
using System;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -36,7 +37,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.DatePlayed;
+ public ItemSortBy Type => ItemSortBy.DatePlayed;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
index 1bcaccd8a..11cad6256 100644
--- a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.IndexNumber;
+ public ItemSortBy Type => ItemSortBy.IndexNumber;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
index 73e628cf7..622a341b6 100644
--- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.IsFavoriteOrLiked;
+ public ItemSortBy Type => ItemSortBy.IsFavoriteOrLiked;
///
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
index 3c5ddeefa..6f0ca59c5 100644
--- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.IsFolder;
+ public ItemSortBy Type => ItemSortBy.IsFolder;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
index 7d77a8bc5..2a3e456c2 100644
--- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.IsUnplayed;
+ public ItemSortBy Type => ItemSortBy.IsUnplayed;
///
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
index 926835f90..afd8ccf9f 100644
--- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.IsUnplayed;
+ public ItemSortBy Type => ItemSortBy.IsUnplayed;
///
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
index 93bec4db9..72d9c7973 100644
--- a/Emby.Server.Implementations/Sorting/NameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.Name;
+ public ItemSortBy Type => ItemSortBy.Name;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
index ce44f99a6..b4ee2c723 100644
--- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Globalization;
@@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.OfficialRating;
+ public ItemSortBy Type => ItemSortBy.OfficialRating;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
index c54750843..5aeac29be 100644
--- a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.ParentIndexNumber;
+ public ItemSortBy Type => ItemSortBy.ParentIndexNumber;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index 16f1b79b3..12f88bf4d 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -1,6 +1,7 @@
#nullable disable
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -23,7 +24,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.PlayCount;
+ public ItemSortBy Type => ItemSortBy.PlayCount;
///
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
index db86b8002..8c8b8824f 100644
--- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.PremiereDate;
+ public ItemSortBy Type => ItemSortBy.PremiereDate;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
index 7fd1e024d..9aec87f18 100644
--- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.ProductionYear;
+ public ItemSortBy Type => ItemSortBy.ProductionYear;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs
index bf0168222..6f8ea5b74 100644
--- a/Emby.Server.Implementations/Sorting/RandomComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.Random;
+ public ItemSortBy Type => ItemSortBy.Random;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index 753e58324..3c096ab02 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.Runtime;
+ public ItemSortBy Type => ItemSortBy.Runtime;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
index 5b6c64f63..ed42fd6d5 100644
--- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.SeriesSortName;
+ public ItemSortBy Type => ItemSortBy.SeriesSortName;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index 19abafe19..314c25d12 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.SortName;
+ public ItemSortBy Type => ItemSortBy.SortName;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
index 2759d20de..e0b438ef1 100644
--- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Sorting;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.StartDate;
+ public ItemSortBy Type => ItemSortBy.StartDate;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 89d10f3d2..0edffb783 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.Studio;
+ public ItemSortBy Type => ItemSortBy.Studio;
///
/// Compares the specified x.
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index c9d2f67f9..c3c43982b 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -113,7 +113,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
@@ -317,7 +317,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index 11c4ac376..fdc16ee23 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -122,7 +122,7 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index da60f2c60..51f04fa27 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -85,7 +85,7 @@ public class GenresController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 80128536d..891cf88a7 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -197,7 +197,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -654,7 +654,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 649397d68..58159406a 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -143,7 +143,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true)
@@ -547,7 +547,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isSports,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 435457af6..94c899357 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -85,7 +85,7 @@ public class MusicGenresController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index b5b640620..1246efd49 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -162,7 +162,7 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index af403fa80..55a30d469 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -219,7 +219,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
- [FromQuery] string? sortBy)
+ [FromQuery] ItemSortBy? sortBy)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
@@ -289,7 +289,7 @@ public class TvShowsController : BaseJellyfinApiController
episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
}
- if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
+ if (sortBy == ItemSortBy.Random)
{
episodes.Shuffle();
}
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index 74370db50..641cdd81e 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -77,7 +77,7 @@ public class YearsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index bc12ca388..be3d4dfb6 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -30,14 +30,14 @@ public static class RequestHelpers
/// Sort By. Comma delimited string.
/// Sort Order. Comma delimited string.
/// Order By.
- public static (string, SortOrder)[] GetOrderBy(IReadOnlyList sortBy, IReadOnlyList requestedSortOrder)
+ public static (ItemSortBy, SortOrder)[] GetOrderBy(IReadOnlyList sortBy, IReadOnlyList requestedSortOrder)
{
if (sortBy.Count == 0)
{
- return Array.Empty<(string, SortOrder)>();
+ return Array.Empty<(ItemSortBy, SortOrder)>();
}
- var result = new (string, SortOrder)[sortBy.Count];
+ var result = new (ItemSortBy, SortOrder)[sortBy.Count];
var i = 0;
// Add elements which have a SortOrder specified
for (; i < requestedSortOrder.Count; i++)
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
index 5e7dd689e..6a30de5e6 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -107,7 +107,7 @@ public class GetProgramsDto
/// Optional.
///
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList SortBy { get; set; } = Array.Empty();
+ public IReadOnlyList SortBy { get; set; } = Array.Empty();
///
/// Gets or sets sort Order - Ascending,Descending.
diff --git a/Jellyfin.Data/Enums/ItemSortBy.cs b/Jellyfin.Data/Enums/ItemSortBy.cs
new file mode 100644
index 000000000..17bf1166d
--- /dev/null
+++ b/Jellyfin.Data/Enums/ItemSortBy.cs
@@ -0,0 +1,167 @@
+namespace Jellyfin.Data.Enums;
+
+///
+/// These represent sort orders.
+///
+public enum ItemSortBy
+{
+ ///
+ /// Default sort order.
+ ///
+ Default = 0,
+
+ ///
+ /// The aired episode order.
+ ///
+ AiredEpisodeOrder = 1,
+
+ ///
+ /// The album.
+ ///
+ Album = 2,
+
+ ///
+ /// The album artist.
+ ///
+ AlbumArtist = 3,
+
+ ///
+ /// The artist.
+ ///
+ Artist = 4,
+
+ ///
+ /// The date created.
+ ///
+ DateCreated = 5,
+
+ ///
+ /// The official rating.
+ ///
+ OfficialRating = 6,
+
+ ///
+ /// The date played.
+ ///
+ DatePlayed = 7,
+
+ ///
+ /// The premiere date.
+ ///
+ PremiereDate = 8,
+
+ ///
+ /// The start date.
+ ///
+ StartDate = 9,
+
+ ///
+ /// The sort name.
+ ///
+ SortName = 10,
+
+ ///
+ /// The name.
+ ///
+ Name = 11,
+
+ ///
+ /// The random.
+ ///
+ Random = 12,
+
+ ///
+ /// The runtime.
+ ///
+ Runtime = 13,
+
+ ///
+ /// The community rating.
+ ///
+ CommunityRating = 14,
+
+ ///
+ /// The production year.
+ ///
+ ProductionYear = 15,
+
+ ///
+ /// The play count.
+ ///
+ PlayCount = 16,
+
+ ///
+ /// The critic rating.
+ ///
+ CriticRating = 17,
+
+ ///
+ /// The IsFolder boolean.
+ ///
+ IsFolder = 18,
+
+ ///
+ /// The IsUnplayed boolean.
+ ///
+ IsUnplayed = 19,
+
+ ///
+ /// The IsPlayed boolean.
+ ///
+ IsPlayed = 20,
+
+ ///
+ /// The series sort.
+ ///
+ SeriesSortName = 21,
+
+ ///
+ /// The video bitrate.
+ ///
+ VideoBitRate = 22,
+
+ ///
+ /// The air time.
+ ///
+ AirTime = 23,
+
+ ///
+ /// The studio.
+ ///
+ Studio = 24,
+
+ ///
+ /// The IsFavouriteOrLiked boolean.
+ ///
+ IsFavoriteOrLiked = 25,
+
+ ///
+ /// The last content added date.
+ ///
+ DateLastContentAdded = 26,
+
+ ///
+ /// The series last played date.
+ ///
+ SeriesDatePlayed = 27,
+
+ ///
+ /// The parent index number.
+ ///
+ ParentIndexNumber = 28,
+
+ ///
+ /// The index number.
+ ///
+ IndexNumber = 29,
+
+ ///
+ /// The similarity score.
+ ///
+ SimilarityScore = 30,
+
+ ///
+ /// The search score.
+ ///
+ SearchScore = 31,
+}
diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
index 14459624e..7a73f3eaf 100644
--- a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
+++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
@@ -1,5 +1,7 @@
#nullable disable
+using Jellyfin.Data.Enums;
+
namespace MediaBrowser.Controller.Entities
{
///
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index a51299284..fb50a4546 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -39,7 +39,7 @@ namespace MediaBrowser.Controller.Entities
MediaTypes = Array.Empty();
MinSimilarityScore = 20;
OfficialRatings = Array.Empty();
- OrderBy = Array.Empty<(string, SortOrder)>();
+ OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
PersonIds = Array.Empty();
PersonTypes = Array.Empty();
PresetViews = Array.Empty();
@@ -284,7 +284,7 @@ namespace MediaBrowser.Controller.Entities
public bool? HasChapterImages { get; set; }
- public IReadOnlyList<(string OrderBy, SortOrder SortOrder)> OrderBy { get; set; }
+ public IReadOnlyList<(ItemSortBy OrderBy, SortOrder SortOrder)> OrderBy { get; set; }
public DateTime? MinDateCreated { get; set; }
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 66210cb6c..d7ccfd8ae 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Controller.Entities.Movies
{
public BoxSet()
{
- DisplayOrder = ItemSortBy.PremiereDate;
+ DisplayOrder = "PremiereDate";
}
[JsonIgnore]
@@ -116,13 +116,13 @@ namespace MediaBrowser.Controller.Entities.Movies
{
var children = base.GetChildren(user, includeLinkedChildren, query);
- if (string.Equals(DisplayOrder, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(DisplayOrder, "SortName", StringComparison.OrdinalIgnoreCase))
{
// Sort by name
return LibraryManager.Sort(children, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
}
- if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase))
{
// Sort by release date
return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
@@ -136,7 +136,7 @@ namespace MediaBrowser.Controller.Entities.Movies
{
var children = base.GetRecursiveChildren(user, query);
- if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(DisplayOrder, "PremiereDate", StringComparison.OrdinalIgnoreCase))
{
// Sort by release date
return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index f34e3d68d..52d546a4f 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -199,9 +199,9 @@ namespace MediaBrowser.Controller.Library
/// The sort by.
/// The sort order.
/// IEnumerable{BaseItem}.
- IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder);
+ IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder);
- IEnumerable Sort(IEnumerable items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy);
+ IEnumerable Sort(IEnumerable items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy);
///
/// Gets the user root folder.
diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
index c4ad352a3..2f7ebb5cc 100644
--- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs
+++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Controller.Providers
{
diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
index 07fe1ea8a..96f8a2af5 100644
--- a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Sorting
@@ -9,9 +10,8 @@ namespace MediaBrowser.Controller.Sorting
public interface IBaseItemComparer : IComparer
{
///
- /// Gets the name.
+ /// Gets the comparer type.
///
- /// The name.
- string Name { get; }
+ ItemSortBy Type { get; }
}
}
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index f913b2320..5a7193079 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -6,6 +6,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
index 673d97a9e..d872572b7 100644
--- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
+++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
@@ -14,7 +14,7 @@ namespace MediaBrowser.Model.LiveTv
public LiveTvChannelQuery()
{
EnableUserData = true;
- SortBy = Array.Empty();
+ SortBy = Array.Empty();
}
///
@@ -99,7 +99,7 @@ namespace MediaBrowser.Model.LiveTv
public bool? IsSeries { get; set; }
- public string[] SortBy { get; set; }
+ public ItemSortBy[] SortBy { get; set; }
///
/// Gets or sets the sort order to return results with.
diff --git a/MediaBrowser.Model/Querying/ItemSortBy.cs b/MediaBrowser.Model/Querying/ItemSortBy.cs
deleted file mode 100644
index 1a7c9a63b..000000000
--- a/MediaBrowser.Model/Querying/ItemSortBy.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-namespace MediaBrowser.Model.Querying
-{
- ///
- /// These represent sort orders that are known by the core.
- ///
- public static class ItemSortBy
- {
- ///
- /// The aired episode order.
- ///
- public const string AiredEpisodeOrder = "AiredEpisodeOrder";
-
- ///
- /// The album.
- ///
- public const string Album = "Album";
-
- ///
- /// The album artist.
- ///
- public const string AlbumArtist = "AlbumArtist";
-
- ///
- /// The artist.
- ///
- public const string Artist = "Artist";
-
- ///
- /// The date created.
- ///
- public const string DateCreated = "DateCreated";
-
- ///
- /// The official rating.
- ///
- public const string OfficialRating = "OfficialRating";
-
- ///
- /// The date played.
- ///
- public const string DatePlayed = "DatePlayed";
-
- ///
- /// The premiere date.
- ///
- public const string PremiereDate = "PremiereDate";
-
- ///
- /// The start date.
- ///
- public const string StartDate = "StartDate";
-
- ///
- /// The sort name.
- ///
- public const string SortName = "SortName";
-
- ///
- /// The name.
- ///
- public const string Name = "Name";
-
- ///
- /// The random.
- ///
- public const string Random = "Random";
-
- ///
- /// The runtime.
- ///
- public const string Runtime = "Runtime";
-
- ///
- /// The community rating.
- ///
- public const string CommunityRating = "CommunityRating";
-
- ///
- /// The production year.
- ///
- public const string ProductionYear = "ProductionYear";
-
- ///
- /// The play count.
- ///
- public const string PlayCount = "PlayCount";
-
- ///
- /// The critic rating.
- ///
- public const string CriticRating = "CriticRating";
-
- ///
- /// The IsFolder boolean.
- ///
- public const string IsFolder = "IsFolder";
-
- ///
- /// The IsUnplayed boolean.
- ///
- public const string IsUnplayed = "IsUnplayed";
-
- ///
- /// The IsPlayed boolean.
- ///
- public const string IsPlayed = "IsPlayed";
-
- ///
- /// The series sort.
- ///
- public const string SeriesSortName = "SeriesSortName";
-
- ///
- /// The video bitrate.
- ///
- public const string VideoBitRate = "VideoBitRate";
-
- ///
- /// The air time.
- ///
- public const string AirTime = "AirTime";
-
- ///
- /// The studio.
- ///
- public const string Studio = "Studio";
-
- ///
- /// The IsFavouriteOrLiked boolean.
- ///
- public const string IsFavoriteOrLiked = "IsFavoriteOrLiked";
-
- ///
- /// The last content added date.
- ///
- public const string DateLastContentAdded = "DateLastContentAdded";
-
- ///
- /// The series last played date.
- ///
- public const string SeriesDatePlayed = "SeriesDatePlayed";
-
- ///
- /// The parent index number.
- ///
- public const string ParentIndexNumber = "ParentIndexNumber";
-
- ///
- /// The index number.
- ///
- public const string IndexNumber = "IndexNumber";
-
- ///
- /// The similarity score.
- ///
- public const string SimilarityScore = "SimilarityScore";
-
- ///
- /// The search score.
- ///
- public const string SearchScore = "SearchScore";
- }
-}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index e336c8825..06445c90d 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index 500ebaf71..72e59c9ac 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
index 2d7741d81..a2d1b3607 100644
--- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
+++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
@@ -14,7 +14,7 @@ namespace Jellyfin.Api.Tests.Helpers
{
[Theory]
[MemberData(nameof(GetOrderBy_Success_TestData))]
- public static void GetOrderBy_Success(IReadOnlyList sortBy, IReadOnlyList requestedSortOrder, (string, SortOrder)[] expected)
+ public static void GetOrderBy_Success(IReadOnlyList sortBy, IReadOnlyList requestedSortOrder, (ItemSortBy, SortOrder)[] expected)
{
Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder));
}
@@ -95,42 +95,42 @@ namespace Jellyfin.Api.Tests.Helpers
Assert.Throws(() => RequestHelpers.GetUserId(principal, requestUserId));
}
- public static TheoryData, IReadOnlyList, (string, SortOrder)[]> GetOrderBy_Success_TestData()
+ public static TheoryData, IReadOnlyList, (ItemSortBy, SortOrder)[]> GetOrderBy_Success_TestData()
{
- var data = new TheoryData, IReadOnlyList, (string, SortOrder)[]>();
+ var data = new TheoryData, IReadOnlyList, (ItemSortBy, SortOrder)[]>();
data.Add(
- Array.Empty(),
+ Array.Empty(),
Array.Empty