Merge pull request #21 from jellyfin/master

nightly
pull/2160/head
artiume 4 years ago committed by GitHub
commit 697aee5b0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -17,4 +17,16 @@
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@ -11,7 +10,6 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@ -23,7 +21,7 @@ namespace Emby.Drawing
/// <summary>
/// Class ImageProcessor.
/// </summary>
public class ImageProcessor : IImageProcessor, IDisposable
public sealed class ImageProcessor : IImageProcessor, IDisposable
{
// Increment this when there's a change requiring caches to be invalidated
private const string Version = "3";
@ -31,28 +29,24 @@ namespace Emby.Drawing
private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
/// <summary>
/// The _logger
/// </summary>
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths;
private IImageEncoder _imageEncoder;
private readonly IImageEncoder _imageEncoder;
private readonly Func<ILibraryManager> _libraryManager;
private readonly Func<IMediaEncoder> _mediaEncoder;
private readonly Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>();
private bool _disposed = false;
/// <summary>
///
/// Initializes a new instance of the <see cref="ImageProcessor"/> class.
/// </summary>
/// <param name="logger"></param>
/// <param name="appPaths"></param>
/// <param name="fileSystem"></param>
/// <param name="imageEncoder"></param>
/// <param name="libraryManager"></param>
/// <param name="mediaEncoder"></param>
/// <param name="logger">The logger.</param>
/// <param name="appPaths">The server application paths.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="imageEncoder">The image encoder.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
public ImageProcessor(
ILogger<ImageProcessor> logger,
IServerApplicationPaths appPaths,
@ -67,16 +61,10 @@ namespace Emby.Drawing
_libraryManager = libraryManager;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
ImageEnhancers = Array.Empty<IImageEnhancer>();
ImageHelper.ImageProcessor = this;
}
private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images");
/// <inheritdoc />
public IReadOnlyCollection<string> SupportedInputFormats =>
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
@ -89,9 +77,7 @@ namespace Emby.Drawing
"aiff",
"cr2",
"crw",
// Remove until supported
//"nef",
"nef",
"orf",
"pef",
"arw",
@ -110,19 +96,9 @@ namespace Emby.Drawing
"wbmp"
};
/// <inheritdoc />
public IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; }
/// <inheritdoc />
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
/// <inheritdoc />
public IImageEncoder ImageEncoder
{
get => _imageEncoder;
set => _imageEncoder = value ?? throw new ArgumentNullException(nameof(value));
}
/// <inheritdoc />
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
{
@ -150,6 +126,8 @@ namespace Emby.Drawing
throw new ArgumentNullException(nameof(options));
}
var libraryManager = _libraryManager();
ItemImageInfo originalImage = options.Image;
BaseItem item = options.Item;
@ -157,9 +135,10 @@ namespace Emby.Drawing
{
if (item == null)
{
item = _libraryManager().GetItemById(options.ItemId);
item = libraryManager.GetItemById(options.ItemId);
}
originalImage = await _libraryManager().ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false);
originalImage = await libraryManager.ConvertImageToLocal(item, originalImage, options.ImageIndex).ConfigureAwait(false);
}
string originalImagePath = originalImage.Path;
@ -186,27 +165,6 @@ namespace Emby.Drawing
dateModified = supportedImageInfo.dateModified;
bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
if (options.Enhancers.Count > 0)
{
if (item == null)
{
item = _libraryManager().GetItemById(options.ItemId);
}
var tuple = await GetEnhancedImage(new ItemImageInfo
{
DateModified = dateModified,
Type = originalImage.Type,
Path = originalImagePath
}, requiresTransparency, item, options.ImageIndex, options.Enhancers, CancellationToken.None).ConfigureAwait(false);
originalImagePath = tuple.path;
dateModified = tuple.dateModified;
requiresTransparency = tuple.transparent;
// TODO: Get this info
originalImageSize = null;
}
bool autoOrient = false;
ImageOrientation? orientation = null;
if (item is Photo photo)
@ -239,12 +197,6 @@ namespace Emby.Drawing
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
CheckDisposed();
LockInfo lockInfo = GetLock(cacheFilePath);
await lockInfo.Lock.WaitAsync().ConfigureAwait(false);
try
{
if (!File.Exists(cacheFilePath))
@ -270,10 +222,6 @@ namespace Emby.Drawing
_logger.LogError(ex, "Error encoding image");
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
finally
{
ReleaseLock(cacheFilePath, lockInfo);
}
}
private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
@ -305,20 +253,18 @@ namespace Emby.Drawing
}
private string GetMimeType(ImageFormat format, string path)
{
switch(format)
{
case ImageFormat.Bmp: return MimeTypes.GetMimeType("i.bmp");
case ImageFormat.Gif: return MimeTypes.GetMimeType("i.gif");
case ImageFormat.Jpg: return MimeTypes.GetMimeType("i.jpg");
case ImageFormat.Png: return MimeTypes.GetMimeType("i.png");
case ImageFormat.Webp: return MimeTypes.GetMimeType("i.webp");
default: return MimeTypes.GetMimeType(path);
}
}
=> format switch
{
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
_ => MimeTypes.GetMimeType(path)
};
/// <summary>
/// Gets the cache file path based on a set of parameters
/// Gets the cache file path based on a set of parameters.
/// </summary>
private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
{
@ -400,11 +346,7 @@ namespace Emby.Drawing
/// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
{
var supportedEnhancers = GetSupportedEnhancers(item, image.Type).ToArray();
return GetImageCacheTag(item, image, supportedEnhancers);
}
=> (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
/// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
@ -424,26 +366,6 @@ namespace Emby.Drawing
}
}
/// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers)
{
string originalImagePath = image.Path;
DateTime dateModified = image.DateModified;
ImageType imageType = image.Type;
// Optimization
if (imageEnhancers.Count == 0)
{
return (originalImagePath + dateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
// Cache name is created with supported enhancers combined with the last config change so we pick up new config changes
var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList();
cacheKeys.Add(originalImagePath + dateModified.Ticks);
return string.Join("|", cacheKeys).GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
{
var inputFormat = Path.GetExtension(originalImagePath)
@ -487,154 +409,6 @@ namespace Emby.Drawing
return (originalImagePath, dateModified);
}
/// <inheritdoc />
public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex)
{
var enhancers = GetSupportedEnhancers(item, imageType).ToArray();
ItemImageInfo imageInfo = item.GetImageInfo(imageType, imageIndex);
bool inputImageSupportsTransparency = SupportsTransparency(imageInfo.Path);
var result = await GetEnhancedImage(imageInfo, inputImageSupportsTransparency, item, imageIndex, enhancers, CancellationToken.None);
return result.path;
}
private async Task<(string path, DateTime dateModified, bool transparent)> GetEnhancedImage(
ItemImageInfo image,
bool inputImageSupportsTransparency,
BaseItem item,
int imageIndex,
IReadOnlyCollection<IImageEnhancer> enhancers,
CancellationToken cancellationToken)
{
var originalImagePath = image.Path;
var dateModified = image.DateModified;
var imageType = image.Type;
try
{
var cacheGuid = GetImageCacheTag(item, image, enhancers);
// Enhance if we have enhancers
var enhancedImageInfo = await GetEnhancedImageInternal(originalImagePath, item, imageType, imageIndex, enhancers, cacheGuid, cancellationToken).ConfigureAwait(false);
string enhancedImagePath = enhancedImageInfo.path;
// If the path changed update dateModified
if (!string.Equals(enhancedImagePath, originalImagePath, StringComparison.OrdinalIgnoreCase))
{
var treatmentRequiresTransparency = enhancedImageInfo.transparent;
return (enhancedImagePath, _fileSystem.GetLastWriteTimeUtc(enhancedImagePath), treatmentRequiresTransparency);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error enhancing image");
}
return (originalImagePath, dateModified, inputImageSupportsTransparency);
}
/// <summary>
/// Gets the enhanced image internal.
/// </summary>
/// <param name="originalImagePath">The original image path.</param>
/// <param name="item">The item.</param>
/// <param name="imageType">Type of the image.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <param name="supportedEnhancers">The supported enhancers.</param>
/// <param name="cacheGuid">The cache unique identifier.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;System.String&gt;.</returns>
/// <exception cref="ArgumentNullException">
/// originalImagePath
/// or
/// item
/// </exception>
private async Task<(string path, bool transparent)> GetEnhancedImageInternal(
string originalImagePath,
BaseItem item,
ImageType imageType,
int imageIndex,
IReadOnlyCollection<IImageEnhancer> supportedEnhancers,
string cacheGuid,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(originalImagePath))
{
throw new ArgumentNullException(nameof(originalImagePath));
}
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
var treatmentRequiresTransparency = false;
foreach (var enhancer in supportedEnhancers)
{
if (!treatmentRequiresTransparency)
{
treatmentRequiresTransparency = enhancer.GetEnhancedImageInfo(item, originalImagePath, imageType, imageIndex).RequiresTransparency;
}
}
// All enhanced images are saved as png to allow transparency
string cacheExtension = _imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp) ?
".webp" :
(treatmentRequiresTransparency ? ".png" : ".jpg");
string enhancedImagePath = GetCachePath(EnhancedImageCachePath, cacheGuid + cacheExtension);
LockInfo lockInfo = GetLock(enhancedImagePath);
await lockInfo.Lock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Check again in case of contention
if (File.Exists(enhancedImagePath))
{
return (enhancedImagePath, treatmentRequiresTransparency);
}
Directory.CreateDirectory(Path.GetDirectoryName(enhancedImagePath));
await ExecuteImageEnhancers(supportedEnhancers, originalImagePath, enhancedImagePath, item, imageType, imageIndex).ConfigureAwait(false);
return (enhancedImagePath, treatmentRequiresTransparency);
}
finally
{
ReleaseLock(enhancedImagePath, lockInfo);
}
}
/// <summary>
/// Executes the image enhancers.
/// </summary>
/// <param name="imageEnhancers">The image enhancers.</param>
/// <param name="inputPath">The input path.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="item">The item.</param>
/// <param name="imageType">Type of the image.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns>Task{EnhancedImage}.</returns>
private static async Task ExecuteImageEnhancers(IEnumerable<IImageEnhancer> imageEnhancers, string inputPath, string outputPath, BaseItem item, ImageType imageType, int imageIndex)
{
// Run the enhancers sequentially in order of priority
foreach (var enhancer in imageEnhancers)
{
await enhancer.EnhanceImageAsync(item, inputPath, outputPath, imageType, imageIndex).ConfigureAwait(false);
// Feed the output into the next enhancer as input
inputPath = outputPath;
}
}
/// <summary>
/// Gets the cache path.
/// </summary>
@ -647,7 +421,7 @@ namespace Emby.Drawing
/// or
/// uniqueName
/// or
/// fileExtension
/// fileExtension.
/// </exception>
public string GetCachePath(string path, string uniqueName, string fileExtension)
{
@ -680,7 +454,7 @@ namespace Emby.Drawing
/// <exception cref="ArgumentNullException">
/// path
/// or
/// filename
/// filename.
/// </exception>
public string GetCachePath(string path, string filename)
{
@ -688,6 +462,7 @@ namespace Emby.Drawing
{
throw new ArgumentNullException(nameof(path));
}
if (string.IsNullOrEmpty(filename))
{
throw new ArgumentNullException(nameof(filename));
@ -709,74 +484,19 @@ namespace Emby.Drawing
}
/// <inheritdoc />
public IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType)
{
foreach (var i in ImageEnhancers)
{
if (i.Supports(item, imageType))
{
yield return i;
}
}
}
private class LockInfo
{
public SemaphoreSlim Lock = new SemaphoreSlim(1, 1);
public int Count = 1;
}
private LockInfo GetLock(string key)
{
lock (_locks)
{
if (_locks.TryGetValue(key, out LockInfo info))
{
info.Count++;
}
else
{
info = new LockInfo();
_locks[key] = info;
}
return info;
}
}
private void ReleaseLock(string key, LockInfo info)
public void Dispose()
{
info.Lock.Release();
lock (_locks)
if (_disposed)
{
info.Count--;
if (info.Count <= 0)
{
_locks.Remove(key);
info.Lock.Dispose();
}
return;
}
}
/// <inheritdoc />
public void Dispose()
{
_disposed = true;
var disposable = _imageEncoder as IDisposable;
if (disposable != null)
if (_imageEncoder is IDisposable disposable)
{
disposable.Dispose();
}
}
private void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
_disposed = true;
}
}
}

