Merge remote-tracking branch 'origin/master' into feature/mount_markerfiles

pull/12832/head
JPVenson 4 weeks ago
commit 126ae3b651

@ -1,43 +1,35 @@
using System;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
namespace Emby.Naming.TV
{
/// <summary>
/// Class to parse season paths.
/// </summary>
public static class SeasonPathParser
public static partial class SeasonPathParser
{
/// <summary>
/// A season folder must contain one of these somewhere in the name.
/// </summary>
private static readonly string[] _seasonFolderNames =
{
"season",
"sæson",
"temporada",
"saison",
"staffel",
"series",
"сезон",
"stagione"
};
private static readonly char[] _splitChars = ['.', '_', ' ', '-'];
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$")]
private static partial Regex ProcessPre();
[GeneratedRegex(@"^\s*(?:[[시즌]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$")]
private static partial Regex ProcessPost();
/// <summary>
/// Attempts to parse season number from path.
/// </summary>
/// <param name="path">Path to season.</param>
/// <param name="parentPath">Folder name of the parent.</param>
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders)
{
var result = new SeasonPathParserResult();
var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders);
result.SeasonNumber = seasonNumber;
@ -54,15 +46,24 @@ namespace Emby.Naming.TV
/// Gets the season number from path.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="parentFolderName">The parent folder name.</param>
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
/// <returns>System.Nullable{System.Int32}.</returns>
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
string path,
string? parentFolderName,
bool supportSpecialAliases,
bool supportNumericSeasonFolders)
{
string filename = Path.GetFileName(path);
filename = Regex.Replace(filename, "[ ._-]", string.Empty);
if (parentFolderName is not null)
{
parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty);
filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase);
}
if (supportSpecialAliases)
{
@ -85,53 +86,38 @@ namespace Emby.Naming.TV
}
}
if (TryGetSeasonNumberFromPart(filename, out int seasonNumber))
if (filename.StartsWith('s'))
{
return (seasonNumber, true);
}
var testFilename = filename.AsSpan()[1..];
// Look for one of the season folder names
foreach (var name in _seasonFolderNames)
{
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
if (result.SeasonNumber.HasValue)
{
return result;
}
break;
return (val, true);
}
}
var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
var preMatch = ProcessPre().Match(filename);
if (preMatch.Success)
{
if (TryGetSeasonNumberFromPart(part, out seasonNumber))
{
return (seasonNumber, true);
}
return CheckMatch(preMatch);
}
return (null, true);
}
private static bool TryGetSeasonNumberFromPart(ReadOnlySpan<char> part, out int seasonNumber)
{
seasonNumber = 0;
if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
else
{
return false;
var postMatch = ProcessPost().Match(filename);
return CheckMatch(postMatch);
}
}
if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
{
var numberString = match.Groups["seasonnumber"];
if (numberString.Success)
{
seasonNumber = value;
return true;
var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture);
return (seasonNumber, true);
}
return false;
return (null, false);
}
/// <summary>

@ -5,80 +5,80 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Trickplay;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data
namespace Emby.Server.Implementations.Data;
public class CleanDatabaseScheduledTask : ILibraryPostScanTask
{
public class CleanDatabaseScheduledTask : ILibraryPostScanTask
private readonly ILibraryManager _libraryManager;
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
public CleanDatabaseScheduledTask(
ILibraryManager libraryManager,
ILogger<CleanDatabaseScheduledTask> logger,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
_libraryManager = libraryManager;
_logger = logger;
_dbProvider = dbProvider;
}
public CleanDatabaseScheduledTask(
ILibraryManager libraryManager,
ILogger<CleanDatabaseScheduledTask> logger,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
_libraryManager = libraryManager;
_logger = logger;
_dbProvider = dbProvider;
}
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
}
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
{
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
}
HasDeadParentId = true
});
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
{
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
HasDeadParentId = true
});
var numComplete = 0;
var numItems = itemIds.Count + 1;
var numComplete = 0;
var numItems = itemIds.Count + 1;
_logger.LogDebug("Cleaning {Number} items with dead parent links", numItems);
_logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
foreach (var itemId in itemIds)
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var itemId in itemIds)
var item = _libraryManager.GetItemById(itemId);
if (item is not null)
{
cancellationToken.ThrowIfCancellationRequested();
var item = _libraryManager.GetItemById(itemId);
_logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
if (item is not null)
_libraryManager.DeleteItem(item, new DeleteOptions
{
_logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
_libraryManager.DeleteItem(item, new DeleteOptions
{
DeleteFileLocation = false
});
}
numComplete++;
double percent = numComplete;
percent /= numItems;
progress.Report(percent * 100);
DeleteFileLocation = false
});
}
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
numComplete++;
double percent = numComplete;
percent /= numItems;
progress.Report(percent * 100);
}
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
progress.Report(100);
}
progress.Report(100);
}
}

