mirror of https://github.com/jellyfin/jellyfin.git
re-factored some file system access
This commit is contained in:
parent
08d9004d8f
commit
02fedead11
|
@ -696,7 +696,8 @@ namespace MediaBrowser.Api.Images
|
||||||
Item = item,
|
Item = item,
|
||||||
Request = request,
|
Request = request,
|
||||||
CropWhiteSpace = request.Type == ImageType.Logo || request.Type == ImageType.Art,
|
CropWhiteSpace = request.Type == ImageType.Logo || request.Type == ImageType.Art,
|
||||||
OriginalImageDateModified = originalFileImageDateModified
|
OriginalImageDateModified = originalFileImageDateModified,
|
||||||
|
Enhancers = supportedImageEnhancers
|
||||||
|
|
||||||
}, contentType);
|
}, contentType);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
using ServiceStack.Service;
|
using ServiceStack.Service;
|
||||||
using ServiceStack.ServiceHost;
|
using ServiceStack.ServiceHost;
|
||||||
using System;
|
using System;
|
||||||
|
@ -14,6 +15,8 @@ namespace MediaBrowser.Api.Images
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ImageWriter : IStreamWriter, IHasOptions
|
public class ImageWriter : IStreamWriter, IHasOptions
|
||||||
{
|
{
|
||||||
|
public List<IImageEnhancer> Enhancers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the request.
|
/// Gets or sets the request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -67,7 +70,7 @@ namespace MediaBrowser.Api.Images
|
||||||
{
|
{
|
||||||
return Kernel.Instance.ImageManager.ProcessImage(Item, Request.Type, Request.Index ?? 0, CropWhiteSpace,
|
return Kernel.Instance.ImageManager.ProcessImage(Item, Request.Type, Request.Index ?? 0, CropWhiteSpace,
|
||||||
OriginalImageDateModified, responseStream, Request.Width, Request.Height, Request.MaxWidth,
|
OriginalImageDateModified, responseStream, Request.Width, Request.Height, Request.MaxWidth,
|
||||||
Request.MaxHeight, Request.Quality);
|
Request.MaxHeight, Request.Quality, Enhancers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -340,6 +340,13 @@ namespace MediaBrowser.Api.Playback
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var parentPath = Path.GetDirectoryName(path);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
var task = MediaEncoder.ExtractTextSubtitle(inputPath, type, subtitleStream.Index, offset, path, CancellationToken.None);
|
var task = MediaEncoder.ExtractTextSubtitle(inputPath, type, subtitleStream.Index, offset, path, CancellationToken.None);
|
||||||
|
|
||||||
Task.WaitAll(task);
|
Task.WaitAll(task);
|
||||||
|
@ -371,6 +378,13 @@ namespace MediaBrowser.Api.Playback
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var parentPath = Path.GetDirectoryName(path);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
var task = MediaEncoder.ConvertTextSubtitleToAss(subtitleStream.Path, path, offset, CancellationToken.None);
|
var task = MediaEncoder.ConvertTextSubtitleToAss(subtitleStream.Path, path, offset, CancellationToken.None);
|
||||||
|
|
||||||
Task.WaitAll(task);
|
Task.WaitAll(task);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
@ -38,6 +37,29 @@ namespace MediaBrowser.Api
|
||||||
/// <value>The limit.</value>
|
/// <value>The limit.</value>
|
||||||
[ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
[ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||||
public int? Limit { get; set; }
|
public int? Limit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fields to return within the items, in addition to basic information
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The fields.</value>
|
||||||
|
[ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: AudioInfo, Budget, Chapters, CriticRatingSummary, DateCreated, DisplayMediaType, EndDate, Genres, HomePageUrl, ItemCounts, IndexOptions, Locations, MediaStreams, Overview, OverviewHtml, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SeriesInfo, SortName, Studios, Taglines, TrailerUrls, UserData", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||||
|
public string Fields { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the item fields.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>IEnumerable{ItemFields}.</returns>
|
||||||
|
public IEnumerable<ItemFields> GetItemFields()
|
||||||
|
{
|
||||||
|
var val = Fields;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(val))
|
||||||
|
{
|
||||||
|
return new ItemFields[] { };
|
||||||
|
}
|
||||||
|
|
||||||
|
return val.Split(',').Select(v => (ItemFields)Enum.Parse(typeof(ItemFields), v, true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -64,8 +86,7 @@ namespace MediaBrowser.Api
|
||||||
(request.UserId.HasValue ? user.RootFolder :
|
(request.UserId.HasValue ? user.RootFolder :
|
||||||
(Folder)libraryManager.RootFolder) : DtoBuilder.GetItemByClientId(request.Id, userManager, libraryManager, request.UserId);
|
(Folder)libraryManager.RootFolder) : DtoBuilder.GetItemByClientId(request.Id, userManager, libraryManager, request.UserId);
|
||||||
|
|
||||||
// Get everything
|
var fields = request.GetItemFields().ToList();
|
||||||
var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList();
|
|
||||||
|
|
||||||
var dtoBuilder = new DtoBuilder(logger, libraryManager, userDataRepository);
|
var dtoBuilder = new DtoBuilder(logger, libraryManager, userDataRepository);
|
||||||
|
|
||||||
|
|
|
@ -123,9 +123,14 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return JsonSerializer.DeserializeFromFile<TaskResult>(GetHistoryFilePath());
|
return JsonSerializer.DeserializeFromFile<TaskResult>(GetHistoryFilePath(false));
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (DirectoryNotFoundException)
|
||||||
|
{
|
||||||
|
// File doesn't exist. No biggie
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
// File doesn't exist. No biggie
|
// File doesn't exist. No biggie
|
||||||
return null;
|
return null;
|
||||||
|
@ -413,63 +418,46 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _scheduled tasks configuration directory
|
|
||||||
/// </summary>
|
|
||||||
private string _scheduledTasksConfigurationDirectory;
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the scheduled tasks configuration directory.
|
/// Gets the scheduled tasks configuration directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The scheduled tasks configuration directory.</value>
|
/// <param name="create">if set to <c>true</c> [create].</param>
|
||||||
private string ScheduledTasksConfigurationDirectory
|
/// <returns>System.String.</returns>
|
||||||
|
private string GetScheduledTasksConfigurationDirectory(bool create)
|
||||||
{
|
{
|
||||||
get
|
var path = Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
|
||||||
{
|
|
||||||
if (_scheduledTasksConfigurationDirectory == null)
|
|
||||||
{
|
|
||||||
_scheduledTasksConfigurationDirectory = Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
|
|
||||||
|
|
||||||
if (!Directory.Exists(_scheduledTasksConfigurationDirectory))
|
if (create && !Directory.Exists(path))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(_scheduledTasksConfigurationDirectory);
|
Directory.CreateDirectory(path);
|
||||||
}
|
|
||||||
}
|
|
||||||
return _scheduledTasksConfigurationDirectory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _scheduled tasks data directory
|
|
||||||
/// </summary>
|
|
||||||
private string _scheduledTasksDataDirectory;
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the scheduled tasks data directory.
|
/// Gets the scheduled tasks data directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The scheduled tasks data directory.</value>
|
/// <param name="create">if set to <c>true</c> [create].</param>
|
||||||
private string ScheduledTasksDataDirectory
|
/// <returns>System.String.</returns>
|
||||||
|
private string GetScheduledTasksDataDirectory(bool create)
|
||||||
{
|
{
|
||||||
get
|
var path = Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks");
|
||||||
{
|
|
||||||
if (_scheduledTasksDataDirectory == null)
|
|
||||||
{
|
|
||||||
_scheduledTasksDataDirectory = Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks");
|
|
||||||
|
|
||||||
if (!Directory.Exists(_scheduledTasksDataDirectory))
|
if (create && !Directory.Exists(path))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(_scheduledTasksDataDirectory);
|
Directory.CreateDirectory(path);
|
||||||
}
|
|
||||||
}
|
|
||||||
return _scheduledTasksDataDirectory;
|
|
||||||
}
|
}
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the history file path.
|
/// Gets the history file path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The history file path.</value>
|
/// <value>The history file path.</value>
|
||||||
private string GetHistoryFilePath()
|
private string GetHistoryFilePath(bool createDirectory)
|
||||||
{
|
{
|
||||||
return Path.Combine(ScheduledTasksDataDirectory, Id + ".js");
|
return Path.Combine(GetScheduledTasksDataDirectory(createDirectory), Id + ".js");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -478,7 +466,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
private string GetConfigurationFilePath()
|
private string GetConfigurationFilePath()
|
||||||
{
|
{
|
||||||
return Path.Combine(ScheduledTasksConfigurationDirectory, Id + ".js");
|
return Path.Combine(GetScheduledTasksConfigurationDirectory(false), Id + ".js");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -493,7 +481,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
||||||
.Select(ScheduledTaskHelpers.GetTrigger)
|
.Select(ScheduledTaskHelpers.GetTrigger)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
// File doesn't exist. No biggie. Return defaults.
|
||||||
|
return ScheduledTask.GetDefaultTriggers();
|
||||||
|
}
|
||||||
|
catch (DirectoryNotFoundException)
|
||||||
{
|
{
|
||||||
// File doesn't exist. No biggie. Return defaults.
|
// File doesn't exist. No biggie. Return defaults.
|
||||||
return ScheduledTask.GetDefaultTriggers();
|
return ScheduledTask.GetDefaultTriggers();
|
||||||
|
@ -530,7 +523,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
||||||
Id = Id
|
Id = Id
|
||||||
};
|
};
|
||||||
|
|
||||||
JsonSerializer.SerializeToFile(result, GetHistoryFilePath());
|
JsonSerializer.SerializeToFile(result, GetHistoryFilePath(true));
|
||||||
|
|
||||||
LastExecutionResult = result;
|
LastExecutionResult = result;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace MediaBrowser.Common.IO
|
namespace MediaBrowser.Common.IO
|
||||||
|
@ -11,12 +10,6 @@ namespace MediaBrowser.Common.IO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FileSystemRepository
|
public class FileSystemRepository
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Contains the list of subfolders under the main directory
|
|
||||||
/// The directory entry is created when the item is first added to the dictionary
|
|
||||||
/// </summary>
|
|
||||||
private readonly ConcurrentDictionary<string, string> _subFolderPaths = new ConcurrentDictionary<string, string>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the path.
|
/// Gets or sets the path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -36,18 +29,6 @@ namespace MediaBrowser.Common.IO
|
||||||
}
|
}
|
||||||
|
|
||||||
Path = path;
|
Path = path;
|
||||||
Initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes this instance.
|
|
||||||
/// </summary>
|
|
||||||
protected void Initialize()
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(Path))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -56,17 +37,18 @@ namespace MediaBrowser.Common.IO
|
||||||
/// <param name="uniqueName">Name of the unique.</param>
|
/// <param name="uniqueName">Name of the unique.</param>
|
||||||
/// <param name="fileExtension">The file extension.</param>
|
/// <param name="fileExtension">The file extension.</param>
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
/// <exception cref="System.ArgumentNullException"></exception>
|
/// <exception cref="System.ArgumentNullException">
|
||||||
|
/// </exception>
|
||||||
public string GetResourcePath(string uniqueName, string fileExtension)
|
public string GetResourcePath(string uniqueName, string fileExtension)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(uniqueName))
|
if (string.IsNullOrEmpty(uniqueName))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException();
|
throw new ArgumentNullException("uniqueName");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(fileExtension))
|
if (string.IsNullOrEmpty(fileExtension))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException();
|
throw new ArgumentNullException("fileExtension");
|
||||||
}
|
}
|
||||||
|
|
||||||
var filename = uniqueName.GetMD5() + fileExtension;
|
var filename = uniqueName.GetMD5() + fileExtension;
|
||||||
|
@ -75,7 +57,7 @@ namespace MediaBrowser.Common.IO
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the full path of where a file should be stored within the repository
|
/// Gets the resource path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="filename">The filename.</param>
|
/// <param name="filename">The filename.</param>
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
|
@ -84,41 +66,14 @@ namespace MediaBrowser.Common.IO
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(filename))
|
if (string.IsNullOrEmpty(filename))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException();
|
throw new ArgumentNullException("filename");
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetInternalResourcePath(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Takes a filename and returns the full path of where it should be stored
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filename">The filename.</param>
|
|
||||||
/// <returns>System.String.</returns>
|
|
||||||
private string GetInternalResourcePath(string filename)
|
|
||||||
{
|
|
||||||
var prefix = filename.Substring(0, 1);
|
var prefix = filename.Substring(0, 1);
|
||||||
|
|
||||||
var folder = _subFolderPaths.GetOrAdd(prefix, GetCachePath);
|
|
||||||
|
|
||||||
return System.IO.Path.Combine(folder, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a subfolder under the image cache directory and returns the full path
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="prefix">The prefix.</param>
|
|
||||||
/// <returns>System.String.</returns>
|
|
||||||
private string GetCachePath(string prefix)
|
|
||||||
{
|
|
||||||
var path = System.IO.Path.Combine(Path, prefix);
|
var path = System.IO.Path.Combine(Path, prefix);
|
||||||
|
|
||||||
if (!Directory.Exists(path))
|
return System.IO.Path.Combine(path, filename);
|
||||||
{
|
|
||||||
Directory.CreateDirectory(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -144,8 +99,8 @@ namespace MediaBrowser.Common.IO
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException();
|
throw new ArgumentNullException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ContainsFilePath(GetInternalResourcePath(filename));
|
return ContainsFilePath(GetResourcePath(filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The image enhancers.</value>
|
/// <value>The image enhancers.</value>
|
||||||
public IEnumerable<IImageEnhancer> ImageEnhancers { get; set; }
|
public IEnumerable<IImageEnhancer> ImageEnhancers { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the image size cache.
|
/// Gets the image size cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -106,9 +106,10 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
/// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
|
/// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
|
||||||
/// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
|
/// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
|
||||||
/// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
|
/// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
|
||||||
|
/// <param name="enhancers">The enhancers.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
/// <exception cref="System.ArgumentNullException">entity</exception>
|
/// <exception cref="System.ArgumentNullException">entity</exception>
|
||||||
public async Task ProcessImage(BaseItem entity, ImageType imageType, int imageIndex, bool cropWhitespace, DateTime dateModified, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality)
|
public async Task ProcessImage(BaseItem entity, ImageType imageType, int imageIndex, bool cropWhitespace, DateTime dateModified, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality, List<IImageEnhancer> enhancers)
|
||||||
{
|
{
|
||||||
if (entity == null)
|
if (entity == null)
|
||||||
{
|
{
|
||||||
|
@ -127,28 +128,13 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
originalImagePath = await GetCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
|
originalImagePath = await GetCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportedEnhancers = ImageEnhancers.Where(i =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return i.Supports(entity, imageType);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error in image enhancer: {0}", ex, i.GetType().Name);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
// No enhancement - don't cache
|
// No enhancement - don't cache
|
||||||
if (supportedEnhancers.Count > 0)
|
if (enhancers.Count > 0)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Enhance if we have enhancers
|
// Enhance if we have enhancers
|
||||||
var ehnancedImagePath = await GetEnhancedImage(originalImagePath, dateModified, entity, imageType, imageIndex, supportedEnhancers).ConfigureAwait(false);
|
var ehnancedImagePath = await GetEnhancedImage(originalImagePath, dateModified, entity, imageType, imageIndex, enhancers).ConfigureAwait(false);
|
||||||
|
|
||||||
// If the path changed update dateModified
|
// If the path changed update dateModified
|
||||||
if (!ehnancedImagePath.Equals(originalImagePath, StringComparison.OrdinalIgnoreCase))
|
if (!ehnancedImagePath.Equals(originalImagePath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
@ -175,6 +161,19 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
|
|
||||||
var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality.Value, dateModified);
|
var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality.Value, dateModified);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
|
||||||
|
{
|
||||||
|
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// Cache file doesn't exist or is currently being written ro
|
||||||
|
}
|
||||||
|
|
||||||
var semaphore = GetLock(cacheFilePath);
|
var semaphore = GetLock(cacheFilePath);
|
||||||
|
|
||||||
await semaphore.WaitAsync().ConfigureAwait(false);
|
await semaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
@ -262,6 +261,13 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
/// <param name="bytes">The bytes.</param>
|
/// <param name="bytes">The bytes.</param>
|
||||||
private async Task CacheResizedImage(string cacheFilePath, byte[] bytes)
|
private async Task CacheResizedImage(string cacheFilePath, byte[] bytes)
|
||||||
{
|
{
|
||||||
|
var parentPath = Path.GetDirectoryName(cacheFilePath);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
// Save to the cache location
|
// Save to the cache location
|
||||||
using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
|
using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
|
||||||
{
|
{
|
||||||
|
@ -323,7 +329,7 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the size of the image.
|
/// Gets the size of the image.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -335,25 +341,53 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
// Now check the file system cache
|
// Now check the file system cache
|
||||||
var fullCachePath = ImageSizeCache.GetResourcePath(keyName, ".txt");
|
var fullCachePath = ImageSizeCache.GetResourcePath(keyName, ".txt");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = File.ReadAllText(fullCachePath).Split('|').Select(i => double.Parse(i, UsCulture)).ToArray();
|
||||||
|
|
||||||
|
return new ImageSize { Width = result[0], Height = result[1] };
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// Cache file doesn't exist or is currently being written to
|
||||||
|
}
|
||||||
|
|
||||||
var semaphore = GetLock(fullCachePath);
|
var semaphore = GetLock(fullCachePath);
|
||||||
|
|
||||||
await semaphore.WaitAsync().ConfigureAwait(false);
|
await semaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
try
|
var result = File.ReadAllText(fullCachePath).Split('|').Select(i => double.Parse(i, UsCulture)).ToArray();
|
||||||
{
|
|
||||||
var result = File.ReadAllText(fullCachePath).Split('|').Select(i => double.Parse(i, UsCulture)).ToArray();
|
|
||||||
|
|
||||||
return new ImageSize { Width = result[0], Height = result[1] };
|
return new ImageSize { Width = result[0], Height = result[1] };
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
// Cache file doesn't exist no biggie
|
// Cache file doesn't exist no biggie
|
||||||
}
|
}
|
||||||
|
catch (DirectoryNotFoundException)
|
||||||
|
{
|
||||||
|
// Cache file doesn't exist no biggie
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
var size = await ImageHeader.GetDimensions(imagePath, _logger).ConfigureAwait(false);
|
var size = await ImageHeader.GetDimensions(imagePath, _logger).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var parentPath = Path.GetDirectoryName(fullCachePath);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
// Update the file system cache
|
// Update the file system cache
|
||||||
File.WriteAllText(fullCachePath, size.Width.ToString(UsCulture) + @"|" + size.Height.ToString(UsCulture));
|
File.WriteAllText(fullCachePath, size.Width.ToString(UsCulture) + @"|" + size.Height.ToString(UsCulture));
|
||||||
|
|
||||||
|
@ -490,12 +524,12 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
await semaphore.WaitAsync().ConfigureAwait(false);
|
await semaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
// Check again in case of contention
|
// Check again in case of contention
|
||||||
if (CroppedImageCache.ContainsFilePath(croppedImagePath))
|
if (File.Exists(croppedImagePath))
|
||||||
{
|
{
|
||||||
semaphore.Release();
|
semaphore.Release();
|
||||||
return croppedImagePath;
|
return croppedImagePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var fileStream = new FileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true))
|
using (var fileStream = new FileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true))
|
||||||
|
@ -511,6 +545,13 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
|
|
||||||
using (var croppedImage = originalImage.CropWhitespace())
|
using (var croppedImage = originalImage.CropWhitespace())
|
||||||
{
|
{
|
||||||
|
var parentPath = Path.GetDirectoryName(croppedImagePath);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
using (var outputStream = new FileStream(croppedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
using (var outputStream = new FileStream(croppedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||||
{
|
{
|
||||||
croppedImage.Save(outputFormat, outputStream, 100);
|
croppedImage.Save(outputFormat, outputStream, 100);
|
||||||
|
@ -568,7 +609,7 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
await semaphore.WaitAsync().ConfigureAwait(false);
|
await semaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
// Check again in case of contention
|
// Check again in case of contention
|
||||||
if (EnhancedImageCache.ContainsFilePath(enhancedImagePath))
|
if (File.Exists(enhancedImagePath))
|
||||||
{
|
{
|
||||||
semaphore.Release();
|
semaphore.Release();
|
||||||
return enhancedImagePath;
|
return enhancedImagePath;
|
||||||
|
@ -588,6 +629,13 @@ namespace MediaBrowser.Controller.Drawing
|
||||||
//Pass the image through registered enhancers
|
//Pass the image through registered enhancers
|
||||||
using (var newImage = await ExecuteImageEnhancers(supportedEnhancers, originalImage, item, imageType, imageIndex).ConfigureAwait(false))
|
using (var newImage = await ExecuteImageEnhancers(supportedEnhancers, originalImage, item, imageType, imageIndex).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
var parentDirectory = Path.GetDirectoryName(enhancedImagePath);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
//And then save it in the cache
|
//And then save it in the cache
|
||||||
using (var outputStream = new FileStream(enhancedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
using (var outputStream = new FileStream(enhancedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||||
{
|
{
|
||||||
|
|
|
@ -304,7 +304,6 @@ namespace MediaBrowser.Controller.Entities
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// We attach these to the item so that we only ever have to hit the file system once
|
/// We attach these to the item so that we only ever have to hit the file system once
|
||||||
/// (this includes the children of the containing folder)
|
/// (this includes the children of the containing folder)
|
||||||
/// Use ResolveArgs.FileSystemDictionary to check for the existence of files instead of File.Exists
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The resolve args.</value>
|
/// <value>The resolve args.</value>
|
||||||
[IgnoreDataMember]
|
[IgnoreDataMember]
|
||||||
|
|
|
@ -19,10 +19,6 @@ namespace MediaBrowser.Controller.Entities
|
||||||
public static IUserManager UserManager { get; set; }
|
public static IUserManager UserManager { get; set; }
|
||||||
public static IXmlSerializer XmlSerializer { get; set; }
|
public static IXmlSerializer XmlSerializer { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _root folder path
|
|
||||||
/// </summary>
|
|
||||||
private string _rootFolderPath;
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the root folder path.
|
/// Gets the root folder path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -32,23 +28,19 @@ namespace MediaBrowser.Controller.Entities
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_rootFolderPath == null)
|
if (Configuration.UseCustomLibrary)
|
||||||
{
|
{
|
||||||
if (Configuration.UseCustomLibrary)
|
var rootFolderPath = GetRootFolderPath(Name);
|
||||||
{
|
|
||||||
_rootFolderPath = GetRootFolderPath(Name);
|
|
||||||
|
|
||||||
if (!Directory.Exists(_rootFolderPath))
|
if (!Directory.Exists(rootFolderPath))
|
||||||
{
|
|
||||||
Directory.CreateDirectory(_rootFolderPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
_rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
|
Directory.CreateDirectory(rootFolderPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return rootFolderPath;
|
||||||
}
|
}
|
||||||
return _rootFolderPath;
|
|
||||||
|
return ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,7 +253,6 @@ namespace MediaBrowser.Controller.Entities
|
||||||
|
|
||||||
// Force these to be lazy loaded again
|
// Force these to be lazy loaded again
|
||||||
_configurationDirectoryPath = null;
|
_configurationDirectoryPath = null;
|
||||||
_rootFolderPath = null;
|
|
||||||
RootFolder = null;
|
RootFolder = null;
|
||||||
|
|
||||||
// Kick off a task to validate the media library
|
// Kick off a task to validate the media library
|
||||||
|
@ -378,7 +369,6 @@ namespace MediaBrowser.Controller.Entities
|
||||||
// Force these to be lazy loaded again
|
// Force these to be lazy loaded again
|
||||||
if (customLibraryChanged)
|
if (customLibraryChanged)
|
||||||
{
|
{
|
||||||
_rootFolderPath = null;
|
|
||||||
RootFolder = null;
|
RootFolder = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,10 +55,6 @@ namespace MediaBrowser.Controller.MediaInfo
|
||||||
SubtitleCache = new FileSystemRepository(SubtitleCachePath);
|
SubtitleCache = new FileSystemRepository(SubtitleCachePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _video images data path
|
|
||||||
/// </summary>
|
|
||||||
private string _videoImagesDataPath;
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the video images data path.
|
/// Gets the video images data path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -67,24 +63,10 @@ namespace MediaBrowser.Controller.MediaInfo
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_videoImagesDataPath == null)
|
return Path.Combine(_appPaths.DataPath, "extracted-video-images");
|
||||||
{
|
|
||||||
_videoImagesDataPath = Path.Combine(_appPaths.DataPath, "extracted-video-images");
|
|
||||||
|
|
||||||
if (!Directory.Exists(_videoImagesDataPath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(_videoImagesDataPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _videoImagesDataPath;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _audio images data path
|
|
||||||
/// </summary>
|
|
||||||
private string _audioImagesDataPath;
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the audio images data path.
|
/// Gets the audio images data path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -93,24 +75,10 @@ namespace MediaBrowser.Controller.MediaInfo
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_audioImagesDataPath == null)
|
return Path.Combine(_appPaths.DataPath, "extracted-audio-images");
|
||||||
{
|
|
||||||
_audioImagesDataPath = Path.Combine(_appPaths.DataPath, "extracted-audio-images");
|
|
||||||
|
|
||||||
if (!Directory.Exists(_audioImagesDataPath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(_audioImagesDataPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _audioImagesDataPath;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _subtitle cache path
|
|
||||||
/// </summary>
|
|
||||||
private string _subtitleCachePath;
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the subtitle cache path.
|
/// Gets the subtitle cache path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -119,17 +87,7 @@ namespace MediaBrowser.Controller.MediaInfo
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_subtitleCachePath == null)
|
return Path.Combine(_appPaths.CachePath, "subtitles");
|
||||||
{
|
|
||||||
_subtitleCachePath = Path.Combine(_appPaths.CachePath, "subtitles");
|
|
||||||
|
|
||||||
if (!Directory.Exists(_subtitleCachePath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(_subtitleCachePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _subtitleCachePath;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +135,7 @@ namespace MediaBrowser.Controller.MediaInfo
|
||||||
|
|
||||||
var path = VideoImageCache.GetResourcePath(filename, ".jpg");
|
var path = VideoImageCache.GetResourcePath(filename, ".jpg");
|
||||||
|
|
||||||
if (!VideoImageCache.ContainsFilePath(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
if (extractImages)
|
if (extractImages)
|
||||||
{
|
{
|
||||||
|
@ -204,6 +162,13 @@ namespace MediaBrowser.Controller.MediaInfo
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var parentPath = Path.GetDirectoryName(path);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
await _encoder.ExtractImage(inputPath, type, time, path, cancellationToken).ConfigureAwait(false);
|
await _encoder.ExtractImage(inputPath, type, time, path, cancellationToken).ConfigureAwait(false);
|
||||||
chapter.ImagePath = path;
|
chapter.ImagePath = path;
|
||||||
changesMade = true;
|
changesMade = true;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using MediaBrowser.Common.IO;
|
using System.IO;
|
||||||
|
using MediaBrowser.Common.IO;
|
||||||
using MediaBrowser.Common.MediaInfo;
|
using MediaBrowser.Common.MediaInfo;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
@ -156,7 +157,7 @@ namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||||
|
|
||||||
var path = ImageCache.GetResourcePath(filename + "_primary", ".jpg");
|
var path = ImageCache.GetResourcePath(filename + "_primary", ".jpg");
|
||||||
|
|
||||||
if (!ImageCache.ContainsFilePath(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
var semaphore = GetLock(path);
|
var semaphore = GetLock(path);
|
||||||
|
|
||||||
|
@ -164,10 +165,17 @@ namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// Check again
|
// Check again
|
||||||
if (!ImageCache.ContainsFilePath(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var parentPath = Path.GetDirectoryName(path);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
await _mediaEncoder.ExtractImage(new[] { item.Path }, InputType.AudioFile, null, path, cancellationToken).ConfigureAwait(false);
|
await _mediaEncoder.ExtractImage(new[] { item.Path }, InputType.AudioFile, null, path, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
@ -244,6 +244,9 @@
|
||||||
<Compile Include="..\MediaBrowser.Model\Querying\PersonsQuery.cs">
|
<Compile Include="..\MediaBrowser.Model\Querying\PersonsQuery.cs">
|
||||||
<Link>Querying\PersonsQuery.cs</Link>
|
<Link>Querying\PersonsQuery.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="..\MediaBrowser.Model\Querying\SimilarItemsQuery.cs">
|
||||||
|
<Link>Querying\SimilarItemsQuery.cs</Link>
|
||||||
|
</Compile>
|
||||||
<Compile Include="..\MediaBrowser.Model\Querying\ThemeSongsResult.cs">
|
<Compile Include="..\MediaBrowser.Model\Querying\ThemeSongsResult.cs">
|
||||||
<Link>Querying\ThemeSongsResult.cs</Link>
|
<Link>Querying\ThemeSongsResult.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
|
|
@ -72,6 +72,34 @@ namespace MediaBrowser.Model.ApiClient
|
||||||
/// <exception cref="ArgumentNullException">query</exception>
|
/// <exception cref="ArgumentNullException">query</exception>
|
||||||
Task<ItemsResult> GetItemsAsync(ItemQuery query);
|
Task<ItemsResult> GetItemsAsync(ItemQuery query);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the similar movies async.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The query.</param>
|
||||||
|
/// <returns>Task{ItemsResult}.</returns>
|
||||||
|
Task<ItemsResult> GetSimilarMoviesAsync(SimilarItemsQuery query);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the similar series async.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The query.</param>
|
||||||
|
/// <returns>Task{ItemsResult}.</returns>
|
||||||
|
Task<ItemsResult> GetSimilarSeriesAsync(SimilarItemsQuery query);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the similar albums async.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The query.</param>
|
||||||
|
/// <returns>Task{ItemsResult}.</returns>
|
||||||
|
Task<ItemsResult> GetSimilarAlbumsAsync(SimilarItemsQuery query);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the similar games async.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The query.</param>
|
||||||
|
/// <returns>Task{ItemsResult}.</returns>
|
||||||
|
Task<ItemsResult> GetSimilarGamesAsync(SimilarItemsQuery query);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the people async.
|
/// Gets the people async.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
<Compile Include="Querying\ItemReviewsResult.cs" />
|
<Compile Include="Querying\ItemReviewsResult.cs" />
|
||||||
<Compile Include="Querying\ItemsByNameQuery.cs" />
|
<Compile Include="Querying\ItemsByNameQuery.cs" />
|
||||||
<Compile Include="Entities\BaseItemInfo.cs" />
|
<Compile Include="Entities\BaseItemInfo.cs" />
|
||||||
|
<Compile Include="Querying\SimilarItemsQuery.cs" />
|
||||||
<Compile Include="Session\BrowseRequest.cs" />
|
<Compile Include="Session\BrowseRequest.cs" />
|
||||||
<Compile Include="Session\PlayRequest.cs" />
|
<Compile Include="Session\PlayRequest.cs" />
|
||||||
<Compile Include="Session\PlaystateRequest.cs" />
|
<Compile Include="Session\PlaystateRequest.cs" />
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
namespace MediaBrowser.Model.Querying
|
||||||
|
{
|
||||||
|
public class SimilarItemsQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user to localize search results for
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The user id.</value>
|
||||||
|
public string UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the id.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The id.</value>
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum number of items to return
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The limit.</value>
|
||||||
|
public int? Limit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fields to return within the items, in addition to basic information
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The fields.</value>
|
||||||
|
public ItemFields[] Fields { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -94,29 +94,20 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _media tools path
|
/// Gets the media tools path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string _mediaToolsPath;
|
/// <param name="create">if set to <c>true</c> [create].</param>
|
||||||
/// <summary>
|
/// <returns>System.String.</returns>
|
||||||
/// Gets the folder path to tools
|
private string GetMediaToolsPath(bool create)
|
||||||
/// </summary>
|
|
||||||
/// <value>The media tools path.</value>
|
|
||||||
private string MediaToolsPath
|
|
||||||
{
|
{
|
||||||
get
|
var path = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
|
||||||
|
|
||||||
|
if (create && !Directory.Exists(path))
|
||||||
{
|
{
|
||||||
if (_mediaToolsPath == null)
|
Directory.CreateDirectory(path);
|
||||||
{
|
|
||||||
_mediaToolsPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
|
|
||||||
|
|
||||||
if (!Directory.Exists(_mediaToolsPath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(_mediaToolsPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _mediaToolsPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -185,7 +176,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
||||||
|
|
||||||
var filename = resource.Substring(resource.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) + prefix.Length);
|
var filename = resource.Substring(resource.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) + prefix.Length);
|
||||||
|
|
||||||
var versionedDirectoryPath = Path.Combine(MediaToolsPath, Path.GetFileNameWithoutExtension(filename));
|
var versionedDirectoryPath = Path.Combine(GetMediaToolsPath(true), Path.GetFileNameWithoutExtension(filename));
|
||||||
|
|
||||||
if (!Directory.Exists(versionedDirectoryPath))
|
if (!Directory.Exists(versionedDirectoryPath))
|
||||||
{
|
{
|
||||||
|
@ -570,14 +561,14 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
||||||
}
|
}
|
||||||
|
|
||||||
var offsetParam = offset.Ticks > 0 ? "-ss " + offset.TotalSeconds + " " : string.Empty;
|
var offsetParam = offset.Ticks > 0 ? "-ss " + offset.TotalSeconds + " " : string.Empty;
|
||||||
|
|
||||||
var process = new Process
|
var process = new Process
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
StartInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
RedirectStandardOutput = false,
|
RedirectStandardOutput = false,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
|
|
||||||
CreateNoWindow = true,
|
CreateNoWindow = true,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
FileName = FFMpegPath,
|
FileName = FFMpegPath,
|
||||||
|
@ -744,7 +735,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
||||||
|
|
||||||
RedirectStandardOutput = false,
|
RedirectStandardOutput = false,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
|
|
||||||
FileName = FFMpegPath,
|
FileName = FFMpegPath,
|
||||||
Arguments = string.Format("{0}-i {1} -map 0:{2} -an -vn -c:s ass \"{3}\"", offsetParam, inputPath, subtitleStreamIndex, outputPath),
|
Arguments = string.Format("{0}-i {1} -map 0:{2} -an -vn -c:s ass \"{3}\"", offsetParam, inputPath, subtitleStreamIndex, outputPath),
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
|
@ -759,7 +750,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
||||||
var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-extract-" + Guid.NewGuid() + ".txt");
|
var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-extract-" + Guid.NewGuid() + ".txt");
|
||||||
|
|
||||||
var logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous);
|
var logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|
|
@ -328,9 +328,7 @@ namespace MediaBrowser.Server.Implementations.Providers
|
||||||
public async Task<string> SaveImage(BaseItem item, Stream source, string targetName, bool saveLocally, CancellationToken cancellationToken)
|
public async Task<string> SaveImage(BaseItem item, Stream source, string targetName, bool saveLocally, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
//download and save locally
|
//download and save locally
|
||||||
var localPath = (saveLocally && item.MetaLocation != null) ?
|
var localPath = GetSavePath(item, targetName, saveLocally);
|
||||||
Path.Combine(item.MetaLocation, targetName) :
|
|
||||||
_remoteImageCache.GetResourcePath(item.GetType().FullName + item.Path.ToLower(), targetName);
|
|
||||||
|
|
||||||
if (saveLocally) // queue to media directories
|
if (saveLocally) // queue to media directories
|
||||||
{
|
{
|
||||||
|
@ -374,9 +372,18 @@ namespace MediaBrowser.Server.Implementations.Providers
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
public string GetSavePath(BaseItem item, string targetFileName, bool saveLocally)
|
public string GetSavePath(BaseItem item, string targetFileName, bool saveLocally)
|
||||||
{
|
{
|
||||||
return (saveLocally && item.MetaLocation != null) ?
|
var path = (saveLocally && item.MetaLocation != null) ?
|
||||||
Path.Combine(item.MetaLocation, targetFileName) :
|
Path.Combine(item.MetaLocation, targetFileName) :
|
||||||
_remoteImageCache.GetResourcePath(item.GetType().FullName + item.Id.ToString(), targetFileName);
|
_remoteImageCache.GetResourcePath(item.GetType().FullName + item.Id.ToString(), targetFileName);
|
||||||
|
|
||||||
|
var parentPath = Path.GetDirectoryName(path);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -168,6 +168,14 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
previouslyFailedImages.Add(key);
|
previouslyFailedImages.Add(key);
|
||||||
|
|
||||||
|
var parentPath = Path.GetDirectoryName(failHistoryPath);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
_jsonSerializer.SerializeToFile(previouslyFailedImages, failHistoryPath);
|
_jsonSerializer.SerializeToFile(previouslyFailedImages, failHistoryPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -165,7 +165,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
||||||
var specialFeattures = _itemRepo.GetItems(movie.SpecialFeatureIds).ToList();
|
var specialFeattures = _itemRepo.GetItems(movie.SpecialFeatureIds).ToList();
|
||||||
images = specialFeattures.Aggregate(images, (current, subItem) => current.Concat(GetPathsInUse(subItem)));
|
images = specialFeattures.Aggregate(images, (current, subItem) => current.Concat(GetPathsInUse(subItem)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,13 +176,20 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
||||||
/// <returns>IEnumerable{System.String}.</returns>
|
/// <returns>IEnumerable{System.String}.</returns>
|
||||||
private IEnumerable<string> GetFiles(string path)
|
private IEnumerable<string> GetFiles(string path)
|
||||||
{
|
{
|
||||||
return Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
|
try
|
||||||
.Where(i =>
|
{
|
||||||
{
|
return Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
|
||||||
var ext = Path.GetExtension(i);
|
.Where(i =>
|
||||||
|
{
|
||||||
|
var ext = Path.GetExtension(i);
|
||||||
|
|
||||||
return !string.IsNullOrEmpty(ext) && BaseItem.SupportedImageExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
|
return !string.IsNullOrEmpty(ext) && BaseItem.SupportedImageExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
catch (DirectoryNotFoundException)
|
||||||
|
{
|
||||||
|
return new string[] { };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -8,14 +8,15 @@ using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using MediaBrowser.Controller.Providers.MediaInfo;
|
using MediaBrowser.Controller.Providers.MediaInfo;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
using MediaBrowser.Model.Logging;
|
||||||
|
using MoreLinq;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Model.Logging;
|
|
||||||
using MoreLinq;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
||||||
{
|
{
|
||||||
|
@ -263,7 +264,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
||||||
|
|
||||||
var path = ImageCache.GetResourcePath(filename, ".jpg");
|
var path = ImageCache.GetResourcePath(filename, ".jpg");
|
||||||
|
|
||||||
if (!ImageCache.ContainsFilePath(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
var semaphore = GetLock(path);
|
var semaphore = GetLock(path);
|
||||||
|
|
||||||
|
@ -271,10 +272,17 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
||||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// Check again
|
// Check again
|
||||||
if (!ImageCache.ContainsFilePath(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var parentPath = Path.GetDirectoryName(path);
|
||||||
|
|
||||||
|
if (!Directory.Exists(parentPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
await ExtractImageInternal(item, path, cancellationToken).ConfigureAwait(false);
|
await ExtractImageInternal(item, path, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
@ -471,20 +471,18 @@ namespace MediaBrowser.Server.Implementations.Sqlite
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the critic reviews path.
|
/// Gets the critic reviews path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The critic reviews path.</value>
|
/// <param name="create">if set to <c>true</c> [create].</param>
|
||||||
private string CriticReviewsPath
|
/// <returns>System.String.</returns>
|
||||||
|
private string GetCriticReviewsPath(bool create)
|
||||||
{
|
{
|
||||||
get
|
var path = Path.Combine(_appPaths.DataPath, "critic-reviews");
|
||||||
|
|
||||||
|
if (create && !Directory.Exists(path))
|
||||||
{
|
{
|
||||||
var path = Path.Combine(_appPaths.DataPath, "critic-reviews");
|
Directory.CreateDirectory(path);
|
||||||
|
|
||||||
if (!Directory.Exists(path))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -499,10 +497,14 @@ namespace MediaBrowser.Server.Implementations.Sqlite
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var path = Path.Combine(CriticReviewsPath, itemId + ".json");
|
var path = Path.Combine(GetCriticReviewsPath(false), itemId + ".json");
|
||||||
|
|
||||||
return _jsonSerializer.DeserializeFromFile<List<ItemReview>>(path);
|
return _jsonSerializer.DeserializeFromFile<List<ItemReview>>(path);
|
||||||
}
|
}
|
||||||
|
catch (DirectoryNotFoundException)
|
||||||
|
{
|
||||||
|
return new List<ItemReview>();
|
||||||
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
return new List<ItemReview>();
|
return new List<ItemReview>();
|
||||||
|
@ -521,7 +523,7 @@ namespace MediaBrowser.Server.Implementations.Sqlite
|
||||||
{
|
{
|
||||||
return Task.Run(() =>
|
return Task.Run(() =>
|
||||||
{
|
{
|
||||||
var path = Path.Combine(CriticReviewsPath, itemId + ".json");
|
var path = Path.Combine(GetCriticReviewsPath(true), itemId + ".json");
|
||||||
|
|
||||||
_jsonSerializer.SerializeToFile(criticReviews.ToList(), path);
|
_jsonSerializer.SerializeToFile(criticReviews.ToList(), path);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
||||||
<metadata>
|
<metadata>
|
||||||
<id>MediaBrowser.Common.Internal</id>
|
<id>MediaBrowser.Common.Internal</id>
|
||||||
<version>3.0.112</version>
|
<version>3.0.113</version>
|
||||||
<title>MediaBrowser.Common.Internal</title>
|
<title>MediaBrowser.Common.Internal</title>
|
||||||
<authors>Luke</authors>
|
<authors>Luke</authors>
|
||||||
<owners>ebr,Luke,scottisafool</owners>
|
<owners>ebr,Luke,scottisafool</owners>
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
<description>Contains common components shared by Media Browser Theatre and Media Browser Server. Not intended for plugin developer consumption.</description>
|
<description>Contains common components shared by Media Browser Theatre and Media Browser Server. Not intended for plugin developer consumption.</description>
|
||||||
<copyright>Copyright © Media Browser 2013</copyright>
|
<copyright>Copyright © Media Browser 2013</copyright>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency id="MediaBrowser.Common" version="3.0.112" />
|
<dependency id="MediaBrowser.Common" version="3.0.113" />
|
||||||
<dependency id="NLog" version="2.0.1.2" />
|
<dependency id="NLog" version="2.0.1.2" />
|
||||||
<dependency id="ServiceStack.Text" version="3.9.45" />
|
<dependency id="ServiceStack.Text" version="3.9.45" />
|
||||||
<dependency id="SimpleInjector" version="2.2.3" />
|
<dependency id="SimpleInjector" version="2.2.3" />
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
|
||||||
<metadata>
|
<metadata>
|
||||||
<id>MediaBrowser.Common</id>
|
<id>MediaBrowser.Common</id>
|
||||||
<version>3.0.112</version>
|
<version>3.0.113</version>
|
||||||
<title>MediaBrowser.Common</title>
|
<title>MediaBrowser.Common</title>
|
||||||
<authors>Media Browser Team</authors>
|
<authors>Media Browser Team</authors>
|
||||||
<owners>ebr,Luke,scottisafool</owners>
|
<owners>ebr,Luke,scottisafool</owners>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||||
<metadata>
|
<metadata>
|
||||||
<id>MediaBrowser.Server.Core</id>
|
<id>MediaBrowser.Server.Core</id>
|
||||||
<version>3.0.112</version>
|
<version>3.0.113</version>
|
||||||
<title>Media Browser.Server.Core</title>
|
<title>Media Browser.Server.Core</title>
|
||||||
<authors>Media Browser Team</authors>
|
<authors>Media Browser Team</authors>
|
||||||
<owners>ebr,Luke,scottisafool</owners>
|
<owners>ebr,Luke,scottisafool</owners>
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
<description>Contains core components required to build plugins for Media Browser Server.</description>
|
<description>Contains core components required to build plugins for Media Browser Server.</description>
|
||||||
<copyright>Copyright © Media Browser 2013</copyright>
|
<copyright>Copyright © Media Browser 2013</copyright>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency id="MediaBrowser.Common" version="3.0.112" />
|
<dependency id="MediaBrowser.Common" version="3.0.113" />
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</metadata>
|
</metadata>
|
||||||
<files>
|
<files>
|
||||||
|
|
Loading…
Reference in New Issue