@ -8,19 +8,12 @@ using Emby.Naming.Common;
namespace Emby.Naming.Audio
{
public class AudioFileParser
public static class AudioFileParser
{
private readonly NamingOptions _options;
public AudioFileParser(NamingOptions options)
{
_options = options;
}
public bool IsAudioFile(string path)
public static bool IsAudioFile(string path, NamingOptions options)
{
var extension = Path.GetExtension(path) ?? string.Empty;
return _options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
}
}

@ -277,7 +277,7 @@ namespace Emby.Naming.Common
// This isn't a Kodi naming rule, but the expression below causes false positives,
// so we make sure this one gets tested first.
// "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/]*$")
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*[^\\\/x]*$")
{
IsNamed = true
},

@ -4,7 +4,6 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
namespace Emby.Naming.TV
{
@ -29,14 +28,14 @@ namespace Emby.Naming.TV
{
var result = new SeasonPathParserResult();
var seasonNumberInfo = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
result.SeasonNumber = seasonNumberInfo.seasonNumber;
result.SeasonNumber = seasonNumber;
if (result.SeasonNumber.HasValue)
{
result.Success = true;
result.IsSeasonFolder = seasonNumberInfo.isSeasonFolder;
result.IsSeasonFolder = isSeasonFolder;
}
return result;
@ -90,12 +89,10 @@ namespace Emby.Naming.TV
// Look for one of the season folder names
foreach (var name in _seasonFolderNames)
{
var index = filename.IndexOf(name, StringComparison.OrdinalIgnoreCase);
if (index != -1)
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
{
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
if (result.Item1.HasValue)
if (result.seasonNumber.HasValue)
{
return result;
}
@ -105,25 +102,32 @@ namespace Emby.Naming.TV
}
var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
var resultNumber = parts.Select(GetSeasonNumberFromPart).FirstOrDefault(i => i.HasValue);
return (resultNumber, true);
for (int i = 0; i < parts.Length; i++)
{
if (TryGetSeasonNumberFromPart(parts[i], out int seasonNumber))
{
return (seasonNumber, true);
}
}
return (null, true);
}
private static int? GetSeasonNumberFromPart(string part)
private static bool TryGetSeasonNumberFromPart(ReadOnlySpan<char> part, out int seasonNumber)
{
seasonNumber = 0;
if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
{
return null;
return false;
}
part = part.Substring(1);
if (int.TryParse(part, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
return value;
seasonNumber = value;
return true;
}
return null;
return false;
}
/// <summary>
@ -131,7 +135,7 @@ namespace Emby.Naming.TV
/// </summary>
/// <param name="path">The path.</param>
/// <returns>System.Nullable{System.Int32}.</returns>
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(string path)
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
{
var numericStart = -1;
var length = 0;
@ -142,7 +146,7 @@ namespace Emby.Naming.TV
// Find out where the numbers start, and then keep going until they end
for (var i = 0; i < path.Length; i++)
{
if (char.IsNumber(path, i))
if (char.IsNumber(path[i]))
{
if (!hasOpenParenth)
{
@ -177,7 +181,7 @@ namespace Emby.Naming.TV
return (null, isSeasonFolder);
}
return (int.Parse(path.Substring(numericStart, length), CultureInfo.InvariantCulture), isSeasonFolder);
return (int.Parse(path.Slice(numericStart, length), provider: CultureInfo.InvariantCulture), isSeasonFolder);
}
}
}

@ -32,7 +32,7 @@ namespace Emby.Naming.Video
if (rule.MediaType == MediaType.Audio)
{
if (!new AudioFileParser(_options).IsAudioFile(path))
if (!AudioFileParser.IsAudioFile(path, _options))
{
return result;
}

@ -194,7 +194,7 @@ namespace Emby.Naming.Video
}
}
private string GetRegexInput(FileSystemMetadata file)
private static string GetRegexInput(FileSystemMetadata file)
{
// For directories, dummy up an extension otherwise the expressions will fail
var input = !file.IsDirectory
@ -204,7 +204,7 @@ namespace Emby.Naming.Video
return Path.GetFileName(input);
}
private Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
{
var regexInput = GetRegexInput(input);

@ -1,5 +1,11 @@
#pragma warning disable CS1591
#pragma warning disable SA1402
#pragma warning disable SA1600
#pragma warning disable SA1649
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -16,7 +22,7 @@ namespace Emby.Notifications.Api
public class GetNotifications : IReturn<NotificationResult>
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UserId { get; set; }
public string UserId { get; set; } = string.Empty;
[ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsRead { get; set; }
@ -30,32 +36,34 @@ namespace Emby.Notifications.Api
public class Notification
{
public string Id { get; set; }
public string Id { get; set; } = string.Empty;
public string UserId { get; set; }
public string UserId { get; set; } = string.Empty;
public DateTime Date { get; set; }
public bool IsRead { get; set; }
public string Name { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; }
public string Description { get; set; } = string.Empty;
public string Url { get; set; }
public string Url { get; set; } = string.Empty;
public NotificationLevel Level { get; set; }
}
public class NotificationResult
{
public Notification[] Notifications { get; set; }
public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>();
public int TotalRecordCount { get; set; }
}
public class NotificationsSummary
{
public int UnreadCount { get; set; }
public NotificationLevel MaxUnreadNotificationLevel { get; set; }
}
@ -63,7 +71,7 @@ namespace Emby.Notifications.Api
public class GetNotificationsSummary : IReturn<NotificationsSummary>
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UserId { get; set; }
public string UserId { get; set; } = string.Empty;
}
[Route("/Notifications/Types", "GET", Summary = "Gets notification types")]
@ -80,16 +88,16 @@ namespace Emby.Notifications.Api
public class AddAdminNotification : IReturnVoid
{
[ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
public string Name { get; set; }
public string Name { get; set; } = string.Empty;
[ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
public string Description { get; set; }
public string Description { get; set; } = string.Empty;
[ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string ImageUrl { get; set; }
public string? ImageUrl { get; set; }
[ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string Url { get; set; }
public string? Url { get; set; }
[ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public NotificationLevel Level { get; set; }
@ -99,20 +107,20 @@ namespace Emby.Notifications.Api
public class MarkRead : IReturnVoid
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string UserId { get; set; }
public string UserId { get; set; } = string.Empty;
[ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
public string Ids { get; set; }
public string Ids { get; set; } = string.Empty;
}
[Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
public class MarkUnread : IReturnVoid
{
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string UserId { get; set; }
public string UserId { get; set; } = string.Empty;
[ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
public string Ids { get; set; }
public string Ids { get; set; } = string.Empty;
}
[Authenticated]
@ -127,32 +135,29 @@ namespace Emby.Notifications.Api
_userManager = userManager;
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
public object Get(GetNotificationTypes request)
{
return _notificationManager.GetNotificationTypes();
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
public object Get(GetNotificationServices request)
{
return _notificationManager.GetNotificationServices().ToList();
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
public object Get(GetNotificationsSummary request)
{
return new NotificationsSummary
{
};
}
public Task Post(AddAdminNotification request)
{
// This endpoint really just exists as post of a real with sickbeard
return AddNotification(request);
}
private Task AddNotification(AddAdminNotification request)
{
var notification = new NotificationRequest
{
Date = DateTime.UtcNow,
@ -166,14 +171,17 @@ namespace Emby.Notifications.Api
return _notificationManager.SendNotification(notification, CancellationToken.None);
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
public void Post(MarkRead request)
{
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
public void Post(MarkUnread request)
{
}
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")]
public object Get(GetNotifications request)
{
return new NotificationResult();

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.Linq;

@ -4,6 +4,8 @@
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@ -16,4 +18,16 @@
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
<!-- Code analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Notifications;
@ -13,7 +16,7 @@ namespace Emby.Notifications
new ConfigurationStore
{
Key = "notifications",
ConfigurationType = typeof (NotificationOptions)
ConfigurationType = typeof(NotificationOptions)
}
};
}

@ -21,70 +21,85 @@ using Microsoft.Extensions.Logging;
namespace Emby.Notifications
{
/// <summary>
/// Creates notifications for various system events
/// Creates notifications for various system events.
/// </summary>
public class Notifications : IServerEntryPoint
public class NotificationEntryPoint : IServerEntryPoint
{
private readonly ILogger _logger;
private readonly IActivityManager _activityManager;
private readonly ILocalizationManager _localization;
private readonly INotificationManager _notificationManager;
private readonly ILibraryManager _libraryManager;
private readonly IServerApplicationHost _appHost;
private readonly IConfigurationManager _config;
private Timer LibraryUpdateTimer { get; set; }
private readonly object _libraryChangedSyncLock = new object();
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
private readonly IConfigurationManager _config;
private readonly ILocalizationManager _localization;
private readonly IActivityManager _activityManager;
private Timer? _libraryUpdateTimer;
private string[] _coreNotificationTypes;
public Notifications(
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="NotificationEntryPoint" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="activityManager">The activity manager.</param>
/// <param name="localization">The localization manager.</param>
/// <param name="notificationManager">The notification manager.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="appHost">The application host.</param>
/// <param name="config">The configuration manager.</param>
public NotificationEntryPoint(
ILogger<NotificationEntryPoint> logger,
IActivityManager activityManager,
ILocalizationManager localization,
ILogger logger,
INotificationManager notificationManager,
ILibraryManager libraryManager,
IServerApplicationHost appHost,
IConfigurationManager config)
{
_logger = logger;
_activityManager = activityManager;
_localization = localization;
_notificationManager = notificationManager;
_libraryManager = libraryManager;
_appHost = appHost;
_config = config;
_localization = localization;
_activityManager = activityManager;
_coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray();
}
/// <inheritdoc />
public Task RunAsync()
{
_libraryManager.ItemAdded += _libraryManager_ItemAdded;
_appHost.HasPendingRestartChanged += _appHost_HasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged += _appHost_HasUpdateAvailableChanged;
_activityManager.EntryCreated += _activityManager_EntryCreated;
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
return Task.CompletedTask;
}
private async void _appHost_HasPendingRestartChanged(object sender, EventArgs e)
private async void OnAppHostHasPendingRestartChanged(object sender, EventArgs e)
{
var type = NotificationType.ServerRestartRequired.ToString();
var notification = new NotificationRequest
{
NotificationType = type,
Name = string.Format(_localization.GetLocalizedString("ServerNameNeedsToBeRestarted"), _appHost.Name)
Name = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("ServerNameNeedsToBeRestarted"),
_appHost.Name)
};
await SendNotification(notification, null).ConfigureAwait(false);
}
private async void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
private async void OnActivityManagerEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
{
var entry = e.Argument;
@ -117,7 +132,7 @@ namespace Emby.Notifications
return _config.GetConfiguration<NotificationOptions>("notifications");
}
private async void _appHost_HasUpdateAvailableChanged(object sender, EventArgs e)
private async void OnAppHostHasUpdateAvailableChanged(object sender, EventArgs e)
{
if (!_appHost.HasUpdateAvailable)
{
@ -136,8 +151,7 @@ namespace Emby.Notifications
await SendNotification(notification, null).ConfigureAwait(false);
}
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
private void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@ -146,14 +160,17 @@ namespace Emby.Notifications
lock (_libraryChangedSyncLock)
{
if (LibraryUpdateTimer == null)
if (_libraryUpdateTimer == null)
{
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, 5000,
Timeout.Infinite);
_libraryUpdateTimer = new Timer(
LibraryUpdateTimerCallback,
null,
5000,
Timeout.Infinite);
}
else
{
LibraryUpdateTimer.Change(5000, Timeout.Infinite);
_libraryUpdateTimer.Change(5000, Timeout.Infinite);
}
_itemsAdded.Add(e.Item);
@ -188,7 +205,8 @@ namespace Emby.Notifications
{
items = _itemsAdded.ToList();
_itemsAdded.Clear();
DisposeLibraryUpdateTimer();
_libraryUpdateTimer!.Dispose(); // Shouldn't be null as it just set off this callback
_libraryUpdateTimer = null;
}
items = items.Take(10).ToList();
@ -198,7 +216,10 @@ namespace Emby.Notifications
var notification = new NotificationRequest
{
NotificationType = NotificationType.NewLibraryContent.ToString(),
Name = string.Format(_localization.GetLocalizedString("ValueHasBeenAddedToLibrary"), GetItemName(item)),
Name = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("ValueHasBeenAddedToLibrary"),
GetItemName(item)),
Description = item.Overview
};
@ -206,6 +227,11 @@ namespace Emby.Notifications
}
}
/// <summary>
/// Creates a human readable name for the item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>A human readable name for the item.</returns>
public static string GetItemName(BaseItem item)
{
var name = item.Name;
@ -219,6 +245,7 @@ namespace Emby.Notifications
episode.IndexNumber.Value,
name);
}
if (episode.ParentIndexNumber.HasValue)
{
name = string.Format(
@ -229,7 +256,6 @@ namespace Emby.Notifications
}
}
if (item is IHasSeries hasSeries)
{
name = hasSeries.SeriesName + " - " + name;
@ -257,7 +283,7 @@ namespace Emby.Notifications
return name;
}
private async Task SendNotification(NotificationRequest notification, BaseItem relatedItem)
private async Task SendNotification(NotificationRequest notification, BaseItem? relatedItem)
{
try
{
@ -269,23 +295,37 @@ namespace Emby.Notifications
}
}
/// <inheritdoc />
public void Dispose()
{
DisposeLibraryUpdateTimer();
_libraryManager.ItemAdded -= _libraryManager_ItemAdded;
_appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged -= _appHost_HasUpdateAvailableChanged;
_activityManager.EntryCreated -= _activityManager_EntryCreated;
Dispose(true);
GC.SuppressFinalize(this);
}
private void DisposeLibraryUpdateTimer()
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (LibraryUpdateTimer != null)
if (_disposed)
{
return;
}
if (disposing)
{
LibraryUpdateTimer.Dispose();
LibraryUpdateTimer = null;
_libraryUpdateTimer?.Dispose();
}
_libraryUpdateTimer = null;
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
_disposed = true;
}
}
}

@ -16,20 +16,32 @@ using Microsoft.Extensions.Logging;
namespace Emby.Notifications
{
/// <summary>
/// NotificationManager class.
/// </summary>
public class NotificationManager : INotificationManager
{
private readonly ILogger _logger;
private readonly IUserManager _userManager;
private readonly IServerConfigurationManager _config;
private INotificationService[] _services;
private INotificationTypeFactory[] _typeFactories;
public NotificationManager(ILoggerFactory loggerFactory, IUserManager userManager, IServerConfigurationManager config)
private INotificationService[] _services = Array.Empty<INotificationService>();
private INotificationTypeFactory[] _typeFactories = Array.Empty<INotificationTypeFactory>();
/// <summary>
/// Initializes a new instance of the <see cref="NotificationManager" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="config">The server configuration manager.</param>
public NotificationManager(
ILogger<NotificationManager> logger,
IUserManager userManager,
IServerConfigurationManager config)
{
_logger = logger;
_userManager = userManager;
_config = config;
_logger = loggerFactory.CreateLogger(GetType().Name);
}
private NotificationOptions GetConfiguration()
@ -37,12 +49,14 @@ namespace Emby.Notifications
return _config.GetConfiguration<NotificationOptions>("notifications");
}
/// <inheritdoc />
public Task SendNotification(NotificationRequest request, CancellationToken cancellationToken)
{
return SendNotification(request, null, cancellationToken);
}
public Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken)
/// <inheritdoc />
public Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken)
{
var notificationType = request.NotificationType;
@ -64,7 +78,8 @@ namespace Emby.Notifications
return Task.WhenAll(tasks);
}
private Task SendNotification(NotificationRequest request,
private Task SendNotification(
NotificationRequest request,
INotificationService service,
IEnumerable<User> users,
string title,
@ -79,7 +94,7 @@ namespace Emby.Notifications
return Task.WhenAll(tasks);
}
private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption options)
private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption? options)
{
if (request.SendToUserMode.HasValue)
{
@ -109,7 +124,8 @@ namespace Emby.Notifications
return request.UserIds;
}
private async Task SendNotification(NotificationRequest request,
private async Task SendNotification(
NotificationRequest request,
INotificationService service,
string title,
string description,
@ -161,12 +177,14 @@ namespace Emby.Notifications
return GetConfiguration().IsServiceEnabled(service.Name, notificationType);
}
/// <inheritdoc />
public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories)
{
_services = services.ToArray();
_typeFactories = notificationTypeFactories.ToArray();
}
/// <inheritdoc />
public List<NotificationTypeInfo> GetNotificationTypes()
{
var list = _typeFactories.Select(i =>
@ -180,7 +198,6 @@ namespace Emby.Notifications
_logger.LogError(ex, "Error in GetNotificationTypes");
return new List<NotificationTypeInfo>();
}
}).SelectMany(i => i).ToList();
var config = GetConfiguration();
@ -193,13 +210,13 @@ namespace Emby.Notifications
return list;
}
/// <inheritdoc />
public IEnumerable<NameIdPair> GetNotificationServices()
{
return _services.Select(i => new NameIdPair
{
Name = i.Name,
Id = i.Name.GetMD5().ToString("N", CultureInfo.InvariantCulture)
}).OrderBy(i => i.Name);
}
}

@ -17,6 +17,7 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- Code Analyzers-->

@ -29,7 +29,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Activity
{
public class ActivityLogEntryPoint : IServerEntryPoint
public sealed class ActivityLogEntryPoint : IServerEntryPoint
{
private readonly ILogger _logger;
private readonly IInstallationManager _installationManager;
@ -39,7 +39,6 @@ namespace Emby.Server.Implementations.Activity
private readonly ILocalizationManager _localization;
private readonly ISubtitleManager _subManager;
private readonly IUserManager _userManager;
private readonly IServerApplicationHost _appHost;
private readonly IDeviceManager _deviceManager;
/// <summary>
@ -64,8 +63,7 @@ namespace Emby.Server.Implementations.Activity
ILocalizationManager localization,
IInstallationManager installationManager,
ISubtitleManager subManager,
IUserManager userManager,
IServerApplicationHost appHost)
IUserManager userManager)
{
_logger = logger;
_sessionManager = sessionManager;
@ -76,7 +74,6 @@ namespace Emby.Server.Implementations.Activity
_installationManager = installationManager;
_subManager = subManager;
_userManager = userManager;
_appHost = appHost;
}
public Task RunAsync()
@ -141,7 +138,7 @@ namespace Emby.Server.Implementations.Activity
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
e.Provider,
Notifications.Notifications.GetItemName(e.Item)),
Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)),
Type = "SubtitleDownloadFailure",
ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
ShortOverview = e.Exception.Message
@ -533,6 +530,7 @@ namespace Emby.Server.Implementations.Activity
private void CreateLogEntry(ActivityLogEntry entry)
=> _activityManager.Create(entry);
/// <inheritdoc />
public void Dispose()
{
_taskManager.TaskCompleted -= OnTaskCompleted;

@ -819,7 +819,18 @@ namespace Emby.Server.Implementations
ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager);
serviceCollection.AddSingleton(ChannelManager);
SessionManager = new SessionManager(UserDataManager, LoggerFactory, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, this, AuthenticationRepository, DeviceManager, MediaSourceManager);
SessionManager = new SessionManager(
LoggerFactory.CreateLogger<SessionManager>(),
UserDataManager,
LibraryManager,
UserManager,
musicManager,
DtoService,
ImageProcessor,
this,
AuthenticationRepository,
DeviceManager,
MediaSourceManager);
serviceCollection.AddSingleton(SessionManager);
serviceCollection.AddSingleton<IDlnaManager>(
@ -836,7 +847,10 @@ namespace Emby.Server.Implementations
UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager);
serviceCollection.AddSingleton(UserViewManager);
NotificationManager = new NotificationManager(LoggerFactory, UserManager, ServerConfigurationManager);
NotificationManager = new NotificationManager(
LoggerFactory.CreateLogger<NotificationManager>(),
UserManager,
ServerConfigurationManager);
serviceCollection.AddSingleton(NotificationManager);
serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager));
@ -1074,8 +1088,6 @@ namespace Emby.Server.Implementations
GetExports<IMetadataSaver>(),
GetExports<IExternalId>());
ImageProcessor.ImageEnhancers = GetExports<IImageEnhancer>();
LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
SubtitleManager.AddParts(GetExports<ISubtitleProvider>());

@ -3521,20 +3521,6 @@ namespace Emby.Server.Implementations.Data
}
var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray();
if (includeTypes.Length == 1)
{
whereClauses.Add("type=@type");
if (statement != null)
{
statement.TryBind("@type", includeTypes[0]);
}
}
else if (includeTypes.Length > 1)
{
var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'"));
whereClauses.Add($"type in ({inClause})");
}
// Only specify excluded types if no included types are specified
if (includeTypes.Length == 0)
{
@ -3553,6 +3539,19 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add($"type not in ({inClause})");
}
}
else if (includeTypes.Length == 1)
{
whereClauses.Add("type=@type");
if (statement != null)
{
statement.TryBind("@type", includeTypes[0]);
}
}
else if (includeTypes.Length > 1)
{
var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'"));
whereClauses.Add($"type in ({inClause})");
}
if (query.ChannelIds.Length == 1)
{
@ -4927,7 +4926,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
// Not crazy about having this all the way down here, but at least it's in one place
readonly Dictionary<string, string[]> _types = GetTypeMapDictionary();
private IEnumerable<string> MapIncludeItemTypes(string value)
private string[] MapIncludeItemTypes(string value)
{
if (_types.TryGetValue(value, out string[] result))
{
@ -5611,32 +5610,32 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return counts;
}
private List<Tuple<int, string>> GetItemValuesToSave(BaseItem item, List<string> inheritedTags)
private List<(int, string)> GetItemValuesToSave(BaseItem item, List<string> inheritedTags)
{
var list = new List<Tuple<int, string>>();
var list = new List<(int, string)>();
if (item is IHasArtist hasArtist)
{
list.AddRange(hasArtist.Artists.Select(i => new Tuple<int, string>(0, i)));
list.AddRange(hasArtist.Artists.Select(i => (0, i)));
}
if (item is IHasAlbumArtist hasAlbumArtist)
{
list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => new Tuple<int, string>(1, i)));
list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i)));
}
list.AddRange(item.Genres.Select(i => new Tuple<int, string>(2, i)));
list.AddRange(item.Studios.Select(i => new Tuple<int, string>(3, i)));
list.AddRange(item.Tags.Select(i => new Tuple<int, string>(4, i)));
list.AddRange(item.Genres.Select(i => (2, i)));
list.AddRange(item.Studios.Select(i => (3, i)));
list.AddRange(item.Tags.Select(i => (4, i)));
// keywords was 5
list.AddRange(inheritedTags.Select(i => new Tuple<int, string>(6, i)));
list.AddRange(inheritedTags.Select(i => (6, i)));
return list;
}
private void UpdateItemValues(Guid itemId, List<Tuple<int, string>> values, IDatabaseConnection db)
private void UpdateItemValues(Guid itemId, List<(int, string)> values, IDatabaseConnection db)
{
if (itemId.Equals(Guid.Empty))
{
@ -5658,7 +5657,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
InsertItemValues(guidBlob, values, db);
}
private void InsertItemValues(byte[] idBlob, List<Tuple<int, string>> values, IDatabaseConnection db)
private void InsertItemValues(byte[] idBlob, List<(int, string)> values, IDatabaseConnection db)
{
var startIndex = 0;
var limit = 100;

@ -142,11 +142,10 @@ namespace Emby.Server.Implementations.Devices
public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
{
var sessions = _authRepo.Get(new AuthenticationInfoQuery
IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
{
//UserId = query.UserId
HasUser = true
}).Items;
// TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
@ -154,23 +153,19 @@ namespace Emby.Server.Implementations.Devices
{
var val = query.SupportsSync.Value;
sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val).ToArray();
sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val);
}
if (!query.UserId.Equals(Guid.Empty))
{
var user = _userManager.GetUserById(query.UserId);
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)).ToArray();
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
}
var array = sessions.Select(ToDeviceInfo).ToArray();
return new QueryResult<DeviceInfo>
{
Items = array,
TotalRecordCount = array.Length
};
return new QueryResult<DeviceInfo>(array);
}
private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
@ -186,7 +181,7 @@ namespace Emby.Server.Implementations.Devices
LastUserName = authInfo.UserName,
Name = authInfo.DeviceName,
DateLastActivity = authInfo.DateLastActivity,
IconUrl = caps == null ? null : caps.IconUrl
IconUrl = caps?.IconUrl
};
}