@ -78,6 +78,7 @@ namespace Emby.Server.Implementations.Library
private readonly NamingOptions _namingOptions;
private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager;
/// <summary>
/// The _root folder sync lock.
@ -113,7 +114,8 @@ namespace Emby.Server.Implementations.Library
/// <param name="imageProcessor">The image processor.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
/// <param name="peopleRepository">The People Repository.</param>
/// <param name="peopleRepository">The people repository.</param>
/// <param name="pathManager">The path manager.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@ -130,7 +132,8 @@ namespace Emby.Server.Implementations.Library
IImageProcessor imageProcessor,
NamingOptions namingOptions,
IDirectoryService directoryService,
IPeopleRepository peopleRepository)
IPeopleRepository peopleRepository,
IPathManager pathManager)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@ -148,6 +151,7 @@ namespace Emby.Server.Implementations.Library
_cache = new ConcurrentDictionary<Guid, BaseItem>();
_namingOptions = namingOptions;
_peopleRepository = peopleRepository;
_pathManager = pathManager;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@ -200,33 +204,33 @@ namespace Emby.Server.Implementations.Library
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>();
private ILibraryPostScanTask[] PostscanTasks { get; set; } = [];
/// <summary>
/// Gets or sets the intro providers.
/// </summary>
/// <value>The intro providers.</value>
private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>();
private IIntroProvider[] IntroProviders { get; set; } = [];
/// <summary>
/// Gets or sets the list of entity resolution ignore rules.
/// </summary>
/// <value>The entity resolution ignore rules.</value>
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>();
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
/// <summary>
/// Gets or sets the list of currently registered entity resolvers.
/// </summary>
/// <value>The entity resolvers enumerable.</value>
private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>();
private IItemResolver[] EntityResolvers { get; set; } = [];
private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>();
private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
/// <summary>
/// Gets or sets the comparers.
/// </summary>
/// <value>The comparers.</value>
private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>();
private IBaseItemComparer[] Comparers { get; set; } = [];
public bool IsScanRunning { get; private set; }
@ -359,7 +363,7 @@ namespace Emby.Server.Implementations.Library
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
: Array.Empty<BaseItem>();
: [];
foreach (var metadataPath in GetMetadataPaths(item, children))
{
@ -465,14 +469,28 @@ namespace Emby.Server.Implementations.Library
ReportItemRemoved(item, parent);
}
private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
{
var list = GetInternalMetadataPaths(item);
foreach (var child in children)
{
list.AddRange(GetInternalMetadataPaths(child));
}
return list;
}
private List<string> GetInternalMetadataPaths(BaseItem item)
{
var list = new List<string>
{
item.GetInternalMetadataPath()
};
list.AddRange(children.Select(i => i.GetInternalMetadataPath()));
if (item is Video video)
{
list.Add(_pathManager.GetTrickplayDirectory(video));
}
return list;
}
@ -593,7 +611,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf);
files = Array.Empty<FileSystemMetadata>();
files = [];
}
else
{
@ -1461,7 +1479,7 @@ namespace Emby.Server.Implementations.Library
// Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
query.AncestorIds = Array.Empty<Guid>();
query.AncestorIds = [];
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
@ -1581,7 +1599,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user);
}
return Array.Empty<Guid>();
return [];
}
if (!view.ParentId.IsEmpty())
@ -1592,7 +1610,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user);
}
return Array.Empty<Guid>();
return [];
}
// Handle grouping
@ -1607,7 +1625,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => GetTopParentIdsForQuery(i, user));
}
return Array.Empty<Guid>();
return [];
}
if (item is CollectionFolder collectionFolder)
@ -1621,7 +1639,7 @@ namespace Emby.Server.Implementations.Library
return new[] { topParent.Id };
}
return Array.Empty<Guid>();
return [];
}
/// <summary>
@ -1665,7 +1683,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error getting intros");
return Enumerable.Empty<IntroInfo>();
return [];
}
}
@ -2492,8 +2510,11 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
public int? GetSeasonNumberFromPath(string path)
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
public int? GetSeasonNumberFromPath(string path, Guid? parentId)
{
var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
}
/// <inheritdoc />
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
@ -2892,7 +2913,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values?
await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
await File.WriteAllBytesAsync(path, []).ConfigureAwait(false);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);

