aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--BDInfo/BDInfo.csproj1
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--DvdLib/DvdLib.csproj1
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj1
-rw-r--r--Emby.Dlna/PlayTo/SsdpHttpClient.cs36
-rw-r--r--Emby.Drawing/Emby.Drawing.csproj7
-rw-r--r--Emby.Drawing/ImageProcessor.cs159
-rw-r--r--Emby.Drawing/NullImageEncoder.cs34
-rw-r--r--Emby.IsoMounting/IsoMounter/Configuration/PluginConfiguration.cs3
-rw-r--r--Emby.IsoMounting/IsoMounter/IsoMounter.csproj13
-rw-r--r--Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs477
-rw-r--r--Emby.IsoMounting/IsoMounter/LinuxMount.cs73
-rw-r--r--Emby.IsoMounting/IsoMounter/Plugin.cs25
-rw-r--r--Emby.Notifications/Emby.Notifications.csproj1
-rw-r--r--Emby.Notifications/NotificationManager.cs2
-rw-r--r--Emby.Photos/Emby.Photos.csproj1
-rw-r--r--Emby.Server.Implementations/Activity/ActivityRepository.cs134
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs50
-rw-r--r--Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs61
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs7
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs484
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserRepository.cs15
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs2
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj2
-rw-r--r--Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs4
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs107
-rw-r--r--Emby.Server.Implementations/IO/IsoManager.cs26
-rw-r--r--Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs44
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs359
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs184
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs128
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs85
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs78
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs19
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs21
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs171
-rw-r--r--Emby.Server.Implementations/Security/AuthenticationRepository.cs85
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs8
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs2
-rw-r--r--Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj1
-rw-r--r--Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj1
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj2
-rw-r--r--Jellyfin.Server/Program.cs28
-rw-r--r--MediaBrowser.Api/Images/ImageService.cs8
-rw-r--r--MediaBrowser.Api/ItemUpdateService.cs4
-rw-r--r--MediaBrowser.Api/LocalizationService.cs5
-rw-r--r--MediaBrowser.Api/MediaBrowser.Api.csproj1
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs7
-rw-r--r--MediaBrowser.Api/Playback/Hls/BaseHlsService.cs2
-rw-r--r--MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs10
-rw-r--r--MediaBrowser.Api/Playback/Hls/VideoHlsService.cs10
-rw-r--r--MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs3
-rw-r--r--MediaBrowser.Api/Playback/Progressive/VideoService.cs2
-rw-r--r--MediaBrowser.Api/UserService.cs48
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs8
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj6
-rw-r--r--MediaBrowser.Controller/Authentication/AuthenticationException.cs1
-rw-r--r--MediaBrowser.Controller/Drawing/IImageEncoder.cs21
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs30
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs5
-rw-r--r--MediaBrowser.Controller/Entities/User.cs66
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs10
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj1
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs139
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs6
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj1
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj1
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs4
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs2
-rw-r--r--MediaBrowser.Model/Globalization/CultureDto.cs1
-rw-r--r--MediaBrowser.Model/Globalization/ILocalizationManager.cs35
-rw-r--r--MediaBrowser.Model/IO/IIsoManager.cs2
-rw-r--r--MediaBrowser.Model/IO/IIsoMounter.cs2
-rw-r--r--MediaBrowser.Model/IO/StreamDefaults.cs2
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj1
-rw-r--r--MediaBrowser.Model/Plugins/BasePluginConfiguration.cs2
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj1
-rw-r--r--MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs32
-rw-r--r--MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs170
-rw-r--r--MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs17
-rw-r--r--MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj1
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj1
-rw-r--r--deployment/windows/build-jellyfin.ps124
-rw-r--r--deployment/windows/jellyfin.nsi386
85 files changed, 2120 insertions, 1905 deletions
diff --git a/BDInfo/BDInfo.csproj b/BDInfo/BDInfo.csproj
index b2c752d0c..9dbaa9e2f 100644
--- a/BDInfo/BDInfo.csproj
+++ b/BDInfo/BDInfo.csproj
@@ -11,6 +11,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index c96228f31..c95133dfd 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -28,6 +28,7 @@
- [DrPandemic](https://github.com/drpandemic)
- [joern-h](https://github.com/joern-h)
- [Khinenw](https://github.com/HelloWorld017)
+ - [fhriley](https://github.com/fhriley)
# Emby Contributors
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
index b2c752d0c..9dbaa9e2f 100644
--- a/DvdLib/DvdLib.csproj
+++ b/DvdLib/DvdLib.csproj
@@ -11,6 +11,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 4c07087c5..34b49120b 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -14,6 +14,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index 217ea3a4b..66c634150 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -16,6 +16,8 @@ namespace Emby.Dlna.PlayTo
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
private const string FriendlyName = "Jellyfin";
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
private readonly IHttpClient _httpClient;
private readonly IServerConfigurationManager _config;
@@ -25,7 +27,8 @@ namespace Emby.Dlna.PlayTo
_config = config;
}
- public async Task<XDocument> SendCommandAsync(string baseUrl,
+ public async Task<XDocument> SendCommandAsync(
+ string baseUrl,
DeviceService service,
string command,
string postData,
@@ -35,12 +38,20 @@ namespace Emby.Dlna.PlayTo
var cancellationToken = CancellationToken.None;
var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
- using (var response = await PostSoapDataAsync(url, '\"' + service.ServiceType + '#' + command + '\"', postData, header, logRequest, cancellationToken)
+ using (var response = await PostSoapDataAsync(
+ url,
+ $"\"{service.ServiceType}#{command}\"",
+ postData,
+ header,
+ logRequest,
+ cancellationToken)
.ConfigureAwait(false))
using (var stream = response.Content)
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
- return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
+ return XDocument.Parse(
+ await reader.ReadToEndAsync().ConfigureAwait(false),
+ LoadOptions.PreserveWhitespace);
}
}
@@ -58,9 +69,8 @@ namespace Emby.Dlna.PlayTo
return baseUrl + serviceUrl;
}
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- public async Task SubscribeAsync(string url,
+ public async Task SubscribeAsync(
+ string url,
string ip,
int port,
string localIp,
@@ -101,14 +111,12 @@ namespace Emby.Dlna.PlayTo
options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false))
+ using (var stream = response.Content)
+ using (var reader = new StreamReader(stream, Encoding.UTF8))
{
- using (var stream = response.Content)
- {
- using (var reader = new StreamReader(stream, Encoding.UTF8))
- {
- return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
- }
- }
+ return XDocument.Parse(
+ await reader.ReadToEndAsync().ConfigureAwait(false),
+ LoadOptions.PreserveWhitespace);
}
}
@@ -122,7 +130,7 @@ namespace Emby.Dlna.PlayTo
{
if (soapAction[0] != '\"')
{
- soapAction = '\"' + soapAction + '\"';
+ soapAction = $"\"{soapAction}\"";
}
var options = new HttpRequestOptions
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj
index 9f97baf77..2e539f2c7 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -3,6 +3,8 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
@@ -15,4 +17,9 @@
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
+ <PropertyGroup>
+ <!-- We need at least C# 7.1 for the "default literal" feature-->
+ <LangVersion>latest</LangVersion>
+ </PropertyGroup>
+
</Project>
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index a7d95eb20..ce8089e59 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -22,42 +22,47 @@ using Microsoft.Extensions.Logging;
namespace Emby.Drawing
{
/// <summary>
- /// Class ImageProcessor
+ /// Class ImageProcessor.
/// </summary>
public class ImageProcessor : IImageProcessor, IDisposable
{
- /// <summary>
- /// The us culture
- /// </summary>
- protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
+ // Increment this when there's a change requiring caches to be invalidated
+ private const string Version = "3";
- /// <summary>
- /// Gets the list of currently registered image processors
- /// Image processors are specialized metadata providers that run after the normal ones
- /// </summary>
- /// <value>The image enhancers.</value>
- public IImageEnhancer[] ImageEnhancers { get; private set; }
+ private static readonly HashSet<string> _transparentImageTypes
+ = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
/// <summary>
/// The _logger
/// </summary>
private readonly ILogger _logger;
-
private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths;
private IImageEncoder _imageEncoder;
private readonly Func<ILibraryManager> _libraryManager;
private readonly Func<IMediaEncoder> _mediaEncoder;
+ private readonly Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>();
+ private bool _disposed = false;
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="logger"></param>
+ /// <param name="appPaths"></param>
+ /// <param name="fileSystem"></param>
+ /// <param name="imageEncoder"></param>
+ /// <param name="libraryManager"></param>
+ /// <param name="mediaEncoder"></param>
public ImageProcessor(
- ILoggerFactory loggerFactory,
+ ILogger<ImageProcessor> logger,
IServerApplicationPaths appPaths,
IFileSystem fileSystem,
IImageEncoder imageEncoder,
Func<ILibraryManager> libraryManager,
Func<IMediaEncoder> mediaEncoder)
{
- _logger = loggerFactory.CreateLogger(nameof(ImageProcessor));
+ _logger = logger;
_fileSystem = fileSystem;
_imageEncoder = imageEncoder;
_libraryManager = libraryManager;
@@ -69,20 +74,11 @@ namespace Emby.Drawing
ImageHelper.ImageProcessor = this;
}
- public IImageEncoder ImageEncoder
- {
- get => _imageEncoder;
- set
- {
- if (value == null)
- {
- throw new ArgumentNullException(nameof(value));
- }
+ private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
- _imageEncoder = value;
- }
- }
+ private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images");
+ /// <inheritdoc />
public IReadOnlyCollection<string> SupportedInputFormats =>
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
@@ -115,18 +111,20 @@ namespace Emby.Drawing
"wbmp"
};
+ /// <inheritdoc />
+ public IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; }
+ /// <inheritdoc />
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
- private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
-
- private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images");
-
- public void AddParts(IEnumerable<IImageEnhancer> enhancers)
+ /// <inheritdoc />
+ public IImageEncoder ImageEncoder
{
- ImageEnhancers = enhancers.ToArray();
+ get => _imageEncoder;
+ set => _imageEncoder = value ?? throw new ArgumentNullException(nameof(value));
}
+ /// <inheritdoc />
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
{
var file = await ProcessImage(options).ConfigureAwait(false);
@@ -137,15 +135,15 @@ namespace Emby.Drawing
}
}
+ /// <inheritdoc />
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
=> _imageEncoder.SupportedOutputFormats;
- private static readonly HashSet<string> TransparentImageTypes
- = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
-
+ /// <inheritdoc />
public bool SupportsTransparency(string path)
- => TransparentImageTypes.Contains(Path.GetExtension(path));
+ => _transparentImageTypes.Contains(Path.GetExtension(path));
+ /// <inheritdoc />
public async Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
{
if (options == null)
@@ -187,9 +185,9 @@ namespace Emby.Drawing
}
dateModified = supportedImageInfo.dateModified;
- bool requiresTransparency = TransparentImageTypes.Contains(Path.GetExtension(originalImagePath));
+ bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
- if (options.Enhancers.Length > 0)
+ if (options.Enhancers.Count > 0)
{
if (item == null)
{
@@ -279,7 +277,7 @@ namespace Emby.Drawing
}
}
- private ImageFormat GetOutputFormat(ImageFormat[] clientSupportedFormats, bool requiresTransparency)
+ private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
{
var serverFormats = GetSupportedImageOutputFormats();
@@ -321,11 +319,6 @@ namespace Emby.Drawing
}
/// <summary>
- /// Increment this when there's a change requiring caches to be invalidated
- /// </summary>
- private const string Version = "3";
-
- /// <summary>
/// Gets the cache file path based on a set of parameters
/// </summary>
private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
@@ -372,9 +365,11 @@ namespace Emby.Drawing
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant());
}
+ /// <inheritdoc />
public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info)
=> GetImageDimensions(item, info, true);
+ /// <inheritdoc />
public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem)
{
int width = info.Width;
@@ -400,26 +395,19 @@ namespace Emby.Drawing
return size;
}
- /// <summary>
- /// Gets the size of the image.
- /// </summary>
+ /// <inheritdoc />
public ImageDimensions GetImageDimensions(string path)
=> _imageEncoder.GetImageSize(path);
- /// <summary>
- /// Gets the image cache tag.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="image">The image.</param>
- /// <returns>Guid.</returns>
- /// <exception cref="ArgumentNullException">item</exception>
+ /// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
{
- var supportedEnhancers = GetSupportedEnhancers(item, image.Type);
+ var supportedEnhancers = GetSupportedEnhancers(item, image.Type).ToArray();
return GetImageCacheTag(item, image, supportedEnhancers);
}
+ /// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ChapterInfo chapter)
{
try
@@ -437,22 +425,15 @@ namespace Emby.Drawing
}
}
- /// <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>
- /// <exception cref="ArgumentNullException">item</exception>
- public string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers)
+ /// <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.Length == 0)
+ if (imageEnhancers.Count == 0)
{
return (originalImagePath + dateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
@@ -480,7 +461,7 @@ namespace Emby.Drawing
{
try
{
- string filename = (originalImagePath + dateModified.Ticks.ToString(UsCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
string cacheExtension = _mediaEncoder().SupportsEncoder("libwebp") ? ".webp" : ".png";
var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
@@ -507,16 +488,10 @@ namespace Emby.Drawing
return (originalImagePath, dateModified);
}
- /// <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>
+ /// <inheritdoc />
public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex)
{
- var enhancers = GetSupportedEnhancers(item, imageType);
+ var enhancers = GetSupportedEnhancers(item, imageType).ToArray();
ItemImageInfo imageInfo = item.GetImageInfo(imageType, imageIndex);
@@ -532,7 +507,7 @@ namespace Emby.Drawing
bool inputImageSupportsTransparency,
BaseItem item,
int imageIndex,
- IImageEnhancer[] enhancers,
+ IReadOnlyCollection<IImageEnhancer> enhancers,
CancellationToken cancellationToken)
{
var originalImagePath = image.Path;
@@ -573,6 +548,7 @@ namespace Emby.Drawing
/// <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
@@ -584,9 +560,9 @@ namespace Emby.Drawing
BaseItem item,
ImageType imageType,
int imageIndex,
- IImageEnhancer[] supportedEnhancers,
+ IReadOnlyCollection<IImageEnhancer> supportedEnhancers,
string cacheGuid,
- CancellationToken cancellationToken)
+ CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(originalImagePath))
{
@@ -680,6 +656,7 @@ namespace Emby.Drawing
{
throw new ArgumentNullException(nameof(path));
}
+
if (string.IsNullOrEmpty(uniqueName))
{
throw new ArgumentNullException(nameof(uniqueName));
@@ -722,6 +699,7 @@ namespace Emby.Drawing
return Path.Combine(path, prefix, filename);
}
+ /// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options)
{
_logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
@@ -731,38 +709,25 @@ namespace Emby.Drawing
_logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
}
- public IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType)
+ /// <inheritdoc />
+ public IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType)
{
- List<IImageEnhancer> list = null;
-
foreach (var i in ImageEnhancers)
{
- try
- {
- if (i.Supports(item, imageType))
- {
- if (list == null)
- {
- list = new List<IImageEnhancer>();
- }
- list.Add(i);
- }
- }
- catch (Exception ex)
+ if (i.Supports(item, imageType))
{
- _logger.LogError(ex, "Error in image enhancer: {0}", i.GetType().Name);
+ yield return i;
}
}
-
- return list == null ? Array.Empty<IImageEnhancer>() : list.ToArray();
}
- private Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>();
+
private class LockInfo
{
public SemaphoreSlim Lock = new SemaphoreSlim(1, 1);
public int Count = 1;
}
+
private LockInfo GetLock(string key)
{
lock (_locks)
@@ -795,7 +760,7 @@ namespace Emby.Drawing
}
}
- private bool _disposed;
+ /// <inheritdoc />
public void Dispose()
{
_disposed = true;
diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs
index fc4a5af9f..5af7f1622 100644
--- a/Emby.Drawing/NullImageEncoder.cs
+++ b/Emby.Drawing/NullImageEncoder.cs
@@ -5,36 +5,40 @@ using MediaBrowser.Model.Drawing;
namespace Emby.Drawing
{
+ /// <summary>
+ /// A fallback implementation of <see cref="IImageEncoder" />.
+ /// </summary>
public class NullImageEncoder : IImageEncoder
{
+ /// <inheritdoc />
public IReadOnlyCollection<string> SupportedInputFormats
=> new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" };
+ /// <inheritdoc />
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
=> new HashSet<ImageFormat>() { ImageFormat.Jpg, ImageFormat.Png };
- public void CropWhiteSpace(string inputPath, string outputPath)
- {
- throw new NotImplementedException();
- }
-
- public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
- {
- throw new NotImplementedException();
- }
-
- public void CreateImageCollage(ImageCollageOptions options)
- {
- throw new NotImplementedException();
- }
-
+ /// <inheritdoc />
public string Name => "Null Image Encoder";
+ /// <inheritdoc />
public bool SupportsImageCollageCreation => false;
+ /// <inheritdoc />
public bool SupportsImageEncoding => false;
+ /// <inheritdoc />
public ImageDimensions GetImageSize(string path)
+ => throw new NotImplementedException();
+
+ /// <inheritdoc />
+ public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public void CreateImageCollage(ImageCollageOptions options)
{
throw new NotImplementedException();
}
diff --git a/Emby.IsoMounting/IsoMounter/Configuration/PluginConfiguration.cs b/Emby.IsoMounting/IsoMounter/Configuration/PluginConfiguration.cs
index 4755e4e82..ca6f40cc4 100644
--- a/Emby.IsoMounting/IsoMounter/Configuration/PluginConfiguration.cs
+++ b/Emby.IsoMounting/IsoMounter/Configuration/PluginConfiguration.cs
@@ -2,6 +2,9 @@ using MediaBrowser.Model.Plugins;
namespace IsoMounter.Configuration
{
+ /// <summary>
+ /// Class PluginConfiguration.
+ /// </summary>
public class PluginConfiguration : BasePluginConfiguration
{
}
diff --git a/Emby.IsoMounting/IsoMounter/IsoMounter.csproj b/Emby.IsoMounting/IsoMounter/IsoMounter.csproj
index dafa51cd5..4fa07fbf1 100644
--- a/Emby.IsoMounting/IsoMounter/IsoMounter.csproj
+++ b/Emby.IsoMounting/IsoMounter/IsoMounter.csproj
@@ -12,6 +12,19 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+
+ <!-- Code analysers-->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
+ </ItemGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>
diff --git a/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs b/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs
index 2f0003be8..48cb2e1d5 100644
--- a/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs
+++ b/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs
@@ -1,9 +1,10 @@
using System;
+using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
@@ -11,441 +12,274 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace IsoMounter
{
+ /// <summary>
+ /// The ISO manager implementation for Linux.
+ /// </summary>
public class LinuxIsoManager : IIsoMounter
{
- [DllImport("libc", SetLastError = true)]
- static extern uint getuid();
+ private const string MountCommand = "mount";
+ private const string UnmountCommand = "umount";
+ private const string SudoCommand = "sudo";
- #region Private Fields
-
- private readonly bool ExecutablesAvailable;
private readonly ILogger _logger;
- private readonly string MountCommand;
- private readonly string MountPointRoot;
- private readonly IProcessFactory ProcessFactory;
- private readonly string SudoCommand;
- private readonly string UmountCommand;
-
- #endregion
+ private readonly string _mountPointRoot;
- #region Constructor(s)
-
- public LinuxIsoManager(ILogger logger, IProcessFactory processFactory)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LinuxIsoManager" /> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ public LinuxIsoManager(ILogger logger)
{
_logger = logger;
- ProcessFactory = processFactory;
- MountPointRoot = Path.DirectorySeparatorChar + "tmp" + Path.DirectorySeparatorChar + "Emby";
+ _mountPointRoot = Path.DirectorySeparatorChar + "tmp" + Path.DirectorySeparatorChar + "Emby";
_logger.LogDebug(
"[{0}] System PATH is currently set to [{1}].",
Name,
- Environment.GetEnvironmentVariable("PATH") ?? ""
- );
+ Environment.GetEnvironmentVariable("PATH") ?? string.Empty);
_logger.LogDebug(
"[{0}] System path separator is [{1}].",
Name,
- Path.PathSeparator
- );
+ Path.PathSeparator);
_logger.LogDebug(
"[{0}] Mount point root is [{1}].",
Name,
- MountPointRoot
- );
-
- //
- // Get the location of the executables we need to support mounting/unmounting ISO images.
- //
-
- SudoCommand = GetFullPathForExecutable("sudo");
-
- _logger.LogInformation(
- "[{0}] Using version of [sudo] located at [{1}].",
- Name,
- SudoCommand
- );
-
- MountCommand = GetFullPathForExecutable("mount");
-
- _logger.LogInformation(
- "[{0}] Using version of [mount] located at [{1}].",
- Name,
- MountCommand
- );
-
- UmountCommand = GetFullPathForExecutable("umount");
-
- _logger.LogInformation(
- "[{0}] Using version of [umount] located at [{1}].",
- Name,
- UmountCommand
- );
-
- if (!string.IsNullOrEmpty(SudoCommand) && !string.IsNullOrEmpty(MountCommand) && !string.IsNullOrEmpty(UmountCommand))
- {
- ExecutablesAvailable = true;
- }
- else
- {
- ExecutablesAvailable = false;
- }
-
+ _mountPointRoot);
}
- #endregion
-
- #region Interface Implementation for IIsoMounter
-
- public bool IsInstalled => true;
-
+ /// <inheritdoc />
public string Name => "LinuxMount";
- public bool RequiresInstallation => false;
+#pragma warning disable SA1300
+#pragma warning disable SA1400
+ [DllImport("libc", SetLastError = true)]
+ static extern uint getuid();
+
+#pragma warning restore SA1300
+#pragma warning restore SA1400
+ /// <inheritdoc />
public bool CanMount(string path)
{
-
if (OperatingSystem.Id != OperatingSystemId.Linux)
{
return false;
}
+
_logger.LogInformation(
- "[{0}] Checking we can attempt to mount [{1}], Extension = [{2}], Operating System = [{3}], Executables Available = [{4}].",
+ "[{0}] Checking we can attempt to mount [{1}], Extension = [{2}], Operating System = [{3}].",
Name,
path,
Path.GetExtension(path),
- OperatingSystem.Name,
- ExecutablesAvailable
- );
+ OperatingSystem.Name);
- if (ExecutablesAvailable)
- {
- return string.Equals(Path.GetExtension(path), ".iso", StringComparison.OrdinalIgnoreCase);
- }
- else
- {
- return false;
- }
- }
-
- public Task Install(CancellationToken cancellationToken)
- {
- return Task.FromResult(false);
+ return string.Equals(Path.GetExtension(path), ".iso", StringComparison.OrdinalIgnoreCase);
}
+ /// <inheritdoc />
public Task<IIsoMount> Mount(string isoPath, CancellationToken cancellationToken)
{
- if (MountISO(isoPath, out LinuxMount mountedISO))
- {
- return Task.FromResult<IIsoMount>(mountedISO);
- }
- else
+ string cmdArguments;
+ string cmdFilename;
+ string mountPoint = Path.Combine(_mountPointRoot, Guid.NewGuid().ToString());
+
+ if (string.IsNullOrEmpty(isoPath))
{
- throw new IOException(string.Format(
- "An error occurred trying to mount image [$0].",
- isoPath
- ));
+ throw new ArgumentNullException(nameof(isoPath));
}
- }
- #endregion
-
- #region Interface Implementation for IDisposable
-
- // Flag: Has Dispose already been called?
- private bool disposed = false;
-
- public void Dispose()
- {
-
- // Dispose of unmanaged resources.
- Dispose(true);
-
- // Suppress finalization.
- GC.SuppressFinalize(this);
+ _logger.LogInformation(
+ "[{Name}] Attempting to mount [{Path}].",
+ Name,
+ isoPath);
- }
+ _logger.LogDebug(
+ "[{Name}] ISO will be mounted at [{Path}].",
+ Name,
+ mountPoint);
- protected virtual void Dispose(bool disposing)
- {
+ try
+ {
+ Directory.CreateDirectory(mountPoint);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ throw new IOException("Unable to create mount point(Permission denied) for " + isoPath, ex);
+ }
+ catch (Exception ex)
+ {
+ throw new IOException("Unable to create mount point for " + isoPath, ex);
+ }
- if (disposed)
+ if (GetUID() == 0)
+ {
+ cmdFilename = MountCommand;
+ cmdArguments = string.Format(
+ CultureInfo.InvariantCulture,
+ "\"{0}\" \"{1}\"",
+ isoPath,
+ mountPoint);
+ }
+ else
{
- return;
+ cmdFilename = SudoCommand;
+ cmdArguments = string.Format(
+ CultureInfo.InvariantCulture,
+ "\"{0}\" \"{1}\" \"{2}\"",
+ MountCommand,
+ isoPath,
+ mountPoint);
}
- _logger.LogInformation(
- "[{0}] Disposing [{1}].",
+ _logger.LogDebug(
+ "[{0}] Mount command [{1}], mount arguments [{2}].",
Name,
- disposing
- );
+ cmdFilename,
+ cmdArguments);
- if (disposing)
+ int exitcode = ExecuteCommand(cmdFilename, cmdArguments);
+ if (exitcode == 0)
{
+ _logger.LogInformation(
+ "[{0}] ISO mount completed successfully.",
+ Name);
- //
- // Free managed objects here.
- //
-
+ return Task.FromResult<IIsoMount>(new LinuxMount(this, isoPath, mountPoint));
}
- //
- // Free any unmanaged objects here.
- //
-
- disposed = true;
-
- }
-
- #endregion
-
- #region Private Methods
-
- private string GetFullPathForExecutable(string name)
- {
+ _logger.LogInformation(
+ "[{0}] ISO mount completed with errors.",
+ Name);
- foreach (string test in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator))
+ try
{
- string path = test.Trim();
-
- if (!string.IsNullOrEmpty(path) && File.Exists(path = Path.Combine(path, name)))
- {
- return Path.GetFullPath(path);
- }
+ Directory.Delete(mountPoint, false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "[{Name}] Unhandled exception removing mount point.", Name);
+ throw;
}
- return string.Empty;
+ throw new ExternalException("Mount command failed", exitcode);
}
private uint GetUID()
{
-
var uid = getuid();
_logger.LogDebug(
"[{0}] GetUserId() returned [{2}].",
Name,
- uid
- );
+ uid);
return uid;
-
}
- private bool ExecuteCommand(string cmdFilename, string cmdArguments)
+ private int ExecuteCommand(string cmdFilename, string cmdArguments)
{
-
- bool processFailed = false;
-
- var process = ProcessFactory.Create(
- new ProcessOptions
- {
- CreateNoWindow = true,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = false,
- FileName = cmdFilename,
- Arguments = cmdArguments,
- IsHidden = true,
- ErrorDialog = false,
- EnableRaisingEvents = true
- }
- );
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = cmdFilename,
+ Arguments = cmdArguments,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ ErrorDialog = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ };
+
+ var process = new Process()
+ {
+ StartInfo = startInfo
+ };
try
{
process.Start();
- //StreamReader outputReader = process.StandardOutput.;
- //StreamReader errorReader = process.StandardError;
-
_logger.LogDebug(
"[{Name}] Standard output from process is [{Error}].",
Name,
- process.StandardOutput.ReadToEnd()
- );
+ process.StandardOutput.ReadToEnd());
_logger.LogDebug(
"[{Name}] Standard error from process is [{Error}].",
Name,
- process.StandardError.ReadToEnd()
- );
+ process.StandardError.ReadToEnd());
+
+ return process.ExitCode;
}
catch (Exception ex)
{
- processFailed = true;
_logger.LogDebug(ex, "[{Name}] Unhandled exception executing command.", Name);
+ throw;
}
-
- if (!processFailed && process.ExitCode == 0)
+ finally
{
- return true;
+ process?.Dispose();
}
- else
- {
- return false;
- }
-
}
- private bool MountISO(string isoPath, out LinuxMount mountedISO)
+ /// <summary>
+ /// Unmounts the specified mount.
+ /// </summary>
+ /// <param name="mount">The mount.</param>
+ internal void OnUnmount(LinuxMount mount)
{
-
- string cmdArguments;
- string cmdFilename;
- string mountPoint = Path.Combine(MountPointRoot, Guid.NewGuid().ToString());
-
- if (!string.IsNullOrEmpty(isoPath))
- {
-
- _logger.LogInformation(
- "[{Name}] Attempting to mount [{Path}].",
- Name,
- isoPath
- );
-
- _logger.LogDebug(
- "[{Name}] ISO will be mounted at [{Path}].",
- Name,
- mountPoint
- );
-
- }
- else
+ if (mount == null)
{
-
- throw new ArgumentNullException(nameof(isoPath));
-
- }
-
- try
- {
- Directory.CreateDirectory(mountPoint);
- }
- catch (UnauthorizedAccessException)
- {
- throw new IOException("Unable to create mount point(Permission denied) for " + isoPath);
- }
- catch (Exception)
- {
- throw new IOException("Unable to create mount point for " + isoPath);
- }
-
- if (GetUID() == 0)
- {
- cmdFilename = MountCommand;
- cmdArguments = string.Format("\"{0}\" \"{1}\"", isoPath, mountPoint);
- }
- else
- {
- cmdFilename = SudoCommand;
- cmdArguments = string.Format("\"{0}\" \"{1}\" \"{2}\"", MountCommand, isoPath, mountPoint);
+ throw new ArgumentNullException(nameof(mount));
}
- _logger.LogDebug(
- "[{0}] Mount command [{1}], mount arguments [{2}].",
+ _logger.LogInformation(
+ "[{0}] Attempting to unmount ISO [{1}] mounted on [{2}].",
Name,
- cmdFilename,
- cmdArguments
- );
-
- if (ExecuteCommand(cmdFilename, cmdArguments))
- {
-
- _logger.LogInformation(
- "[{0}] ISO mount completed successfully.",
- Name
- );
-
- mountedISO = new LinuxMount(this, isoPath, mountPoint);
-
- }
- else
- {
-
- _logger.LogInformation(
- "[{0}] ISO mount completed with errors.",
- Name
- );
-
- try
- {
- Directory.Delete(mountPoint, false);
- }
- catch (Exception ex)
- {
- _logger.LogInformation(ex, "[{Name}] Unhandled exception removing mount point.", Name);
- }
-
- mountedISO = null;
-
- }
-
- return mountedISO != null;
-
- }
-
- private void UnmountISO(LinuxMount mount)
- {
+ mount.IsoPath,
+ mount.MountedPath);
string cmdArguments;
string cmdFilename;
- if (mount != null)
- {
-
- _logger.LogInformation(
- "[{0}] Attempting to unmount ISO [{1}] mounted on [{2}].",
- Name,
- mount.IsoPath,
- mount.MountedPath
- );
-
- }
- else
- {
-
- throw new ArgumentNullException(nameof(mount));
-
- }
-
if (GetUID() == 0)
{
- cmdFilename = UmountCommand;
- cmdArguments = string.Format("\"{0}\"", mount.MountedPath);
+ cmdFilename = UnmountCommand;
+ cmdArguments = string.Format(
+ CultureInfo.InvariantCulture,
+ "\"{0}\"",
+ mount.MountedPath);
}
else
{
cmdFilename = SudoCommand;
- cmdArguments = string.Format("\"{0}\" \"{1}\"", UmountCommand, mount.MountedPath);
+ cmdArguments = string.Format(
+ CultureInfo.InvariantCulture,
+ "\"{0}\" \"{1}\"",
+ UnmountCommand,
+ mount.MountedPath);
}
_logger.LogDebug(
"[{0}] Umount command [{1}], umount arguments [{2}].",
Name,
cmdFilename,
- cmdArguments
- );
+ cmdArguments);
- if (ExecuteCommand(cmdFilename, cmdArguments))
+ int exitcode = ExecuteCommand(cmdFilename, cmdArguments);
+ if (exitcode == 0)
{
-
_logger.LogInformation(
"[{0}] ISO unmount completed successfully.",
- Name
- );
-
+ Name);
}
else
{
-
_logger.LogInformation(
"[{0}] ISO unmount completed with errors.",
- Name
- );
-
+ Name);
}
try
@@ -454,24 +288,11 @@ namespace IsoMounter
}
catch (Exception ex)
{
- _logger.LogInformation(ex, "[{Name}] Unhandled exception removing mount point.", Name);
+ _logger.LogError(ex, "[{Name}] Unhandled exception removing mount point.", Name);
+ throw;
}
- }
-
- #endregion
-
- #region Internal Methods
-
- internal void OnUnmount(LinuxMount mount)
- {
-
- UnmountISO(mount);
+ throw new ExternalException("Mount command failed", exitcode);
}
-
- #endregion
-
}
-
}
-
diff --git a/Emby.IsoMounting/IsoMounter/LinuxMount.cs b/Emby.IsoMounting/IsoMounter/LinuxMount.cs
index b8636822b..ccad8ce20 100644
--- a/Emby.IsoMounting/IsoMounter/LinuxMount.cs
+++ b/Emby.IsoMounting/IsoMounter/LinuxMount.cs
@@ -3,81 +3,56 @@ using MediaBrowser.Model.IO;
namespace IsoMounter
{
+ /// <summary>
+ /// Class LinuxMount.
+ /// </summary>
internal class LinuxMount : IIsoMount
{
+ private readonly LinuxIsoManager _linuxIsoManager;
- #region Private Fields
-
- private readonly LinuxIsoManager linuxIsoManager;
-
- #endregion
-
- #region Constructor(s)
+ private bool _disposed = false;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LinuxMount" /> class.
+ /// </summary>
+ /// <param name="isoManager">The ISO manager that mounted this ISO file.</param>
+ /// <param name="isoPath">The path to the ISO file.</param>
+ /// <param name="mountFolder">The folder the ISO is mounted in.</param>
internal LinuxMount(LinuxIsoManager isoManager, string isoPath, string mountFolder)
{
-
- linuxIsoManager = isoManager;
+ _linuxIsoManager = isoManager;
IsoPath = isoPath;
MountedPath = mountFolder;
-
}
- #endregion
-
- #region Interface Implementation for IDisposable
+ /// <inheritdoc />
+ public string IsoPath { get; }
- // Flag: Has Dispose already been called?
- private bool disposed = false;
+ /// <inheritdoc />
+ public string MountedPath { get; }
+ /// <inheritdoc />
public void Dispose()
{
-
- // Dispose of unmanaged resources.
Dispose(true);
-
- // Suppress finalization.
GC.SuppressFinalize(this);
-
}
+ /// <summary>
+ /// Releases the unmanaged resources and disposes of the managed resources used.
+ /// </summary>
+ /// <param name="disposing">Whether or not the managed resources should be disposed.</param>
protected virtual void Dispose(bool disposing)
{
-
- if (disposed)
+ if (_disposed)
{
return;
}
- if (disposing)
- {
-
- //
- // Free managed objects here.
- //
-
- linuxIsoManager.OnUnmount(this);
-
- }
-
- //
- // Free any unmanaged objects here.
- //
-
- disposed = true;
+ _linuxIsoManager.OnUnmount(this);
+ _disposed = true;
}
-
- #endregion
-
- #region Interface Implementation for IIsoMount
-
- public string IsoPath { get; private set; }
- public string MountedPath { get; private set; }
-
- #endregion
-
}
-
}
diff --git a/Emby.IsoMounting/IsoMounter/Plugin.cs b/Emby.IsoMounting/IsoMounter/Plugin.cs
index f45b39d3e..433294d74 100644
--- a/Emby.IsoMounting/IsoMounter/Plugin.cs
+++ b/Emby.IsoMounting/IsoMounter/Plugin.cs
@@ -6,25 +6,28 @@ using MediaBrowser.Model.Serialization;
namespace IsoMounter
{
+ /// <summary>
+ /// The LinuxMount plugin class.
+ /// </summary>
public class Plugin : BasePlugin<PluginConfiguration>
{
- public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Plugin" /> class.
+ /// </summary>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="xmlSerializer">The XML serializer.</param>
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
{
}
- private Guid _id = new Guid("4682DD4C-A675-4F1B-8E7C-79ADF137A8F8");
- public override Guid Id => _id;
+ /// <inheritdoc />
+ public override Guid Id { get; } = new Guid("4682DD4C-A675-4F1B-8E7C-79ADF137A8F8");
- /// <summary>
- /// Gets the name of the plugin
- /// </summary>
- /// <value>The name.</value>
+ /// <inheritdoc />
public override string Name => "Iso Mounter";
- /// <summary>
- /// Gets the description.
- /// </summary>
- /// <value>The description.</value>
+ /// <inheritdoc />
public override string Description => "Mount and stream ISO contents";
}
}
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 5c68e48c8..cbd3bde4f 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs
index a767e541e..eecbbea07 100644
--- a/Emby.Notifications/NotificationManager.cs
+++ b/Emby.Notifications/NotificationManager.cs
@@ -89,7 +89,7 @@ namespace Emby.Notifications
return _userManager.Users.Where(i => i.Policy.IsAdministrator)
.Select(i => i.Id);
case SendToUserType.All:
- return _userManager.Users.Select(i => i.Id);
+ return _userManager.UsersIds;
case SendToUserType.Custom:
return request.UserIds;
default:
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 8a79bf7e1..db73cb521 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -16,6 +16,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs
index 91371b833..541b23afd 100644
--- a/Emby.Server.Implementations/Activity/ActivityRepository.cs
+++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs
@@ -155,94 +155,100 @@ namespace Emby.Server.Implementations.Activity
public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit)
{
- using (var connection = GetConnection(true))
- {
- var commandText = BaseActivitySelectText;
- var whereClauses = new List<string>();
+ var commandText = BaseActivitySelectText;
+ var whereClauses = new List<string>();
- if (minDate.HasValue)
+ if (minDate.HasValue)
+ {
+ whereClauses.Add("DateCreated>=@DateCreated");
+ }
+ if (hasUserId.HasValue)
+ {
+ if (hasUserId.Value)
{
- whereClauses.Add("DateCreated>=@DateCreated");
+ whereClauses.Add("UserId not null");
}
- if (hasUserId.HasValue)
+ else
{
- if (hasUserId.Value)
- {
- whereClauses.Add("UserId not null");
- }
- else
- {
- whereClauses.Add("UserId is null");
- }
+ whereClauses.Add("UserId is null");
}
+ }
- var whereTextWithoutPaging = whereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", whereClauses.ToArray());
-
- if (startIndex.HasValue && startIndex.Value > 0)
- {
- var pagingWhereText = whereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", whereClauses.ToArray());
-
- whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})",
- pagingWhereText,
- startIndex.Value.ToString(_usCulture)));
- }
+ var whereTextWithoutPaging = whereClauses.Count == 0 ?
+ string.Empty :
+ " where " + string.Join(" AND ", whereClauses.ToArray());
- var whereText = whereClauses.Count == 0 ?
+ if (startIndex.HasValue && startIndex.Value > 0)
+ {
+ var pagingWhereText = whereClauses.Count == 0 ?
string.Empty :
" where " + string.Join(" AND ", whereClauses.ToArray());
- commandText += whereText;
+ whereClauses.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})",
+ pagingWhereText,
+ startIndex.Value));
+ }
- commandText += " ORDER BY DateCreated DESC";
+ var whereText = whereClauses.Count == 0 ?
+ string.Empty :
+ " where " + string.Join(" AND ", whereClauses.ToArray());
- if (limit.HasValue)
- {
- commandText += " LIMIT " + limit.Value.ToString(_usCulture);
- }
+ commandText += whereText;
- var statementTexts = new List<string>();
- statementTexts.Add(commandText);
- statementTexts.Add("select count (Id) from ActivityLog" + whereTextWithoutPaging);
+ commandText += " ORDER BY DateCreated DESC";
- return connection.RunInTransaction(db =>
- {
- var list = new List<ActivityLogEntry>();
- var result = new QueryResult<ActivityLogEntry>();
+ if (limit.HasValue)
+ {
+ commandText += " LIMIT " + limit.Value.ToString(_usCulture);
+ }
+
+ var statementTexts = new[]
+ {
+ commandText,
+ "select count (Id) from ActivityLog" + whereTextWithoutPaging
+ };
- var statements = PrepareAll(db, statementTexts).ToList();
+ var list = new List<ActivityLogEntry>();
+ var result = new QueryResult<ActivityLogEntry>();
- using (var statement = statements[0])
+ using (var connection = GetConnection(true))
+ {
+ connection.RunInTransaction(
+ db =>
{
- if (minDate.HasValue)
- {
- statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue());
- }
+ var statements = PrepareAll(db, statementTexts).ToList();
- foreach (var row in statement.ExecuteQuery())
+ using (var statement = statements[0])
{
- list.Add(GetEntry(row));
+ if (minDate.HasValue)
+ {
+ statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue());
+ }
+
+ foreach (var row in statement.ExecuteQuery())
+ {
+ list.Add(GetEntry(row));
+ }
}
- }
- using (var statement = statements[1])
- {
- if (minDate.HasValue)
+ using (var statement = statements[1])
{
- statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue());
- }
+ if (minDate.HasValue)
+ {
+ statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue());
+ }
- result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
- }
-
- result.Items = list.ToArray();
- return result;
-
- }, ReadTransactionMode);
+ result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
+ }
+ },
+ ReadTransactionMode);
}
+
+ result.Items = list.ToArray();
+ return result;
}
private static ActivityLogEntry GetEntry(IReadOnlyList<IResultSetValue> reader)
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index e5e095ca1..6ab3d1bb1 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -315,8 +315,6 @@ namespace Emby.Server.Implementations
private IMediaSourceManager MediaSourceManager { get; set; }
- private IPlaylistManager PlaylistManager { get; set; }
-
private readonly IConfiguration _configuration;
/// <summary>
@@ -325,14 +323,6 @@ namespace Emby.Server.Implementations
/// <value>The installation manager.</value>
protected IInstallationManager InstallationManager { get; private set; }
- /// <summary>
- /// Gets or sets the zip client.
- /// </summary>
- /// <value>The zip client.</value>
- protected IZipClient ZipClient { get; private set; }
-
- protected IHttpResultFactory HttpResultFactory { get; private set; }
-
protected IAuthService AuthService { get; private set; }
public IStartupOptions StartupOptions { get; }
@@ -512,13 +502,8 @@ namespace Emby.Server.Implementations
return AllConcreteTypes.Where(i => currentType.IsAssignableFrom(i));
}
- /// <summary>
- /// Gets the exports.
- /// </summary>
- /// <typeparam name="T">The type</typeparam>
- /// <param name="manageLifetime">if set to <c>true</c> [manage lifetime].</param>
- /// <returns>IEnumerable{``0}.</returns>
- public IEnumerable<T> GetExports<T>(bool manageLifetime = true)
+ /// <inheritdoc />
+ public IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true)
{
var parts = GetExportTypes<T>()
.Select(CreateInstanceSafe)
@@ -540,6 +525,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Runs the startup tasks.
/// </summary>
+ /// <returns><see cref="Task" />.</returns>
public async Task RunStartupTasksAsync()
{
Logger.LogInformation("Running startup tasks");
@@ -552,7 +538,7 @@ namespace Emby.Server.Implementations
Logger.LogInformation("ServerId: {0}", SystemId);
- var entryPoints = GetExports<IServerEntryPoint>().ToList();
+ var entryPoints = GetExports<IServerEntryPoint>();
var stopWatch = new Stopwatch();
stopWatch.Start();
@@ -684,8 +670,6 @@ namespace Emby.Server.Implementations
await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
}
- public static IStreamHelper StreamHelper { get; set; }
-
/// <summary>
/// Registers resources that classes will depend on
/// </summary>
@@ -729,8 +713,7 @@ namespace Emby.Server.Implementations
ProcessFactory = new ProcessFactory();
serviceCollection.AddSingleton(ProcessFactory);
- ApplicationHost.StreamHelper = new StreamHelper();
- serviceCollection.AddSingleton(StreamHelper);
+ serviceCollection.AddSingleton(typeof(IStreamHelper), typeof(StreamHelper));
serviceCollection.AddSingleton(typeof(ICryptoProvider), typeof(CryptographyProvider));
@@ -739,18 +722,16 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager));
- ZipClient = new ZipClient();
- serviceCollection.AddSingleton(ZipClient);
+ serviceCollection.AddSingleton(typeof(IZipClient), typeof(ZipClient));
- HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, StreamHelper);
- serviceCollection.AddSingleton(HttpResultFactory);
+ serviceCollection.AddSingleton(typeof(IHttpResultFactory), typeof(HttpResultFactory));
serviceCollection.AddSingleton<IServerApplicationHost>(this);
serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton(ServerConfigurationManager);
- LocalizationManager = new LocalizationManager(ServerConfigurationManager, JsonSerializer, LoggerFactory);
+ LocalizationManager = new LocalizationManager(ServerConfigurationManager, JsonSerializer, LoggerFactory.CreateLogger<LocalizationManager>());
await LocalizationManager.LoadAll().ConfigureAwait(false);
serviceCollection.AddSingleton<ILocalizationManager>(LocalizationManager);
@@ -774,7 +755,8 @@ namespace Emby.Server.Implementations
_userRepository = GetUserRepository();
- UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager);
+ UserManager = new UserManager(LoggerFactory.CreateLogger<UserManager>(), _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager);
+
serviceCollection.AddSingleton(UserManager);
LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager);
@@ -807,7 +789,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(HttpServer);
- ImageProcessor = GetImageProcessor();
+ ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
serviceCollection.AddSingleton(ImageProcessor);
TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
@@ -840,8 +822,7 @@ namespace Emby.Server.Implementations
CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager);
serviceCollection.AddSingleton(CollectionManager);
- PlaylistManager = new PlaylistManager(LibraryManager, FileSystemManager, LibraryMonitor, LoggerFactory, UserManager, ProviderManager);
- serviceCollection.AddSingleton(PlaylistManager);
+ serviceCollection.AddSingleton(typeof(IPlaylistManager), typeof(PlaylistManager));
LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, LoggerFactory, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, FileSystemManager, () => ChannelManager);
serviceCollection.AddSingleton(LiveTvManager);
@@ -959,11 +940,6 @@ namespace Emby.Server.Implementations
}
}
- private IImageProcessor GetImageProcessor()
- {
- return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
- }
-
/// <summary>
/// Gets the user repository.
/// </summary>
@@ -1096,7 +1072,7 @@ namespace Emby.Server.Implementations
GetExports<IMetadataSaver>(),
GetExports<IExternalId>());
- ImageProcessor.AddParts(GetExports<IImageEnhancer>());
+ ImageProcessor.ImageEnhancers = GetExports<IImageEnhancer>();
LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
index 77f5d9479..1a67ab912 100644
--- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
@@ -62,14 +62,14 @@ namespace Emby.Server.Implementations.Data
/// <returns>Task.</returns>
private void InitializeInternal()
{
- using (var connection = GetConnection())
+ string[] queries =
{
- string[] queries = {
-
- "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
- "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
- };
+ "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
+ "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
+ };
+ using (var connection = GetConnection())
+ {
connection.RunQueries(queries);
}
}
@@ -81,7 +81,6 @@ namespace Emby.Server.Implementations.Data
/// <param name="userId">The user id.</param>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
{
@@ -99,10 +98,9 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
- {
- SaveDisplayPreferences(displayPreferences, userId, client, db);
- }, TransactionMode);
+ connection.RunInTransaction(
+ db => SaveDisplayPreferences(displayPreferences, userId, client, db),
+ TransactionMode);
}
}
@@ -127,7 +125,6 @@ namespace Emby.Server.Implementations.Data
/// <param name="displayPreferences">The display preferences.</param>
/// <param name="userId">The user id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
{
@@ -140,13 +137,15 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
- {
- foreach (var displayPreference in displayPreferences)
+ connection.RunInTransaction(
+ db =>
{
- SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
- }
- }, TransactionMode);
+ foreach (var displayPreference in displayPreferences)
+ {
+ SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
+ }
+ },
+ TransactionMode);
}
}
@@ -180,12 +179,12 @@ namespace Emby.Server.Implementations.Data
return Get(row);
}
}
-
- return new DisplayPreferences
- {
- Id = guidId.ToString("N", CultureInfo.InvariantCulture)
- };
}
+
+ return new DisplayPreferences
+ {
+ Id = guidId.ToString("N", CultureInfo.InvariantCulture)
+ };
}
/// <summary>
@@ -215,22 +214,12 @@ namespace Emby.Server.Implementations.Data
}
private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
- {
- using (var stream = new MemoryStream(row[0].ToBlob()))
- {
- stream.Position = 0;
- return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream);
- }
- }
+ => _jsonSerializer.DeserializeFromString<DisplayPreferences>(row.GetString(0));
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
- {
- SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
- }
+ => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
- {
- return GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
- }
+ => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index c0f27b25a..0fb2c10fd 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -18,10 +18,6 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(conn =>
{
- //foreach (var query in queries)
- //{
- // conn.Execute(query);
- //}
conn.ExecuteAll(string.Join(";", queries));
});
}
@@ -38,7 +34,8 @@ namespace Emby.Server.Implementations.Data
public static Guid ReadGuidFromBlob(this IResultSetValue result)
{
- return new Guid(result.ToBlob());
+ // TODO: Remove ToArray when upgrading to netstandard2.1
+ return new Guid(result.ToBlob().ToArray());
}
public static string ToDateTimeParamValue(this DateTime dateValue)
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index bb4c34f02..9d983307f 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -99,35 +99,114 @@ namespace Emby.Server.Implementations.Data
/// </summary>
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
{
- using (var connection = GetConnection())
- {
- const string createMediaStreamsTableCommand
+ const string CreateMediaStreamsTableCommand
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))";
- string[] queries = {
- "PRAGMA locking_mode=EXCLUSIVE",
+ string[] queries =
+ {
+ "PRAGMA locking_mode=EXCLUSIVE",
- "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)",
+ "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)",
- "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))",
- "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)",
- "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)",
+ "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))",
+ "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)",
+ "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)",
- "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)",
+ "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)",
- "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)",
+ "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)",
- "drop index if exists idxPeopleItemId",
- "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)",
- "create index if not exists idxPeopleName on People(Name)",
+ "drop index if exists idxPeopleItemId",
+ "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)",
+ "create index if not exists idxPeopleName on People(Name)",
- "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
+ "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
- createMediaStreamsTableCommand,
+ CreateMediaStreamsTableCommand,
- "pragma shrink_memory"
- };
+ "pragma shrink_memory"
+ };
+
+ string[] postQueries =
+ {
+ // obsolete
+ "drop index if exists idx_TypedBaseItems",
+ "drop index if exists idx_mediastreams",
+ "drop index if exists idx_mediastreams1",
+ "drop index if exists idx_"+ChaptersTableName,
+ "drop index if exists idx_UserDataKeys1",
+ "drop index if exists idx_UserDataKeys2",
+ "drop index if exists idx_TypeTopParentId3",
+ "drop index if exists idx_TypeTopParentId2",
+ "drop index if exists idx_TypeTopParentId4",
+ "drop index if exists idx_Type",
+ "drop index if exists idx_TypeTopParentId",
+ "drop index if exists idx_GuidType",
+ "drop index if exists idx_TopParentId",
+ "drop index if exists idx_TypeTopParentId6",
+ "drop index if exists idx_ItemValues2",
+ "drop index if exists Idx_ProviderIds",
+ "drop index if exists idx_ItemValues3",
+ "drop index if exists idx_ItemValues4",
+ "drop index if exists idx_ItemValues5",
+ "drop index if exists idx_UserDataKeys3",
+ "drop table if exists UserDataKeys",
+ "drop table if exists ProviderIds",
+ "drop index if exists Idx_ProviderIds1",
+ "drop table if exists Images",
+ "drop index if exists idx_Images",
+ "drop index if exists idx_TypeSeriesPresentationUniqueKey",
+ "drop index if exists idx_SeriesPresentationUniqueKey",
+ "drop index if exists idx_TypeSeriesPresentationUniqueKey2",
+ "drop index if exists idx_AncestorIds3",
+ "drop index if exists idx_AncestorIds4",
+ "drop index if exists idx_AncestorIds2",
+
+ "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)",
+ "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)",
+
+ "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)",
+ "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)",
+ "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)",
+
+ // covering index
+ "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)",
+
+ // series
+ "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)",
+
+ // series counts
+ // seriesdateplayed sort order
+ "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)",
+
+ // live tv programs
+ "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)",
+
+ // covering index for getitemvalues
+ "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)",
+
+ // used by movie suggestions
+ "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)",
+ "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)",
+
+ // latest items
+ "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)",
+ "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)",
+
+ // resume
+ "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)",
+
+ // items by name
+ "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)",
+ "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)",
+
+ // Used to update inherited tags
+ "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)",
+ };
+
+ using (var connection = GetConnection())
+ {
connection.RunQueries(queries);
connection.RunInTransaction(db =>
@@ -235,83 +314,6 @@ namespace Emby.Server.Implementations.Data
}, TransactionMode);
- string[] postQueries =
- {
- // obsolete
- "drop index if exists idx_TypedBaseItems",
- "drop index if exists idx_mediastreams",
- "drop index if exists idx_mediastreams1",
- "drop index if exists idx_"+ChaptersTableName,
- "drop index if exists idx_UserDataKeys1",
- "drop index if exists idx_UserDataKeys2",
- "drop index if exists idx_TypeTopParentId3",
- "drop index if exists idx_TypeTopParentId2",
- "drop index if exists idx_TypeTopParentId4",
- "drop index if exists idx_Type",
- "drop index if exists idx_TypeTopParentId",
- "drop index if exists idx_GuidType",
- "drop index if exists idx_TopParentId",
- "drop index if exists idx_TypeTopParentId6",
- "drop index if exists idx_ItemValues2",
- "drop index if exists Idx_ProviderIds",
- "drop index if exists idx_ItemValues3",
- "drop index if exists idx_ItemValues4",
- "drop index if exists idx_ItemValues5",
- "drop index if exists idx_UserDataKeys3",
- "drop table if exists UserDataKeys",
- "drop table if exists ProviderIds",
- "drop index if exists Idx_ProviderIds1",
- "drop table if exists Images",
- "drop index if exists idx_Images",
- "drop index if exists idx_TypeSeriesPresentationUniqueKey",
- "drop index if exists idx_SeriesPresentationUniqueKey",
- "drop index if exists idx_TypeSeriesPresentationUniqueKey2",
- "drop index if exists idx_AncestorIds3",
- "drop index if exists idx_AncestorIds4",
- "drop index if exists idx_AncestorIds2",
-
- "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)",
- "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)",
-
- "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)",
- "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)",
- "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)",
-
- // covering index
- "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)",
-
- // series
- "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)",
-
- // series counts
- // seriesdateplayed sort order
- "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)",
-
- // live tv programs
- "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)",
-
- // covering index for getitemvalues
- "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)",
-
- // used by movie suggestions
- "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)",
- "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)",
-
- // latest items
- "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)",
- "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)",
-
- // resume
- "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)",
-
- // items by name
- "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)",
- "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)",
-
- // Used to update inherited tags
- "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)",
- };
-
connection.RunQueries(postQueries);
}
@@ -1298,18 +1300,13 @@ namespace Emby.Server.Implementations.Data
if (TypeRequiresDeserialization(type))
{
- using (var stream = new MemoryStream(reader[1].ToBlob()))
+ try
{
- stream.Position = 0;
-
- try
- {
- item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem;
- }
- catch (SerializationException ex)
- {
- Logger.LogError(ex, "Error deserializing item");
- }
+ item = _jsonSerializer.DeserializeFromString(reader.GetString(1), type) as BaseItem;
+ }
+ catch (SerializationException ex)
+ {
+ Logger.LogError(ex, "Error deserializing item");
}
}
@@ -1999,14 +1996,14 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(chapters));
}
+ var idBlob = id.ToGuidBlob();
+
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
{
- var idBlob = id.ToGuidBlob();
-
- // First delete chapters
- db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
+ // First delete chapters
+ db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
InsertChapters(idBlob, chapters, db);
@@ -2535,6 +2532,7 @@ namespace Emby.Server.Implementations.Data
commandText += " where " + string.Join(" AND ", whereClauses);
}
+ int count;
using (var connection = GetConnection(true))
{
using (var statement = PrepareStatement(connection, commandText))
@@ -2550,11 +2548,12 @@ namespace Emby.Server.Implementations.Data
// Running this again will bind the params
GetWhereClauses(query, statement);
- var count = statement.ExecuteQuery().SelectScalarInt().First();
- LogQueryTime("GetCount", commandText, now);
- return count;
+ count = statement.ExecuteQuery().SelectScalarInt().First();
}
}
+
+ LogQueryTime("GetCount", commandText, now);
+ return count;
}
public List<BaseItem> GetItemList(InternalItemsQuery query)
@@ -2604,10 +2603,9 @@ namespace Emby.Server.Implementations.Data
}
}
+ var items = new List<BaseItem>();
using (var connection = GetConnection(true))
{
- var items = new List<BaseItem>();
-
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2658,11 +2656,11 @@ namespace Emby.Server.Implementations.Data
items = newList;
}
+ }
- LogQueryTime("GetItemList", commandText, now);
+ LogQueryTime("GetItemList", commandText, now);
- return items;
- }
+ return items;
}
private string FixUnicodeChars(string buffer)
@@ -2755,8 +2753,6 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
- var list = new List<BaseItem>();
-
// Hack for right now since we currently don't support filtering out these duplicates within a query
if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
{
@@ -2822,11 +2818,13 @@ namespace Emby.Server.Implementations.Data
statementTexts.Add(commandText);
}
+ var list = new List<BaseItem>();
+ var result = new QueryResult<BaseItem>();
using (var connection = GetConnection(true))
{
- return connection.RunInTransaction(db =>
+ connection.RunInTransaction(db =>
{
- var result = new QueryResult<BaseItem>();
+
var statements = PrepareAll(db, statementTexts).ToList();
if (!isReturningZeroItems)
@@ -2881,14 +2879,12 @@ namespace Emby.Server.Implementations.Data
result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
}
}
-
- LogQueryTime("GetItems", commandText, now);
-
- result.Items = list.ToArray();
- return result;
-
}, ReadTransactionMode);
}
+
+ LogQueryTime("GetItems", commandText, now);
+ result.Items = list.ToArray();
+ return result;
}
private string GetOrderByText(InternalItemsQuery query)
@@ -3054,10 +3050,9 @@ namespace Emby.Server.Implementations.Data
}
}
+ var list = new List<Guid>();
using (var connection = GetConnection(true))
{
- var list = new List<Guid>();
-
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -3076,11 +3071,10 @@ namespace Emby.Server.Implementations.Data
list.Add(row[0].ReadGuidFromBlob());
}
}
-
- LogQueryTime("GetItemList", commandText, now);
-
- return list;
}
+
+ LogQueryTime("GetItemList", commandText, now);
+ return list;
}
public List<Tuple<Guid, string>> GetItemIdsWithPath(InternalItemsQuery query)
@@ -3142,6 +3136,7 @@ namespace Emby.Server.Implementations.Data
{
path = row.GetString(1);
}
+
list.Add(new Tuple<Guid, string>(id, path));
}
}
@@ -3203,7 +3198,7 @@ namespace Emby.Server.Implementations.Data
}
}
- var list = new List<Guid>();
+
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
var statementTexts = new List<string>();
@@ -3233,12 +3228,12 @@ namespace Emby.Server.Implementations.Data
statementTexts.Add(commandText);
}
+ var list = new List<Guid>();
+ var result = new QueryResult<Guid>();
using (var connection = GetConnection(true))
{
- return connection.RunInTransaction(db =>
+ connection.RunInTransaction(db =>
{
- var result = new QueryResult<Guid>();
-
var statements = PrepareAll(db, statementTexts).ToList();
if (!isReturningZeroItems)
@@ -3281,14 +3276,13 @@ namespace Emby.Server.Implementations.Data
result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
}
}
-
- LogQueryTime("GetItemIds", commandText, now);
-
- result.Items = list.ToArray();
- return result;
-
}, ReadTransactionMode);
}
+
+ LogQueryTime("GetItemIds", commandText, now);
+
+ result.Items = list.ToArray();
+ return result;
}
private bool IsAlphaNumeric(string str)
@@ -4864,22 +4858,25 @@ namespace Emby.Server.Implementations.Data
private void UpdateInheritedTags(CancellationToken cancellationToken)
{
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(db =>
+ string sql = string.Join(
+ ";",
+ new string[]
{
- connection.ExecuteAll(string.Join(";", new string[]
- {
- "delete from itemvalues where type = 6",
+ "delete from itemvalues where type = 6",
- "insert into itemvalues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
+ "insert into itemvalues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
- @"insert into itemvalues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
+ @"insert into itemvalues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
FROM AncestorIds
LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 "
+ });
- }));
+ using (var connection = GetConnection())
+ {
+ connection.RunInTransaction(db =>
+ {
+ connection.ExecuteAll(sql);
}, TransactionMode);
}
@@ -4933,23 +4930,23 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{
var idBlob = id.ToGuidBlob();
- // Delete people
- ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob);
+ // Delete people
+ ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob);
- // Delete chapters
- ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob);
+ // Delete chapters
+ ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob);
- // Delete media streams
- ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob);
+ // Delete media streams
+ ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob);
- // Delete ancestors
- ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob);
+ // Delete ancestors
+ ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob);
- // Delete item values
- ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob);
+ // Delete item values
+ ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob);
- // Delete the item
- ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob);
+ // Delete the item
+ ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob);
}, TransactionMode);
}
}
@@ -4997,6 +4994,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
list.Add(row.GetString(0));
}
}
+
return list;
}
}
@@ -5247,10 +5245,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
commandText += " Group By CleanValue";
+ var list = new List<string>();
using (var connection = GetConnection(true))
{
- var list = new List<string>();
-
using (var statement = PrepareStatement(connection, commandText))
{
foreach (var row in statement.ExecuteQuery())
@@ -5262,10 +5259,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
}
- LogQueryTime("GetItemValueNames", commandText, now);
-
- return list;
}
+
+ LogQueryTime("GetItemValueNames", commandText, now);
+ return list;
}
private QueryResult<(BaseItem, ItemCounts)> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType)
@@ -5422,6 +5419,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{
statementTexts.Add(commandText);
}
+
if (query.EnableTotalRecordCount)
{
var countText = "select "
@@ -5433,98 +5431,98 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
statementTexts.Add(countText);
}
+ var list = new List<(BaseItem, ItemCounts)>();
+ var result = new QueryResult<(BaseItem, ItemCounts)>();
using (var connection = GetConnection(true))
{
- return connection.RunInTransaction(db =>
- {
- var list = new List<(BaseItem, ItemCounts)>();
- var result = new QueryResult<(BaseItem, ItemCounts)>();
-
- var statements = PrepareAll(db, statementTexts).ToList();
-
- if (!isReturningZeroItems)
+ connection.RunInTransaction(
+ db =>
{
- using (var statement = statements[0])
+ var statements = PrepareAll(db, statementTexts).ToList();
+
+ if (!isReturningZeroItems)
{
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
+ using (var statement = statements[0])
{
- statement.TryBind("@UserId", query.User.InternalId);
- }
+ statement.TryBind("@SelectType", returnType);
+ if (EnableJoinUserData(query))
+ {
+ statement.TryBind("@UserId", query.User.InternalId);
+ }
- if (typeSubQuery != null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
+ if (typeSubQuery != null)
+ {
+ GetWhereClauses(typeSubQuery, null);
+ }
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
+ BindSimilarParams(query, statement);
+ BindSearchParams(query, statement);
+ GetWhereClauses(innerQuery, statement);
+ GetWhereClauses(outerQuery, statement);
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
- if (item != null)
+ var hasEpisodeAttributes = HasEpisodeAttributes(query);
+ var hasProgramAttributes = HasProgramAttributes(query);
+ var hasServiceName = HasServiceName(query);
+ var hasStartDate = HasStartDate(query);
+ var hasTrailerTypes = HasTrailerTypes(query);
+ var hasArtistFields = HasArtistFields(query);
+ var hasSeriesFields = HasSeriesFields(query);
+
+ foreach (var row in statement.ExecuteQuery())
{
- var countStartColumn = columns.Count - 1;
+ var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
+ if (item != null)
+ {
+ var countStartColumn = columns.Count - 1;
- list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
+ list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
+ }
}
}
-
- LogQueryTime("GetItemValues", commandText, now);
}
- }
-
- if (query.EnableTotalRecordCount)
- {
- commandText = "select "
- + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query)
- + whereText;
- using (var statement = statements[statements.Count - 1])
+ if (query.EnableTotalRecordCount)
{
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
+ commandText = "select "
+ + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+ + GetFromText()
+ + GetJoinUserDataText(query)
+ + whereText;
- if (typeSubQuery != null)
+ using (var statement = statements[statements.Count - 1])
{
- GetWhereClauses(typeSubQuery, null);
- }
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
+ statement.TryBind("@SelectType", returnType);
+ if (EnableJoinUserData(query))
+ {
+ statement.TryBind("@UserId", query.User.InternalId);
+ }
- result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
+ if (typeSubQuery != null)
+ {
+ GetWhereClauses(typeSubQuery, null);
+ }
+ BindSimilarParams(query, statement);
+ BindSearchParams(query, statement);
+ GetWhereClauses(innerQuery, statement);
+ GetWhereClauses(outerQuery, statement);
- LogQueryTime("GetItemValues", commandText, now);
+ result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
+ }
}
- }
-
- if (result.TotalRecordCount == 0)
- {
- result.TotalRecordCount = list.Count;
- }
- result.Items = list.ToArray();
+ },
+ ReadTransactionMode);
+ }
- return result;
+ LogQueryTime("GetItemValues", commandText, now);
- }, ReadTransactionMode);
+ if (result.TotalRecordCount == 0)
+ {
+ result.TotalRecordCount = list.Count;
}
+
+ result.Items = list.ToArray();
+
+ return result;
}
private ItemCounts GetItemCounts(IReadOnlyList<IResultSetValue> reader, int countStartColumn, string[] typesToCount)
@@ -5707,8 +5705,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{
var itemIdBlob = itemId.ToGuidBlob();
- // First delete chapters
- db.Execute("delete from People where ItemId=@ItemId", itemIdBlob);
+ // First delete chapters
+ db.Execute("delete from People where ItemId=@ItemId", itemIdBlob);
InsertPeople(itemIdBlob, people, db);
@@ -5868,8 +5866,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{
var itemIdBlob = id.ToGuidBlob();
- // First delete chapters
- db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);
+ // First delete chapters
+ db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);
InsertMediaStreams(itemIdBlob, streams, db);
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 4035bb99d..9d4855bcf 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Data
var userDatasTableExists = TableExists(connection, "UserDatas");
var userDataTableExists = TableExists(connection, "userdata");
- var users = userDatasTableExists ? null : userManager.Users.ToArray();
+ var users = userDatasTableExists ? null : userManager.Users;
connection.RunInTransaction(db =>
{
@@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.Data
}
}
- private void ImportUserIds(IDatabaseConnection db, User[] users)
+ private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users)
{
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs
index d6d250fb3..11629b389 100644
--- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs
@@ -35,9 +35,8 @@ namespace Emby.Server.Implementations.Data
public string Name => "SQLite";
/// <summary>
- /// Opens the connection to the database
+ /// Opens the connection to the database.
/// </summary>
- /// <returns>Task.</returns>
public void Initialize()
{
using (var connection = GetConnection())
@@ -180,14 +179,10 @@ namespace Emby.Server.Implementations.Data
var id = row[0].ToInt64();
var guid = row[1].ReadGuidFromBlob();
- using (var stream = new MemoryStream(row[2].ToBlob()))
- {
- stream.Position = 0;
- var user = _jsonSerializer.DeserializeFromStream<User>(stream);
- user.InternalId = id;
- user.Id = guid;
- return user;
- }
+ var user = _jsonSerializer.DeserializeFromString<User>(row.GetString(2));
+ user.InternalId = id;
+ user.Id = guid;
+ return user;
}
/// <summary>
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 6e7aa1313..1a7f10634 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1364,7 +1364,7 @@ namespace Emby.Server.Implementations.Dto
return null;
}
- var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary);
+ var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToArray();
ImageDimensions size;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index c78d96d4a..b48193c58 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -34,7 +34,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.6.0" />
<PackageReference Include="sharpcompress" Version="0.23.0" />
- <PackageReference Include="SQLitePCL.pretty.netstandard" Version="1.0.0" />
+ <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
index b7565adec..b2328121e 100644
--- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
+++ b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
@@ -50,9 +50,7 @@ namespace Emby.Server.Implementations.EntryPoints
public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
- var users = _userManager.Users.ToList();
-
- foreach (var user in users)
+ foreach (var user in _userManager.Users)
{
cancellationToken.ThrowIfCancellationRequested();
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index 4c233456c..bdcf5d0b7 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -7,6 +7,7 @@ using System.Net.Sockets;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
+using Emby.Server.Implementations.Configuration;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Services;
using MediaBrowser.Common.Extensions;
@@ -470,64 +471,10 @@ namespace Emby.Server.Implementations.HttpServer
urlToLog = GetUrlToLog(urlString);
- if (string.Equals(localPath, "/emby/", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(localPath, "/" + _config.Configuration.BaseUrl + "/", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(localPath, "/" + _config.Configuration.BaseUrl, StringComparison.OrdinalIgnoreCase))
{
- httpRes.Redirect(_defaultRedirectPath);
- return;
- }
-
- if (string.Equals(localPath, "/emby", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase))
- {
- httpRes.Redirect("emby/" + _defaultRedirectPath);
- return;
- }
-
- if (localPath.IndexOf("mediabrowser/web", StringComparison.OrdinalIgnoreCase) != -1)
- {
- httpRes.StatusCode = 200;
- httpRes.ContentType = "text/html";
- var newUrl = urlString.Replace("mediabrowser", "emby", StringComparison.OrdinalIgnoreCase)
- .Replace("/dashboard/", "/web/", StringComparison.OrdinalIgnoreCase);
-
- if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase))
- {
- await httpRes.WriteAsync(
- "<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" +
- newUrl + "\">" + newUrl + "</a></body></html>",
- cancellationToken).ConfigureAwait(false);
- return;
- }
- }
-
- if (localPath.IndexOf("dashboard/", StringComparison.OrdinalIgnoreCase) != -1 &&
- localPath.IndexOf("web/dashboard", StringComparison.OrdinalIgnoreCase) == -1)
- {
- httpRes.StatusCode = 200;
- httpRes.ContentType = "text/html";
- var newUrl = urlString.Replace("mediabrowser", "emby", StringComparison.OrdinalIgnoreCase)
- .Replace("/dashboard/", "/web/", StringComparison.OrdinalIgnoreCase);
-
- if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase))
- {
- await httpRes.WriteAsync(
- "<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" +
- newUrl + "\">" + newUrl + "</a></body></html>",
- cancellationToken).ConfigureAwait(false);
- return;
- }
- }
-
- if (string.Equals(localPath, "/web", StringComparison.OrdinalIgnoreCase))
- {
- httpRes.Redirect(_defaultRedirectPath);
- return;
- }
-
- if (string.Equals(localPath, "/web/", StringComparison.OrdinalIgnoreCase))
- {
- httpRes.Redirect("../" + _defaultRedirectPath);
+ httpRes.Redirect("/" + _config.Configuration.BaseUrl + "/" + _defaultRedirectPath);
return;
}
@@ -543,19 +490,6 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
- if (!string.Equals(httpReq.QueryString["r"], "0", StringComparison.OrdinalIgnoreCase))
- {
- if (localPath.EndsWith("web/dashboard.html", StringComparison.OrdinalIgnoreCase))
- {
- httpRes.Redirect("index.html#!/dashboard.html");
- }
-
- if (localPath.EndsWith("web/home.html", StringComparison.OrdinalIgnoreCase))
- {
- httpRes.Redirect("index.html");
- }
- }
-
if (!string.IsNullOrEmpty(GlobalResponse))
{
// We don't want the address pings in ApplicationHost to fail
@@ -569,7 +503,6 @@ namespace Emby.Server.Implementations.HttpServer
}
var handler = GetServiceHandler(httpReq);
-
if (handler != null)
{
await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
@@ -663,22 +596,14 @@ namespace Emby.Server.Implementations.HttpServer
foreach (var route in clone)
{
- routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs)
+ routes.Add(new RouteAttribute(NormalizeCustomRoutePath(_config.Configuration.BaseUrl, route.Path), route.Verbs)
{
Notes = route.Notes,
Priority = route.Priority,
Summary = route.Summary
});
- routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs)
- {
- Notes = route.Notes,
- Priority = route.Priority,
- Summary = route.Summary
- });
-
- // needed because apps add /emby, and some users also add /emby, thereby double prefixing
- routes.Add(new RouteAttribute(DoubleNormalizeEmbyRoutePath(route.Path), route.Verbs)
+ routes.Add(new RouteAttribute(NormalizeOldRoutePath(route.Path), route.Verbs)
{
Notes = route.Notes,
Priority = route.Priority,
@@ -719,8 +644,8 @@ namespace Emby.Server.Implementations.HttpServer
return _socketListener.ProcessWebSocketRequest(context);
}
- //TODO Add Jellyfin Route Path Normalizer
- private static string NormalizeEmbyRoutePath(string path)
+ // this method was left for compatibility with third party clients
+ private static string NormalizeOldRoutePath(string path)
{
if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase))
{
@@ -730,24 +655,14 @@ namespace Emby.Server.Implementations.HttpServer
return "emby/" + path;
}
- private static string NormalizeMediaBrowserRoutePath(string path)
- {
- if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase))
- {
- return "/mediabrowser" + path;
- }
-
- return "mediabrowser/" + path;
- }
-
- private static string DoubleNormalizeEmbyRoutePath(string path)
+ private static string NormalizeCustomRoutePath(string baseUrl, string path)
{
if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase))
{
- return "/emby/emby" + path;
+ return "/" + baseUrl + path;
}
- return "emby/emby/" + path;
+ return baseUrl + "/" + path;
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/IO/IsoManager.cs b/Emby.Server.Implementations/IO/IsoManager.cs
index f0a15097c..94e92c2a6 100644
--- a/Emby.Server.Implementations/IO/IsoManager.cs
+++ b/Emby.Server.Implementations/IO/IsoManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -8,12 +9,12 @@ using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.IO
{
/// <summary>
- /// Class IsoManager
+ /// Class IsoManager.
/// </summary>
public class IsoManager : IIsoManager
{
/// <summary>
- /// The _mounters
+ /// The _mounters.
/// </summary>
private readonly List<IIsoMounter> _mounters = new List<IIsoMounter>();
@@ -22,9 +23,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="isoPath">The iso path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>IsoMount.</returns>
- /// <exception cref="ArgumentNullException">isoPath</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <returns><see creaf="IsoMount" />.</returns>
public Task<IIsoMount> Mount(string isoPath, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(isoPath))
@@ -36,7 +35,11 @@ namespace Emby.Server.Implementations.IO
if (mounter == null)
{
- throw new ArgumentException(string.Format("No mounters are able to mount {0}", isoPath));
+ throw new ArgumentException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "No mounters are able to mount {0}",
+ isoPath));
}
return mounter.Mount(isoPath, cancellationToken);
@@ -60,16 +63,5 @@ namespace Emby.Server.Implementations.IO
{
_mounters.AddRange(mounters);
}
-
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- public void Dispose()
- {
- foreach (var mounter in _mounters)
- {
- mounter.Dispose();
- }
- }
}
}
diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs
index c7044820c..fa6bbcf91 100644
--- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs
+++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs
@@ -1,15 +1,12 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
-using System.Linq;
-using System.Text;
+using System.Security.Cryptography;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
@@ -17,32 +14,37 @@ namespace Emby.Server.Implementations.Library
{
public class DefaultPasswordResetProvider : IPasswordResetProvider
{
- public string Name => "Default Password Reset Provider";
+ private const string BaseResetFileName = "passwordreset";
- public bool IsEnabled => true;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IUserManager _userManager;
private readonly string _passwordResetFileBase;
private readonly string _passwordResetFileBaseDir;
- private readonly string _passwordResetFileBaseName = "passwordreset";
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IUserManager _userManager;
- private readonly ICryptoProvider _crypto;
-
- public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider)
+ public DefaultPasswordResetProvider(
+ IServerConfigurationManager configurationManager,
+ IJsonSerializer jsonSerializer,
+ IUserManager userManager)
{
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
- _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName);
+ _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
_jsonSerializer = jsonSerializer;
_userManager = userManager;
- _crypto = cryptoProvider;
}
+ /// <inheritdoc />
+ public string Name => "Default Password Reset Provider";
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{
SerializablePasswordReset spr;
- HashSet<string> usersreset = new HashSet<string>();
- foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*"))
+ List<string> usersreset = new List<string>();
+ foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{
using (var str = File.OpenRead(resetfile))
{
@@ -53,12 +55,15 @@ namespace Emby.Server.Implementations.Library
{
File.Delete(resetfile);
}
- else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase))
+ else if (string.Equals(
+ spr.Pin.Replace("-", string.Empty),
+ pin.Replace("-", string.Empty),
+ StringComparison.InvariantCultureIgnoreCase))
{
var resetUser = _userManager.GetUserByName(spr.UserName);
if (resetUser == null)
{
- throw new Exception($"User with a username of {spr.UserName} not found");
+ throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
}
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
@@ -81,10 +86,11 @@ namespace Emby.Server.Implementations.Library
}
}
+ /// <inheritdoc />
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
{
string pin = string.Empty;
- using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create())
+ using (var cryptoRandom = RandomNumberGenerator.Create())
{
byte[] bytes = new byte[4];
cryptoRandom.GetBytes(bytes);
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
index c8c8a108d..a7ea13ca6 100644
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ b/Emby.Server.Implementations/Library/UserManager.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -11,13 +12,11 @@ using MediaBrowser.Common.Events;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
@@ -41,34 +40,19 @@ namespace Emby.Server.Implementations.Library
public class UserManager : IUserManager
{
/// <summary>
- /// Gets the users.
- /// </summary>
- /// <value>The users.</value>
- public IEnumerable<User> Users => _users;
-
- private User[] _users;
-
- /// <summary>
/// The _logger
/// </summary>
private readonly ILogger _logger;
- /// <summary>
- /// Gets or sets the configuration manager.
- /// </summary>
- /// <value>The configuration manager.</value>
- private IServerConfigurationManager ConfigurationManager { get; set; }
+ private readonly object _policySyncLock = new object();
/// <summary>
/// Gets the active user repository
/// </summary>
/// <value>The user repository.</value>
- private IUserRepository UserRepository { get; set; }
- public event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
-
+ private readonly IUserRepository _userRepository;
private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
-
private readonly INetworkManager _networkManager;
private readonly Func<IImageProcessor> _imageProcessorFactory;
@@ -76,6 +60,8 @@ namespace Emby.Server.Implementations.Library
private readonly IServerApplicationHost _appHost;
private readonly IFileSystem _fileSystem;
+ private ConcurrentDictionary<Guid, User> _users;
+
private IAuthenticationProvider[] _authenticationProviders;
private DefaultAuthenticationProvider _defaultAuthenticationProvider;
@@ -85,8 +71,7 @@ namespace Emby.Server.Implementations.Library
private DefaultPasswordResetProvider _defaultPasswordResetProvider;
public UserManager(
- ILoggerFactory loggerFactory,
- IServerConfigurationManager configurationManager,
+ ILogger<UserManager> logger,
IUserRepository userRepository,
IXmlSerializer xmlSerializer,
INetworkManager networkManager,
@@ -96,8 +81,8 @@ namespace Emby.Server.Implementations.Library
IJsonSerializer jsonSerializer,
IFileSystem fileSystem)
{
- _logger = loggerFactory.CreateLogger(nameof(UserManager));
- UserRepository = userRepository;
+ _logger = logger;
+ _userRepository = userRepository;
_xmlSerializer = xmlSerializer;
_networkManager = networkManager;
_imageProcessorFactory = imageProcessorFactory;
@@ -105,8 +90,51 @@ namespace Emby.Server.Implementations.Library
_appHost = appHost;
_jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
- ConfigurationManager = configurationManager;
- _users = Array.Empty<User>();
+ _users = null;
+ }
+
+ public event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
+
+ /// <summary>
+ /// Occurs when [user updated].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<User>> UserUpdated;
+
+ public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated;
+
+ public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
+
+ public event EventHandler<GenericEventArgs<User>> UserLockedOut;
+
+ public event EventHandler<GenericEventArgs<User>> UserCreated;
+
+ /// <summary>
+ /// Occurs when [user deleted].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<User>> UserDeleted;
+
+ /// <inheritdoc />
+ public IEnumerable<User> Users => _users.Values;
+
+ /// <inheritdoc />
+ public IEnumerable<Guid> UsersIds => _users.Keys;
+
+ /// <summary>
+ /// Called when [user updated].
+ /// </summary>
+ /// <param name="user">The user.</param>
+ private void OnUserUpdated(User user)
+ {
+ UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
+ }
+
+ /// <summary>
+ /// Called when [user deleted].
+ /// </summary>
+ /// <param name="user">The user.</param>
+ private void OnUserDeleted(User user)
+ {
+ UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user });
}
public NameIdPair[] GetAuthenticationProviders()
@@ -137,7 +165,7 @@ namespace Emby.Server.Implementations.Library
.ToArray();
}
- public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders,IEnumerable<IPasswordResetProvider> passwordResetProviders)
+ public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
{
_authenticationProviders = authenticationProviders.ToArray();
@@ -150,54 +178,21 @@ namespace Emby.Server.Implementations.Library
_defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
}
- #region UserUpdated Event
- /// <summary>
- /// Occurs when [user updated].
- /// </summary>
- public event EventHandler<GenericEventArgs<User>> UserUpdated;
- public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated;
- public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
- public event EventHandler<GenericEventArgs<User>> UserLockedOut;
-
- /// <summary>
- /// Called when [user updated].
- /// </summary>
- /// <param name="user">The user.</param>
- private void OnUserUpdated(User user)
- {
- UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
- }
- #endregion
-
- #region UserDeleted Event
- /// <summary>
- /// Occurs when [user deleted].
- /// </summary>
- public event EventHandler<GenericEventArgs<User>> UserDeleted;
- /// <summary>
- /// Called when [user deleted].
- /// </summary>
- /// <param name="user">The user.</param>
- private void OnUserDeleted(User user)
- {
- UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user });
- }
- #endregion
-
/// <summary>
- /// Gets a User by Id
+ /// Gets a User by Id.
/// </summary>
/// <param name="id">The id.</param>
/// <returns>User.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentException"></exception>
public User GetUserById(Guid id)
{
if (id == Guid.Empty)
{
- throw new ArgumentException(nameof(id), "Guid can't be empty");
+ throw new ArgumentException("Guid can't be empty", nameof(id));
}
- return Users.FirstOrDefault(u => u.Id == id);
+ _users.TryGetValue(id, out User user);
+ return user;
}
/// <summary>
@@ -206,15 +201,13 @@ namespace Emby.Server.Implementations.Library
/// <param name="id">The identifier.</param>
/// <returns>User.</returns>
public User GetUserById(string id)
- {
- return GetUserById(new Guid(id));
- }
+ => GetUserById(new Guid(id));
public User GetUserByName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
- throw new ArgumentNullException(nameof(name));
+ throw new ArgumentException("Invalid username", nameof(name));
}
return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase));
@@ -222,8 +215,9 @@ namespace Emby.Server.Implementations.Library
public void Initialize()
{
- var users = LoadUsers();
- _users = users.ToArray();
+ LoadUsers();
+
+ var users = Users;
// If there are no local users with admin rights, make them all admins
if (!users.Any(i => i.Policy.IsAdministrator))
@@ -240,14 +234,12 @@ namespace Emby.Server.Implementations.Library
{
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
- // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
+ // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
return Regex.IsMatch(username, @"^[\w\-'._@]*$");
}
private static bool IsValidUsernameCharacter(char i)
- {
- return IsValidUsername(i.ToString());
- }
+ => IsValidUsername(i.ToString(CultureInfo.InvariantCulture));
public string MakeValidUsername(string username)
{
@@ -277,8 +269,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(username));
}
- var user = Users
- .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
+ var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
var success = false;
string updatedUsername = null;
@@ -299,13 +290,12 @@ namespace Emby.Server.Implementations.Library
updatedUsername = authResult.username;
success = authResult.success;
- if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider))
+ if (success
+ && authenticationProvider != null
+ && !(authenticationProvider is DefaultAuthenticationProvider))
{
// We should trust the user that the authprovider says, not what was typed
- if (updatedUsername != username)
- {
- username = updatedUsername;
- }
+ username = updatedUsername;
// Search the database for the user again; the authprovider might have created it
user = Users
@@ -337,10 +327,11 @@ namespace Emby.Server.Implementations.Library
if (user.Policy.IsDisabled)
{
- throw new AuthenticationException(string.Format(
- CultureInfo.InvariantCulture,
- "The {0} account is currently disabled. Please consult with your administrator.",
- user.Name));
+ throw new AuthenticationException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "The {0} account is currently disabled. Please consult with your administrator.",
+ user.Name));
}
if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
@@ -386,7 +377,7 @@ namespace Emby.Server.Implementations.Library
private IAuthenticationProvider GetAuthenticationProvider(User user)
{
- return GetAuthenticationProviders(user).First();
+ return GetAuthenticationProviders(user)[0];
}
private IPasswordResetProvider GetPasswordResetProvider(User user)
@@ -396,7 +387,7 @@ namespace Emby.Server.Implementations.Library
private IAuthenticationProvider[] GetAuthenticationProviders(User user)
{
- var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
+ var authenticationProviderId = user?.Policy.AuthenticationProviderId;
var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray();
@@ -438,16 +429,10 @@ namespace Emby.Server.Implementations.Library
{
try
{
- var requiresResolvedUser = provider as IRequiresResolvedUser;
- ProviderAuthenticationResult authenticationResult = null;
- if (requiresResolvedUser != null)
- {
- authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
- }
- else
- {
- authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false);
- }
+
+ var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
+ ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
+ : await provider.Authenticate(username, password).ConfigureAwait(false);
if (authenticationResult.Username != username)
{
@@ -467,7 +452,6 @@ namespace Emby.Server.Implementations.Library
private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
{
- string updatedUsername = null;
bool success = false;
IAuthenticationProvider authenticationProvider = null;
@@ -487,7 +471,7 @@ namespace Emby.Server.Implementations.Library
foreach (var provider in GetAuthenticationProviders(user))
{
var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
- updatedUsername = providerAuthResult.username;
+ var updatedUsername = providerAuthResult.username;
success = providerAuthResult.success;
if (success)
@@ -499,25 +483,32 @@ namespace Emby.Server.Implementations.Library
}
}
- if (user != null)
+ if (user != null
+ && !success
+ && _networkManager.IsInLocalNetwork(remoteEndPoint)
+ && user.Configuration.EnableLocalPassword)
{
- if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword)
+ if (password == null)
{
- if (password == null)
- {
- // legacy
- success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
- }
- else
- {
- success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
- }
+ // legacy
+ success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
}
}
return (authenticationProvider, username, success);
}
+ private string GetLocalPasswordHash(User user)
+ {
+ return string.IsNullOrEmpty(user.EasyPassword)
+ ? null
+ : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash);
+ }
+
private void UpdateInvalidLoginAttemptCount(User user, int newValue)
{
if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0)
@@ -556,17 +547,18 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Loads the users from the repository
+ /// Loads the users from the repository.
/// </summary>
- /// <returns>IEnumerable{User}.</returns>
- private List<User> LoadUsers()
+ private void LoadUsers()
{
- var users = UserRepository.RetrieveAllUsers();
+ var users = _userRepository.RetrieveAllUsers();
// There always has to be at least one user.
if (users.Count != 0)
{
- return users;
+ _users = new ConcurrentDictionary<Guid, User>(
+ users.Select(x => new KeyValuePair<Guid, User>(x.Id, x)));
+ return;
}
var defaultName = Environment.UserName;
@@ -581,14 +573,15 @@ namespace Emby.Server.Implementations.Library
user.DateLastSaved = DateTime.UtcNow;
- UserRepository.CreateUser(user);
+ _userRepository.CreateUser(user);
user.Policy.IsAdministrator = true;
user.Policy.EnableContentDeletion = true;
user.Policy.EnableRemoteControlOfOtherUsers = true;
UpdateUserPolicy(user, user.Policy, false);
- return new List<User> { user };
+ _users = new ConcurrentDictionary<Guid, User>();
+ _users[user.Id] = user;
}
public UserDto GetUserDto(User user, string remoteEndPoint = null)
@@ -619,7 +612,7 @@ namespace Emby.Server.Implementations.Library
Policy = user.Policy
};
- if (!hasPassword && Users.Count() == 1)
+ if (!hasPassword && _users.Count == 1)
{
dto.EnableAutoLogin = true;
}
@@ -694,22 +687,26 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(user));
}
- if (string.IsNullOrEmpty(newName))
+ if (string.IsNullOrWhiteSpace(newName))
{
- throw new ArgumentNullException(nameof(newName));
+ throw new ArgumentException("Invalid username", nameof(newName));
}
- if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)))
+ if (user.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))
{
- throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName));
+ throw new ArgumentException("The new and old names must be different.");
}
- if (user.Name.Equals(newName, StringComparison.Ordinal))
+ if (Users.Any(
+ u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)))
{
- throw new ArgumentException("The new and old names must be different.");
+ throw new ArgumentException(string.Format(
+ CultureInfo.InvariantCulture,
+ "A user with the name '{0}' already exists.",
+ newName));
}
- await user.Rename(newName);
+ await user.Rename(newName).ConfigureAwait(false);
OnUserUpdated(user);
}
@@ -727,23 +724,30 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(user));
}
- if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id)))
+ if (user.Id == Guid.Empty)
+ {
+ throw new ArgumentException("Id can't be empty.", nameof(user));
+ }
+
+ if (!_users.ContainsKey(user.Id))
{
- throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id));
+ throw new ArgumentException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "A user '{0}' with Id {1} does not exist.",
+ user.Name,
+ user.Id),
+ nameof(user));
}
user.DateModified = DateTime.UtcNow;
user.DateLastSaved = DateTime.UtcNow;
- UserRepository.UpdateUser(user);
+ _userRepository.UpdateUser(user);
OnUserUpdated(user);
}
- public event EventHandler<GenericEventArgs<User>> UserCreated;
-
- private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1);
-
/// <summary>
/// Creates the user.
/// </summary>
@@ -751,7 +755,7 @@ namespace Emby.Server.Implementations.Library
/// <returns>User.</returns>
/// <exception cref="ArgumentNullException">name</exception>
/// <exception cref="ArgumentException"></exception>
- public async Task<User> CreateUser(string name)
+ public User CreateUser(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
@@ -768,28 +772,17 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name));
}
- await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
-
- try
- {
- var user = InstantiateNewUser(name);
+ var user = InstantiateNewUser(name);
- var list = Users.ToList();
- list.Add(user);
- _users = list.ToArray();
+ _users[user.Id] = user;
- user.DateLastSaved = DateTime.UtcNow;
+ user.DateLastSaved = DateTime.UtcNow;
- UserRepository.CreateUser(user);
+ _userRepository.CreateUser(user);
- EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger);
+ EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger);
- return user;
- }
- finally
- {
- _userListLock.Release();
- }
+ return user;
}
/// <summary>
@@ -799,57 +792,59 @@ namespace Emby.Server.Implementations.Library
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">user</exception>
/// <exception cref="ArgumentException"></exception>
- public async Task DeleteUser(User user)
+ public void DeleteUser(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
- var allUsers = Users.ToList();
-
- if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null)
+ if (!_users.ContainsKey(user.Id))
{
- throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id));
+ throw new ArgumentException(string.Format(
+ CultureInfo.InvariantCulture,
+ "The user cannot be deleted because there is no user with the Name {0} and Id {1}.",
+ user.Name,
+ user.Id));
}
- if (allUsers.Count == 1)
+ if (_users.Count == 1)
{
- throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name));
+ throw new ArgumentException(string.Format(
+ CultureInfo.InvariantCulture,
+ "The user '{0}' cannot be deleted because there must be at least one user in the system.",
+ user.Name));
}
- if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1)
+ if (user.Policy.IsAdministrator
+ && Users.Count(i => i.Policy.IsAdministrator) == 1)
{
- throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name));
+ throw new ArgumentException(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
+ user.Name),
+ nameof(user));
}
- await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
+ var configPath = GetConfigurationFilePath(user);
+
+ _userRepository.DeleteUser(user);
try
{
- var configPath = GetConfigurationFilePath(user);
-
- UserRepository.DeleteUser(user);
-
- try
- {
- _fileSystem.DeleteFile(configPath);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting file {path}", configPath);
- }
-
- DeleteUserPolicy(user);
-
- _users = allUsers.Where(i => i.Id != user.Id).ToArray();
-
- OnUserDeleted(user);
+ _fileSystem.DeleteFile(configPath);
}
- finally
+ catch (IOException ex)
{
- _userListLock.Release();
+ _logger.LogError(ex, "Error deleting file {path}", configPath);
}
+
+ DeleteUserPolicy(user);
+
+ _users.TryRemove(user.Id, out _);
+
+ OnUserDeleted(user);
}
/// <summary>
@@ -906,8 +901,7 @@ namespace Emby.Server.Implementations.Library
Name = name,
Id = Guid.NewGuid(),
DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow,
- UsesIdForConfigurationPath = true
+ DateModified = DateTime.UtcNow
};
}
@@ -989,7 +983,6 @@ namespace Emby.Server.Implementations.Library
};
}
- private readonly object _policySyncLock = new object();
public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy)
{
var user = GetUserById(userId);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index ed254accb..85754ca8b 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -31,6 +32,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IServerApplicationHost _appHost;
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
+ private readonly IStreamHelper _streamHelper;
public HdHomerunHost(
IServerConfigurationManager config,
@@ -40,29 +42,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHttpClient httpClient,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
- INetworkManager networkManager)
+ INetworkManager networkManager,
+ IStreamHelper streamHelper)
: base(config, logger, jsonSerializer, fileSystem)
{
_httpClient = httpClient;
_appHost = appHost;
_socketFactory = socketFactory;
_networkManager = networkManager;
+ _streamHelper = streamHelper;
}
public string Name => "HD Homerun";
- public override string Type => DeviceType;
-
- public static string DeviceType => "hdhomerun";
+ public override string Type => "hdhomerun";
protected override string ChannelIdPrefix => "hdhr_";
private string GetChannelId(TunerHostInfo info, Channels i)
- {
- var id = ChannelIdPrefix + i.GuideNumber;
-
- return id;
- }
+ => ChannelIdPrefix + i.GuideNumber;
private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
{
@@ -74,19 +72,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
CancellationToken = cancellationToken,
BufferContent = false
};
- using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false))
- {
- using (var stream = response.Content)
- {
- var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>();
- if (info.ImportFavoritesOnly)
- {
- lineup = lineup.Where(i => i.Favorite).ToList();
- }
+ using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
+ using (var stream = response.Content)
+ {
+ var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>();
- return lineup.Where(i => !i.DRM).ToList();
+ if (info.ImportFavoritesOnly)
+ {
+ lineup = lineup.Where(i => i.Favorite).ToList();
}
+
+ return lineup.Where(i => !i.DRM).ToList();
}
}
@@ -139,23 +136,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Url = string.Format("{0}/discover.json", GetApiUrl(info)),
CancellationToken = cancellationToken,
BufferContent = false
-
- }, "GET").ConfigureAwait(false))
+ }, HttpMethod.Get).ConfigureAwait(false))
+ using (var stream = response.Content)
{
- using (var stream = response.Content)
- {
- var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false);
+ var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false);
- if (!string.IsNullOrEmpty(cacheKey))
+ if (!string.IsNullOrEmpty(cacheKey))
+ {
+ lock (_modelCache)
{
- lock (_modelCache)
- {
- _modelCache[cacheKey] = discoverResponse;
- }
+ _modelCache[cacheKey] = discoverResponse;
}
-
- return discoverResponse;
}
+
+ return discoverResponse;
}
}
catch (HttpException ex)
@@ -186,36 +180,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- using (var stream = await _httpClient.Get(new HttpRequestOptions()
+ using (var response = await _httpClient.SendAsync(new HttpRequestOptions()
{
Url = string.Format("{0}/tuners.html", GetApiUrl(info)),
CancellationToken = cancellationToken,
BufferContent = false
- }))
+ }, HttpMethod.Get))
+ using (var stream = response.Content)
+ using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
{
var tuners = new List<LiveTvTunerInfo>();
- using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
+ while (!sr.EndOfStream)
{
- while (!sr.EndOfStream)
+ string line = StripXML(sr.ReadLine());
+ if (line.Contains("Channel"))
{
- string line = StripXML(sr.ReadLine());
- if (line.Contains("Channel"))
+ LiveTvTunerStatus status;
+ var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+ var name = line.Substring(0, index - 1);
+ var currentChannel = line.Substring(index + 7);
+ if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; }
+ tuners.Add(new LiveTvTunerInfo
{
- LiveTvTunerStatus status;
- var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
- var name = line.Substring(0, index - 1);
- var currentChannel = line.Substring(index + 7);
- if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; }
- tuners.Add(new LiveTvTunerInfo
- {
- Name = name,
- SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
- ProgramName = currentChannel,
- Status = status
- });
- }
+ Name = name,
+ SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
+ ProgramName = currentChannel,
+ Status = status
+ });
}
}
+
return tuners;
}
}
@@ -245,6 +239,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
bufferIndex++;
}
}
+
return new string(buffer, 0, bufferIndex);
}
@@ -256,7 +251,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var uri = new Uri(GetApiUrl(info));
- using (var manager = new HdHomerunManager(Logger))
+ using (var manager = new HdHomerunManager())
{
// Legacy HdHomeruns are IPv4 only
var ipInfo = IPAddress.Parse(uri.Host);
@@ -276,6 +271,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
});
}
}
+
return tuners;
}
@@ -434,12 +430,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
videoCodec = channelInfo.VideoCodec;
}
+
string audioCodec = channelInfo.AudioCodec;
if (!videoBitrate.HasValue)
{
videoBitrate = isHd ? 15000000 : 2000000;
}
+
int? audioBitrate = isHd ? 448000 : 192000;
// normalize
@@ -461,6 +459,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
id = "native";
}
+
id += "_" + channelId.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_" + url.GetMD5().ToString("N", CultureInfo.InvariantCulture);
var mediaSource = new MediaSourceInfo
@@ -527,29 +526,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
else
{
- try
- {
- var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
+ var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- if (modelInfo != null && modelInfo.SupportsTranscoding)
+ if (modelInfo != null && modelInfo.SupportsTranscoding)
+ {
+ if (info.AllowHWTranscoding)
{
- if (info.AllowHWTranscoding)
- {
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy"));
-
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile"));
- }
+ list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
+ list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540"));
+ list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480"));
+ list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360"));
+ list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240"));
+ list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile"));
}
- }
- catch
- {
+ list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
}
if (list.Count == 0)
@@ -582,7 +574,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
{
- return new HdHomerunUdpStream(mediaSource, info, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
+ return new HdHomerunUdpStream(
+ mediaSource,
+ info,
+ streamId,
+ new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path),
+ modelInfo.TunerCount,
+ FileSystem,
+ Logger,
+ Config.ApplicationPaths,
+ _appHost,
+ _socketFactory,
+ _networkManager,
+ _streamHelper);
}
var enableHttpStream = true;
@@ -599,10 +603,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
mediaSource.Path = httpUrl;
- return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
- }
-
- return new HdHomerunUdpStream(mediaSource, info, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
+ return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _streamHelper);
+ }
+
+ return new HdHomerunUdpStream(
+ mediaSource,
+ info,
+ streamId,
+ new HdHomerunChannelCommands(hdhomerunChannel.Number, profile),
+ modelInfo.TunerCount,
+ FileSystem,
+ Logger,
+ Config.ApplicationPaths,
+ _appHost,
+ _socketFactory,
+ _networkManager,
+ _streamHelper);
}
public async Task Validate(TunerHostInfo info)
@@ -701,9 +717,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
catch (OperationCanceledException)
{
}
- catch
+ catch (Exception ex)
{
// Socket timeout indicates all messages have been received.
+ Logger.LogError(ex, "Error while sending discovery message");
}
}
@@ -718,21 +735,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Url = url
};
- try
- {
- var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false);
-
- hostInfo.DeviceId = modelInfo.DeviceID;
- hostInfo.FriendlyName = modelInfo.FriendlyName;
+ var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false);
- return hostInfo;
- }
- catch
- {
- // logged at lower levels
- }
+ hostInfo.DeviceId = modelInfo.DeviceID;
+ hostInfo.FriendlyName = modelInfo.FriendlyName;
- return null;
+ return hostInfo;
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index c19552428..3699b988c 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -1,6 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
+using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Text;
@@ -8,13 +9,12 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.LiveTv;
-using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public interface IHdHomerunChannelCommands
{
- IEnumerable<Tuple<string, string>> GetCommands();
+ IEnumerable<(string, string)> GetCommands();
}
public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
@@ -33,16 +33,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- public IEnumerable<Tuple<string, string>> GetCommands()
+ public IEnumerable<(string, string)> GetCommands()
{
- var commands = new List<Tuple<string, string>>();
-
if (!string.IsNullOrEmpty(_channel))
- commands.Add(Tuple.Create("channel", _channel));
+ {
+ yield return ("channel", _channel);
+ }
if (!string.IsNullOrEmpty(_program))
- commands.Add(Tuple.Create("program", _program));
- return commands;
+ {
+ yield return ("program", _program);
+ }
}
}
@@ -57,29 +58,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_profile = profile;
}
- public IEnumerable<Tuple<string, string>> GetCommands()
+ public IEnumerable<(string, string)> GetCommands()
{
- var commands = new List<Tuple<string, string>>();
-
if (!string.IsNullOrEmpty(_channel))
{
- if (!string.IsNullOrEmpty(_profile) && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(_profile)
+ && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
{
- commands.Add(Tuple.Create("vchannel", string.Format("{0} transcode={1}", _channel, _profile)));
+ yield return ("vchannel", $"{_channel} transcode={_profile}");
}
else
{
- commands.Add(Tuple.Create("vchannel", _channel));
+ yield return ("vchannel", _channel);
}
}
-
- return commands;
}
}
public class HdHomerunManager : IDisposable
{
public const int HdHomeRunPort = 65001;
+
// Message constants
private const byte GetSetName = 3;
private const byte GetSetValue = 4;
@@ -87,19 +86,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private const ushort GetSetRequest = 4;
private const ushort GetSetReply = 5;
- private readonly ILogger _logger;
-
private uint? _lockkey = null;
private int _activeTuner = -1;
private IPEndPoint _remoteEndPoint;
private TcpClient _tcpClient;
- public HdHomerunManager(ILogger logger)
- {
- _logger = logger;
- }
-
public void Dispose()
{
using (var socket = _tcpClient)
@@ -108,8 +100,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
_tcpClient = null;
- var task = StopStreaming(socket);
- Task.WaitAll(task);
+ StopStreaming(socket).GetAwaiter().GetResult();
}
}
}
@@ -173,20 +164,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out var returnVal))
+ if (!ParseReturnMessage(buffer, receivedBytes, out _))
{
continue;
}
var commandList = commands.GetCommands();
- foreach (Tuple<string, string> command in commandList)
+ foreach (var command in commandList)
{
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out returnVal))
+ if (!ParseReturnMessage(buffer, receivedBytes, out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
@@ -198,8 +191,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out returnVal))
+ if (!ParseReturnMessage(buffer, receivedBytes, out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
@@ -231,13 +225,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
- foreach (Tuple<string, string> command in commandList)
+ foreach (var command in commandList)
{
var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+
// parse response to make sure it worked
- if (!ParseReturnMessage(buffer, receivedBytes, out string returnVal))
+ if (!ParseReturnMessage(buffer, receivedBytes, out _))
{
return;
}
@@ -264,21 +259,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private async Task ReleaseLockkey(TcpClient client, uint lockKeyValue)
{
- _logger.LogInformation("HdHomerunManager.ReleaseLockkey {0}", lockKeyValue);
-
var stream = client.GetStream();
var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue);
- await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length, CancellationToken.None).ConfigureAwait(false);
+ await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false);
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
- await stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false);
+ await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue);
_lockkey = null;
- await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length, CancellationToken.None).ConfigureAwait(false);
- await stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false);
+ await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false);
+ await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
}
finally
{
@@ -288,7 +281,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private static byte[] CreateGetMessage(int tuner, string name)
{
- var byteName = Encoding.UTF8.GetBytes(string.Format("/tuner{0}/{1}\0", tuner, name));
+ var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length
var message = new byte[messageLength];
@@ -311,12 +304,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey)
{
- var byteName = Encoding.UTF8.GetBytes(string.Format("/tuner{0}/{1}\0", tuner, name));
- var byteValue = Encoding.UTF8.GetBytes(string.Format("{0}\0", value));
+ var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
+ var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value));
int messageLength = byteName.Length + byteValue.Length + 12;
if (lockkey.HasValue)
+ {
messageLength += 6;
+ }
var message = new byte[messageLength];
@@ -324,21 +319,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
bool flipEndian = BitConverter.IsLittleEndian;
- message[offset] = GetSetValue;
- offset++;
- message[offset] = Convert.ToByte(byteValue.Length);
- offset++;
+ message[offset++] = GetSetValue;
+ message[offset++] = Convert.ToByte(byteValue.Length);
Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length);
offset += byteValue.Length;
if (lockkey.HasValue)
{
- message[offset] = GetSetLockkey;
- offset++;
- message[offset] = (byte)4;
- offset++;
+ message[offset++] = GetSetLockkey;
+ message[offset++] = 4;
var lockKeyBytes = BitConverter.GetBytes(lockkey.Value);
if (flipEndian)
+ {
Array.Reverse(lockKeyBytes);
+ }
+
Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4);
offset += 4;
}
@@ -346,7 +340,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// calculate crc and insert at the end of the message
var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
if (flipEndian)
+ {
Array.Reverse(crcBytes);
+ }
+
Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
return message;
@@ -375,10 +372,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
offset += 2;
// insert tag name and length
- message[offset] = GetSetName;
- offset++;
- message[offset] = Convert.ToByte(byteName.Length);
- offset++;
+ message[offset++] = GetSetName;
+ message[offset++] = Convert.ToByte(byteName.Length);
// insert name string
Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length);
@@ -392,7 +387,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
returnVal = string.Empty;
if (numBytes < 4)
+ {
return false;
+ }
var flipEndian = BitConverter.IsLittleEndian;
int offset = 0;
@@ -400,45 +397,49 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length);
if (flipEndian)
+ {
Array.Reverse(msgTypeBytes);
+ }
var msgType = BitConverter.ToUInt16(msgTypeBytes, 0);
offset += 2;
if (msgType != GetSetReply)
+ {
return false;
+ }
byte[] msgLengthBytes = new byte[2];
Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length);
if (flipEndian)
+ {
Array.Reverse(msgLengthBytes);
+ }
var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0);
offset += 2;
if (numBytes < msgLength + 8)
+ {
return false;
+ }
- var nameTag = buf[offset];
- offset++;
+ var nameTag = buf[offset++];
- var nameLength = buf[offset];
- offset++;
+ var nameLength = buf[offset++];
// skip the name field to get to value for return
offset += nameLength;
- var valueTag = buf[offset];
- offset++;
+ var valueTag = buf[offset++];
- var valueLength = buf[offset];
- offset++;
+ var valueLength = buf[offset++];
returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator
return true;
}
- private class HdHomerunCrc
+ private static class HdHomerunCrc
{
private static uint[] crc_table = {
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
@@ -510,15 +511,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var hash = 0xffffffff;
for (var i = 0; i < numBytes; i++)
+ {
hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff];
+ }
var tmp = ~hash & 0xffffffff;
var b0 = tmp & 0xff;
var b1 = (tmp >> 8) & 0xff;
var b2 = (tmp >> 16) & 0xff;
var b3 = (tmp >> 24) & 0xff;
- hash = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
- return hash;
+ return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 1d79a5f96..fbbab07f8 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Net;
@@ -18,6 +19,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunUdpStream : LiveStream, IDirectStreamProvider
{
+ private const int RtpHeaderBytes = 12;
+
private readonly IServerApplicationHost _appHost;
private readonly MediaBrowser.Model.Net.ISocketFactory _socketFactory;
@@ -32,13 +35,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHdHomerunChannelCommands channelCommands,
int numTuners,
IFileSystem fileSystem,
- IHttpClient httpClient,
ILogger logger,
IServerApplicationPaths appPaths,
IServerApplicationHost appHost,
MediaBrowser.Model.Net.ISocketFactory socketFactory,
- INetworkManager networkManager)
- : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths)
+ INetworkManager networkManager,
+ IStreamHelper streamHelper)
+ : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths, streamHelper)
{
_appHost = appHost;
_socketFactory = socketFactory;
@@ -80,12 +83,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
var udpClient = _socketFactory.CreateUdpSocket(localPort);
- var hdHomerunManager = new HdHomerunManager(Logger);
+ var hdHomerunManager = new HdHomerunManager();
try
{
// send url to start streaming
- await hdHomerunManager.StartStreaming(remoteAddress, localAddress, localPort, _channelCommands, _numTuners, openCancellationToken).ConfigureAwait(false);
+ await hdHomerunManager.StartStreaming(
+ remoteAddress,
+ localAddress,
+ localPort,
+ _channelCommands,
+ _numTuners,
+ openCancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -103,7 +112,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var taskCompletionSource = new TaskCompletionSource<bool>();
- await StartStreaming(udpClient, hdHomerunManager, remoteAddress, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
+ await StartStreaming(
+ udpClient,
+ hdHomerunManager,
+ remoteAddress,
+ taskCompletionSource,
+ LiveStreamCancellationTokenSource.Token).ConfigureAwait(false);
//OpenedMediaSource.Protocol = MediaProtocol.File;
//OpenedMediaSource.Path = tempFile;
@@ -148,50 +162,43 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
});
}
- private static void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)
- {
- Task.Run(() =>
- {
- openTaskCompletionSource.TrySetResult(true);
- });
- }
-
- private const int RtpHeaderBytes = 12;
-
private async Task CopyTo(MediaBrowser.Model.Net.ISocket udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
- var bufferSize = 81920;
-
- byte[] buffer = new byte[bufferSize];
- int read;
- var resolved = false;
-
- using (var source = _socketFactory.CreateNetworkStream(udpClient, false))
- using (var fileStream = FileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None))
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamDefaults.DefaultCopyToBufferSize);
+ try
{
- var currentCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token).Token;
-
- while ((read = await source.ReadAsync(buffer, 0, buffer.Length, currentCancellationToken).ConfigureAwait(false)) != 0)
+ using (var source = _socketFactory.CreateNetworkStream(udpClient, false))
+ using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
{
- cancellationToken.ThrowIfCancellationRequested();
+ var currentCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token).Token;
+ int read;
+ var resolved = false;
+ while ((read = await source.ReadAsync(buffer, 0, buffer.Length, currentCancellationToken).ConfigureAwait(false)) != 0)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- currentCancellationToken = cancellationToken;
+ currentCancellationToken = cancellationToken;
- read -= RtpHeaderBytes;
+ read -= RtpHeaderBytes;
- if (read > 0)
- {
- fileStream.Write(buffer, RtpHeaderBytes, read);
- }
+ if (read > 0)
+ {
+ await fileStream.WriteAsync(buffer, RtpHeaderBytes, read).ConfigureAwait(false);
+ }
- if (!resolved)
- {
- resolved = true;
- DateOpened = DateTime.UtcNow;
- Resolve(openTaskCompletionSource);
+ if (!resolved)
+ {
+ resolved = true;
+ DateOpened = DateTime.UtcNow;
+ openTaskCompletionSource.TrySetResult(true);
+ }
}
}
}
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index b4395e2e1..d12c96392 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -16,27 +16,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class LiveStream : ILiveStream
{
- public MediaSourceInfo OriginalMediaSource { get; set; }
- public MediaSourceInfo MediaSource { get; set; }
-
- public int ConsumerCount { get; set; }
-
- public string OriginalStreamId { get; set; }
- public bool EnableStreamSharing { get; set; }
- public string UniqueId { get; }
-
protected readonly IFileSystem FileSystem;
protected readonly IServerApplicationPaths AppPaths;
+ protected readonly IStreamHelper StreamHelper;
protected string TempFilePath;
protected readonly ILogger Logger;
protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource();
- public string TunerHostId { get; }
-
- public DateTime DateOpened { get; protected set; }
-
- public LiveStream(MediaSourceInfo mediaSource, TunerHostInfo tuner, IFileSystem fileSystem, ILogger logger, IServerApplicationPaths appPaths)
+ public LiveStream(
+ MediaSourceInfo mediaSource,
+ TunerHostInfo tuner,
+ IFileSystem fileSystem,
+ ILogger logger,
+ IServerApplicationPaths appPaths,
+ IStreamHelper streamHelper)
{
OriginalMediaSource = mediaSource;
FileSystem = fileSystem;
@@ -51,11 +45,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
AppPaths = appPaths;
+ StreamHelper = streamHelper;
ConsumerCount = 1;
SetTempFilePath("ts");
}
+ protected virtual int EmptyReadLimit => 1000;
+
+ public MediaSourceInfo OriginalMediaSource { get; set; }
+ public MediaSourceInfo MediaSource { get; set; }
+
+ public int ConsumerCount { get; set; }
+
+ public string OriginalStreamId { get; set; }
+ public bool EnableStreamSharing { get; set; }
+ public string UniqueId { get; }
+
+ public string TunerHostId { get; }
+
+ public DateTime DateOpened { get; protected set; }
+
protected void SetTempFilePath(string extension)
{
TempFilePath = Path.Combine(AppPaths.GetTranscodingTempPath(), UniqueId + "." + extension);
@@ -71,24 +81,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
EnableStreamSharing = false;
- Logger.LogInformation("Closing " + GetType().Name);
+ Logger.LogInformation("Closing {Type}", GetType().Name);
LiveStreamCancellationTokenSource.Cancel();
return Task.CompletedTask;
}
- protected Stream GetInputStream(string path, bool allowAsyncFileRead)
- {
- var fileOpenOptions = FileOpenOptions.SequentialScan;
-
- if (allowAsyncFileRead)
- {
- fileOpenOptions |= FileOpenOptions.Asynchronous;
- }
-
- return FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions);
- }
+ protected FileStream GetInputStream(string path, bool allowAsyncFileRead)
+ => new FileStream(
+ path,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.ReadWrite,
+ StreamDefaults.DefaultFileStreamBufferSize,
+ allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan);
public Task DeleteTempFiles()
{
@@ -144,8 +151,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
var nextFileInfo = GetNextFile(null);
- var nextFile = nextFileInfo.Item1;
- var isLastFile = nextFileInfo.Item2;
+ var nextFile = nextFileInfo.file;
+ var isLastFile = nextFileInfo.isLastFile;
while (!string.IsNullOrEmpty(nextFile))
{
@@ -155,8 +162,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
seekFile = false;
nextFileInfo = GetNextFile(nextFile);
- nextFile = nextFileInfo.Item1;
- isLastFile = nextFileInfo.Item2;
+ nextFile = nextFileInfo.file;
+ isLastFile = nextFileInfo.isLastFile;
}
Logger.LogInformation("Live Stream ended.");
@@ -180,19 +187,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken)
{
- using (var inputStream = (FileStream)GetInputStream(path, allowAsync))
+ using (var inputStream = GetInputStream(path, allowAsync))
{
if (seekFile)
{
TrySeek(inputStream, -20000);
}
- await ApplicationHost.StreamHelper.CopyToAsync(inputStream, stream, 81920, emptyReadLimit, cancellationToken).ConfigureAwait(false);
+ await StreamHelper.CopyToAsync(
+ inputStream,
+ stream,
+ StreamDefaults.DefaultCopyToBufferSize,
+ emptyReadLimit,
+ cancellationToken).ConfigureAwait(false);
}
}
- protected virtual int EmptyReadLimit => 1000;
-
private void TrySeek(FileStream stream, long offset)
{
if (!stream.CanSeek)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 6c5c80827..a02a9ade4 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -28,14 +28,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private readonly IServerApplicationHost _appHost;
private readonly INetworkManager _networkManager;
private readonly IMediaSourceManager _mediaSourceManager;
-
- public M3UTunerHost(IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, INetworkManager networkManager)
+ private readonly IStreamHelper _streamHelper;
+
+ public M3UTunerHost(
+ IServerConfigurationManager config,
+ IMediaSourceManager mediaSourceManager,
+ ILogger logger,
+ IJsonSerializer jsonSerializer,
+ IFileSystem fileSystem,
+ IHttpClient httpClient,
+ IServerApplicationHost appHost,
+ INetworkManager networkManager,
+ IStreamHelper streamHelper)
: base(config, logger, jsonSerializer, fileSystem)
{
_httpClient = httpClient;
_appHost = appHost;
_networkManager = networkManager;
_mediaSourceManager = mediaSourceManager;
+ _streamHelper = streamHelper;
}
public override string Type => "m3u";
@@ -103,11 +114,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
- return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
+ return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _streamHelper);
}
}
- return new LiveStream(mediaSource, info, FileSystem, Logger, Config.ApplicationPaths);
+ return new LiveStream(mediaSource, info, FileSystem, Logger, Config.ApplicationPaths, _streamHelper);
}
public async Task Validate(TunerHostInfo info)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 7de9931c7..c6e894560 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -19,8 +19,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private readonly IHttpClient _httpClient;
private readonly IServerApplicationHost _appHost;
- public SharedHttpStream(MediaSourceInfo mediaSource, TunerHostInfo tunerHostInfo, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost)
- : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths)
+ public SharedHttpStream(
+ MediaSourceInfo mediaSource,
+ TunerHostInfo tunerHostInfo,
+ string originalStreamId,
+ IFileSystem fileSystem,
+ IHttpClient httpClient,
+ ILogger logger,
+ IServerApplicationPaths appPaths,
+ IServerApplicationHost appHost,
+ IStreamHelper streamHelper)
+ : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths, streamHelper)
{
_httpClient = httpClient;
_appHost = appHost;
@@ -118,7 +127,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
using (var stream = response.Content)
using (var fileStream = FileSystem.GetFileStream(TempFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None))
{
- await ApplicationHost.StreamHelper.CopyToAsync(stream, fileStream, 81920, () => Resolve(openTaskCompletionSource), cancellationToken).ConfigureAwait(false);
+ await StreamHelper.CopyToAsync(
+ stream,
+ fileStream,
+ StreamDefaults.DefaultCopyToBufferSize,
+ () => Resolve(openTaskCompletionSource),
+ cancellationToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
@@ -128,6 +142,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
Logger.LogError(ex, "Error copying live stream.");
}
+
EnableStreamSharing = false;
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
});
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 8c49b6405..13cdc50ca 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -17,43 +17,49 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Localization
{
/// <summary>
- /// Class LocalizationManager
+ /// Class LocalizationManager.
/// </summary>
public class LocalizationManager : ILocalizationManager
{
- /// <summary>
- /// The _configuration manager
- /// </summary>
- private readonly IServerConfigurationManager _configurationManager;
+ private const string DefaultCulture = "en-US";
+ private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
+ private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
/// <summary>
- /// The us culture
+ /// The _configuration manager.
/// </summary>
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly ILogger _logger;
private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings =
new Dictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase);
- private readonly IJsonSerializer _jsonSerializer;
- private readonly ILogger _logger;
- private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
+ private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries =
+ new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
+
+ private List<CultureDto> _cultures;
/// <summary>
/// Initializes a new instance of the <see cref="LocalizationManager" /> class.
/// </summary>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="jsonSerializer">The json serializer.</param>
- /// <param name="loggerFactory">The logger factory</param>
+ /// <param name="logger">The logger.</param>
public LocalizationManager(
IServerConfigurationManager configurationManager,
IJsonSerializer jsonSerializer,
- ILoggerFactory loggerFactory)
+ ILogger<LocalizationManager> logger)
{
_configurationManager = configurationManager;
_jsonSerializer = jsonSerializer;
- _logger = loggerFactory.CreateLogger(nameof(LocalizationManager));
+ _logger = logger;
}
+ /// <summary>
+ /// Loads all resources into memory.
+ /// </summary>
+ /// <returns><see cref="Task" />.</returns>
public async Task LoadAll()
{
const string RatingsResource = "Emby.Server.Implementations.Localization.Ratings.";
@@ -82,9 +88,10 @@ namespace Emby.Server.Implementations.Localization
string[] parts = line.Split(',');
if (parts.Length == 2
- && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value))
+ && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
- dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value });
+ var name = parts[0];
+ dict.Add(name, new ParentalRating(name, value));
}
#if DEBUG
else
@@ -101,16 +108,11 @@ namespace Emby.Server.Implementations.Localization
await LoadCultures().ConfigureAwait(false);
}
- public string NormalizeFormKD(string text)
- => text.Normalize(NormalizationForm.FormKD);
-
- private CultureDto[] _cultures;
-
/// <summary>
/// Gets the cultures.
/// </summary>
- /// <returns>IEnumerable{CultureDto}.</returns>
- public CultureDto[] GetCultures()
+ /// <returns><see cref="IEnumerable{CultureDto}" />.</returns>
+ public IEnumerable<CultureDto> GetCultures()
=> _cultures;
private async Task LoadCultures()
@@ -168,9 +170,10 @@ namespace Emby.Server.Implementations.Localization
}
}
- _cultures = list.ToArray();
+ _cultures = list;
}
+ /// <inheritdoc />
public CultureDto FindLanguageInfo(string language)
=> GetCultures()
.FirstOrDefault(i =>
@@ -179,25 +182,19 @@ namespace Emby.Server.Implementations.Localization
|| i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase)
|| string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase));
- /// <summary>
- /// Gets the countries.
- /// </summary>
- /// <returns>IEnumerable{CountryInfo}.</returns>
- public Task<CountryInfo[]> GetCountries()
- => _jsonSerializer.DeserializeFromStreamAsync<CountryInfo[]>(
+ /// <inheritdoc />
+ public IEnumerable<CountryInfo> GetCountries()
+ => _jsonSerializer.DeserializeFromStream<IEnumerable<CountryInfo>>(
_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
- /// <summary>
- /// Gets the parental ratings.
- /// </summary>
- /// <returns>IEnumerable{ParentalRating}.</returns>
+ /// <inheritdoc />
public IEnumerable<ParentalRating> GetParentalRatings()
=> GetParentalRatingsDictionary().Values;
/// <summary>
/// Gets the parental ratings dictionary.
/// </summary>
- /// <returns>Dictionary{System.StringParentalRating}.</returns>
+ /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns>
private Dictionary<string, ParentalRating> GetParentalRatingsDictionary()
{
var countryCode = _configurationManager.Configuration.MetadataCountryCode;
@@ -207,14 +204,14 @@ namespace Emby.Server.Implementations.Localization
countryCode = "us";
}
- return GetRatings(countryCode) ?? GetRatings("us");
+ return GetRatings(countryCode) ?? GetRatings("us");
}
/// <summary>
/// Gets the ratings.
/// </summary>
/// <param name="countryCode">The country code.</param>
- /// <returns>The ratings</returns>
+ /// <returns>The ratings.</returns>
private Dictionary<string, ParentalRating> GetRatings(string countryCode)
{
_allParentalRatings.TryGetValue(countryCode, out var value);
@@ -222,14 +219,7 @@ namespace Emby.Server.Implementations.Localization
return value;
}
- private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
-
/// <inheritdoc />
- /// <summary>
- /// Gets the rating level.
- /// </summary>
- /// <param name="rating">Rating field</param>
- /// <returns>The rating level</returns>&gt;
public int? GetRatingLevel(string rating)
{
if (string.IsNullOrEmpty(rating))
@@ -277,6 +267,7 @@ namespace Emby.Server.Implementations.Localization
return null;
}
+ /// <inheritdoc />
public bool HasUnicodeCategory(string value, UnicodeCategory category)
{
foreach (var chr in value)
@@ -290,11 +281,13 @@ namespace Emby.Server.Implementations.Localization
return false;
}
+ /// <inheritdoc />
public string GetLocalizedString(string phrase)
{
return GetLocalizedString(phrase, _configurationManager.Configuration.UICulture);
}
+ /// <inheritdoc />
public string GetLocalizedString(string phrase, string culture)
{
if (string.IsNullOrEmpty(culture))
@@ -317,12 +310,7 @@ namespace Emby.Server.Implementations.Localization
return phrase;
}
- private const string DefaultCulture = "en-US";
-
- private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries =
- new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
-
- public Dictionary<string, string> GetLocalizationDictionary(string culture)
+ private Dictionary<string, string> GetLocalizationDictionary(string culture)
{
if (string.IsNullOrEmpty(culture))
{
@@ -332,8 +320,9 @@ namespace Emby.Server.Implementations.Localization
const string prefix = "Core";
var key = prefix + culture;
- return _dictionaries.GetOrAdd(key,
- f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
+ return _dictionaries.GetOrAdd(
+ key,
+ f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
}
private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
@@ -390,45 +379,45 @@ namespace Emby.Server.Implementations.Localization
return culture + ".json";
}
- public LocalizationOption[] GetLocalizationOptions()
- => new LocalizationOption[]
- {
- new LocalizationOption("Arabic", "ar"),
- new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG"),
- new LocalizationOption("Catalan", "ca"),
- new LocalizationOption("Chinese Simplified", "zh-CN"),
- new LocalizationOption("Chinese Traditional", "zh-TW"),
- new LocalizationOption("Croatian", "hr"),
- new LocalizationOption("Czech", "cs"),
- new LocalizationOption("Danish", "da"),
- new LocalizationOption("Dutch", "nl"),
- new LocalizationOption("English (United Kingdom)", "en-GB"),
- new LocalizationOption("English (United States)", "en-US"),
- new LocalizationOption("French", "fr"),
- new LocalizationOption("French (Canada)", "fr-CA"),
- new LocalizationOption("German", "de"),
- new LocalizationOption("Greek", "el"),
- new LocalizationOption("Hebrew", "he"),
- new LocalizationOption("Hungarian", "hu"),
- new LocalizationOption("Italian", "it"),
- new LocalizationOption("Kazakh", "kk"),
- new LocalizationOption("Korean", "ko"),
- new LocalizationOption("Lithuanian", "lt-LT"),
- new LocalizationOption("Malay", "ms"),
- new LocalizationOption("Norwegian Bokmål", "nb"),
- new LocalizationOption("Persian", "fa"),
- new LocalizationOption("Polish", "pl"),
- new LocalizationOption("Portuguese (Brazil)", "pt-BR"),
- new LocalizationOption("Portuguese (Portugal)", "pt-PT"),
- new LocalizationOption("Russian", "ru"),
- new LocalizationOption("Slovak", "sk"),
- new LocalizationOption("Slovenian (Slovenia)", "sl-SI"),
- new LocalizationOption("Spanish", "es"),
- new LocalizationOption("Spanish (Argentina)", "es-AR"),
- new LocalizationOption("Spanish (Mexico)", "es-MX"),
- new LocalizationOption("Swedish", "sv"),
- new LocalizationOption("Swiss German", "gsw"),
- new LocalizationOption("Turkish", "tr")
- };
+ /// <inheritdoc />
+ public IEnumerable<LocalizationOption> GetLocalizationOptions()
+ {
+ yield return new LocalizationOption("Arabic", "ar");
+ yield return new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG");
+ yield return new LocalizationOption("Catalan", "ca");
+ yield return new LocalizationOption("Chinese Simplified", "zh-CN");
+ yield return new LocalizationOption("Chinese Traditional", "zh-TW");
+ yield return new LocalizationOption("Croatian", "hr");
+ yield return new LocalizationOption("Czech", "cs");
+ yield return new LocalizationOption("Danish", "da");
+ yield return new LocalizationOption("Dutch", "nl");
+ yield return new LocalizationOption("English (United Kingdom)", "en-GB");
+ yield return new LocalizationOption("English (United States)", "en-US");
+ yield return new LocalizationOption("French", "fr");
+ yield return new LocalizationOption("French (Canada)", "fr-CA");
+ yield return new LocalizationOption("German", "de");
+ yield return new LocalizationOption("Greek", "el");
+ yield return new LocalizationOption("Hebrew", "he");
+ yield return new LocalizationOption("Hungarian", "hu");
+ yield return new LocalizationOption("Italian", "it");
+ yield return new LocalizationOption("Kazakh", "kk");
+ yield return new LocalizationOption("Korean", "ko");
+ yield return new LocalizationOption("Lithuanian", "lt-LT");
+ yield return new LocalizationOption("Malay", "ms");
+ yield return new LocalizationOption("Norwegian Bokmål", "nb");
+ yield return new LocalizationOption("Persian", "fa");
+ yield return new LocalizationOption("Polish", "pl");
+ yield return new LocalizationOption("Portuguese (Brazil)", "pt-BR");
+ yield return new LocalizationOption("Portuguese (Portugal)", "pt-PT");
+ yield return new LocalizationOption("Russian", "ru");
+ yield return new LocalizationOption("Slovak", "sk");
+ yield return new LocalizationOption("Slovenian (Slovenia)", "sl-SI");
+ yield return new LocalizationOption("Spanish", "es");
+ yield return new LocalizationOption("Spanish (Argentina)", "es-AR");
+ yield return new LocalizationOption("Spanish (Mexico)", "es-MX");
+ yield return new LocalizationOption("Swedish", "sv");
+ yield return new LocalizationOption("Swiss German", "gsw");
+ yield return new LocalizationOption("Turkish", "tr");
+ }
}
}
diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
index 0b5ee5d03..1ef5c4b99 100644
--- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs
+++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
@@ -23,22 +23,22 @@ namespace Emby.Server.Implementations.Security
public void Initialize()
{
- using (var connection = GetConnection())
+ string[] queries =
{
- var tableNewlyCreated = !TableExists(connection, "Tokens");
+ "create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)",
+ "create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)",
+ "drop index if exists idx_AccessTokens",
+ "drop index if exists Tokens1",
+ "drop index if exists Tokens2",
- string[] queries = {
-
- "create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)",
- "create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)",
+ "create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)",
+ "create index if not exists Tokens4 on Tokens (Id, DateLastActivity)",
+ "create index if not exists Devices1 on Devices (Id)"
+ };
- "drop index if exists idx_AccessTokens",
- "drop index if exists Tokens1",
- "drop index if exists Tokens2",
- "create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)",
- "create index if not exists Tokens4 on Tokens (Id, DateLastActivity)",
- "create index if not exists Devices1 on Devices (Id)"
- };
+ using (var connection = GetConnection())
+ {
+ var tableNewlyCreated = !TableExists(connection, "Tokens");
connection.RunQueries(queries);
@@ -244,45 +244,46 @@ namespace Emby.Server.Implementations.Security
}
}
- var list = new List<AuthenticationInfo>();
+ var statementTexts = new[]
+ {
+ commandText,
+ "select count (Id) from Tokens" + whereTextWithoutPaging
+ };
+ var list = new List<AuthenticationInfo>();
+ var result = new QueryResult<AuthenticationInfo>();
using (var connection = GetConnection(true))
{
- return connection.RunInTransaction(db =>
- {
- var result = new QueryResult<AuthenticationInfo>();
-
- var statementTexts = new List<string>();
- statementTexts.Add(commandText);
- statementTexts.Add("select count (Id) from Tokens" + whereTextWithoutPaging);
-
- var statements = PrepareAll(db, statementTexts)
- .ToList();
-
- using (var statement = statements[0])
+ connection.RunInTransaction(
+ db =>
{
- BindAuthenticationQueryParams(query, statement);
+ var statements = PrepareAll(db, statementTexts)
+ .ToList();
- foreach (var row in statement.ExecuteQuery())
+ using (var statement = statements[0])
{
- list.Add(Get(row));
- }
+ BindAuthenticationQueryParams(query, statement);
- using (var totalCountStatement = statements[1])
- {
- BindAuthenticationQueryParams(query, totalCountStatement);
-
- result.TotalRecordCount = totalCountStatement.ExecuteQuery()
- .SelectScalarInt()
- .First();
- }
- }
+ foreach (var row in statement.ExecuteQuery())
+ {
+ list.Add(Get(row));
+ }
- result.Items = list.ToArray();
- return result;
+ using (var totalCountStatement = statements[1])
+ {
+ BindAuthenticationQueryParams(query, totalCountStatement);
- }, ReadTransactionMode);
+ result.TotalRecordCount = totalCountStatement.ExecuteQuery()
+ .SelectScalarInt()
+ .First();
+ }
+ }
+ },
+ ReadTransactionMode);
}
+
+ result.Items = list.ToArray();
+ return result;
}
private static AuthenticationInfo Get(IReadOnlyList<IResultSetValue> reader)
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 0347100a4..61329160a 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1375,16 +1375,14 @@ namespace Emby.Server.Implementations.Session
CheckDisposed();
User user = null;
- if (!request.UserId.Equals(Guid.Empty))
+ if (request.UserId != Guid.Empty)
{
- user = _userManager.Users
- .FirstOrDefault(i => i.Id == request.UserId);
+ user = _userManager.GetUserById(request.UserId);
}
if (user == null)
{
- user = _userManager.Users
- .FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase));
+ user = _userManager.GetUserByName(request.Username);
}
if (user != null)
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
index dd313b336..e93bff124 100644
--- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
+++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
@@ -117,7 +117,7 @@ namespace Emby.Server.Implementations.SocketSharp
/// <summary>
/// Releases the unmanaged resources and disposes of the managed resources used.
/// </summary>
- /// <param name="disposing">Whether or not the managed resources should be disposed</param>
+ /// <param name="disposing">Whether or not the managed resources should be disposed.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
diff --git a/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj b/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj
index 0225be2c2..04f558173 100644
--- a/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj
+++ b/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj
@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index f023bc55d..396bdd4b7 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index ec7c026e5..e87283477 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -43,7 +43,7 @@
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="SkiaSharp" Version="1.68.0" />
- <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.14" />
+ <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.0" />
<PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
</ItemGroup>
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 5e4e36a34..594441af0 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -150,14 +150,15 @@ namespace Jellyfin.Server
_logger.LogWarning("Failed to enable shared cache for SQLite");
}
- using (var appHost = new CoreAppHost(
+ var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
new NullImageEncoder(),
new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
- appConfig))
+ appConfig);
+ try
{
await appHost.InitAsync(new ServiceCollection()).ConfigureAwait(false);
@@ -165,15 +166,20 @@ namespace Jellyfin.Server
await appHost.RunStartupTasksAsync().ConfigureAwait(false);
- try
- {
- // Block main thread until shutdown
- await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
- }
- catch (TaskCanceledException)
- {
- // Don't throw on cancellation
- }
+ // Block main thread until shutdown
+ await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
+ }
+ catch (TaskCanceledException)
+ {
+ // Don't throw on cancellation
+ }
+ catch (Exception ex)
+ {
+ _logger.LogCritical(ex, "Error while starting server.");
+ }
+ finally
+ {
+ appHost?.Dispose();
}
if (_restartOnShutdown)
diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs
index 23c7339d2..6d3037b24 100644
--- a/MediaBrowser.Api/Images/ImageService.cs
+++ b/MediaBrowser.Api/Images/ImageService.cs
@@ -550,14 +550,14 @@ namespace MediaBrowser.Api.Images
}
IImageEnhancer[] supportedImageEnhancers;
- if (_imageProcessor.ImageEnhancers.Length > 0)
+ if (_imageProcessor.ImageEnhancers.Count > 0)
{
if (item == null)
{
item = _libraryManager.GetItemById(itemId);
}
- supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.GetSupportedEnhancers(item, request.Type) : Array.Empty<IImageEnhancer>();
+ supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.GetSupportedEnhancers(item, request.Type).ToArray() : Array.Empty<IImageEnhancer>();
}
else
{
@@ -606,8 +606,8 @@ namespace MediaBrowser.Api.Images
ImageRequest request,
ItemImageInfo image,
bool cropwhitespace,
- ImageFormat[] supportedFormats,
- IImageEnhancer[] enhancers,
+ IReadOnlyCollection<ImageFormat> supportedFormats,
+ IReadOnlyCollection<IImageEnhancer> enhancers,
TimeSpan? cacheDuration,
IDictionary<string, string> headers,
bool isHeadRequest)
diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs
index 825732888..d6514d62e 100644
--- a/MediaBrowser.Api/ItemUpdateService.cs
+++ b/MediaBrowser.Api/ItemUpdateService.cs
@@ -69,8 +69,8 @@ namespace MediaBrowser.Api
{
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
- Countries = await _localizationManager.GetCountries(),
- Cultures = _localizationManager.GetCultures()
+ Countries = _localizationManager.GetCountries().ToArray(),
+ Cultures = _localizationManager.GetCultures().ToArray()
};
if (!item.IsVirtualItem && !(item is ICollectionFolder) && !(item is UserView) && !(item is AggregateFolder) && !(item is LiveTvChannel) && !(item is IItemByName) &&
diff --git a/MediaBrowser.Api/LocalizationService.cs b/MediaBrowser.Api/LocalizationService.cs
index eeff67e13..3b2e18852 100644
--- a/MediaBrowser.Api/LocalizationService.cs
+++ b/MediaBrowser.Api/LocalizationService.cs
@@ -1,4 +1,3 @@
-using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -82,9 +81,9 @@ namespace MediaBrowser.Api
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
- public async Task<object> Get(GetCountries request)
+ public object Get(GetCountries request)
{
- var result = await _localization.GetCountries();
+ var result = _localization.GetCountries();
return ToOptimizedResult(result);
}
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
index ba29c656b..f653270a6 100644
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ b/MediaBrowser.Api/MediaBrowser.Api.csproj
@@ -12,6 +12,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
index b42e115a7..496c2032a 100644
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs
@@ -151,7 +151,12 @@ namespace MediaBrowser.Api.Playback
return Path.Combine(folder, filename + ext);
}
- protected virtual string GetDefaultH264Preset() => "superfast";
+ protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ protected virtual string GetDefaultEncoderPreset()
+ {
+ return "superfast";
+ }
private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
{
diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
index 3c6d33e7e..27eb67ee6 100644
--- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
@@ -304,7 +304,7 @@ namespace MediaBrowser.Api.Playback.Hls
return args;
}
- protected override string GetDefaultH264Preset()
+ protected override string GetDefaultEncoderPreset()
{
return "veryfast";
}
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
index b88a09dd6..fd686d441 100644
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
@@ -895,9 +895,13 @@ namespace MediaBrowser.Api.Playback.Hls
// See if we can save come cpu cycles by avoiding encoding
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
{
- if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+ if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
- args += " -bsf:v h264_mp4toannexb";
+ string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
+ if (!string.IsNullOrEmpty(bitStreamArgs))
+ {
+ args += " " + bitStreamArgs;
+ }
}
//args += " -flags -global_header";
@@ -911,7 +915,7 @@ namespace MediaBrowser.Api.Playback.Hls
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
- args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
+ args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset()) + keyFrameArg;
//args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
index 3c715c5ad..4a5f4025b 100644
--- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
@@ -92,10 +92,14 @@ namespace MediaBrowser.Api.Playback.Hls
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
{
// if h264_mp4toannexb is ever added, do not use it for live tv
- if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) &&
+ if (state.VideoStream != null &&
!string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
- args += " -bsf:v h264_mp4toannexb";
+ string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
+ if (!string.IsNullOrEmpty(bitStreamArgs))
+ {
+ args += " " + bitStreamArgs;
+ }
}
}
else
@@ -105,7 +109,7 @@ namespace MediaBrowser.Api.Playback.Hls
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
- args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
+ args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset()) + keyFrameArg;
// Add resolution params, if specified
if (!hasGraphicalSubs)
diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
index 83a3f3e3c..c15681654 100644
--- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
@@ -77,7 +77,8 @@ namespace MediaBrowser.Api.Playback.Progressive
{
var videoCodec = state.VideoRequest.VideoCodec;
- if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
{
return ".ts";
}
diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
index ab19fdc26..cfc8a283d 100644
--- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
@@ -120,7 +120,7 @@ namespace MediaBrowser.Api.Playback.Progressive
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
{
- return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultH264Preset());
+ return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultEncoderPreset());
}
}
}
diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs
index fa70a52aa..f08d070ca 100644
--- a/MediaBrowser.Api/UserService.cs
+++ b/MediaBrowser.Api/UserService.cs
@@ -13,6 +13,7 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.Users;
+using Microsoft.Extensions.Logging;
namespace MediaBrowser.Api
{
@@ -247,8 +248,9 @@ namespace MediaBrowser.Api
private readonly INetworkManager _networkManager;
private readonly IDeviceManager _deviceManager;
private readonly IAuthorizationContext _authContext;
+ private readonly ILogger _logger;
- public UserService(IUserManager userManager, ISessionManager sessionMananger, IServerConfigurationManager config, INetworkManager networkManager, IDeviceManager deviceManager, IAuthorizationContext authContext)
+ public UserService(IUserManager userManager, ISessionManager sessionMananger, IServerConfigurationManager config, INetworkManager networkManager, IDeviceManager deviceManager, IAuthorizationContext authContext, ILoggerFactory loggerFactory)
{
_userManager = userManager;
_sessionMananger = sessionMananger;
@@ -256,6 +258,7 @@ namespace MediaBrowser.Api
_networkManager = networkManager;
_deviceManager = deviceManager;
_authContext = authContext;
+ _logger = loggerFactory.CreateLogger(nameof(UserService));
}
public object Get(GetPublicUsers request)
@@ -365,8 +368,8 @@ namespace MediaBrowser.Api
}
_sessionMananger.RevokeUserTokens(user.Id, null);
-
- return _userManager.DeleteUser(user);
+ _userManager.DeleteUser(user);
+ return Task.CompletedTask;
}
/// <summary>
@@ -399,19 +402,27 @@ namespace MediaBrowser.Api
{
var auth = _authContext.GetAuthorizationInfo(Request);
- var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest
+ try
{
- App = auth.Client,
- AppVersion = auth.Version,
- DeviceId = auth.DeviceId,
- DeviceName = auth.Device,
- Password = request.Pw,
- PasswordSha1 = request.Password,
- RemoteEndPoint = Request.RemoteIp,
- Username = request.Username
- }).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
+ var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest
+ {
+ App = auth.Client,
+ AppVersion = auth.Version,
+ DeviceId = auth.DeviceId,
+ DeviceName = auth.Device,
+ Password = request.Pw,
+ PasswordSha1 = request.Password,
+ RemoteEndPoint = Request.RemoteIp,
+ Username = request.Username
+ }).ConfigureAwait(false);
+
+ return ToOptimizedResult(result);
+ }
+ catch(SecurityException e)
+ {
+ // rethrow adding IP address to message
+ throw new SecurityException($"[{Request.RemoteIp}] {e.Message}");
+ }
}
/// <summary>
@@ -503,9 +514,14 @@ namespace MediaBrowser.Api
}
}
+ /// <summary>
+ /// Posts the specified request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>System.Object.</returns>
public async Task<object> Post(CreateUserByName request)
{
- var newUser = await _userManager.CreateUser(request.Name).ConfigureAwait(false);
+ var newUser = _userManager.CreateUser(request.Name);
// no need to authenticate password for new user
if (request.Password != null)
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index cb7343440..2248e9c85 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -75,10 +75,10 @@ namespace MediaBrowser.Common
/// <summary>
/// Gets the exports.
/// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="manageLiftime">if set to <c>true</c> [manage liftime].</param>
- /// <returns>IEnumerable{``0}.</returns>
- IEnumerable<T> GetExports<T>(bool manageLifetime = true);
+ /// <typeparam name="T">The type.</typeparam>
+ /// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param>
+ /// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
+ IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true);
/// <summary>
/// Resolves this instance.
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 05b48a2a1..91ab066f9 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -23,6 +23,12 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <!-- We need at least C# 7.1 for the "default literal" feature-->
+ <LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationException.cs b/MediaBrowser.Controller/Authentication/AuthenticationException.cs
index 045cbcdae..62eca3ea9 100644
--- a/MediaBrowser.Controller/Authentication/AuthenticationException.cs
+++ b/MediaBrowser.Controller/Authentication/AuthenticationException.cs
@@ -1,4 +1,5 @@
using System;
+
namespace MediaBrowser.Controller.Authentication
{
/// <summary>
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index 4eaecd0a0..a0f9ae46e 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -18,16 +18,6 @@ namespace MediaBrowser.Controller.Drawing
IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; }
/// <summary>
- /// Encodes the image.
- /// </summary>
- string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat);
-
- /// <summary>
- /// Creates the image collage.
- /// </summary>
- /// <param name="options">The options.</param>
- void CreateImageCollage(ImageCollageOptions options);
- /// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
@@ -46,5 +36,16 @@ namespace MediaBrowser.Controller.Drawing
bool SupportsImageEncoding { get; }
ImageDimensions GetImageSize(string path);
+
+ /// <summary>
+ /// Encodes the image.
+ /// </summary>
+ string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat);
+
+ /// <summary>
+ /// Creates the image collage.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ void CreateImageCollage(ImageCollageOptions options);
}
}
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index a11e2186f..a58a11bd1 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -24,7 +24,15 @@ namespace MediaBrowser.Controller.Drawing
/// Gets the image enhancers.
/// </summary>
/// <value>The image enhancers.</value>
- IImageEnhancer[] ImageEnhancers { get; }
+ IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether [supports image collage creation].
+ /// </summary>
+ /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
+ bool SupportsImageCollageCreation { get; }
+
+ IImageEncoder ImageEncoder { get; set; }
/// <summary>
/// Gets the dimensions of the image.
@@ -51,18 +59,12 @@ namespace MediaBrowser.Controller.Drawing
ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem);
/// <summary>
- /// Adds the parts.
- /// </summary>
- /// <param name="enhancers">The enhancers.</param>
- void AddParts(IEnumerable<IImageEnhancer> enhancers);
-
- /// <summary>
/// Gets the supported enhancers.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="imageType">Type of the image.</param>
/// <returns>IEnumerable{IImageEnhancer}.</returns>
- IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType);
+ IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType);
/// <summary>
/// Gets the image cache tag.
@@ -80,7 +82,7 @@ namespace MediaBrowser.Controller.Drawing
/// <param name="image">The image.</param>
/// <param name="imageEnhancers">The image enhancers.</param>
/// <returns>Guid.</returns>
- string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers);
+ string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers);
/// <summary>
/// Processes the image.
@@ -109,7 +111,7 @@ namespace MediaBrowser.Controller.Drawing
/// <summary>
/// Gets the supported image output formats.
/// </summary>
- /// <returns>IReadOnlyCollection{ImageOutput}.</returns>
+ /// <returns><see cref="IReadOnlyCollection{ImageOutput}" />.</returns>
IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats();
/// <summary>
@@ -118,14 +120,6 @@ namespace MediaBrowser.Controller.Drawing
/// <param name="options">The options.</param>
void CreateImageCollage(ImageCollageOptions options);
- /// <summary>
- /// Gets a value indicating whether [supports image collage creation].
- /// </summary>
- /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
- bool SupportsImageCollageCreation { get; }
-
- IImageEncoder ImageEncoder { get; set; }
-
bool SupportsTransparency(string path);
}
}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
index db432f500..29addf6e6 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using MediaBrowser.Controller.Entities;
@@ -33,9 +34,9 @@ namespace MediaBrowser.Controller.Drawing
public int Quality { get; set; }
- public IImageEnhancer[] Enhancers { get; set; }
+ public IReadOnlyCollection<IImageEnhancer> Enhancers { get; set; }
- public ImageFormat[] SupportedOutputFormats { get; set; }
+ public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; }
public bool AddPlayedIndicator { get; set; }
diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs
index 968d72579..7d245d4aa 100644
--- a/MediaBrowser.Controller/Entities/User.cs
+++ b/MediaBrowser.Controller/Entities/User.cs
@@ -17,13 +17,6 @@ namespace MediaBrowser.Controller.Entities
public class User : BaseItem
{
public static IUserManager UserManager { get; set; }
- public static IXmlSerializer XmlSerializer { get; set; }
-
- /// <summary>
- /// From now on all user paths will be Id-based.
- /// This is for backwards compatibility.
- /// </summary>
- public bool UsesIdForConfigurationPath { get; set; }
/// <summary>
/// Gets or sets the password.
@@ -31,7 +24,6 @@ namespace MediaBrowser.Controller.Entities
/// <value>The password.</value>
public string Password { get; set; }
public string EasyPassword { get; set; }
- public string Salt { get; set; }
// Strictly to remove IgnoreDataMember
public override ItemImageInfo[] ImageInfos
@@ -148,46 +140,23 @@ namespace MediaBrowser.Controller.Entities
/// <exception cref="ArgumentNullException"></exception>
public Task Rename(string newName)
{
- if (string.IsNullOrEmpty(newName))
- {
- throw new ArgumentNullException(nameof(newName));
- }
-
- // If only the casing is changing, leave the file system alone
- if (!UsesIdForConfigurationPath && !string.Equals(newName, Name, StringComparison.OrdinalIgnoreCase))
+ if (string.IsNullOrWhiteSpace(newName))
{
- UsesIdForConfigurationPath = true;
-
- // Move configuration
- var newConfigDirectory = GetConfigurationDirectoryPath(newName);
- var oldConfigurationDirectory = ConfigurationDirectoryPath;
-
- // Exceptions will be thrown if these paths already exist
- if (Directory.Exists(newConfigDirectory))
- {
- Directory.Delete(newConfigDirectory, true);
- }
-
- if (Directory.Exists(oldConfigurationDirectory))
- {
- Directory.Move(oldConfigurationDirectory, newConfigDirectory);
- }
- else
- {
- Directory.CreateDirectory(newConfigDirectory);
- }
+ throw new ArgumentException("Username can't be empty", nameof(newName));
}
Name = newName;
- return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))
- {
- ReplaceAllMetadata = true,
- ImageRefreshMode = MetadataRefreshMode.FullRefresh,
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
- ForceSave = true
+ return RefreshMetadata(
+ new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))
+ {
+ ReplaceAllMetadata = true,
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+ ForceSave = true
- }, CancellationToken.None);
+ },
+ CancellationToken.None);
}
public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
@@ -216,19 +185,6 @@ namespace MediaBrowser.Controller.Entities
{
var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath;
- // Legacy
- if (!UsesIdForConfigurationPath)
- {
- if (string.IsNullOrEmpty(username))
- {
- throw new ArgumentNullException(nameof(username));
- }
-
- var safeFolderName = FileSystem.GetValidFilename(username);
-
- return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName);
- }
-
// TODO: Remove idPath and just use usernamePath for future releases
var usernamePath = System.IO.Path.Combine(parentPath, username);
var idPath = System.IO.Path.Combine(parentPath, Id.ToString("N", CultureInfo.InvariantCulture));
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index 7f7370893..bbedc0442 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -23,6 +23,12 @@ namespace MediaBrowser.Controller.Library
IEnumerable<User> Users { get; }
/// <summary>
+ /// Gets the user ids.
+ /// </summary>
+ /// <value>The users ids.</value>
+ IEnumerable<Guid> UsersIds { get; }
+
+ /// <summary>
/// Occurs when [user updated].
/// </summary>
event EventHandler<GenericEventArgs<User>> UserUpdated;
@@ -92,7 +98,7 @@ namespace MediaBrowser.Controller.Library
/// <returns>User.</returns>
/// <exception cref="ArgumentNullException">name</exception>
/// <exception cref="ArgumentException"></exception>
- Task<User> CreateUser(string name);
+ User CreateUser(string name);
/// <summary>
/// Deletes the user.
@@ -101,7 +107,7 @@ namespace MediaBrowser.Controller.Library
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">user</exception>
/// <exception cref="ArgumentException"></exception>
- Task DeleteUser(User user);
+ void DeleteUser(User user);
/// <summary>
/// Resets the password.
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 01893f1b5..c6bca2518 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -19,6 +19,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index b9ccc93ef..87874001a 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -34,8 +34,16 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
{
- var defaultEncoder = "libx264";
+ return GetH264OrH265Encoder("libx264", "h264", state, encodingOptions);
+ }
+ public string GetH265Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
+ return GetH264OrH265Encoder("libx265", "hevc", state, encodingOptions);
+ }
+
+ private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
@@ -45,14 +53,14 @@ namespace MediaBrowser.Controller.MediaEncoding
var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
- {"qsv", "h264_qsv"},
- {"h264_qsv", "h264_qsv"},
- {"nvenc", "h264_nvenc"},
- {"amf", "h264_amf"},
- {"omx", "h264_omx"},
- {"h264_v4l2m2m", "h264_v4l2m2m"},
- {"mediacodec", "h264_mediacodec"},
- {"vaapi", "h264_vaapi"}
+ {"qsv", hwEncoder + "_qsv"},
+ {hwEncoder + "_qsv", hwEncoder + "_qsv"},
+ {"nvenc", hwEncoder + "_nvenc"},
+ {"amf", hwEncoder + "_amf"},
+ {"omx", hwEncoder + "_omx"},
+ {hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m"},
+ {"mediacodec", hwEncoder + "_mediacodec"},
+ {"vaapi", hwEncoder + "_vaapi"}
};
if (!string.IsNullOrEmpty(hwType)
@@ -119,6 +127,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(codec))
{
+ if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetH265Encoder(state, encodingOptions);
+ }
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
{
return GetH264Encoder(state, encodingOptions);
@@ -476,6 +489,30 @@ namespace MediaBrowser.Controller.MediaEncoding
codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
}
+ public bool IsH265(MediaStream stream)
+ {
+ var codec = stream.Codec ?? string.Empty;
+
+ return codec.IndexOf("265", StringComparison.OrdinalIgnoreCase) != -1 ||
+ codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+
+ public string GetBitStreamArgs(MediaStream stream)
+ {
+ if (IsH264(stream))
+ {
+ return "-bsf:v h264_mp4toannexb";
+ }
+ else if (IsH265(stream))
+ {
+ return "-bsf:v hevc_mp4toannexb";
+ }
+ else
+ {
+ return null;
+ }
+ }
+
public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{
var bitrate = state.OutputVideoBitrate;
@@ -494,7 +531,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(" -b:v {0}", bitrate.Value.ToString(_usCulture));
}
- if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
{
// h264
return string.Format(" -maxrate {0} -bufsize {1}",
@@ -515,8 +553,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// Clients may direct play higher than level 41, but there's no reason to transcode higher
if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel)
- && string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
- && requestLevel > 41)
+ && requestLevel > 41
+ && (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)))
{
return "41";
}
@@ -620,49 +660,53 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the video bitrate to specify on the command line
/// </summary>
- public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultH264Preset)
+ public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultPreset)
{
var param = string.Empty;
var isVc1 = state.VideoStream != null &&
string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
+ var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
- if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265)
{
- if (!string.IsNullOrEmpty(encodingOptions.H264Preset))
+ if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset))
{
- param += "-preset " + encodingOptions.H264Preset;
+ param += "-preset " + encodingOptions.EncoderPreset;
}
else
{
- param += "-preset " + defaultH264Preset;
+ param += "-preset " + defaultPreset;
}
- if (encodingOptions.H264Crf >= 0 && encodingOptions.H264Crf <= 51)
+ int encodeCrf = encodingOptions.H264Crf;
+ if (isLibX265)
{
- param += " -crf " + encodingOptions.H264Crf.ToString(CultureInfo.InvariantCulture);
+ encodeCrf = encodingOptions.H265Crf;
+ }
+ if (encodeCrf >= 0 && encodeCrf <= 51)
+ {
+ param += " -crf " + encodeCrf.ToString(CultureInfo.InvariantCulture);
}
else
{
- param += " -crf 23";
+ string defaultCrf = "23";
+ if (isLibX265)
+ {
+ defaultCrf = "28";
+ }
+ param += " -crf " + defaultCrf;
}
}
- else if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
- {
- param += "-preset fast";
-
- param += " -crf 28";
- }
-
// h264 (h264_qsv)
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{
string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
- if (valid_h264_qsv.Contains(encodingOptions.H264Preset, StringComparer.OrdinalIgnoreCase))
+ if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase))
{
- param += "-preset " + encodingOptions.H264Preset;
+ param += "-preset " + encodingOptions.EncoderPreset;
}
else
{
@@ -674,9 +718,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// h264 (h264_nvenc)
- else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
{
- switch (encodingOptions.H264Preset)
+ switch (encodingOptions.EncoderPreset)
{
case "veryslow":
@@ -790,7 +835,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
// also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
+ string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
switch (level)
{
@@ -827,9 +873,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
// nvenc doesn't decode with param -level set ?!
- else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
{
- //param += "";
+ // todo param += "";
}
else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
{
@@ -842,6 +889,11 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -x264opts:0 subme=0:me_range=4:rc_lookahead=10:me=dia:no_chroma_me:8x8dct=0:partitions=none";
}
+ if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
+ {
+ // todo
+ }
+
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) &&
@@ -1743,7 +1795,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var videoStream = state.VideoStream;
- if (state.DeInterlace("h264", true) && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ if ((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)) &&
+ !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
{
var inputFramerate = videoStream == null ? null : videoStream.RealFrameRate;
@@ -2404,7 +2457,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return args;
}
- public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultH264Preset)
+ public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultPreset)
{
// Get the output codec name
var videoCodec = GetVideoEncoder(state, encodingOptions);
@@ -2428,7 +2481,7 @@ namespace MediaBrowser.Controller.MediaEncoding
GetInputArgument(state, encodingOptions),
keyFrame,
GetMapArgs(state),
- GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultH264Preset),
+ GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultPreset),
threads,
GetProgressiveVideoAudioArguments(state, encodingOptions),
GetSubtitleEmbedArguments(state),
@@ -2453,7 +2506,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultH264Preset)
+ public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultPreset)
{
var args = "-codec:v:0 " + videoCodec;
@@ -2464,11 +2517,15 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
- if (state.VideoStream != null && IsH264(state.VideoStream) &&
+ if (state.VideoStream != null &&
string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
- args += " -bsf:v h264_mp4toannexb";
+ string bitStreamArgs = GetBitStreamArgs(state.VideoStream);
+ if (!string.IsNullOrEmpty(bitStreamArgs))
+ {
+ args += " " + bitStreamArgs;
+ }
}
if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps)
@@ -2514,7 +2571,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += GetGraphicalSubtitleParam(state, encodingOptions, videoCodec);
}
- var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultH264Preset);
+ var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultPreset);
if (!string.IsNullOrEmpty(qualityParam))
{
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
index be97c75a2..d64feb2f7 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
@@ -118,14 +118,14 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Gets or sets the profile.
/// </summary>
/// <value>The profile.</value>
- [ApiMember(Name = "Profile", Description = "Optional. Specify a specific h264 profile, e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+ [ApiMember(Name = "Profile", Description = "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string Profile { get; set; }
/// <summary>
/// Gets or sets the level.
/// </summary>
/// <value>The level.</value>
- [ApiMember(Name = "Level", Description = "Optional. Specify a level for the h264 profile, e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+ [ApiMember(Name = "Level", Description = "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string Level { get; set; }
/// <summary>
@@ -212,7 +212,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Gets or sets the video codec.
/// </summary>
/// <value>The video codec.</value>
- [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+ [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string VideoCodec { get; set; }
public string SubtitleCodec { get; set; }
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index 867b82ede..a8f8da9b8 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -12,6 +12,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 681a2e372..fdb20477f 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 285ff4ba5..9ae10d980 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -18,7 +18,8 @@ namespace MediaBrowser.Model.Configuration
public string EncoderAppPathDisplay { get; set; }
public string VaapiDevice { get; set; }
public int H264Crf { get; set; }
- public string H264Preset { get; set; }
+ public int H265Crf { get; set; }
+ public string EncoderPreset { get; set; }
public string DeinterlaceMethod { get; set; }
public bool EnableHardwareEncoding { get; set; }
public bool EnableSubtitleExtraction { get; set; }
@@ -34,6 +35,7 @@ namespace MediaBrowser.Model.Configuration
// This is a DRM device that is almost guaranteed to be there on every intel platform, plus it's the default one in ffmpeg if you don't specify anything
VaapiDevice = "/dev/dri/renderD128";
H264Crf = 23;
+ H265Crf = 28;
EnableHardwareEncoding = true;
EnableSubtitleExtraction = true;
HardwareDecodingCodecs = new string[] { "h264", "vc1" };
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 2673597ca..d64ea35eb 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -163,6 +163,7 @@ namespace MediaBrowser.Model.Configuration
public string ServerName { get; set; }
public string WanDdns { get; set; }
+ public string BaseUrl { get; set; }
public string UICulture { get; set; }
@@ -243,6 +244,7 @@ namespace MediaBrowser.Model.Configuration
SortRemoveCharacters = new[] { ",", "&", "-", "{", "}", "'" };
SortRemoveWords = new[] { "the", "a", "an" };
+ BaseUrl = "jellyfin";
UICulture = "en-US";
MetadataOptions = new[]
diff --git a/MediaBrowser.Model/Globalization/CultureDto.cs b/MediaBrowser.Model/Globalization/CultureDto.cs
index f229f2055..a213d4147 100644
--- a/MediaBrowser.Model/Globalization/CultureDto.cs
+++ b/MediaBrowser.Model/Globalization/CultureDto.cs
@@ -38,6 +38,7 @@ namespace MediaBrowser.Model.Globalization
{
return vals[0];
}
+
return null;
}
}
diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
index a9ce60a2a..91d946db8 100644
--- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs
+++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
-using System.Threading.Tasks;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Globalization
@@ -13,23 +12,26 @@ namespace MediaBrowser.Model.Globalization
/// <summary>
/// Gets the cultures.
/// </summary>
- /// <returns>IEnumerable{CultureDto}.</returns>
- CultureDto[] GetCultures();
+ /// <returns><see cref="IEnumerable{CultureDto}" />.</returns>
+ IEnumerable<CultureDto> GetCultures();
+
/// <summary>
/// Gets the countries.
/// </summary>
- /// <returns>IEnumerable{CountryInfo}.</returns>
- Task<CountryInfo[]> GetCountries();
+ /// <returns><see cref="IEnumerable{CountryInfo}" />.</returns>
+ IEnumerable<CountryInfo> GetCountries();
+
/// <summary>
/// Gets the parental ratings.
/// </summary>
- /// <returns>IEnumerable{ParentalRating}.</returns>
+ /// <returns><see cref="IEnumerable{ParentalRating}" />.</returns>
IEnumerable<ParentalRating> GetParentalRatings();
+
/// <summary>
/// Gets the rating level.
/// </summary>
/// <param name="rating">The rating.</param>
- /// <returns>System.Int32.</returns>
+ /// <returns><see cref="int" /> or <c>null</c>.</returns>
int? GetRatingLevel(string rating);
/// <summary>
@@ -37,7 +39,7 @@ namespace MediaBrowser.Model.Globalization
/// </summary>
/// <param name="phrase">The phrase.</param>
/// <param name="culture">The culture.</param>
- /// <returns>System.String.</returns>
+ /// <returns><see cref="string" />.</returns>
string GetLocalizedString(string phrase, string culture);
/// <summary>
@@ -50,13 +52,22 @@ namespace MediaBrowser.Model.Globalization
/// <summary>
/// Gets the localization options.
/// </summary>
- /// <returns>IEnumerable{LocalizatonOption}.</returns>
- LocalizationOption[] GetLocalizationOptions();
-
- string NormalizeFormKD(string text);
+ /// <returns><see cref="IEnumerable{LocalizatonOption}" />.</returns>
+ IEnumerable<LocalizationOption> GetLocalizationOptions();
+ /// <summary>
+ /// Checks if the string contains a character with the specified unicode category.
+ /// </summary>
+ /// <param name="value">The string.</param>
+ /// <param name="category">The unicode category.</param>
+ /// <returns>Wether or not the string contains a character with the specified unicode category.</returns>
bool HasUnicodeCategory(string value, UnicodeCategory category);
+ /// <summary>
+ /// Returns the correct <see cref="Cultureinfo" /> for the given language.
+ /// </summary>
+ /// <param name="language">The language.</param>
+ /// <returns>The correct <see cref="Cultureinfo" /> for the given language.</returns>
CultureDto FindLanguageInfo(string language);
}
}
diff --git a/MediaBrowser.Model/IO/IIsoManager.cs b/MediaBrowser.Model/IO/IIsoManager.cs
index 24b6e5f05..eb0cb4bfb 100644
--- a/MediaBrowser.Model/IO/IIsoManager.cs
+++ b/MediaBrowser.Model/IO/IIsoManager.cs
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace MediaBrowser.Model.IO
{
- public interface IIsoManager : IDisposable
+ public interface IIsoManager
{
/// <summary>
/// Mounts the specified iso path.
diff --git a/MediaBrowser.Model/IO/IIsoMounter.cs b/MediaBrowser.Model/IO/IIsoMounter.cs
index f0153a928..766a9e4e6 100644
--- a/MediaBrowser.Model/IO/IIsoMounter.cs
+++ b/MediaBrowser.Model/IO/IIsoMounter.cs
@@ -5,7 +5,7 @@ using System.Threading.Tasks;
namespace MediaBrowser.Model.IO
{
- public interface IIsoMounter : IDisposable
+ public interface IIsoMounter
{
/// <summary>
/// Mounts the specified iso path.
diff --git a/MediaBrowser.Model/IO/StreamDefaults.cs b/MediaBrowser.Model/IO/StreamDefaults.cs
index bef20e74f..1dc29e06e 100644
--- a/MediaBrowser.Model/IO/StreamDefaults.cs
+++ b/MediaBrowser.Model/IO/StreamDefaults.cs
@@ -13,6 +13,6 @@ namespace MediaBrowser.Model.IO
/// <summary>
/// The default file stream buffer size
/// </summary>
- public const int DefaultFileStreamBufferSize = 81920;
+ public const int DefaultFileStreamBufferSize = 4096;
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 3de2cca2d..e9f43ea56 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -10,6 +10,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
diff --git a/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs b/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs
index 39db22133..ac540782c 100644
--- a/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs
+++ b/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs
@@ -1,7 +1,7 @@
namespace MediaBrowser.Model.Plugins
{
/// <summary>
- /// Class BasePluginConfiguration
+ /// Class BasePluginConfiguration.
/// </summary>
public class BasePluginConfiguration
{
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 5941ed436..ab4759c61 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -21,6 +21,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs
index c04e98e64..eaebc13e3 100644
--- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs
@@ -50,27 +50,25 @@ namespace MediaBrowser.Providers.TV.TheTVDB
var language = item.GetPreferredMetadataLanguage();
if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds))
{
- var episodeTvdbId = episode.GetProviderId(MetadataProviders.Tvdb);
-
// Process images
try
{
+ var episodeInfo = new EpisodeInfo
+ {
+ IndexNumber = episode.IndexNumber.Value,
+ ParentIndexNumber = episode.ParentIndexNumber.Value,
+ SeriesProviderIds = series.ProviderIds
+ };
+ string episodeTvdbId = await _tvDbClientManager
+ .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(episodeTvdbId))
{
- var episodeInfo = new EpisodeInfo
- {
- IndexNumber = episode.IndexNumber.Value,
- ParentIndexNumber = episode.ParentIndexNumber.Value,
- SeriesProviderIds = series.ProviderIds
- };
- episodeTvdbId = await _tvDbClientManager
- .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false);
- if (string.IsNullOrEmpty(episodeTvdbId))
- {
- _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
- episodeInfo.ParentIndexNumber, episodeInfo.IndexNumber, series.GetProviderId(MetadataProviders.Tvdb));
- return imageResult;
- }
+ _logger.LogError(
+ "Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
+ episodeInfo.ParentIndexNumber,
+ episodeInfo.IndexNumber,
+ series.GetProviderId(MetadataProviders.Tvdb));
+ return imageResult;
}
var episodeResult =
@@ -86,7 +84,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB
}
catch (TvDbServerException e)
{
- _logger.LogError(e, "Failed to retrieve episode images for {TvDbId}", episodeTvdbId);
+ _logger.LogError(e, "Failed to retrieve episode images for series {TvDbId}", series.GetProviderId(MetadataProviders.Tvdb));
}
}
diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs
index 302d40c6b..e5287048d 100644
--- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs
@@ -36,57 +36,33 @@ namespace MediaBrowser.Providers.TV.TheTVDB
var list = new List<RemoteSearchResult>();
// The search query must either provide an episode number or date
- if (!searchInfo.IndexNumber.HasValue || !searchInfo.PremiereDate.HasValue)
+ if (!searchInfo.IndexNumber.HasValue
+ || !searchInfo.PremiereDate.HasValue
+ || !TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds))
{
return list;
}
- if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds))
+ var metadataResult = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false);
+
+ if (!metadataResult.HasMetadata)
{
- try
- {
- var episodeTvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb);
- if (string.IsNullOrEmpty(episodeTvdbId))
- {
- searchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(),
- out var seriesTvdbId);
- episodeTvdbId = await _tvDbClientManager
- .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken)
- .ConfigureAwait(false);
- if (string.IsNullOrEmpty(episodeTvdbId))
- {
- _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
- searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId);
- return list;
- }
- }
+ return list;
+ }
- var episodeResult = await _tvDbClientManager.GetEpisodesAsync(Convert.ToInt32(episodeTvdbId),
- searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
- var metadataResult = MapEpisodeToResult(searchInfo, episodeResult.Data);
+ var item = metadataResult.Item;
- if (metadataResult.HasMetadata)
- {
- var item = metadataResult.Item;
-
- list.Add(new RemoteSearchResult
- {
- IndexNumber = item.IndexNumber,
- Name = item.Name,
- ParentIndexNumber = item.ParentIndexNumber,
- PremiereDate = item.PremiereDate,
- ProductionYear = item.ProductionYear,
- ProviderIds = item.ProviderIds,
- SearchProviderName = Name,
- IndexNumberEnd = item.IndexNumberEnd
- });
- }
- }
- catch (TvDbServerException e)
- {
- _logger.LogError(e, "Failed to retrieve episode with id {TvDbId}", searchInfo.IndexNumber);
- }
- }
+ list.Add(new RemoteSearchResult
+ {
+ IndexNumber = item.IndexNumber,
+ Name = item.Name,
+ ParentIndexNumber = item.ParentIndexNumber,
+ PremiereDate = item.PremiereDate,
+ ProductionYear = item.ProductionYear,
+ ProviderIds = item.ProviderIds,
+ SearchProviderName = Name,
+ IndexNumberEnd = item.IndexNumberEnd
+ });
return list;
}
@@ -103,36 +79,46 @@ namespace MediaBrowser.Providers.TV.TheTVDB
if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) &&
(searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue))
{
- var tvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb);
- try
- {
- if (string.IsNullOrEmpty(tvdbId))
- {
- tvdbId = await _tvDbClientManager
- .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken)
- .ConfigureAwait(false);
- if (string.IsNullOrEmpty(tvdbId))
- {
- _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
- searchInfo.ParentIndexNumber, searchInfo.IndexNumber, tvdbId);
- return result;
- }
- }
+ result = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ _logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name);
+ }
- var episodeResult = await _tvDbClientManager.GetEpisodesAsync(
- Convert.ToInt32(tvdbId), searchInfo.MetadataLanguage,
- cancellationToken).ConfigureAwait(false);
+ return result;
+ }
- result = MapEpisodeToResult(searchInfo, episodeResult.Data);
- }
- catch (TvDbServerException e)
+ private async Task<MetadataResult<Episode>> GetEpisode(EpisodeInfo searchInfo, CancellationToken cancellationToken)
+ {
+ var result = new MetadataResult<Episode>
+ {
+ QueriedById = true
+ };
+
+ string seriesTvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb);
+ string episodeTvdbId = null;
+ try
+ {
+ episodeTvdbId = await _tvDbClientManager
+ .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+ if (string.IsNullOrEmpty(episodeTvdbId))
{
- _logger.LogError(e, "Failed to retrieve episode with id {TvDbId}", tvdbId);
+ _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
+ searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId);
+ return result;
}
+
+ var episodeResult = await _tvDbClientManager.GetEpisodesAsync(
+ Convert.ToInt32(episodeTvdbId), searchInfo.MetadataLanguage,
+ cancellationToken).ConfigureAwait(false);
+
+ result = MapEpisodeToResult(searchInfo, episodeResult.Data);
}
- else
+ catch (TvDbServerException e)
{
- _logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name);
+ _logger.LogError(e, "Failed to retrieve episode with id {EpisodeTvDbId}, series id {SeriesTvdbId}", episodeTvdbId, seriesTvdbId);
}
return result;
@@ -193,24 +179,54 @@ namespace MediaBrowser.Providers.TV.TheTVDB
});
}
- foreach (var person in episode.GuestStars)
+ // GuestStars is a weird list of names and roles
+ // Example:
+ // 1: Some Actor (Role1
+ // 2: Role2
+ // 3: Role3)
+ // 4: Another Actor (Role1
+ // ...
+ for (var i = 0; i < episode.GuestStars.Length; ++i)
{
- var index = person.IndexOf('(');
- string role = null;
- var name = person;
+ var currentActor = episode.GuestStars[i];
+ var roleStartIndex = currentActor.IndexOf('(');
- if (index != -1)
+ if (roleStartIndex == -1)
{
- role = person.Substring(index + 1).Trim().TrimEnd(')');
+ result.AddPerson(new PersonInfo
+ {
+ Type = PersonType.GuestStar,
+ Name = currentActor,
+ Role = string.Empty
+ });
+ continue;
+ }
+
+ var roles = new List<string> {currentActor.Substring(roleStartIndex + 1)};
+
+ // Fetch all roles
+ for (var j = i + 1; j < episode.GuestStars.Length; ++j)
+ {
+ var currentRole = episode.GuestStars[j];
+ var roleEndIndex = currentRole.IndexOf(')');
+
+ if (roleEndIndex == -1)
+ {
+ roles.Add(currentRole);
+ continue;
+ }
- name = person.Substring(0, index).Trim();
+ roles.Add(currentRole.TrimEnd(')'));
+ // Update the outer index (keep in mind it adds 1 after the iteration)
+ i = j;
+ break;
}
result.AddPerson(new PersonInfo
{
Type = PersonType.GuestStar,
- Name = name,
- Role = role
+ Name = currentActor.Substring(0, roleStartIndex).Trim(),
+ Role = string.Join(", ", roles)
});
}
diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs
index c739f3f49..1578e4341 100644
--- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs
@@ -285,7 +285,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB
private string GetComparableName(string name)
{
name = name.ToLowerInvariant();
- name = _localizationManager.NormalizeFormKD(name);
+ name = name.Normalize(NormalizationForm.FormKD);
var sb = new StringBuilder();
foreach (var c in name)
{
@@ -310,19 +310,16 @@ namespace MediaBrowser.Providers.TV.TheTVDB
sb.Append(c);
}
}
- name = sb.ToString();
- name = name.Replace(", the", "");
- name = name.Replace("the ", " ");
- name = name.Replace(" the ", " ");
+ sb.Replace(", the", string.Empty).Replace("the ", " ").Replace(" the ", " ");
- string prevName;
+ int prevLength;
do
{
- prevName = name;
- name = name.Replace(" ", " ");
- } while (name.Length != prevName.Length);
+ prevLength = sb.Length;
+ sb.Replace(" ", " ");
+ } while (name.Length != prevLength);
- return name.Trim();
+ return sb.ToString().Trim();
}
private void MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage)
diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
index c099e77d6..883986894 100644
--- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
+++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
@@ -18,6 +18,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index ba29c656b..f653270a6 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -12,6 +12,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1
index 2999912b3..aaa4bcf6c 100644
--- a/deployment/windows/build-jellyfin.ps1
+++ b/deployment/windows/build-jellyfin.ps1
@@ -1,5 +1,6 @@
[CmdletBinding()]
param(
+ [switch]$MakeNSIS,
[switch]$InstallFFMPEG,
[switch]$InstallNSSM,
[switch]$GenerateZip,
@@ -96,6 +97,24 @@ function Install-NSSM {
Remove-Item "$tempdir/nssm.zip" -Force -ErrorAction Continue | Write-Verbose
}
+function Make-NSIS {
+ param(
+ [string]$InstallLocation
+ )
+ Write-Verbose "Downloading NSIS"
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Invoke-WebRequest -Uri https://nchc.dl.sourceforge.net/project/nsis/NSIS%203/3.04/nsis-3.04.zip -UseBasicParsing -OutFile "$tempdir/nsis.zip" | Write-Verbose
+
+ Expand-Archive "$tempdir/nsis.zip" -DestinationPath "$tempdir/nsis/" -Force | Write-Verbose
+ $env:InstallLocation = $InstallLocation
+ & "$tempdir/nsis/nsis-3.04/makensis.exe" ".\deployment\windows\jellyfin.nsi"
+ Copy-Item .\deployment\windows\jellyfin_*.exe $InstallLocation\..\
+
+ Remove-Item "$tempdir/nsis/" -Recurse -Force -ErrorAction Continue | Write-Verbose
+ Remove-Item "$tempdir/nsis.zip" -Force -ErrorAction Continue | Write-Verbose
+}
+
+
Write-Verbose "Starting Build Process: Selected Environment is $WindowsVersion-$Architecture"
Build-JellyFin
if($InstallFFMPEG.IsPresent -or ($InstallFFMPEG -eq $true)){
@@ -108,6 +127,11 @@ if($InstallNSSM.IsPresent -or ($InstallNSSM -eq $true)){
}
Copy-Item .\deployment\windows\install-jellyfin.ps1 $InstallLocation\install-jellyfin.ps1
Copy-Item .\deployment\windows\install.bat $InstallLocation\install.bat
+Copy-Item .\LICENSE $InstallLocation\LICENSE
+if($MakeNSIS.IsPresent -or ($MakeNSIS -eq $true)){
+ Write-Verbose "Starting NSIS Package creation"
+ Make-NSIS $InstallLocation
+}
if($GenerateZip.IsPresent -or ($GenerateZip -eq $true)){
Compress-Archive -Path $InstallLocation -DestinationPath "$InstallLocation/jellyfin.zip" -Force
}
diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi
new file mode 100644
index 000000000..a374ebe45
--- /dev/null
+++ b/deployment/windows/jellyfin.nsi
@@ -0,0 +1,386 @@
+; Shows a lot of debug information while compiling
+; This can be removed once stable.
+!verbose 4
+;--------------------------------
+!define SF_USELECTED 0 ; used to check selected options status, rest are inherited from Sections.nsh
+
+ !include "MUI2.nsh"
+ !include "Sections.nsh"
+ !include "LogicLib.nsh"
+
+; Global variables that we'll use
+ Var _JELLYFINVERSION_
+ Var _JELLYFINDATADIR_
+ Var _INSTALLSERVICE_
+ Var _SERVICESTART_
+ Var _LOCALSYSTEMACCOUNT_
+ Var _EXISTINGINSTALLATION_
+ Var _EXISTINGSERVICE_
+ Var _CUSTOMDATAFOLDER_
+
+!if ${NSIS_PTR_SIZE} > 4
+ !define BITS 64
+ !define NAMESUFFIX " (64 bit)"
+!else
+ !define BITS 32
+ !define NAMESUFFIX ""
+!endif
+
+;--------------------------------
+
+ !define REG_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\Jellyfin" ;Registry to show up in Add/Remove Programs
+
+ !getdllversion "$%InstallLocation%\jellyfin.dll" ver_ ;Align installer version with jellyfin.dll version
+ Name "Jellyfin Server ${ver_1}.${ver_2}.${ver_3}" ; This is referred in various header text labels
+ OutFile "jellyfin_${ver_1}.${ver_2}.${ver_3}_windows.exe" ; Naming convention jellyfin_{version}_windows-{arch].exe
+ BrandingText "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} Installer" ; This shows in just over the buttons
+
+; installer attributes, these show up in details tab on installer properties
+ VIProductVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIProductVersion format, should be X.X.X.X
+ VIFileVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIFileVersion format, should be X.X.X.X
+ VIAddVersionKey "ProductName" "Jellyfin Server"
+ VIAddVersionKey "FileVersion" "${ver_1}.${ver_2}.${ver_3}.0"
+ VIAddVersionKey "LegalCopyright" "Jellyfin, Free Software Media System"
+ VIAddVersionKey "FileDescription" "Jellyfin Server"
+
+;TODO, check defaults
+ InstallDir "$PROGRAMFILES\Jellyfin" ;Default installation folder
+ InstallDirRegKey HKLM "Software\Jellyfin" "InstallFolder" ;Read the registry for install folder,
+
+ RequestExecutionLevel admin ; ask it upfront for service control, and installing in priv folders
+
+ CRCCheck on ; make sure the installer wasn't corrupted while downloading
+
+ !define MUI_ABORTWARNING ;Prompts user in case of aborting install
+
+; TODO: Replace with nice Jellyfin Icons
+ !define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\nsis3-install.ico" ; Installer Icon
+ !define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\nsis3-uninstall.ico" ; Uninstaller Icon
+
+ !define MUI_HEADERIMAGE
+ !define MUI_HEADERIMAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Header\nsis3-branding.bmp"
+ !define MUI_WELCOMEFINISHPAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Wizard\nsis3-branding.bmp"
+
+;--------------------------------
+;Pages
+
+; Welcome Page
+ !define MUI_WELCOMEPAGE_TEXT "The installer will ask for details to install Jellyfin Server.$\r$\n$\r$\n$\r$\n\
+ ADVANCED:$\r$\n\
+ The default service install uses Network Service account and is sufficient for most users. $\r$\n$\r$\n\
+ You can choose to install using Local System account under Advanced options. This also affects where Jellyfin Server and Jellyfin data can be installed. The installer will NOT check this, you should know what you are doing.$\r$\n$\r$\n\
+ You can choose the folder for Jellyfin Metadata under advanced options based on your needs."
+ !insertmacro MUI_PAGE_WELCOME
+; License Page
+ !insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL
+; Components Page
+ !define MUI_COMPONENTSPAGE_SMALLDESC
+ !insertmacro MUI_PAGE_COMPONENTS
+ !define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show
+ !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install folder" ; shows just above the folder selection dialog
+ !insertmacro MUI_PAGE_DIRECTORY
+
+; Metadata folder Page
+ !define MUI_PAGE_CUSTOMFUNCTION_PRE HideDataDirectoryPage ; Controls when to hide / show
+ !define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install the Jellyfin Server metadata."
+ !define MUI_DIRECTORYPAGE_TEXT_TOP "The installer will set the following folder for Jellyfin Server metadata. To install in a differenct folder, click Browse and select another folder. Please make sure the folder exists. Click Next to continue."
+ !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Metadata folder"
+ !define MUI_DIRECTORYPAGE_VARIABLE $_JELLYFINDATADIR_
+ !insertmacro MUI_PAGE_DIRECTORY
+
+; Confirmation Page
+ Page custom ConfirmationPage ; just letting the user know what they chose to install
+
+; Actual Installion Page
+ !insertmacro MUI_PAGE_INSTFILES
+
+ !insertmacro MUI_UNPAGE_CONFIRM
+ !insertmacro MUI_UNPAGE_INSTFILES
+ !insertmacro MUI_UNPAGE_FINISH
+
+;--------------------------------
+;Languages; Add more languages later here if needed
+ !insertmacro MUI_LANGUAGE "English"
+
+;--------------------------------
+;Installer Sections
+Section "Jellyfin Server (required)" InstallJellyfin
+ SectionIn RO ; Mandatory section, isn't this the whole purpose to run the installer.
+
+ StrCmp "$_EXISTINGINSTALLATION_" "YES" RunUninstaller CarryOn ; Silently uninstall in case of previous installation
+
+ RunUninstaller:
+ DetailPrint "Looking for uninstaller at $INSTDIR"
+ FindFirst $0 $1 "$INSTDIR\Uninstall.exe"
+ FindClose $0
+ StrCmp $1 "" CarryOn ; the registry key was there but uninstaller was not found
+
+ DetailPrint "Silently running the uninstaller at $INSTDIR"
+ ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR' $0
+ DetailPrint "Uninstall finished, $0"
+
+ CarryOn:
+
+ SetOutPath "$INSTDIR"
+
+ File /r $%InstallLocation%\*
+
+; Write the InstallFolder, DataFolder, Network Service info into the registry for later use
+ WriteRegExpandStr HKLM "Software\Jellyfin" "InstallFolder" "$INSTDIR"
+ WriteRegExpandStr HKLM "Software\Jellyfin" "DataFolder" "$_JELLYFINDATADIR_"
+ WriteRegStr HKLM "Software\Jellyfin" "LocalSystemAccount" "$_LOCALSYSTEMACCOUNT_"
+
+ !getdllversion "$%InstallLocation%\jellyfin.dll" ver_
+ StrCpy $_JELLYFINVERSION_ "${ver_1}.${ver_2}.${ver_3}" ;
+
+; Write the uninstall keys for Windows
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayName" "Jellyfin $_JELLYFINVERSION_"
+ WriteRegExpandStr HKLM "${REG_UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayIcon" '"$INSTDIR\Jellyfin.exe",0'
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "Publisher" "The Jellyfin project"
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.github.io/"
+ WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayVersion" "$_JELLYFINVERSION_"
+ WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoModify" 1
+ WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoRepair" 1
+
+;Create uninstaller
+ WriteUninstaller "$INSTDIR\Uninstall.exe"
+
+SectionEnd
+
+Section "Jellyfin Service" InstallService
+
+ ExecWait '"$INSTDIR\nssm.exe" install Jellyfin "$INSTDIR\jellyfin.exe" --datadir "$_JELLYFINDATADIR_"' $0
+ DetailPrint "Jellyfin Service install, $0"
+
+ Sleep 3000 ; Give time for Windows to catchup
+
+ ExecWait '"$INSTDIR\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START' $0
+ DetailPrint "Jellyfin Service setting, $0"
+
+ Sleep 3000
+ ${If} $_LOCALSYSTEMACCOUNT_ == "NO" ; the default install using NSSM is Local System
+ DetailPrint "Attempting to change service account to Network Service"
+ ExecWait '"$INSTDIR\nssm.exe" set Jellyfin Objectname "Network Service"' $0
+ DetailPrint "Jellyfin service account change, $0"
+ ${EndIf}
+
+SectionEnd
+
+Section "Start Jellyfin service after install" StartService
+
+ ExecWait '"$INSTDIR\nssm.exe" start Jellyfin' $0
+ DetailPrint "Jellyfin service start, $0"
+
+SectionEnd
+
+SectionGroup "Advanced" Advanced
+Section /o "Use Local System account" LocalSystemAccount
+ ; The section is for user choice, nothing to do here
+SectionEnd
+Section /o "Custom Jellyfin metadata folder" CustomDataFolder
+ ; The section is for user choice, nothing to do here
+SectionEnd
+SectionGroupEnd
+
+
+;--------------------------------
+;Descriptions
+
+;Language strings
+ LangString DESC_InstallJellyfin ${LANG_ENGLISH} "Install Jellyfin Server"
+ LangString DESC_InstallService ${LANG_ENGLISH} "Install As a Service"
+ LangString DESC_StartService ${LANG_ENGLISH} "Start Jellyfin service after Install"
+ LangString DESC_LocalSystemAccount ${LANG_ENGLISH} "Use Local System account to start windows service"
+ LangString DESC_CustomDataFolder ${LANG_ENGLISH} "Choose Jellyfin Server metadata folder in subsequent steps"
+
+;Assign language strings to sections
+ !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
+ !insertmacro MUI_DESCRIPTION_TEXT ${InstallJellyfin} $(DESC_InstallJellyfin)
+ !insertmacro MUI_DESCRIPTION_TEXT ${InstallService} $(DESC_InstallService)
+ !insertmacro MUI_DESCRIPTION_TEXT ${StartService} $(DESC_StartService)
+ !insertmacro MUI_DESCRIPTION_TEXT ${LocalSystemAccount} $(DESC_LocalSystemAccount)
+ !insertmacro MUI_DESCRIPTION_TEXT ${CustomDataFolder} $(DESC_CustomDataFolder)
+ !insertmacro MUI_FUNCTION_DESCRIPTION_END
+
+;--------------------------------
+;Uninstaller Section
+
+Section "Uninstall"
+
+ ReadRegStr $INSTDIR HKLM "Software\Jellyfin" "InstallFolder" ; read the installation folder
+ ReadRegStr $_JELLYFINDATADIR_ HKLM "Software\Jellyfin" "DataFolder" ; read the metadata folder
+
+ DetailPrint "Jellyfin Install location : $INSTDIR"
+ DetailPrint "Jellyfin data folder : $_JELLYFINDATADIR_"
+
+ MessageBox MB_YESNO|MB_ICONINFORMATION "Do you want to retain Jellyfin metadata folder? The media will not be touched. $\r$\nIf unsure choose YES." /SD IDYES IDYES PreserveData
+
+ RMDir /r /REBOOTOK "$_JELLYFINDATADIR_"
+
+ PreserveData:
+
+ DetailPrint "Attempting to stop Jellyfin Server"
+ ExecWait '"$INSTDIR\nssm.exe" stop Jellyfin' $0
+ DetailPrint "Jellyfin service stop, $0"
+ DetailPrint "Attempting to remove Jellyfin service"
+ ExecWait '"$INSTDIR\nssm.exe" remove Jellyfin confirm' $0
+ DetailPrint "Jellyfin Service remove, $0"
+
+ Delete "$INSTDIR\Uninstall.exe"
+
+ RMDir /r /REBOOTOK "$INSTDIR"
+
+ DeleteRegKey HKLM "Software\Jellyfin"
+ DeleteRegKey HKLM "${REG_UNINST_KEY}"
+
+SectionEnd
+
+
+
+Function .onInit
+; Setting up defaults
+ StrCpy $_INSTALLSERVICE_ "YES"
+ StrCpy $_SERVICESTART_ "YES"
+ StrCpy $_CUSTOMDATAFOLDER_ "NO"
+ StrCpy $_LOCALSYSTEMACCOUNT_ "NO"
+ StrCpy $_EXISTINGINSTALLATION_ "NO"
+ StrCpy $_EXISTINGSERVICE_ "NO"
+
+ SetShellVarContext current
+ StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\jellyfin\"
+
+;Detect if Jellyfin is already installed.
+; In case it is installed, let the user choose either
+; 1. Exit installer
+; 2. Upgrade without messing with data
+; 2a. Don't ask for any details, uninstall and install afresh with old settings
+
+; Read Registry for previous installation
+ ClearErrors
+ ReadRegStr "$0" HKLM "Software\Jellyfin" "InstallFolder"
+ IfErrors NoExisitingInstall
+
+ DetailPrint "Existing Jellyfin detected at: $0"
+ StrCpy "$INSTDIR" "$0" ; set the location fro registry as new default
+
+ StrCpy $_EXISTINGINSTALLATION_ "YES" ; Set our flag to be used later
+ SectionSetText ${InstallJellyfin} "Upgrade Jellyfin Server(required)" ; Change install text to "Upgrade"
+
+; check if there is a service called Jellyfin, there should be
+; hack : nssm statuscode Jellyfin will return non zero return code in case it exists
+ ExecWait '"$INSTDIR\nssm.exe" statuscode Jellyfin' $0
+ DetailPrint "Jellyfin service statuscode, $0"
+ IntCmp $0 0 NoService ; service doesn't exist, may be run from desktop shortcut
+
+ ; if service was detected, set defaults going forward.
+ StrCpy $_EXISTINGSERVICE_ "YES"
+ StrCpy $_INSTALLSERVICE_ "YES"
+ StrCpy $_SERVICESTART_ "YES"
+
+ ; check if service was run using Network Service account
+ ClearErrors
+ ReadRegStr "$_LOCALSYSTEMACCOUNT_" HKLM "Software\Jellyfin" "LocalSystemAccount" ; in case of error _LOCALSYSTEMACCOUNT_ will be NO as default
+
+ ClearErrors
+ ReadRegStr $_JELLYFINDATADIR_ HKLM "Software\Jellyfin" "DataFolder" ; in case of error, the default holds
+
+ ; Hide sections which will not be needed in case of previous install
+ SectionSetText ${InstallService} ""
+ SectionSetText ${StartService} ""
+ SectionSetText ${LocalSystemAccount} ""
+ SectionSetText ${CustomDataFolder} ""
+ SectionSetText ${Advanced} ""
+
+
+ NoService: ; existing install was present but no service was detected
+
+; Let the user know that we'll upgrade and provide an option to quit.
+ MessageBox MB_OKCANCEL|MB_ICONINFORMATION "Existing installation of Jellyfin was detected, it'll be upgraded, settings will be retained. \
+ $\r$\nClick OK to proceed, Cancel to exit installer." /SD IDOK IDOK ProceedWithUpgrade
+ Quit ; Quit if the user is not sure about upgrade
+
+ ProceedWithUpgrade:
+
+ NoExisitingInstall:
+; by this time, the variables have been correctly set to reflect previous install details
+
+FunctionEnd
+
+Function HideInstallDirectoryPage
+ ${If} $_EXISTINGINSTALLATION_ == "YES" ; Existing installation detected, so don't ask for InstallFolder
+ Abort
+ ${EndIf}
+FunctionEnd
+
+; Don't show custom folder option in case it wasn't chosen
+Function HideDataDirectoryPage
+ ${If} $_CUSTOMDATAFOLDER_ == "NO"
+ Abort
+ ${EndIf}
+FunctionEnd
+
+; This function handles the choices during component selection
+Function .onSelChange
+ SectionGetFlags ${CustomDataFolder} $0
+ ${If} $0 = ${SF_SELECTED}
+ StrCpy $_CUSTOMDATAFOLDER_ "YES"
+ ${Else}
+ StrCpy $_CUSTOMDATAFOLDER_ "NO"
+ ${EndIf}
+
+; If we are not installing service, we don't need to set the NetworkService account or StartService
+ SectionGetFlags ${InstallService} $0
+ ${If} $0 = ${SF_SELECTED}
+ StrCpy $_INSTALLSERVICE_ "YES"
+ SectionGetFlags ${LocalSystemAccount} $0
+ IntOp $0 $0 | ${SF_RO}
+ IntOp $0 $0 ^ ${SF_RO}
+ SectionSetFlags ${LocalSystemAccount} $0
+ SectionGetFlags ${StartService} $0
+ IntOp $0 $0 | ${SF_RO}
+ IntOp $0 $0 ^ ${SF_RO}
+ SectionSetFlags ${StartService} $0
+ ${Else}
+ StrCpy $_INSTALLSERVICE_ "NO"
+ IntOp $0 ${SF_USELECTED} | ${SF_RO}
+ SectionSetFlags ${LocalSystemAccount} $0
+ SectionSetFlags ${StartService} $0
+ ${EndIf}
+
+ SectionGetFlags ${StartService} $0
+ ${If} $0 = ${SF_SELECTED}
+ StrCpy $_SERVICESTART_ "YES"
+ ${Else}
+ StrCpy $_SERVICESTART_ "NO"
+ ${EndIf}
+
+ SectionGetFlags ${LocalSystemAccount} $0
+ ${If} $0 = ${SF_SELECTED}
+ StrCpy $_LOCALSYSTEMACCOUNT_ "YES"
+ ${Else}
+ StrCpy $_LOCALSYSTEMACCOUNT_ "NO"
+ ${EndIf}
+
+
+FunctionEnd
+
+Function ConfirmationPage
+ !insertmacro MUI_HEADER_TEXT "Confirmation Page" "Please confirm your choices for Jellyfin Server installation"
+
+ nsDialogs::Create 1018
+
+ ${NSD_CreateLabel} 0 0 100% 100% "The installer will proceed based on the following inputs gathered on earlier screens.$\r$\n$\r$\n\
+ Installation Folder : $INSTDIR$\r$\n\
+ Service install : $_INSTALLSERVICE_$\r$\n\
+ Service start : $_SERVICESTART_$\r$\n\
+ Local System account for service: $_LOCALSYSTEMACCOUNT_$\r$\n\
+ Custom Metadata folder : $_CUSTOMDATAFOLDER_$\r$\n\
+ Jellyfin Metadata Folder: $_JELLYFINDATADIR_"
+ nsDialogs::Show
+
+FunctionEnd
+
+Function .onInstSuccess
+ ExecShell "open" "http://localhost:8096"
+FunctionEnd