@ -1362,56 +1362,33 @@ namespace Emby.Server.Implementations.Dto
return null;
}
var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToArray();
ImageDimensions size;
var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
if (defaultAspectRatio > 0)
{
if (supportedEnhancers.Length == 0)
{
return defaultAspectRatio;
}
return defaultAspectRatio;
}
int dummyWidth = 200;
int dummyHeight = Convert.ToInt32(dummyWidth / defaultAspectRatio);
size = new ImageDimensions(dummyWidth, dummyHeight);
if (!imageInfo.IsLocalFile)
{
return null;
}
else
try
{
if (!imageInfo.IsLocalFile)
{
return null;
}
size = _imageProcessor.GetImageDimensions(item, imageInfo);
try
if (size.Width <= 0 || size.Height <= 0)
{
size = _imageProcessor.GetImageDimensions(item, imageInfo);
if (size.Width <= 0 || size.Height <= 0)
{
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
return null;
}
}
foreach (var enhancer in supportedEnhancers)
catch (Exception ex)
{
try
{
size = enhancer.GetEnhancedImageSize(item, ImageType.Primary, 0, size);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in image enhancer: {0}", enhancer.GetType().Name);
}
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
return null;
}
var width = size.Width;

@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
{
public class RecordingNotifier : IServerEntryPoint
public sealed class RecordingNotifier : IServerEntryPoint
{
private readonly ILiveTvManager _liveTvManager;
private readonly ISessionManager _sessionManager;
@ -28,32 +28,33 @@ namespace Emby.Server.Implementations.EntryPoints
_liveTvManager = liveTvManager;
}
/// <inheritdoc />
public Task RunAsync()
{
_liveTvManager.TimerCancelled += _liveTvManager_TimerCancelled;
_liveTvManager.SeriesTimerCancelled += _liveTvManager_SeriesTimerCancelled;
_liveTvManager.TimerCreated += _liveTvManager_TimerCreated;
_liveTvManager.SeriesTimerCreated += _liveTvManager_SeriesTimerCreated;
_liveTvManager.TimerCancelled += OnLiveTvManagerTimerCancelled;
_liveTvManager.SeriesTimerCancelled += OnLiveTvManagerSeriesTimerCancelled;
_liveTvManager.TimerCreated += OnLiveTvManagerTimerCreated;
_liveTvManager.SeriesTimerCreated += OnLiveTvManagerSeriesTimerCreated;
return Task.CompletedTask;
}
private void _liveTvManager_SeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
private void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
{
SendMessage("SeriesTimerCreated", e.Argument);
}
private void _liveTvManager_TimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
private void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
{
SendMessage("TimerCreated", e.Argument);
}
private void _liveTvManager_SeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
private void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
{
SendMessage("SeriesTimerCancelled", e.Argument);
}
private void _liveTvManager_TimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
private void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
{
SendMessage("TimerCancelled", e.Argument);
}
@ -64,11 +65,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
await _sessionManager.SendMessageToUserSessions(users, name, info, CancellationToken.None);
}
catch (ObjectDisposedException)
{
// TODO Log exception or Investigate and properly fix.
await _sessionManager.SendMessageToUserSessions(users, name, info, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
@ -76,12 +73,13 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
/// <inheritdoc />
public void Dispose()
{
_liveTvManager.TimerCancelled -= _liveTvManager_TimerCancelled;
_liveTvManager.SeriesTimerCancelled -= _liveTvManager_SeriesTimerCancelled;
_liveTvManager.TimerCreated -= _liveTvManager_TimerCreated;
_liveTvManager.SeriesTimerCreated -= _liveTvManager_SeriesTimerCreated;
_liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled;
_liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled;
_liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated;
_liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated;
}
}
}

@ -6,7 +6,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
{
@ -15,21 +14,17 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask
{
private readonly ILogger _logger;
/// <summary>
/// The user manager.
/// </summary>
private readonly IUserManager _userManager;
private IFileSystem _fileSystem;
private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class.
/// </summary>
public RefreshUsersMetadata(ILogger logger, IUserManager userManager, IFileSystem fileSystem)
public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem)
{
_logger = logger;
_userManager = userManager;
_fileSystem = fileSystem;
}

@ -3,37 +3,28 @@ using Emby.Server.Implementations.Browser;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
{
/// <summary>
/// Class StartupWizard.
/// </summary>
public class StartupWizard : IServerEntryPoint
public sealed class StartupWizard : IServerEntryPoint
{
/// <summary>
/// The app host.
/// </summary>
private readonly IServerApplicationHost _appHost;
/// <summary>
/// The user manager.
/// </summary>
private readonly ILogger _logger;
private IServerConfigurationManager _config;
private readonly IServerConfigurationManager _config;
/// <summary>
/// Initializes a new instance of the <see cref="StartupWizard"/> class.
/// </summary>
/// <param name="appHost">The application host.</param>
/// <param name="logger">The logger.</param>
/// <param name="config">The configuration manager.</param>
public StartupWizard(IServerApplicationHost appHost, ILogger logger, IServerConfigurationManager config)
public StartupWizard(IServerApplicationHost appHost, IServerConfigurationManager config)
{
_appHost = appHost;
_logger = logger;
_config = config;
}

@ -3,8 +3,6 @@ using System.Threading.Tasks;
using Emby.Server.Implementations.Udp;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@ -23,9 +21,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// The logger.
/// </summary>
private readonly ILogger _logger;
private readonly ISocketFactory _socketFactory;
private readonly IServerApplicationHost _appHost;
private readonly IJsonSerializer _json;
/// <summary>
/// The UDP server.
@ -64,7 +60,7 @@ namespace Emby.Server.Implementations.EntryPoints
_cancellationTokenSource.Cancel();
_udpServer.Dispose();
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
_udpServer = null;

@ -13,39 +13,38 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
{
public class UserDataChangeNotifier : IServerEntryPoint
public sealed class UserDataChangeNotifier : IServerEntryPoint
{
private const int UpdateDuration = 500;
private readonly ISessionManager _sessionManager;
private readonly ILogger _logger;
private readonly IUserDataManager _userDataManager;
private readonly IUserManager _userManager;
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
private readonly object _syncLock = new object();
private Timer UpdateTimer { get; set; }
private const int UpdateDuration = 500;
private Timer _updateTimer;
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, ILogger logger, IUserManager userManager)
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
{
_userDataManager = userDataManager;
_sessionManager = sessionManager;
_logger = logger;
_userManager = userManager;
}
public Task RunAsync()
{
_userDataManager.UserDataSaved += _userDataManager_UserDataSaved;
_userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved;
return Task.CompletedTask;
}
void _userDataManager_UserDataSaved(object sender, UserDataSaveEventArgs e)
void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
{
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
{
@ -54,14 +53,17 @@ namespace Emby.Server.Implementations.EntryPoints
lock (_syncLock)
{
if (UpdateTimer == null)
if (_updateTimer == null)
{
UpdateTimer = new Timer(UpdateTimerCallback, null, UpdateDuration,
Timeout.Infinite);
_updateTimer = new Timer(
UpdateTimerCallback,
null,
UpdateDuration,
Timeout.Infinite);
}
else
{
UpdateTimer.Change(UpdateDuration, Timeout.Infinite);
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
}
if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
@ -97,10 +99,10 @@ namespace Emby.Server.Implementations.EntryPoints
var task = SendNotifications(changes, CancellationToken.None);
if (UpdateTimer != null)
if (_updateTimer != null)
{
UpdateTimer.Dispose();
UpdateTimer = null;
_updateTimer.Dispose();
_updateTimer = null;
}
}
}
@ -145,13 +147,13 @@ namespace Emby.Server.Implementations.EntryPoints
public void Dispose()
{
if (UpdateTimer != null)
if (_updateTimer != null)
{
UpdateTimer.Dispose();
UpdateTimer = null;
_updateTimer.Dispose();
_updateTimer = null;
}
_userDataManager.UserDataSaved -= _userDataManager_UserDataSaved;
_userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
}
}
}

@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.HttpClientManager
if (!string.IsNullOrWhiteSpace(userInfo))
{
_logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
url = url.Replace(userInfo + '@', string.Empty);
url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal);
}
var request = new HttpRequestMessage(method, url);

@ -40,9 +40,9 @@ namespace Emby.Server.Implementations.HttpServer
private readonly Func<Type, Func<string, object>> _funcParseFn;
private readonly string _defaultRedirectPath;
private readonly string _baseUrlPrefix;
private readonly Dictionary<Type, Type> ServiceOperationsMap = new Dictionary<Type, Type>();
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
private bool _disposed = false;
public HttpListenerHost(
@ -72,6 +72,8 @@ namespace Emby.Server.Implementations.HttpServer
ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
}
public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
public static HttpListenerHost Instance { get; protected set; }
@ -82,8 +84,6 @@ namespace Emby.Server.Implementations.HttpServer
public ServiceController ServiceController { get; private set; }
public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
public object CreateInstance(Type type)
{
return _appHost.CreateInstance(type);
@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.HttpServer
private static string NormalizeUrlPath(string path)
{
if (path.StartsWith("/"))
if (path.Length > 0 && path[0] == '/')
{
// If the path begins with a leading slash, just return it as-is
return path;
@ -131,13 +131,13 @@ namespace Emby.Server.Implementations.HttpServer
public Type GetServiceTypeByRequest(Type requestType)
{
ServiceOperationsMap.TryGetValue(requestType, out var serviceType);
_serviceOperationsMap.TryGetValue(requestType, out var serviceType);
return serviceType;
}
public void AddServiceInfo(Type serviceType, Type requestType)
{
ServiceOperationsMap[requestType] = serviceType;
_serviceOperationsMap[requestType] = serviceType;
}
private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
@ -199,7 +199,7 @@ namespace Emby.Server.Implementations.HttpServer
else
{
var inners = agg.InnerExceptions;
if (inners != null && inners.Count > 0)
if (inners.Count > 0)
{
return GetActualException(inners[0]);
}
@ -362,7 +362,7 @@ namespace Emby.Server.Implementations.HttpServer
return true;
}
host = host ?? string.Empty;
host ??= string.Empty;
if (_networkManager.IsInPrivateAddressSpace(host))
{
@ -433,7 +433,7 @@ namespace Emby.Server.Implementations.HttpServer
}
/// <summary>
/// Overridable method that can be used to implement a custom hnandler
/// Overridable method that can be used to implement a custom handler.
/// </summary>
public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
{
@ -492,7 +492,7 @@ namespace Emby.Server.Implementations.HttpServer
|| string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
|| string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
|| string.IsNullOrEmpty(localPath)
|| !localPath.StartsWith(_baseUrlPrefix))
|| !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
{
// Always redirect back to the default path if the base prefix is invalid or missing
_logger.LogDebug("Normalizing a URL at {0}", localPath);
@ -693,7 +693,10 @@ namespace Emby.Server.Implementations.HttpServer
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (_disposed)
{
return;
}
if (disposing)
{

@ -6,7 +6,9 @@ namespace Emby.Server.Implementations.IO
public class ExtendedFileSystemInfo
{
public bool IsHidden { get; set; }
public bool IsReadOnly { get; set; }
public bool Exists { get; set; }
}
}

@ -15,27 +15,29 @@ namespace Emby.Server.Implementations.IO
{
public class FileRefresher : IDisposable
{
private ILogger Logger { get; set; }
private ILibraryManager LibraryManager { get; set; }
private IServerConfigurationManager ConfigurationManager { get; set; }
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly List<string> _affectedPaths = new List<string>();
private Timer _timer;
private readonly object _timerLock = new object();
public string Path { get; private set; }
public event EventHandler<EventArgs> Completed;
private Timer _timer;
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
{
logger.LogDebug("New file refresher created for {0}", path);
Path = path;
ConfigurationManager = configurationManager;
LibraryManager = libraryManager;
Logger = logger;
_configurationManager = configurationManager;
_libraryManager = libraryManager;
_logger = logger;
AddPath(path);
}
public event EventHandler<EventArgs> Completed;
public string Path { get; private set; }
private void AddAffectedPath(string path)
{
if (string.IsNullOrEmpty(path))
@ -80,11 +82,11 @@ namespace Emby.Server.Implementations.IO
if (_timer == null)
{
_timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
_timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
}
else
{
_timer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
_timer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
}
}
}
@ -93,7 +95,7 @@ namespace Emby.Server.Implementations.IO
{
lock (_timerLock)
{
Logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
_logger.LogDebug("Resetting file refresher from {0} to {1}", Path, path);
Path = path;
AddAffectedPath(path);
@ -116,7 +118,7 @@ namespace Emby.Server.Implementations.IO
paths = _affectedPaths.ToList();
}
Logger.LogDebug("Timer stopped.");
_logger.LogDebug("Timer stopped.");
DisposeTimer();
Completed?.Invoke(this, EventArgs.Empty);
@ -127,7 +129,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
Logger.LogError(ex, "Error processing directory changes");
_logger.LogError(ex, "Error processing directory changes");
}
}
@ -147,7 +149,7 @@ namespace Emby.Server.Implementations.IO
continue;
}
Logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
_logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
try
{
@ -158,11 +160,11 @@ namespace Emby.Server.Implementations.IO
// For now swallow and log.
// Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
// Should we remove it from it's parent?
Logger.LogError(ex, "Error refreshing {name}", item.Name);
_logger.LogError(ex, "Error refreshing {name}", item.Name);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error refreshing {name}", item.Name);
_logger.LogError(ex, "Error refreshing {name}", item.Name);
}
}
}
@ -178,7 +180,7 @@ namespace Emby.Server.Implementations.IO
while (item == null && !string.IsNullOrEmpty(path))
{
item = LibraryManager.FindByPath(path, null);
item = _libraryManager.FindByPath(path, null);
path = System.IO.Path.GetDirectoryName(path);
}

@ -1174,7 +1174,6 @@ namespace Emby.Server.Implementations.Library
return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath)
.Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
.OrderBy(i => i.Name)
.ToList();
}
@ -1406,25 +1405,32 @@ namespace Emby.Server.Implementations.Library
private void SetTopParentOrAncestorIds(InternalItemsQuery query)
{
if (query.AncestorIds.Length == 0)
var ancestorIds = query.AncestorIds;
int len = ancestorIds.Length;
if (len == 0)
{
return;
}
var parents = query.AncestorIds.Select(i => GetItemById(i)).ToList();
if (parents.All(i => i is ICollectionFolder || i is UserView))
var parents = new BaseItem[len];
for (int i = 0; i < len; i++)
{
// Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
query.AncestorIds = Array.Empty<Guid>();
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
parents[i] = GetItemById(ancestorIds[i]);
if (!(parents[i] is ICollectionFolder || parents[i] is UserView))
{
query.TopParentIds = new[] { Guid.NewGuid() };
return;
}
}
// Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
query.AncestorIds = Array.Empty<Guid>();
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
query.TopParentIds = new[] { Guid.NewGuid() };
}
}
public QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
@ -1585,7 +1591,7 @@ namespace Emby.Server.Implementations.Library
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
{
var tasks = IntroProviders
.OrderBy(i => i.GetType().Name.IndexOf("Default", StringComparison.OrdinalIgnoreCase) == -1 ? 0 : 1)
.OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
.Take(1)
.Select(i => GetIntros(i, item, user));
@ -2363,33 +2369,22 @@ namespace Emby.Server.Implementations.Library
new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
}
public bool IsVideoFile(string path, LibraryOptions libraryOptions)
/// <inheritdoc />
public bool IsVideoFile(string path)
{
var resolver = new VideoResolver(GetNamingOptions());
return resolver.IsVideoFile(path);
}
public bool IsVideoFile(string path)
{
return IsVideoFile(path, new LibraryOptions());
}
public bool IsAudioFile(string path, LibraryOptions libraryOptions)
{
var parser = new AudioFileParser(GetNamingOptions());
return parser.IsAudioFile(path);
}
/// <inheritdoc />
public bool IsAudioFile(string path)
{
return IsAudioFile(path, new LibraryOptions());
}
=> AudioFileParser.IsAudioFile(path, GetNamingOptions());
/// <inheritdoc />
public int? GetSeasonNumberFromPath(string path)
{
return SeasonPathParser.Parse(path, true, true).SeasonNumber;
}
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
/// <inheritdoc />
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
{
var series = episode.Series;

@ -73,7 +73,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{
// Return audio if the path is a file and has a matching extension
var libraryOptions = args.GetLibraryOptions();
var collectionType = args.GetCollectionType();
var isBooksCollectionType = string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase);
@ -92,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (LibraryManager.IsAudioFile(args.Path, libraryOptions))
if (LibraryManager.IsAudioFile(args.Path))
{
var extension = Path.GetExtension(args.Path);
@ -105,7 +104,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
// For conflicting extensions, give priority to videos
if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path, libraryOptions))
if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path))
{
return null;
}
@ -121,7 +120,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{
item = new MediaBrowser.Controller.Entities.Audio.Audio();
}
else if (isBooksCollectionType)
{
item = new AudioBook();

@ -5,7 +5,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@ -78,9 +77,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <summary>
/// Determine if the supplied file data points to a music album.
/// </summary>
public bool IsMusicAlbum(string path, IDirectoryService directoryService, LibraryOptions libraryOptions)
public bool IsMusicAlbum(string path, IDirectoryService directoryService)
{
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, libraryOptions, _libraryManager);
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager);
}
/// <summary>
@ -94,7 +93,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (args.IsDirectory)
{
// if (args.Parent is MusicArtist) return true; //saves us from testing children twice
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, args.GetLibraryOptions(), _libraryManager))
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager))
{
return true;
}
@ -112,7 +111,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
IDirectoryService directoryService,
ILogger logger,
IFileSystem fileSystem,
LibraryOptions libraryOptions,
ILibraryManager libraryManager)
{
var discSubfolderCount = 0;
@ -132,7 +130,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
var path = fileSystemInfo.FullName;
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryOptions, libraryManager);
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
if (hasMusic)
{
@ -153,7 +151,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{
var fullName = fileSystemInfo.FullName;
if (libraryManager.IsAudioFile(fullName, libraryOptions))
if (libraryManager.IsAudioFile(fullName))
{
return true;
}

@ -80,14 +80,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
// Avoid mis-identifying top folders
if (args.Parent.IsRoot) return null;
if (args.Parent.IsRoot)
{
return null;
}
var directoryService = args.DirectoryService;
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
// If we contain an album assume we are an artist folder
return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService, args.GetLibraryOptions())) ? new MusicArtist() : null;
return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null;
}
}
}