@ -0,0 +1,36 @@
using System.Globalization;
using System.IO;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
namespace Emby.Server.Implementations.Library;
/// <summary>
/// IPathManager implementation.
/// </summary>
public class PathManager : IPathManager
{
private readonly IServerConfigurationManager _config;
/// <summary>
/// Initializes a new instance of the <see cref="PathManager"/> class.
/// </summary>
/// <param name="config">The server configuration manager.</param>
public PathManager(
IServerConfigurationManager config)
{
_config = config;
}
/// <inheritdoc />
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
{
var basePath = _config.ApplicationPaths.TrickplayPath;
var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
return saveWithMedia
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
: Path.Combine(basePath, idString);
}
}

@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var path = args.Path;
var seasonParserResult = SeasonPathParser.Parse(path, true, true);
var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true);
var season = new Season
{

@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (child.IsDirectory)
{
if (IsSeasonFolder(child.FullName, isTvContentType))
if (IsSeasonFolder(child.FullName, path, isTvContentType))
{
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true;
@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// Determines whether [is season folder] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
/// <param name="parentPath">The parentpath.</param>
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
private static bool IsSeasonFolder(string path, bool isTvContentType)
private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType)
{
var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber;
return seasonNumber.HasValue;
}

@ -336,7 +336,7 @@
"TwoLetterISORegionName": "IE"
},
{
"DisplayName": "Islamic Republic of Pakistan",
"DisplayName": "Pakistan",
"Name": "PK",
"ThreeLetterISORegionName": "PAK",
"TwoLetterISORegionName": "PK"

@ -101,16 +101,23 @@ public sealed class BaseItemRepository
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete();
context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.SaveChanges();
transaction.Commit();
}

@ -12,6 +12,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Trickplay;
@ -37,9 +38,10 @@ public class TrickplayManager : ITrickplayManager
private readonly IImageEncoder _imageEncoder;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IApplicationPaths _appPaths;
private readonly IPathManager _pathManager;
private static readonly AsyncNonKeyedLocker _resourcePool = new(1);
private static readonly string[] _trickplayImgExtensions = { ".jpg" };
private static readonly string[] _trickplayImgExtensions = [".jpg"];
/// <summary>
/// Initializes a new instance of the <see cref="TrickplayManager"/> class.
@ -53,6 +55,7 @@ public class TrickplayManager : ITrickplayManager
/// <param name="imageEncoder">The image encoder.</param>
/// <param name="dbProvider">The database provider.</param>
/// <param name="appPaths">The application paths.</param>
/// <param name="pathManager">The path manager.</param>
public TrickplayManager(
ILogger<TrickplayManager> logger,
IMediaEncoder mediaEncoder,
@ -62,7 +65,8 @@ public class TrickplayManager : ITrickplayManager
IServerConfigurationManager config,
IImageEncoder imageEncoder,
IDbContextFactory<JellyfinDbContext> dbProvider,
IApplicationPaths appPaths)
IApplicationPaths appPaths,
IPathManager pathManager)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
@ -73,6 +77,7 @@ public class TrickplayManager : ITrickplayManager
_imageEncoder = imageEncoder;
_dbProvider = dbProvider;
_appPaths = appPaths;
_pathManager = pathManager;
}
/// <inheritdoc />
@ -610,12 +615,7 @@ public class TrickplayManager : ITrickplayManager
/// <inheritdoc />
public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
{
var basePath = _config.ApplicationPaths.TrickplayPath;
var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
var path = saveWithMedia
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
: Path.Combine(basePath, idString);
var path = _pathManager.GetTrickplayDirectory(item, saveWithMedia);
var subdirectory = string.Format(
CultureInfo.InvariantCulture,
"{0} - {1}x{2}",

@ -10,7 +10,9 @@ using Emby.Server.Implementations;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Data.Sqlite;
@ -43,6 +45,9 @@ namespace Jellyfin.Server
public const string LoggingConfigFileSystem = "logging.json";
private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory();
private static SetupServer _setupServer = new();
private static CoreAppHost? _appHost;
private static IHost? _jellyfinHost = null;
private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
@ -70,6 +75,7 @@ namespace Jellyfin.Server
_startTimestamp = Stopwatch.GetTimestamp();
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
appPaths.MakeSanityCheckOrThrow();
await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
@ -124,22 +130,23 @@ namespace Jellyfin.Server
if (_restartOnShutdown)
{
_startTimestamp = Stopwatch.GetTimestamp();
_setupServer = new SetupServer();
await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false);
}
} while (_restartOnShutdown);
}
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
{
using var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
startupConfig);
IHost? host = null;
using CoreAppHost appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
startupConfig);
_appHost = appHost;
try
{
host = Host.CreateDefaultBuilder()
_jellyfinHost = Host.CreateDefaultBuilder()
.UseConsoleLifetime()
.ConfigureServices(services => appHost.Init(services))
.ConfigureWebHostDefaults(webHostBuilder =>
@ -156,14 +163,17 @@ namespace Jellyfin.Server
.Build();
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
appHost.ServiceProvider = host.Services;
appHost.ServiceProvider = _jellyfinHost.Services;
await appHost.InitializeServices().ConfigureAwait(false);
Migrations.MigrationRunner.Run(appHost, _loggerFactory);
try
{
await host.StartAsync().ConfigureAwait(false);
await _setupServer.StopAsync().ConfigureAwait(false);
_setupServer.Dispose();
_setupServer = null!;
await _jellyfinHost.StartAsync().ConfigureAwait(false);
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{
@ -182,7 +192,7 @@ namespace Jellyfin.Server
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
await host.WaitForShutdownAsync().ConfigureAwait(false);
await _jellyfinHost.WaitForShutdownAsync().ConfigureAwait(false);
_restartOnShutdown = appHost.ShouldRestart;
}
catch (Exception ex)
@ -214,7 +224,8 @@ namespace Jellyfin.Server
}
}
host?.Dispose();
_appHost = null;
_jellyfinHost?.Dispose();
}
}

