Merge pull request #21 from jellyfin/master

nightly
This commit is contained in:
artiume 2020-02-23 10:57:52 -05:00 committed by GitHub
commit 697aee5b0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 833 additions and 1393 deletions

View file

@ -17,4 +17,16 @@
<Compile Include="..\SharedVersion.cs" /> <Compile Include="..\SharedVersion.cs" />
</ItemGroup> </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> </Project>

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -11,7 +10,6 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -23,7 +21,7 @@ namespace Emby.Drawing
/// <summary> /// <summary>
/// Class ImageProcessor. /// Class ImageProcessor.
/// </summary> /// </summary>
public class ImageProcessor : IImageProcessor, IDisposable public sealed class ImageProcessor : IImageProcessor, IDisposable
{ {
// Increment this when there's a change requiring caches to be invalidated // Increment this when there's a change requiring caches to be invalidated
private const string Version = "3"; private const string Version = "3";
@ -31,28 +29,24 @@ namespace Emby.Drawing
private static readonly HashSet<string> _transparentImageTypes private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
/// <summary>
/// The _logger
/// </summary>
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths; private readonly IServerApplicationPaths _appPaths;
private IImageEncoder _imageEncoder; private readonly IImageEncoder _imageEncoder;
private readonly Func<ILibraryManager> _libraryManager; private readonly Func<ILibraryManager> _libraryManager;
private readonly Func<IMediaEncoder> _mediaEncoder; private readonly Func<IMediaEncoder> _mediaEncoder;
private readonly Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>();
private bool _disposed = false; private bool _disposed = false;
/// <summary> /// <summary>
/// /// Initializes a new instance of the <see cref="ImageProcessor"/> class.
/// </summary> /// </summary>
/// <param name="logger"></param> /// <param name="logger">The logger.</param>
/// <param name="appPaths"></param> /// <param name="appPaths">The server application paths.</param>
/// <param name="fileSystem"></param> /// <param name="fileSystem">The filesystem.</param>
/// <param name="imageEncoder"></param> /// <param name="imageEncoder">The image encoder.</param>
/// <param name="libraryManager"></param> /// <param name="libraryManager">The library manager.</param>
/// <param name="mediaEncoder"></param> /// <param name="mediaEncoder">The media encoder.</param>
public ImageProcessor( public ImageProcessor(
ILogger<ImageProcessor> logger, ILogger<ImageProcessor> logger,
IServerApplicationPaths appPaths, IServerApplicationPaths appPaths,
@ -67,16 +61,10 @@ namespace Emby.Drawing
_libraryManager = libraryManager; _libraryManager = libraryManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_appPaths = appPaths; _appPaths = appPaths;
ImageEnhancers = Array.Empty<IImageEnhancer>();
ImageHelper.ImageProcessor = this;
} }
private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images");
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<string> SupportedInputFormats => public IReadOnlyCollection<string> SupportedInputFormats =>
new HashSet<string>(StringComparer.OrdinalIgnoreCase) new HashSet<string>(StringComparer.OrdinalIgnoreCase)
@ -89,9 +77,7 @@ namespace Emby.Drawing
"aiff", "aiff",
"cr2", "cr2",
"crw", "crw",
"nef",
// Remove until supported
//"nef",
"orf", "orf",
"pef", "pef",
"arw", "arw",
@ -110,19 +96,9 @@ namespace Emby.Drawing
"wbmp" "wbmp"
}; };
/// <inheritdoc />
public IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
/// <inheritdoc />
public IImageEncoder ImageEncoder
{
get => _imageEncoder;
set => _imageEncoder = value ?? throw new ArgumentNullException(nameof(value));
}
/// <inheritdoc /> /// <inheritdoc />
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
{ {
@ -150,6 +126,8 @@ namespace Emby.Drawing
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
var libraryManager = _libraryManager();
ItemImageInfo originalImage = options.Image; ItemImageInfo originalImage = options.Image;
BaseItem item = options.Item; BaseItem item = options.Item;
@ -157,9 +135,10 @@ namespace Emby.Drawing
{ {
if (item == null) 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; string originalImagePath = originalImage.Path;
@ -186,27 +165,6 @@ namespace Emby.Drawing
dateModified = supportedImageInfo.dateModified; dateModified = supportedImageInfo.dateModified;
bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath)); 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; bool autoOrient = false;
ImageOrientation? orientation = null; ImageOrientation? orientation = null;
if (item is Photo photo) if (item is Photo photo)
@ -239,12 +197,6 @@ namespace Emby.Drawing
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency); 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); 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 try
{ {
if (!File.Exists(cacheFilePath)) if (!File.Exists(cacheFilePath))
@ -270,10 +222,6 @@ namespace Emby.Drawing
_logger.LogError(ex, "Error encoding image"); _logger.LogError(ex, "Error encoding image");
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
} }
finally
{
ReleaseLock(cacheFilePath, lockInfo);
}
} }
private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency) private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
@ -305,20 +253,18 @@ namespace Emby.Drawing
} }
private string GetMimeType(ImageFormat format, string path) private string GetMimeType(ImageFormat format, string path)
{ => format switch
switch(format)
{ {
case ImageFormat.Bmp: return MimeTypes.GetMimeType("i.bmp"); ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
case ImageFormat.Gif: return MimeTypes.GetMimeType("i.gif"); ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
case ImageFormat.Jpg: return MimeTypes.GetMimeType("i.jpg"); ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
case ImageFormat.Png: return MimeTypes.GetMimeType("i.png"); ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
case ImageFormat.Webp: return MimeTypes.GetMimeType("i.webp"); ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
default: return MimeTypes.GetMimeType(path); _ => MimeTypes.GetMimeType(path)
} };
}
/// <summary> /// <summary>
/// Gets the cache file path based on a set of parameters /// Gets the cache file path based on a set of parameters.
/// </summary> /// </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) 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 /> /// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ItemImageInfo image) public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
{ => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
var supportedEnhancers = GetSupportedEnhancers(item, image.Type).ToArray();
return GetImageCacheTag(item, image, supportedEnhancers);
}
/// <inheritdoc /> /// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) 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) private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
{ {
var inputFormat = Path.GetExtension(originalImagePath) var inputFormat = Path.GetExtension(originalImagePath)
@ -487,154 +409,6 @@ namespace Emby.Drawing
return (originalImagePath, dateModified); 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> /// <summary>
/// Gets the cache path. /// Gets the cache path.
/// </summary> /// </summary>
@ -647,7 +421,7 @@ namespace Emby.Drawing
/// or /// or
/// uniqueName /// uniqueName
/// or /// or
/// fileExtension /// fileExtension.
/// </exception> /// </exception>
public string GetCachePath(string path, string uniqueName, string fileExtension) public string GetCachePath(string path, string uniqueName, string fileExtension)
{ {
@ -680,7 +454,7 @@ namespace Emby.Drawing
/// <exception cref="ArgumentNullException"> /// <exception cref="ArgumentNullException">
/// path /// path
/// or /// or
/// filename /// filename.
/// </exception> /// </exception>
public string GetCachePath(string path, string filename) public string GetCachePath(string path, string filename)
{ {
@ -688,6 +462,7 @@ namespace Emby.Drawing
{ {
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
} }
if (string.IsNullOrEmpty(filename)) if (string.IsNullOrEmpty(filename))
{ {
throw new ArgumentNullException(nameof(filename)); throw new ArgumentNullException(nameof(filename));
@ -708,75 +483,20 @@ namespace Emby.Drawing
_logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath); _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
} }
/// <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)
{
info.Lock.Release();
lock (_locks)
{
info.Count--;
if (info.Count <= 0)
{
_locks.Remove(key);
info.Lock.Dispose();
}
}
}
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{
_disposed = true;
var disposable = _imageEncoder as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
private void CheckDisposed()
{ {
if (_disposed) if (_disposed)
{ {
throw new ObjectDisposedException(GetType().Name); return;
} }
if (_imageEncoder is IDisposable disposable)
{
disposable.Dispose();
}
_disposed = true;
} }
} }
} }

View file

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

View file

@ -277,7 +277,7 @@ namespace Emby.Naming.Common
// This isn't a Kodi naming rule, but the expression below causes false positives, // This isn't a Kodi naming rule, but the expression below causes false positives,
// so we make sure this one gets tested first. // so we make sure this one gets tested first.
// "Foo Bar 889" // "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 IsNamed = true
}, },