@ -80,6 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
};
break;
}
if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
{
videoInfo = parser.ResolveDirectory(args.Path);
@ -137,7 +138,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
if (LibraryManager.IsVideoFile(args.Path, args.GetLibraryOptions()) || videoInfo.IsStub)
if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub)
{
var path = args.Path;

@ -436,7 +436,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (result.Items.Count == 1)
{
var videoPath = result.Items[0].Path;
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, libraryOptions, videoPath, i.Name));
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, videoPath, i.Name));
if (!hasPhotos)
{
@ -446,8 +446,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return movie;
}
}
if (result.Items.Count == 0 && multiDiscFolders.Count > 0)
else if (result.Items.Count == 0 && multiDiscFolders.Count > 0)
{
return GetMultiDiscMovie<T>(multiDiscFolders, directoryService);
}
@ -519,14 +518,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
int additionalPartsLen = folderPaths.Count - 1;
var additionalParts = new string[additionalPartsLen];
folderPaths.CopyTo(1, additionalParts, 0, additionalPartsLen);
var returnVideo = new T
{
Path = folderPaths[0],
AdditionalParts = folderPaths.Skip(1).ToArray(),
AdditionalParts = additionalParts,
VideoType = videoTypes[0],
Name = result[0].Name
};

@ -63,13 +63,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (!file.IsDirectory && PhotoResolver.IsImageFile(file.FullName, _imageProcessor))
{
var libraryOptions = args.GetLibraryOptions();
var filename = file.Name;
var ownedByMedia = false;
foreach (var siblingFile in files)
{
if (PhotoResolver.IsOwnedByMedia(_libraryManager, libraryOptions, siblingFile.FullName, filename))
if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename))
{
ownedByMedia = true;
break;

@ -8,7 +8,6 @@ using System.Linq;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers
@ -57,11 +56,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Make sure the image doesn't belong to a video file
var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path));
var libraryOptions = args.GetLibraryOptions();
foreach (var file in files)
{
if (IsOwnedByMedia(_libraryManager, libraryOptions, file.FullName, filename))
if (IsOwnedByMedia(_libraryManager, file.FullName, filename))
{
return null;
}
@ -78,17 +76,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
internal static bool IsOwnedByMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename)
internal static bool IsOwnedByMedia(ILibraryManager libraryManager, string file, string imageFilename)
{
if (libraryManager.IsVideoFile(file, libraryOptions))
if (libraryManager.IsVideoFile(file))
{
return IsOwnedByResolvedMedia(libraryManager, libraryOptions, file, imageFilename);
return IsOwnedByResolvedMedia(libraryManager, file, imageFilename);
}
return false;
}
internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename)
internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, string file, string imageFilename)
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)