@ -0,0 +1,172 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
namespace Jellyfin.Server.ServerSetupApp;
/// <summary>
/// Creates a fake application pipeline that will only exist for as long as the main app is not started.
/// </summary>
public sealed class SetupServer : IDisposable
{
private IHost? _startupServer;
private bool _disposed;
/// <summary>
/// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup.
/// </summary>
/// <param name="networkManagerFactory">The networkmanager.</param>
/// <param name="applicationPaths">The application paths.</param>
/// <param name="serverApplicationHost">The servers application host.</param>
/// <returns>A Task.</returns>
public async Task RunAsync(
Func<INetworkManager?> networkManagerFactory,
IApplicationPaths applicationPaths,
Func<IServerApplicationHost?> serverApplicationHost)
{
ThrowIfDisposed();
_startupServer = Host.CreateDefaultBuilder()
.UseConsoleLifetime()
.ConfigureServices(serv =>
{
serv.AddHealthChecks()
.AddCheck<SetupHealthcheck>("StartupCheck");
})
.ConfigureWebHostDefaults(webHostBuilder =>
{
webHostBuilder
.UseKestrel()
.Configure(app =>
{
app.UseHealthChecks("/health");
app.Map("/startup/logger", loggerRoute =>
{
loggerRoute.Run(async context =>
{
var networkManager = networkManagerFactory();
if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress))
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
var logFilePath = new DirectoryInfo(applicationPaths.LogDirectoryPath)
.EnumerateFiles()
.OrderBy(f => f.CreationTimeUtc)
.FirstOrDefault()
?.FullName;
if (logFilePath is not null)
{
await context.Response.SendFileAsync(logFilePath, CancellationToken.None).ConfigureAwait(false);
}
});
});
app.Map("/System/Info/Public", systemRoute =>
{
systemRoute.Run(async context =>
{
var jfApplicationHost = serverApplicationHost();
var retryCounter = 0;
while (jfApplicationHost is null && retryCounter < 5)
{
await Task.Delay(500).ConfigureAwait(false);
jfApplicationHost = serverApplicationHost();
retryCounter++;
}
if (jfApplicationHost is null)
{
context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60");
return;
}
var sysInfo = new PublicSystemInfo
{
Version = jfApplicationHost.ApplicationVersionString,
ProductName = jfApplicationHost.Name,
Id = jfApplicationHost.SystemId,
ServerName = jfApplicationHost.FriendlyName,
LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request),
StartupWizardCompleted = false
};
await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false);
});
});
app.Run((context) =>
{
context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60");
context.Response.WriteAsync("<p>Jellyfin Server still starting. Please wait.</p>");
var networkManager = networkManagerFactory();
if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress))
{
context.Response.WriteAsync("<p>You can download the current logfiles <a href='/startup/logger'>here</a>.</p>");
}
return Task.CompletedTask;
});
});
})
.Build();
await _startupServer.StartAsync().ConfigureAwait(false);
}
/// <summary>
/// Stops the Setup server.
/// </summary>
/// <returns>A task. Duh.</returns>
public async Task StopAsync()
{
ThrowIfDisposed();
if (_startupServer is null)
{
throw new InvalidOperationException("Tried to stop a non existing startup server");
}
await _startupServer.StopAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_startupServer?.Dispose();
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
private class SetupHealthcheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up."));
}
}
}

