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/Configuration')
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/Configuration')
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/Configuration')
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/Configuration')
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/Configuration')
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 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/Configuration')
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/Configuration')
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/Configuration')
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/Configuration')
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 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/Configuration')
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/Configuration')
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 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/Configuration')
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