@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, args.GetLibraryOptions(), false))
if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, false))
{
return new Series
{
@ -123,24 +123,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
IFileSystem fileSystem,
ILogger logger,
ILibraryManager libraryManager,
LibraryOptions libraryOptions,
bool isTvContentType)
{
foreach (var child in fileSystemChildren)
{
//if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
//{
// //logger.LogDebug("Igoring series file or folder marked hidden: {0}", child.FullName);
// continue;
//}
// Can't enforce this because files saved by Bitcasa are always marked System
//if ((attributes & FileAttributes.System) == FileAttributes.System)
//{
// logger.LogDebug("Igoring series subfolder marked system: {0}", child.FullName);
// continue;
//}
if (child.IsDirectory)
{
if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager))
@ -152,7 +138,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
else
{
string fullName = child.FullName;
if (libraryManager.IsVideoFile(fullName, libraryOptions))
if (libraryManager.IsVideoFile(fullName))
{
if (isTvContentType)
{

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.IO;
using System.Net.Http;

@ -30,7 +30,6 @@ using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.Globalization;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System.Threading.Tasks;
using MediaBrowser.Controller.Plugins;
@ -5,11 +8,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class EntryPoint : IServerEntryPoint
{
/// <inheritdoc />
public Task RunAsync()
{
return EmbyTV.Current.Start();
}
/// <inheritdoc />
public void Dispose()
{
}

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Threading;
using System.Threading.Tasks;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.IO;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Globalization;
using MediaBrowser.Controller.LiveTv;
@ -21,7 +24,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (info.SeasonNumber.HasValue && info.EpisodeNumber.HasValue)
{
name += string.Format(" S{0}E{1}", info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture), info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
name += string.Format(
CultureInfo.InvariantCulture,
" S{0}E{1}",
info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture),
info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
addHyphen = false;
}
else if (info.OriginalAirDate.HasValue)
@ -32,7 +39,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
else
{
name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd");
name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
}
}
else
@ -67,14 +74,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
date = date.ToLocalTime();
return string.Format("{0}_{1}_{2}_{3}_{4}_{5}",
return string.Format(
CultureInfo.InvariantCulture,
"{0}_{1}_{2}_{3}_{4}_{5}",
date.Year.ToString("0000", CultureInfo.InvariantCulture),
date.Month.ToString("00", CultureInfo.InvariantCulture),
date.Day.ToString("00", CultureInfo.InvariantCulture),
date.Hour.ToString("00", CultureInfo.InvariantCulture),
date.Minute.ToString("00", CultureInfo.InvariantCulture),
date.Second.ToString("00", CultureInfo.InvariantCulture)
);
date.Second.ToString("00", CultureInfo.InvariantCulture));
}
}
}

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Serialization;
@ -12,6 +15,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
}
/// <inheritdoc />
public override void Add(SeriesTimerInfo item)
{
if (string.IsNullOrEmpty(item.Id))

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Concurrent;
using System.Globalization;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.Globalization;
@ -91,12 +94,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using (var gzStream = new GZipStream(stream, CompressionMode.Decompress))
{
await gzStream.CopyToAsync(fileStream).ConfigureAwait(false);
await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
}
else
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
}

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.LiveTv;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Globalization;
using System.Linq;

@ -1,5 +1,5 @@
#pragma warning disable SA1600
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;

@ -1,52 +1,48 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv
{
public class LiveTvMediaSourceProvider : IMediaSourceProvider
{
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char StreamIdDelimeter = '_';
private const string StreamIdDelimeterString = "_";
private readonly ILiveTvManager _liveTvManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly ILogger _logger;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationHost _appHost;
private IApplicationPaths _appPaths;
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILoggerFactory loggerFactory, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerApplicationHost appHost)
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, ILogger<LiveTvMediaSourceProvider> logger, IMediaSourceManager mediaSourceManager, IServerApplicationHost appHost)
{
_liveTvManager = liveTvManager;
_jsonSerializer = jsonSerializer;
_logger = logger;
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_appHost = appHost;
_logger = loggerFactory.CreateLogger(GetType().Name);
_appPaths = appPaths;
}
public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
{
var baseItem = (BaseItem)item;
if (baseItem.SourceType == SourceType.LiveTV)
if (item.SourceType == SourceType.LiveTV)
{
var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path);
if (string.IsNullOrEmpty(baseItem.Path) || activeRecordingInfo != null)
if (string.IsNullOrEmpty(item.Path) || activeRecordingInfo != null)
{
return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken);
}
@ -55,10 +51,6 @@ namespace Emby.Server.Implementations.LiveTv
return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>());
}
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char StreamIdDelimeter = '_';
private const string StreamIdDelimeterString = "_";
private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
{
IEnumerable<MediaSourceInfo> sources;
@ -91,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv
foreach (var source in list)
{
source.Type = MediaSourceType.Default;
source.BufferMs = source.BufferMs ?? 1500;
source.BufferMs ??= 1500;
if (source.RequiresOpening || forceRequireOpening)
{
@ -100,11 +92,14 @@ namespace Emby.Server.Implementations.LiveTv
if (source.RequiresOpening)
{
var openKeys = new List<string>();
openKeys.Add(item.GetType().Name);
openKeys.Add(item.Id.ToString("N", CultureInfo.InvariantCulture));
openKeys.Add(source.Id ?? string.Empty);
source.OpenToken = string.Join(StreamIdDelimeterString, openKeys.ToArray());
var openKeys = new List<string>
{
item.GetType().Name,
item.Id.ToString("N", CultureInfo.InvariantCulture),
source.Id ?? string.Empty
};
source.OpenToken = string.Join(StreamIdDelimeterString, openKeys);
}
// Dummy this up so that direct play checks can still run
@ -114,11 +109,12 @@ namespace Emby.Server.Implementations.LiveTv
}
}
_logger.LogDebug("MediaSources: {0}", _jsonSerializer.SerializeToString(list));
_logger.LogDebug("MediaSources: {@MediaSources}", list);
return list;
}
/// <inheritdoc />
public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.Globalization;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Buffers;
using System.Collections.Generic;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.IO;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.Globalization;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.Globalization;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.Globalization;

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
using System.Collections.Generic;
using System.IO;