@ -257,7 +257,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
{
IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path);
IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path, ParentId);
// If a change was made record it
if (IndexNumber.HasValue)

@ -0,0 +1,17 @@
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.IO;
/// <summary>
/// Interface ITrickplayManager.
/// </summary>
public interface IPathManager
{
/// <summary>
/// Gets the path to the trickplay image base folder.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param>
/// <returns>The absolute path.</returns>
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false);
}

@ -426,8 +426,9 @@ namespace MediaBrowser.Controller.Library
/// Gets the season number from path.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="parentId">The parent id.</param>
/// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
int? GetSeasonNumberFromPath(string path);
int? GetSeasonNumberFromPath(string path, Guid? parentId);
/// <summary>
/// Fills the missing episode numbers from path.

@ -6,32 +6,54 @@ namespace Jellyfin.Naming.Tests.TV;
public class SeasonPathParserTests
{
[Theory]
[InlineData("/Drive/Season 1", 1, true)]
[InlineData("/Drive/s1", 1, true)]
[InlineData("/Drive/S1", 1, true)]
[InlineData("/Drive/Season 2", 2, true)]
[InlineData("/Drive/Season 02", 2, true)]
[InlineData("/Drive/Seinfeld/S02", 2, true)]
[InlineData("/Drive/Seinfeld/2", 2, true)]
[InlineData("/Drive/Seinfeld - S02", 2, true)]
[InlineData("/Drive/Season 2009", 2009, true)]
[InlineData("/Drive/Season1", 1, true)]
[InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
[InlineData("/Drive/Season 7 (2016)", 7, false)]
[InlineData("/Drive/Staffel 7 (2016)", 7, false)]
[InlineData("/Drive/Stagione 7 (2016)", 7, false)]
[InlineData("/Drive/Season (8)", null, false)]
[InlineData("/Drive/3.Staffel", 3, false)]
[InlineData("/Drive/s06e05", null, false)]
[InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
[InlineData("/Drive/extras", 0, true)]
[InlineData("/Drive/specials", 0, true)]
public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
[InlineData("/Drive/Season 1", "/Drive", 1, true)]
[InlineData("/Drive/Staffel 1", "/Drive", 1, true)]
[InlineData("/Drive/Stagione 1", "/Drive", 1, true)]
[InlineData("/Drive/sæson 1", "/Drive", 1, true)]
[InlineData("/Drive/Temporada 1", "/Drive", 1, true)]
[InlineData("/Drive/series 1", "/Drive", 1, true)]
[InlineData("/Drive/Kausi 1", "/Drive", 1, true)]
[InlineData("/Drive/Säsong 1", "/Drive", 1, true)]
[InlineData("/Drive/Seizoen 1", "/Drive", 1, true)]
[InlineData("/Drive/Seasong 1", "/Drive", 1, true)]
[InlineData("/Drive/Sezon 1", "/Drive", 1, true)]
[InlineData("/Drive/sezona 1", "/Drive", 1, true)]
[InlineData("/Drive/sezóna 1", "/Drive", 1, true)]
[InlineData("/Drive/Sezonul 1", "/Drive", 1, true)]
[InlineData("/Drive/시즌 1", "/Drive", 1, true)]
[InlineData("/Drive/シーズン 1", "/Drive", 1, true)]
[InlineData("/Drive/сезон 1", "/Drive", 1, true)]
[InlineData("/Drive/Сезон 1", "/Drive", 1, true)]
[InlineData("/Drive/Season 10", "/Drive", 10, true)]
[InlineData("/Drive/Season 100", "/Drive", 100, true)]
[InlineData("/Drive/s1", "/Drive", 1, true)]
[InlineData("/Drive/S1", "/Drive", 1, true)]
[InlineData("/Drive/Season 2", "/Drive", 2, true)]
[InlineData("/Drive/Season 02", "/Drive", 2, true)]
[InlineData("/Drive/Seinfeld/S02", "/Seinfeld", 2, true)]
[InlineData("/Drive/Seinfeld/2", "/Seinfeld", 2, true)]
[InlineData("/Drive/Seinfeld Season 2", "/Drive", null, false)]
[InlineData("/Drive/Season 2009", "/Drive", 2009, true)]
[InlineData("/Drive/Season1", "/Drive", 1, true)]
[InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", "/The Wonder Years", 4, true)]
[InlineData("/Drive/Season 7 (2016)", "/Drive", 7, true)]
[InlineData("/Drive/Staffel 7 (2016)", "/Drive", 7, true)]
[InlineData("/Drive/Stagione 7 (2016)", "/Drive", 7, true)]
[InlineData("/Drive/Stargate SG-1/Season 1", "/Drive/Stargate SG-1", 1, true)]
[InlineData("/Drive/Stargate SG-1/Stargate SG-1 Season 1", "/Drive/Stargate SG-1", 1, true)]
[InlineData("/Drive/Season (8)", "/Drive", null, false)]
[InlineData("/Drive/3.Staffel", "/Drive", 3, true)]
[InlineData("/Drive/s06e05", "/Drive", null, false)]
[InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", "/Drive", null, false)]
[InlineData("/Drive/extras", "/Drive", 0, true)]
[InlineData("/Drive/specials", "/Drive", 0, true)]
[InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)]
public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory)
{
var result = SeasonPathParser.Parse(path, true, true);
var result = SeasonPathParser.Parse(path, parentPath, true, true);
Assert.Equal(result.SeasonNumber is not null, result.Success);
Assert.Equal(result.SeasonNumber, seasonNumber);
Assert.Equal(seasonNumber, result.SeasonNumber);
Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
}
}

Loading…
Cancel
Save