View file

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

View file

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

View file

@ -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 // For directories, dummy up an extension otherwise the expressions will fail
var input = !file.IsDirectory var input = !file.IsDirectory
@ -204,7 +204,7 @@ namespace Emby.Naming.Video
return Path.GetFileName(input); 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); var regexInput = GetRegexInput(input);

View file

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

View file

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

View file

@ -4,6 +4,8 @@
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -16,4 +18,16 @@
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup> </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> </Project>

View file

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

View file

@ -21,70 +21,85 @@ using Microsoft.Extensions.Logging;
namespace Emby.Notifications namespace Emby.Notifications
{ {
/// <summary> /// <summary>
/// Creates notifications for various system events /// Creates notifications for various system events.
/// </summary> /// </summary>
public class Notifications : IServerEntryPoint public class NotificationEntryPoint : IServerEntryPoint
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IActivityManager _activityManager;
private readonly ILocalizationManager _localization;
private readonly INotificationManager _notificationManager; private readonly INotificationManager _notificationManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private Timer LibraryUpdateTimer { get; set; }
private readonly object _libraryChangedSyncLock = new object();
private readonly IConfigurationManager _config; private readonly IConfigurationManager _config;
private readonly ILocalizationManager _localization;
private readonly IActivityManager _activityManager; private readonly object _libraryChangedSyncLock = new object();
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
private Timer? _libraryUpdateTimer;
private string[] _coreNotificationTypes; 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, IActivityManager activityManager,
ILocalizationManager localization, ILocalizationManager localization,
ILogger logger,
INotificationManager notificationManager, INotificationManager notificationManager,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IServerApplicationHost appHost, IServerApplicationHost appHost,
IConfigurationManager config) IConfigurationManager config)
{ {
_logger = logger; _logger = logger;
_activityManager = activityManager;
_localization = localization;
_notificationManager = notificationManager; _notificationManager = notificationManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_appHost = appHost; _appHost = appHost;
_config = config; _config = config;
_localization = localization;
_activityManager = activityManager;
_coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray(); _coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray();
} }
/// <inheritdoc />
public Task RunAsync() public Task RunAsync()
{ {
_libraryManager.ItemAdded += _libraryManager_ItemAdded; _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged += _appHost_HasPendingRestartChanged; _appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged += _appHost_HasUpdateAvailableChanged; _appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated += _activityManager_EntryCreated; _activityManager.EntryCreated += OnActivityManagerEntryCreated;
return Task.CompletedTask; 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 type = NotificationType.ServerRestartRequired.ToString();
var notification = new NotificationRequest var notification = new NotificationRequest
{ {
NotificationType = type, 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); 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; var entry = e.Argument;
@ -117,7 +132,7 @@ namespace Emby.Notifications
return _config.GetConfiguration<NotificationOptions>("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) if (!_appHost.HasUpdateAvailable)
{ {
@ -136,8 +151,7 @@ namespace Emby.Notifications
await SendNotification(notification, null).ConfigureAwait(false); await SendNotification(notification, null).ConfigureAwait(false);
} }
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>(); private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
private void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
{ {
if (!FilterItem(e.Item)) if (!FilterItem(e.Item))
{ {
@ -146,14 +160,17 @@ namespace Emby.Notifications
lock (_libraryChangedSyncLock) lock (_libraryChangedSyncLock)
{ {
if (LibraryUpdateTimer == null) if (_libraryUpdateTimer == null)
{ {
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, 5000, _libraryUpdateTimer = new Timer(
Timeout.Infinite); LibraryUpdateTimerCallback,
null,
5000,
Timeout.Infinite);
} }
else else
{ {
LibraryUpdateTimer.Change(5000, Timeout.Infinite); _libraryUpdateTimer.Change(5000, Timeout.Infinite);
} }
_itemsAdded.Add(e.Item); _itemsAdded.Add(e.Item);
@ -188,7 +205,8 @@ namespace Emby.Notifications
{ {
items = _itemsAdded.ToList(); items = _itemsAdded.ToList();
_itemsAdded.Clear(); _itemsAdded.Clear();
DisposeLibraryUpdateTimer(); _libraryUpdateTimer!.Dispose(); // Shouldn't be null as it just set off this callback
_libraryUpdateTimer = null;
} }
items = items.Take(10).ToList(); items = items.Take(10).ToList();
@ -198,7 +216,10 @@ namespace Emby.Notifications
var notification = new NotificationRequest var notification = new NotificationRequest
{ {
NotificationType = NotificationType.NewLibraryContent.ToString(), 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 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) public static string GetItemName(BaseItem item)
{ {
var name = item.Name; var name = item.Name;
@ -219,6 +245,7 @@ namespace Emby.Notifications
episode.IndexNumber.Value, episode.IndexNumber.Value,
name); name);
} }
if (episode.ParentIndexNumber.HasValue) if (episode.ParentIndexNumber.HasValue)
{ {
name = string.Format( name = string.Format(
@ -229,7 +256,6 @@ namespace Emby.Notifications
} }
} }
if (item is IHasSeries hasSeries) if (item is IHasSeries hasSeries)
{ {
name = hasSeries.SeriesName + " - " + name; name = hasSeries.SeriesName + " - " + name;
@ -257,7 +283,7 @@ namespace Emby.Notifications
return name; return name;
} }
private async Task SendNotification(NotificationRequest notification, BaseItem relatedItem) private async Task SendNotification(NotificationRequest notification, BaseItem? relatedItem)
{ {
try try
{ {
@ -269,23 +295,37 @@ namespace Emby.Notifications
} }
} }
/// <inheritdoc />
public void Dispose() public void Dispose()
{ {
DisposeLibraryUpdateTimer(); Dispose(true);
GC.SuppressFinalize(this);
_libraryManager.ItemAdded -= _libraryManager_ItemAdded;
_appHost.HasPendingRestartChanged -= _appHost_HasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged -= _appHost_HasUpdateAvailableChanged;
_activityManager.EntryCreated -= _activityManager_EntryCreated;
} }
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)
{ {
LibraryUpdateTimer.Dispose(); return;
LibraryUpdateTimer = null;
} }
if (disposing)
{
_libraryUpdateTimer?.Dispose();
}
_libraryUpdateTimer = null;
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
_disposed = true;
} }
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -3521,20 +3521,6 @@ namespace Emby.Server.Implementations.Data
} }
var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); 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 // Only specify excluded types if no included types are specified
if (includeTypes.Length == 0) if (includeTypes.Length == 0)
{ {
@ -3553,6 +3539,19 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add($"type not in ({inClause})"); 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) 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 // Not crazy about having this all the way down here, but at least it's in one place
readonly Dictionary<string, string[]> _types = GetTypeMapDictionary(); readonly Dictionary<string, string[]> _types = GetTypeMapDictionary();
private IEnumerable<string> MapIncludeItemTypes(string value) private string[] MapIncludeItemTypes(string value)
{ {
if (_types.TryGetValue(value, out string[] result)) 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; 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) 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) 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.Genres.Select(i => (2, i)));
list.AddRange(item.Studios.Select(i => new Tuple<int, string>(3, i))); list.AddRange(item.Studios.Select(i => (3, i)));
list.AddRange(item.Tags.Select(i => new Tuple<int, string>(4, i))); list.AddRange(item.Tags.Select(i => (4, i)));
// keywords was 5 // keywords was 5
list.AddRange(inheritedTags.Select(i => new Tuple<int, string>(6, i))); list.AddRange(inheritedTags.Select(i => (6, i)));
return list; 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)) 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); 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 startIndex = 0;
var limit = 100; var limit = 100;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.HttpClientManager
if (!string.IsNullOrWhiteSpace(userInfo)) if (!string.IsNullOrWhiteSpace(userInfo))
{ {
_logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url); _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); var request = new HttpRequestMessage(method, url);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -80,14 +80,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
} }
// Avoid mis-identifying top folders // Avoid mis-identifying top folders
if (args.Parent.IsRoot) return null; if (args.Parent.IsRoot)
{
return null;
}
var directoryService = args.DirectoryService; var directoryService = args.DirectoryService;
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager); var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
// If we contain an album assume we are an artist folder // 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;
} }
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null; 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 return new Series
{ {
@ -123,24 +123,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
IFileSystem fileSystem, IFileSystem fileSystem,
ILogger logger, ILogger logger,
ILibraryManager libraryManager, ILibraryManager libraryManager,
LibraryOptions libraryOptions,
bool isTvContentType) bool isTvContentType)
{ {
foreach (var child in fileSystemChildren) 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 (child.IsDirectory)
{ {
if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager)) if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager))
@ -152,7 +138,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
else else
{ {
string fullName = child.FullName; string fullName = child.FullName;
if (libraryManager.IsVideoFile(fullName, libraryOptions)) if (libraryManager.IsVideoFile(fullName))
{ {
if (isTvContentType) if (isTvContentType)
{ {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@
"ItemRemovedWithName": "{0} var fjarlægt úr safninu", "ItemRemovedWithName": "{0} var fjarlægt úr safninu",
"ItemAddedWithName": "{0} var bætt í safnið", "ItemAddedWithName": "{0} var bætt í safnið",
"Inherit": "Erfa", "Inherit": "Erfa",
"HomeVideos": "Myndbönd að heiman", "HomeVideos": "Heimamyndbönd",
"HeaderRecordingGroups": "Upptökuhópar", "HeaderRecordingGroups": "Upptökuhópar",
"HeaderNextUp": "Næst á dagskrá", "HeaderNextUp": "Næst á dagskrá",
"HeaderLiveTV": "Sjónvarp í beinni útsendingu", "HeaderLiveTV": "Sjónvarp í beinni útsendingu",
@ -36,10 +36,10 @@
"NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð", "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
"NotificationOptionVideoPlayback": "Myndbandafspilun hafin", "NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
"NotificationOptionUserLockedOut": "Notandi læstur úti", "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", "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett",
"NotificationOptionPluginUninstalled": "Viðbót fjarlægð", "NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
"NotificationOptionPluginInstalled": "Viðbót settur upp", "NotificationOptionPluginInstalled": "Viðbót sett upp",
"NotificationOptionPluginError": "Bilun í viðbót", "NotificationOptionPluginError": "Bilun í viðbót",
"NotificationOptionInstallationFailed": "Uppsetning tókst ekki", "NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
"NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp", "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
@ -50,15 +50,15 @@
"NameSeasonUnknown": "Sería óþekkt", "NameSeasonUnknown": "Sería óþekkt",
"NameSeasonNumber": "Sería {0}", "NameSeasonNumber": "Sería {0}",
"MixedContent": "Blandað efni", "MixedContent": "Blandað efni",
"MessageServerConfigurationUpdated": "Stillingar miðlarans hefur verið uppfærð", "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
"MessageApplicationUpdatedTo": "Jellyfin Server hefur verið uppfærður í {0}", "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
"MessageApplicationUpdated": "Jellyfin Server hefur verið uppfærður", "MessageApplicationUpdated": "Jellyfin þjónn hefur verið uppfærður",
"Latest": "Nýjasta", "Latest": "Nýjasta",
"LabelRunningTimeValue": "Keyrslutími kerfis: {0}", "LabelRunningTimeValue": "spilunartími: {0}",
"User": "Notandi", "User": "Notandi",
"System": "Kerfi", "System": "Kerfi",
"NotificationOptionNewLibraryContent": "Nýju efni bætt við", "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", "NameInstallFailed": "{0} uppsetning mistókst",
"MusicVideos": "Tónlistarmyndbönd", "MusicVideos": "Tónlistarmyndbönd",
"Music": "Tónlist", "Music": "Tónlist",
@ -74,5 +74,23 @@
"PluginUpdatedWithName": "{0} var uppfært", "PluginUpdatedWithName": "{0} var uppfært",
"PluginUninstalledWithName": "{0} var fjarlægt", "PluginUninstalledWithName": "{0} var fjarlægt",
"PluginInstalledWithName": "{0} var sett upp", "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"
} }

View file

@ -0,0 +1 @@
{}

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ using Microsoft.Net.Http.Headers;
namespace MediaBrowser.Api.Images namespace MediaBrowser.Api.Images
{ {
/// <summary> /// <summary>
/// Class GetItemImage /// Class GetItemImage.
/// </summary> /// </summary>
[Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")] [Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")]
[Authenticated] [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)); 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; bool cropwhitespace;
if (request.CropWhitespace.HasValue) if (request.CropWhitespace.HasValue)
{ {
@ -598,25 +583,25 @@ namespace MediaBrowser.Api.Images
{"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"} {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
}; };
return GetImageResult(item, return GetImageResult(
item,
itemId, itemId,
request, request,
imageInfo, imageInfo,
cropwhitespace, cropwhitespace,
outputFormats, outputFormats,
supportedImageEnhancers,
cacheDuration, cacheDuration,
responseHeaders, responseHeaders,
isHeadRequest); isHeadRequest);
} }
private async Task<object> GetImageResult(BaseItem item, private async Task<object> GetImageResult(
BaseItem item,
Guid itemId, Guid itemId,
ImageRequest request, ImageRequest request,
ItemImageInfo image, ItemImageInfo image,
bool cropwhitespace, bool cropwhitespace,
IReadOnlyCollection<ImageFormat> supportedFormats, IReadOnlyCollection<ImageFormat> supportedFormats,
IReadOnlyCollection<IImageEnhancer> enhancers,
TimeSpan? cacheDuration, TimeSpan? cacheDuration,
IDictionary<string, string> headers, IDictionary<string, string> headers,
bool isHeadRequest) bool isHeadRequest)
@ -624,7 +609,6 @@ namespace MediaBrowser.Api.Images
var options = new ImageProcessingOptions var options = new ImageProcessingOptions
{ {
CropWhiteSpace = cropwhitespace, CropWhiteSpace = cropwhitespace,
Enhancers = enhancers,
Height = request.Height, Height = request.Height,
ImageIndex = request.Index ?? 0, ImageIndex = request.Index ?? 0,
Image = image, Image = image,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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; }
}
}

View file

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

View file

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

View file

@ -5,6 +5,8 @@
<Rule Id="SA1202" Action="Info" /> <Rule Id="SA1202" Action="Info" />
<!-- disable warning SA1204: Static members must appear before non-static members --> <!-- disable warning SA1204: Static members must appear before non-static members -->
<Rule Id="SA1204" Action="Info" /> <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. --> <!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" /> <Rule Id="SA1009" Action="None" />

View file

@ -6,388 +6,78 @@ namespace Jellyfin.Naming.Tests.TV
{ {
public class EpisodeNumberTests public class EpisodeNumberTests
{ {
[Fact] private readonly NamingOptions _namingOptions = new NamingOptions();
public void TestEpisodeNumber1()
{
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 02/S02E03 blah.avi"));
}
[Fact] [Theory]
public void TestEpisodeNumber40() [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)
{ {
Assert.Equal(03, GetEpisodeNumberFromFile(@"Season 2/02x03 - 02x04 - 02x15 - Ep Name.mp4")); var result = new EpisodePathParser(_namingOptions)
}
[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)
.Parse(path, false); .Parse(path, false);
return result.EpisodeNumber; Assert.Equal(expected, result.EpisodeNumber);
} }
} }
} }

View file

@ -6,11 +6,21 @@ namespace Jellyfin.Naming.Tests.TV
{ {
public class SeasonNumberTests public class SeasonNumberTests
{ {
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 result = new EpisodeResolver(_namingOptions)
.Resolve(path, false);
Assert.Equal(expected, result.SeasonNumber);
}
private int? GetSeasonNumberFromEpisodeFile(string path) private int? GetSeasonNumberFromEpisodeFile(string path)
{ {
var options = new NamingOptions(); var result = new EpisodeResolver(_namingOptions)
var result = new EpisodeResolver(options)
.Resolve(path, false); .Resolve(path, false);
return result.SeasonNumber; return result.SeasonNumber;