@ -3,7 +3,7 @@
"ItemRemovedWithName": "{0} var fjarlægt úr safninu",
"ItemAddedWithName": "{0} var bætt í safnið",
"Inherit": "Erfa",
"HomeVideos": "Myndbönd að heiman",
"HomeVideos": "Heimamyndbönd",
"HeaderRecordingGroups": "Upptökuhópar",
"HeaderNextUp": "Næst á dagskrá",
"HeaderLiveTV": "Sjónvarp í beinni útsendingu",
@ -36,10 +36,10 @@
"NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
"NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
"NotificationOptionUserLockedOut": "Notandi læstur úti",
"NotificationOptionServerRestartRequired": "Endurræsing miðlara nauðsynileg",
"NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg",
"NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett",
"NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
"NotificationOptionPluginInstalled": "Viðbót settur upp",
"NotificationOptionPluginInstalled": "Viðbót sett upp",
"NotificationOptionPluginError": "Bilun í viðbót",
"NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
"NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
@ -50,15 +50,15 @@
"NameSeasonUnknown": "Sería óþekkt",
"NameSeasonNumber": "Sería {0}",
"MixedContent": "Blandað efni",
"MessageServerConfigurationUpdated": "Stillingar miðlarans hefur verið uppfærð",
"MessageApplicationUpdatedTo": "Jellyfin Server hefur verið uppfærður í {0}",
"MessageApplicationUpdated": "Jellyfin Server hefur verið uppfærður",
"MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
"MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
"MessageApplicationUpdated": "Jellyfin þjónn hefur verið uppfærður",
"Latest": "Nýjasta",
"LabelRunningTimeValue": "Keyrslutími kerfis: {0}",
"LabelRunningTimeValue": "spilunartími: {0}",
"User": "Notandi",
"System": "Kerfi",
"NotificationOptionNewLibraryContent": "Nýju efni bætt við",
"NewVersionIsAvailable": "Ný útgáfa af Jellyfin Server er fáanleg til niðurhals.",
"NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.",
"NameInstallFailed": "{0} uppsetning mistókst",
"MusicVideos": "Tónlistarmyndbönd",
"Music": "Tónlist",
@ -74,5 +74,23 @@
"PluginUpdatedWithName": "{0} var uppfært",
"PluginUninstalledWithName": "{0} var fjarlægt",
"PluginInstalledWithName": "{0} var sett upp",
"NotificationOptionTaskFailed": "Tímasett verkefni mistókst"
"NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
"StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.",
"VersionNumber": "Útgáfa {0}",
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
"UserStartedPlayingItemWithValues": "{0} er að spila {1} á {2}",
"UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir notanda {0}",
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
"UserLockedOutWithName": "Notanda {0} hefur verið hindraður aðgangur",
"UserDownloadingItemWithValues": "{0} Hleður niður {1}",
"SubtitlesDownloadedForItem": "Skjátextum halað niður fyrir {0}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
"ProviderValue": "Veitandi: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
"ValueSpecialEpisodeName": "Sérstakt - {0}",
"Shows": "Þættir",
"Playlists": "Spilunarlisti"
}

@ -62,8 +62,43 @@ namespace Emby.Server.Implementations.Session
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections =
new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer;
private DtoOptions _itemInfoDtoOptions;
private bool _disposed = false;
public SessionManager(
ILogger<SessionManager> logger,
IUserDataManager userDataManager,
ILibraryManager libraryManager,
IUserManager userManager,
IMusicManager musicManager,
IDtoService dtoService,
IImageProcessor imageProcessor,
IServerApplicationHost appHost,
IAuthenticationRepository authRepo,
IDeviceManager deviceManager,
IMediaSourceManager mediaSourceManager)
{
_logger = logger;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_userManager = userManager;
_musicManager = musicManager;
_dtoService = dtoService;
_imageProcessor = imageProcessor;
_appHost = appHost;
_authRepo = authRepo;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
}
/// <inheritdoc />
public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
/// <inheritdoc />
public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded;
/// <summary>
@ -81,40 +116,23 @@ namespace Emby.Server.Implementations.Session
/// </summary>
public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
/// <inheritdoc />
public event EventHandler<SessionEventArgs> SessionStarted;
/// <inheritdoc />
public event EventHandler<SessionEventArgs> CapabilitiesChanged;
/// <inheritdoc />
public event EventHandler<SessionEventArgs> SessionEnded;
/// <inheritdoc />
public event EventHandler<SessionEventArgs> SessionActivity;
public SessionManager(
IUserDataManager userDataManager,
ILoggerFactory loggerFactory,
ILibraryManager libraryManager,
IUserManager userManager,
IMusicManager musicManager,
IDtoService dtoService,
IImageProcessor imageProcessor,
IServerApplicationHost appHost,
IAuthenticationRepository authRepo,
IDeviceManager deviceManager,
IMediaSourceManager mediaSourceManager)
{
_userDataManager = userDataManager;
_logger = loggerFactory.CreateLogger(nameof(SessionManager));
_libraryManager = libraryManager;
_userManager = userManager;
_musicManager = musicManager;
_dtoService = dtoService;
_imageProcessor = imageProcessor;
_appHost = appHost;
_authRepo = authRepo;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
}
/// <summary>
/// Gets all connections.
/// </summary>
/// <value>All connections.</value>
public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate);
private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e)
{
@ -135,14 +153,17 @@ namespace Emby.Server.Implementations.Session
}
}
private bool _disposed = false;
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
@ -152,15 +173,17 @@ namespace Emby.Server.Implementations.Session
if (disposing)
{
// TODO: dispose stuff
_idleTimer?.Dispose();
}
_idleTimer = null;
_deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
_disposed = true;
}
public void CheckDisposed()
private void CheckDisposed()
{
if (_disposed)
{
@ -168,12 +191,6 @@ namespace Emby.Server.Implementations.Session
}
}
/// <summary>
/// Gets all connections.
/// </summary>
/// <value>All connections.</value>
public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate);
private void OnSessionStarted(SessionInfo info)
{
if (!string.IsNullOrEmpty(info.DeviceId))
@ -204,13 +221,13 @@ namespace Emby.Server.Implementations.Session
new SessionEventArgs
{
SessionInfo = info
},
_logger);
info.Dispose();
}
/// <inheritdoc />
public void UpdateDeviceName(string sessionId, string deviceName)
{
var session = GetSession(sessionId);
@ -230,7 +247,6 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
/// <exception cref="ArgumentNullException">user</exception>
public SessionInfo LogSessionActivity(
string appName,
string appVersion,
@ -268,14 +284,7 @@ namespace Emby.Server.Implementations.Session
if ((activityDate - userLastActivityDate).TotalSeconds > 60)
{
try
{
_userManager.UpdateUser(user);
}
catch (Exception ex)
{
_logger.LogError("Error updating user", ex);
}
_userManager.UpdateUser(user);
}
}
@ -292,18 +301,20 @@ namespace Emby.Server.Implementations.Session
return session;
}
/// <inheritdoc />
public void CloseIfNeeded(SessionInfo session)
{
if (!session.SessionControllers.Any(i => i.IsSessionActive))
{
var key = GetSessionKey(session.Client, session.DeviceId);
_activeConnections.TryRemove(key, out var removed);
_activeConnections.TryRemove(key, out _);
OnSessionEnded(session);
}
}
/// <inheritdoc />
public void ReportSessionEnded(string sessionId)
{
CheckDisposed();
@ -313,7 +324,7 @@ namespace Emby.Server.Implementations.Session
{
var key = GetSessionKey(session.Client, session.DeviceId);
_activeConnections.TryRemove(key, out var removed);
_activeConnections.TryRemove(key, out _);
OnSessionEnded(session);
}
@ -344,7 +355,7 @@ namespace Emby.Server.Implementations.Session
var runtimeTicks = libraryItem.RunTimeTicks;
MediaSourceInfo mediaSource = null;
if (libraryItem is IHasMediaSources hasMediaSources)
if (libraryItem is IHasMediaSources)
{
mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
@ -396,7 +407,6 @@ namespace Emby.Server.Implementations.Session
/// Removes the now playing item id.
/// </summary>
/// <param name="session">The session.</param>
/// <exception cref="ArgumentNullException">item</exception>
private void RemoveNowPlayingItem(SessionInfo session)
{
session.NowPlayingItem = null;
@ -409,9 +419,7 @@ namespace Emby.Server.Implementations.Session
}
private static string GetSessionKey(string appName, string deviceId)
{
return appName + deviceId;
}
=> appName + deviceId;
/// <summary>
/// Gets the connection.
@ -431,6 +439,7 @@ namespace Emby.Server.Implementations.Session
{
throw new ArgumentNullException(nameof(deviceId));
}
var key = GetSessionKey(appName, deviceId);
CheckDisposed();
@ -503,7 +512,7 @@ namespace Emby.Server.Implementations.Session
{
var users = new List<User>();
if (!session.UserId.Equals(Guid.Empty))
if (session.UserId != Guid.Empty)
{
var user = _userManager.GetUserById(session.UserId);
@ -522,8 +531,6 @@ namespace Emby.Server.Implementations.Session
return users;
}
private Timer _idleTimer;
private void StartIdleCheckTimer()
{
if (_idleTimer == null)
@ -599,11 +606,11 @@ namespace Emby.Server.Implementations.Session
}
/// <summary>
/// Used to report that playback has started for an item
/// Used to report that playback has started for an item.
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">info</exception>
/// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
public async Task OnPlaybackStart(PlaybackStartInfo info)
{
CheckDisposed();
@ -615,7 +622,7 @@ namespace Emby.Server.Implementations.Session
var session = GetSession(info.SessionId);
var libraryItem = info.ItemId.Equals(Guid.Empty)
var libraryItem = info.ItemId == Guid.Empty
? null
: GetNowPlayingItem(session, info.ItemId);
@ -653,7 +660,6 @@ namespace Emby.Server.Implementations.Session
ClientName = session.Client,
DeviceId = session.DeviceId,
Session = session
},
_logger);
@ -684,6 +690,7 @@ namespace Emby.Server.Implementations.Session
_userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
}
/// <inheritdoc />
public Task OnPlaybackProgress(PlaybackProgressInfo info)
{
return OnPlaybackProgress(info, false);
@ -857,7 +864,7 @@ namespace Emby.Server.Implementations.Session
{
MediaSourceInfo mediaSource = null;
if (libraryItem is IHasMediaSources hasMediaSources)
if (libraryItem is IHasMediaSources)
{
mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
}
@ -966,13 +973,17 @@ namespace Emby.Server.Implementations.Session
/// <param name="sessionId">The session identifier.</param>
/// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
/// <returns>SessionInfo.</returns>
/// <exception cref="ResourceNotFoundException">sessionId</exception>
/// <exception cref="ResourceNotFoundException">
/// No session with an Id equal to <c>sessionId</c> was found
/// and <c>throwOnMissing</c> is <c>true</c>.
/// </exception>
private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
{
var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
if (session == null && throwOnMissing)
{
throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId));
throw new ResourceNotFoundException(
string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
}
return session;
@ -985,12 +996,14 @@ namespace Emby.Server.Implementations.Session
if (session == null)
{
throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId));
throw new ResourceNotFoundException(
string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
}
return session;
}
/// <inheritdoc />
public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1011,6 +1024,7 @@ namespace Emby.Server.Implementations.Session
return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
}
/// <inheritdoc />
public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1055,6 +1069,7 @@ namespace Emby.Server.Implementations.Session
return Task.WhenAll(GetTasks());
}
/// <inheritdoc />
public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1096,7 +1111,8 @@ namespace Emby.Server.Implementations.Session
{
if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
{
throw new ArgumentException(string.Format("{0} is not allowed to play media.", user.Name));
throw new ArgumentException(
string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Name));
}
}
@ -1204,6 +1220,7 @@ namespace Emby.Server.Implementations.Session
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
}
/// <inheritdoc />
public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken)
{
var generalCommand = new GeneralCommand
@ -1220,6 +1237,7 @@ namespace Emby.Server.Implementations.Session
return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
}
/// <inheritdoc />
public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1303,12 +1321,12 @@ namespace Emby.Server.Implementations.Session
var session = GetSession(sessionId);
if (session.UserId.Equals(userId))
if (session.UserId == userId)
{
throw new ArgumentException("The requested user is already the primary user of the session.");
}
if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
if (session.AdditionalUsers.All(i => i.UserId != userId))
{
var user = _userManager.GetUserById(userId);
@ -1430,12 +1448,13 @@ namespace Emby.Server.Implementations.Session
private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
{
var existing = _authRepo.Get(new AuthenticationInfoQuery
{
DeviceId = deviceId,
UserId = user.Id,
Limit = 1
}).Items.FirstOrDefault();
var existing = _authRepo.Get(
new AuthenticationInfoQuery
{
DeviceId = deviceId,
UserId = user.Id,
Limit = 1
}).Items.FirstOrDefault();
var allExistingForDevice = _authRepo.Get(
new AuthenticationInfoQuery
@ -1460,7 +1479,7 @@ namespace Emby.Server.Implementations.Session
if (existing != null)
{
_logger.LogInformation("Reissuing access token: " + existing.AccessToken);
_logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken);
return existing.AccessToken;
}
@ -1485,6 +1504,7 @@ namespace Emby.Server.Implementations.Session
return newToken.AccessToken;
}
/// <inheritdoc />
public void Logout(string accessToken)
{
CheckDisposed();
@ -1494,18 +1514,20 @@ namespace Emby.Server.Implementations.Session
throw new ArgumentNullException(nameof(accessToken));
}
var existing = _authRepo.Get(new AuthenticationInfoQuery
{
Limit = 1,
AccessToken = accessToken
}).Items.FirstOrDefault();
var existing = _authRepo.Get(
new AuthenticationInfoQuery
{
Limit = 1,
AccessToken = accessToken
}).Items;
if (existing != null)
if (existing.Count > 0)
{
Logout(existing);
Logout(existing[0]);
}
}
/// <inheritdoc />
public void Logout(AuthenticationInfo existing)
{
CheckDisposed();
@ -1531,6 +1553,7 @@ namespace Emby.Server.Implementations.Session
}
}
/// <inheritdoc />
public void RevokeUserTokens(Guid userId, string currentAccessToken)
{
CheckDisposed();
@ -1549,6 +1572,7 @@ namespace Emby.Server.Implementations.Session
}
}
/// <inheritdoc />
public void RevokeToken(string token)
{
Logout(token);
@ -1605,8 +1629,6 @@ namespace Emby.Server.Implementations.Session
_deviceManager.SaveCapabilities(deviceId, capabilities);
}
private DtoOptions _itemInfoDtoOptions;
/// <summary>
/// Converts a BaseItem to a BaseItemInfo.
/// </summary>
@ -1683,6 +1705,7 @@ namespace Emby.Server.Implementations.Session
}
}
/// <inheritdoc />
public void ReportNowViewingItem(string sessionId, string itemId)
{
if (string.IsNullOrEmpty(itemId))
@ -1697,6 +1720,7 @@ namespace Emby.Server.Implementations.Session
ReportNowViewingItem(sessionId, info);
}
/// <inheritdoc />
public void ReportNowViewingItem(string sessionId, BaseItemDto item)
{
var session = GetSession(sessionId);
@ -1704,6 +1728,7 @@ namespace Emby.Server.Implementations.Session
session.NowViewingItem = item;
}
/// <inheritdoc />
public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
{
var session = Sessions.FirstOrDefault(i =>
@ -1715,11 +1740,13 @@ namespace Emby.Server.Implementations.Session
}
}
/// <inheritdoc />
public void ClearTranscodingInfo(string deviceId)
{
ReportTranscodingInfo(deviceId, null);
}
/// <inheritdoc />
public SessionInfo GetSession(string deviceId, string client, string version)
{
return Sessions.FirstOrDefault(i =>
@ -1727,6 +1754,7 @@ namespace Emby.Server.Implementations.Session
&& string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
}
/// <inheritdoc />
public SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion)
{
if (info == null)
@ -1759,23 +1787,24 @@ namespace Emby.Server.Implementations.Session
return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
}
/// <inheritdoc />
public SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
{
var result = _authRepo.Get(new AuthenticationInfoQuery
var items = _authRepo.Get(new AuthenticationInfoQuery
{
AccessToken = token
});
var info = result.Items.FirstOrDefault();
AccessToken = token,
Limit = 1
}).Items;
if (info == null)
if (items.Count == 0)
{
return null;
}
return GetSessionByAuthenticationToken(info, deviceId, remoteEndpoint, null);
return GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null);
}
/// <inheritdoc />
public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1785,6 +1814,7 @@ namespace Emby.Server.Implementations.Session
return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
}
/// <inheritdoc />
public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1796,11 +1826,10 @@ namespace Emby.Server.Implementations.Session
return Task.CompletedTask;
}
var data = dataFn();
return SendMessageToSessions(sessions, name, data, cancellationToken);
return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
}
/// <inheritdoc />
public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1809,6 +1838,7 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
/// <inheritdoc />
public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1817,22 +1847,5 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
public Task SendMessageToUserDeviceAndAdminSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
var sessions = Sessions
.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i));
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
private bool IsAdminSession(SessionInfo s)
{
var user = _userManager.GetUserById(s.UserId);
return user != null && user.Policy.IsAdministrator;
}
}
}

@ -6,7 +6,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Globalization;
using Microsoft.Extensions.Logging;
using SkiaSharp;
using static Jellyfin.Drawing.Skia.SkiaHelper;
@ -18,27 +17,23 @@ namespace Jellyfin.Drawing.Skia
/// </summary>
public class SkiaEncoder : IImageEncoder
{
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
private readonly ILocalizationManager _localizationManager;
private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
/// </summary>
/// <param name="logger">The application logger.</param>
/// <param name="appPaths">The application paths.</param>
/// <param name="localizationManager">The application localization manager.</param>
public SkiaEncoder(
ILogger<SkiaEncoder> logger,
IApplicationPaths appPaths,
ILocalizationManager localizationManager)
IApplicationPaths appPaths)
{
_logger = logger;
_appPaths = appPaths;
_localizationManager = localizationManager;
}
/// <inheritdoc/>
@ -235,9 +230,12 @@ namespace Jellyfin.Drawing.Skia
private bool RequiresSpecialCharacterHack(string path)
{
if (_localizationManager.HasUnicodeCategory(path, UnicodeCategory.OtherLetter))
for (int i = 0; i < path.Length; i++)
{
return true;
if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
{
return true;
}
}
if (HasDiacritics(path))

@ -168,7 +168,7 @@ namespace Jellyfin.Server
_loggerFactory,
options,
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
new NullImageEncoder(),
GetImageEncoder(appPaths),
new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
appConfig);
try
@ -192,8 +192,6 @@ namespace Jellyfin.Server
throw;
}
appHost.ImageProcessor.ImageEncoder = GetImageEncoder(appPaths, appHost.LocalizationManager);
await appHost.RunStartupTasksAsync().ConfigureAwait(false);
stopWatch.Stop();
@ -491,9 +489,7 @@ namespace Jellyfin.Server
}
}
private static IImageEncoder GetImageEncoder(
IApplicationPaths appPaths,
ILocalizationManager localizationManager)
private static IImageEncoder GetImageEncoder(IApplicationPaths appPaths)
{
try
{
@ -502,8 +498,7 @@ namespace Jellyfin.Server
return new SkiaEncoder(
_loggerFactory.CreateLogger<SkiaEncoder>(),
appPaths,
localizationManager);
appPaths);
}
catch (Exception ex)
{

@ -24,7 +24,7 @@ using Microsoft.Net.Http.Headers;
namespace MediaBrowser.Api.Images
{
/// <summary>
/// Class GetItemImage
/// Class GetItemImage.
/// </summary>
[Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")]
[Authenticated]
@ -558,21 +558,6 @@ namespace MediaBrowser.Api.Images
throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", displayText, request.Type));
}
IImageEnhancer[] supportedImageEnhancers;
if (_imageProcessor.ImageEnhancers.Count > 0)
{
if (item == null)
{
item = _libraryManager.GetItemById(itemId);
}
supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.GetSupportedEnhancers(item, request.Type).ToArray() : Array.Empty<IImageEnhancer>();
}
else
{
supportedImageEnhancers = Array.Empty<IImageEnhancer>();
}
bool cropwhitespace;
if (request.CropWhitespace.HasValue)
{
@ -598,25 +583,25 @@ namespace MediaBrowser.Api.Images
{"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
};
return GetImageResult(item,
return GetImageResult(
item,
itemId,
request,
imageInfo,
cropwhitespace,
outputFormats,
supportedImageEnhancers,
cacheDuration,
responseHeaders,
isHeadRequest);
}
private async Task<object> GetImageResult(BaseItem item,
private async Task<object> GetImageResult(
BaseItem item,
Guid itemId,
ImageRequest request,
ItemImageInfo image,
bool cropwhitespace,
IReadOnlyCollection<ImageFormat> supportedFormats,
IReadOnlyCollection<IImageEnhancer> enhancers,
TimeSpan? cacheDuration,
IDictionary<string, string> headers,
bool isHeadRequest)
@ -624,7 +609,6 @@ namespace MediaBrowser.Api.Images
var options = new ImageProcessingOptions
{
CropWhiteSpace = cropwhitespace,
Enhancers = enhancers,
Height = request.Height,
ImageIndex = request.Index ?? 0,
Image = image,

@ -250,11 +250,11 @@ namespace MediaBrowser.Api.Playback
{
if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
logFilePrefix = "ffmpeg-directstream";
logFilePrefix = "ffmpeg-remux";
}
else
{
logFilePrefix = "ffmpeg-remux";
logFilePrefix = "ffmpeg-directstream";
}
}

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
@ -20,20 +19,12 @@ namespace MediaBrowser.Controller.Drawing
/// <value>The supported input formats.</value>
IReadOnlyCollection<string> SupportedInputFormats { get; }
/// <summary>
/// Gets the image enhancers.
/// </summary>
/// <value>The image enhancers.</value>
IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; }
/// <summary>
/// Gets a value indicating whether [supports image collage creation].
/// </summary>
/// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
bool SupportsImageCollageCreation { get; }
IImageEncoder ImageEncoder { get; set; }
/// <summary>
/// Gets the dimensions of the image.
/// </summary>
@ -58,14 +49,6 @@ namespace MediaBrowser.Controller.Drawing
/// <returns>ImageDimensions</returns>
ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem);
/// <summary>
/// Gets the supported enhancers.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="imageType">Type of the image.</param>
/// <returns>IEnumerable{IImageEnhancer}.</returns>
IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType);
/// <summary>
/// Gets the image cache tag.
/// </summary>
@ -75,15 +58,6 @@ namespace MediaBrowser.Controller.Drawing
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
string GetImageCacheTag(BaseItem item, ChapterInfo info);
/// <summary>
/// Gets the image cache tag.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="image">The image.</param>
/// <param name="imageEnhancers">The image enhancers.</param>
/// <returns>Guid.</returns>
string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers);
/// <summary>
/// Processes the image.
/// </summary>
@ -99,15 +73,6 @@ namespace MediaBrowser.Controller.Drawing
/// <returns>Task.</returns>
Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options);
/// <summary>
/// Gets the enhanced image.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="imageType">Type of the image.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns>Task{System.String}.</returns>
Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex);
/// <summary>
/// Gets the supported image output formats.
/// </summary>

@ -19,8 +19,6 @@ namespace MediaBrowser.Controller.Drawing
return GetSizeEstimate(options);
}
public static IImageProcessor ImageProcessor { get; set; }
private static ImageDimensions GetSizeEstimate(ImageProcessingOptions options)
{
if (options.Width.HasValue && options.Height.HasValue)

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
namespace MediaBrowser.Controller.Drawing
@ -34,8 +33,6 @@ namespace MediaBrowser.Controller.Drawing
public int Quality { get; set; }
public IReadOnlyCollection<IImageEnhancer> Enhancers { get; set; }
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; }
public bool AddPlayedIndicator { get; set; }

@ -41,10 +41,10 @@ namespace MediaBrowser.Controller.Entities
public override double GetDefaultPrimaryImageAspectRatio()
{
// REVIEW: @bond
if (Width.HasValue && Height.HasValue)
if (Width != 0 && Height != 0)
{
double width = Width.Value;
double height = Height.Value;
double width = Width;
double height = Height;
if (Orientation.HasValue)
{
@ -67,8 +67,6 @@ namespace MediaBrowser.Controller.Entities
return base.GetDefaultPrimaryImageAspectRatio();
}
public new int? Width { get; set; }
public new int? Height { get; set; }
public string CameraMake { get; set; }
public string CameraModel { get; set; }
public string Software { get; set; }

@ -157,7 +157,8 @@ namespace MediaBrowser.Controller.Library
/// <param name="introProviders">The intro providers.</param>
/// <param name="itemComparers">The item comparers.</param>
/// <param name="postscanTasks">The postscan tasks.</param>
void AddParts(IEnumerable<IResolverIgnoreRule> rules,
void AddParts(
IEnumerable<IResolverIgnoreRule> rules,
IEnumerable<IItemResolver> resolvers,
IEnumerable<IIntroProvider> introProviders,
IEnumerable<IBaseItemComparer> itemComparers,
@ -349,9 +350,6 @@ namespace MediaBrowser.Controller.Library
/// <returns><c>true</c> if [is audio file] [the specified path]; otherwise, <c>false</c>.</returns>
bool IsAudioFile(string path);
bool IsAudioFile(string path, LibraryOptions libraryOptions);
bool IsVideoFile(string path, LibraryOptions libraryOptions);
/// <summary>
/// Gets the season number from path.
/// </summary>

@ -1,3 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

@ -316,11 +316,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
{
var size = new ImageDimensions
{
Width = VideoStream.Width.Value,
Height = VideoStream.Height.Value
};
var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
var newSize = DrawingUtils.Resize(size,
BaseRequest.Width ?? 0,
@ -346,11 +342,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
{
var size = new ImageDimensions
{
Width = VideoStream.Width.Value,
Height = VideoStream.Height.Value
};
var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
var newSize = DrawingUtils.Resize(size,
BaseRequest.Width ?? 0,

@ -1,61 +0,0 @@
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Providers
{
public interface IImageEnhancer
{
/// <summary>
/// Return true only if the given image for the given item will be enhanced by this enhancer.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="imageType">Type of the image.</param>
/// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns>
bool Supports(BaseItem item, ImageType imageType);
/// <summary>
/// Gets the priority or order in which this enhancer should be run.
/// </summary>
/// <value>The priority.</value>
MetadataProviderPriority Priority { get; }
/// <summary>
/// Return a key incorporating all configuration information related to this item
/// </summary>
/// <param name="item">The item.</param>
/// <param name="imageType">Type of the image.</param>
/// <returns>Cache key relating to the current state of this item and configuration</returns>
string GetConfigurationCacheKey(BaseItem item, ImageType imageType);
/// <summary>
/// Gets the size of the enhanced image.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="imageType">Type of the image.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <param name="originalImageSize">Size of the original image.</param>
/// <returns>ImageSize.</returns>
ImageDimensions GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageDimensions originalImageSize);
EnhancedImageInfo GetEnhancedImageInfo(BaseItem item, string inputFile, ImageType imageType, int imageIndex);
/// <summary>
/// Enhances the image async.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="inputFile">The input file.</param>
/// <param name="outputFile">The output file.</param>
/// <param name="imageType">Type of the image.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns>Task{Image}.</returns>
/// <exception cref="System.ArgumentNullException"></exception>
Task EnhanceImageAsync(BaseItem item, string inputFile, string outputFile, ImageType imageType, int imageIndex);
}
public class EnhancedImageInfo
{
public bool RequiresTransparency { get; set; }
}
}

@ -1,39 +1,46 @@
#pragma warning disable CS1591
#pragma warning disable SA1600
using System.Globalization;
namespace MediaBrowser.Model.Drawing
{
/// <summary>
/// Struct ImageDimensions.
/// </summary>
public struct ImageDimensions
public readonly struct ImageDimensions
{
public ImageDimensions(int width, int height)
{
Width = width;
Height = height;
}
/// <summary>
/// Gets or sets the height.
/// Gets the height.
/// </summary>
/// <value>The height.</value>
public int Height { get; set; }
public int Height { get; }
/// <summary>
/// Gets or sets the width.
/// Gets the width.
/// </summary>
/// <value>The width.</value>
public int Width { get; set; }
public int Width { get; }
public bool Equals(ImageDimensions size)
{
return Width.Equals(size.Width) && Height.Equals(size.Height);
}
/// <inheritdoc />
public override string ToString()
{
return string.Format("{0}-{1}", Width, Height);
}
public ImageDimensions(int width, int height)
{
Width = width;
Height = height;
return string.Format(
CultureInfo.InvariantCulture,
"{0}-{1}",
Width,
Height);
}
}
}

@ -30,5 +30,11 @@ namespace MediaBrowser.Model.Querying
{
Items = Array.Empty<T>();
}
public QueryResult(IReadOnlyList<T> items)
{
Items = items;
TotalRecordCount = items.Count;
}
}
}

@ -5,6 +5,8 @@
<Rule Id="SA1202" Action="Info" />
<!-- disable warning SA1204: Static members must appear before non-static members -->
<Rule Id="SA1204" Action="Info" />
<!-- disable warning SA1404: Code analysis suppression should have justification -->
<Rule Id="SA1404" Action="Info" />
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />

@ -6,388 +6,78 @@ namespace Jellyfin.Naming.Tests.TV
{
public class EpisodeNumberTests
{
[Fact]
public void TestEpisodeNumber1()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/S02E03 blah.avi"));
}
[Fact]
public void TestEpisodeNumber40()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber41()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/01x02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber42()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01x02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber43()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01E02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber44()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/Elementary - 02x03-04-15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber45()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/S01xE02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber46()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01E02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber47()
{
Assert.Equal(36, GetEpisodeNumberFromFile(@"Season 2/[HorribleSubs] Hunter X Hunter - 136 [720p].mkv"));
}
[Fact]
public void TestEpisodeNumber52()
{
Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode - 16.avi"));
}
[Fact]
public void TestEpisodeNumber53()
{
Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode 16.avi"));
}
[Fact]
public void TestEpisodeNumber54()
{
Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/Episode 16 - Some Title.avi"));
}
[Fact]
public void TestEpisodeNumber57()
{
Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 Some Title.avi"));
}
[Fact]
public void TestEpisodeNumber58()
{
Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 - 12 Some Title.avi"));
}
[Fact]
public void TestEpisodeNumber59()
{
Assert.Equal(7, GetEpisodeNumberFromFile(@"Season 2/7 - 12 Angry Men.avi"));
}
// FIXME
// [Fact]
public void TestEpisodeNumber60()
{
Assert.Equal(16, GetEpisodeNumberFromFile(@"Season 2/16 12 Some Title.avi"));
}
// FIXME
// [Fact]
public void TestEpisodeNumber61()
{
Assert.Equal(7, GetEpisodeNumberFromFile(@"Season 2/7 12 Angry Men.avi"));
}
// FIXME
// [Fact]
public void TestEpisodeNumber62()
{
// This is not supported. Expected to fail, although it would be a good one to add support for.
Assert.Equal(3, GetEpisodeNumberFromFile(@"Season 4/Uchuu.Senkan.Yamato.2199.E03.avi"));
}
[Fact]
public void TestEpisodeNumber63()
{
Assert.Equal(3, GetEpisodeNumberFromFile(@"Season 4/Uchuu.Senkan.Yamato.2199.S04E03.avi"));
}
[Fact]
public void TestEpisodeNumber64()
{
Assert.Equal(368, GetEpisodeNumberFromFile(@"Running Man/Running Man S2017E368.mkv"));
}
// FIXME
// [Fact]
public void TestEpisodeNumber65()
{
// Not supported yet
Assert.Equal(7, GetEpisodeNumberFromFile(@"/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv"));
}
[Fact]
public void TestEpisodeNumber30()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4"));
}
// FIXME
// [Fact]
public void TestEpisodeNumber31()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname 01x02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber32()
{
Assert.Equal(9, GetEpisodeNumberFromFile(@"Season 25/The Simpsons.S25E09.Steal this episode.mp4"));
}
[Fact]
public void TestEpisodeNumber33()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01x02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber34()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber35()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/seriesname S01xE02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber36()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03 - x04 - x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber37()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber38()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03x04x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber39()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03x04x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber20()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03-04-15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber21()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/02x03-E15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber22()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/Elementary - 02x03-E15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber23()
{
Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4"));
}
[Fact]
public void TestEpisodeNumber24()
{
Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 2009/S2009E23-E24-E26 - The Woman.mp4"));
}
[Fact]
public void TestEpisodeNumber25()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/2009x02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber26()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009x02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber27()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009E02 blah.avi"));
}
// FIXME
// [Fact]
public void TestEpisodeNumber28()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname 2009x02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber29()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber11()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03x04x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber12()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber13()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/S2009xE02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber14()
{
Assert.Equal(23, GetEpisodeNumberFromFile(@"Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4"));
}
[Fact]
public void TestEpisodeNumber15()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009xE02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber16()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03-E15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber17()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009E02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber18()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber19()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03 - x04 - x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber2()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2009/seriesname S2009x02 blah.avi"));
}
[Fact]
public void TestEpisodeNumber3()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber4()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber5()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/2009x03-04-15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber6()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4"));
}
[Fact]
public void TestEpisodeNumber7()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/02 - blah-02 a.avi"));
}
[Fact]
public void TestEpisodeNumber8()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 1/02 - blah.avi"));
}
[Fact]
public void TestEpisodeNumber9()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/02 - blah 14 blah.avi"));
}
[Fact]
public void TestEpisodeNumber10()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/02.avi"));
}
[Fact]
public void TestEpisodeNumber48()
{
Assert.Equal(02, GetEpisodeNumberFromFile(@"Season 2/2. Infestation.avi"));
}
[Fact]
public void TestEpisodeNumber49()
{
Assert.Equal(7, GetEpisodeNumberFromFile(@"The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi"));
}
private int? GetEpisodeNumberFromFile(string path)
{
var options = new NamingOptions();
var result = new EpisodePathParser(options)
private readonly NamingOptions _namingOptions = new NamingOptions();
[Theory]
[InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", 3)]
[InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", 22)]
[InlineData("Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", 1)]
[InlineData("After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", 6)]
[InlineData("Season 02/S02E03 blah.avi", 3)]
[InlineData("Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4", 3)]
[InlineData("Season 02/02x03 - x04 - x15 - Ep Name.mp4", 3)]
[InlineData("Season 1/01x02 blah.avi", 2)]
[InlineData("Season 1/S01x02 blah.avi", 2)]
[InlineData("Season 1/S01E02 blah.avi", 2)]
[InlineData("Season 2/Elementary - 02x03-04-15 - Ep Name.mp4", 3)]
[InlineData("Season 1/S01xE02 blah.avi", 2)]
[InlineData("Season 1/seriesname S01E02 blah.avi", 2)]
[InlineData("Season 2/Episode - 16.avi", 16)]
[InlineData("Season 2/Episode 16.avi", 16)]
[InlineData("Season 2/Episode 16 - Some Title.avi", 16)]
[InlineData("Season 2/16 Some Title.avi", 16)]
[InlineData("Season 2/16 - 12 Some Title.avi", 16)]
[InlineData("Season 2/7 - 12 Angry Men.avi", 7)]
[InlineData("Season 1/seriesname 01x02 blah.avi", 2)]
[InlineData("Season 25/The Simpsons.S25E09.Steal this episode.mp4", 9)]
[InlineData("Season 1/seriesname S01x02 blah.avi", 2)]
[InlineData("Season 2/Elementary - 02x03 - 02x04 - 02x15 - Ep Name.mp4", 3)]
[InlineData("Season 1/seriesname S01xE02 blah.avi", 2)]
[InlineData("Season 02/Elementary - 02x03 - x04 - x15 - Ep Name.mp4", 3)]
[InlineData("Season 02/Elementary - 02x03x04x15 - Ep Name.mp4", 3)]
[InlineData("Season 2/02x03-04-15 - Ep Name.mp4", 3)]
[InlineData("Season 02/02x03-E15 - Ep Name.mp4", 3)]
[InlineData("Season 02/Elementary - 02x03-E15 - Ep Name.mp4", 3)]
[InlineData("Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", 23)]
[InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 23)]
[InlineData("Season 2009/2009x02 blah.avi", 2)]
[InlineData("Season 2009/S2009x02 blah.avi", 2)]
[InlineData("Season 2009/S2009E02 blah.avi", 2)]
[InlineData("Season 2009/seriesname 2009x02 blah.avi", 2)]
[InlineData("Season 2009/Elementary - 2009x03x04x15 - Ep Name.mp4", 3)]
[InlineData("Season 2009/2009x03x04x15 - Ep Name.mp4", 3)]
[InlineData("Season 2009/Elementary - 2009x03-E15 - Ep Name.mp4", 3)]
[InlineData("Season 2009/S2009xE02 blah.avi", 2)]
[InlineData("Season 2009/Elementary - S2009E23-E24-E26 - The Woman.mp4", 23)]
[InlineData("Season 2009/seriesname S2009xE02 blah.avi", 2)]
[InlineData("Season 2009/2009x03-E15 - Ep Name.mp4", 3)]
[InlineData("Season 2009/seriesname S2009E02 blah.avi", 2)]
[InlineData("Season 2009/2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 3)]
[InlineData("Season 2009/2009x03 - x04 - x15 - Ep Name.mp4", 3)]
[InlineData("Season 2009/seriesname S2009x02 blah.avi", 2)]
[InlineData("Season 2009/Elementary - 2009x03 - 2009x04 - 2009x15 - Ep Name.mp4", 3)]
[InlineData("Season 2009/Elementary - 2009x03-04-15 - Ep Name.mp4", 3)]
[InlineData("Season 2009/2009x03-04-15 - Ep Name.mp4", 3)]
[InlineData("Season 2009/Elementary - 2009x03 - x04 - x15 - Ep Name.mp4", 3)]
[InlineData("Season 1/02 - blah-02 a.avi", 2)]
[InlineData("Season 1/02 - blah.avi", 2)]
[InlineData("Season 2/02 - blah 14 blah.avi", 2)]
[InlineData("Season 2/02.avi", 2)]
[InlineData("Season 2/2. Infestation.avi", 2)]
[InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", 7)]
[InlineData("Running Man/Running Man S2017E368.mkv", 368)]
// TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)]
// TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)]
// TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)]
// TODO: [InlineData("Season 2/7 12 Angry Men.avi", 7)]
// TODO: [InlineData("Season 02/02x03x04x15 - Ep Name.mp4", 2)]
// TODO: [InlineData("Season 2/[HorribleSubs] Hunter X Hunter - 136 [720p].mkv", 136)]
public void GetEpisodeNumberFromFileTest(string path, int? expected)
{
var result = new EpisodePathParser(_namingOptions)
.Parse(path, false);
return result.EpisodeNumber;
Assert.Equal(expected, result.EpisodeNumber);
}
}
}

@ -6,11 +6,21 @@ namespace Jellyfin.Naming.Tests.TV
{
public class SeasonNumberTests
{
private int? GetSeasonNumberFromEpisodeFile(string path)
private readonly NamingOptions _namingOptions = new NamingOptions();
[Theory]
[InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", 25)]
public void GetSeasonNumberFromEpisodeFileTest(string path, int? expected)
{
var options = new NamingOptions();
var result = new EpisodeResolver(_namingOptions)
.Resolve(path, false);
var result = new EpisodeResolver(options)
Assert.Equal(expected, result.SeasonNumber);
}
private int? GetSeasonNumberFromEpisodeFile(string path)
{
var result = new EpisodeResolver(_namingOptions)
.Resolve(path, false);
return result.SeasonNumber;

Loading…
Cancel
Save