mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-22 23:50:24 +01:00
Pushing missing changes
This commit is contained in:
@@ -1,81 +0,0 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
public static class DrawingUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Resizes a set of dimensions
|
||||
/// </summary>
|
||||
public static Size Resize(int currentWidth, int currentHeight, int? width, int? height, int? maxWidth, int? maxHeight)
|
||||
{
|
||||
return Resize(new Size(currentWidth, currentHeight), width, height, maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes a set of dimensions
|
||||
/// </summary>
|
||||
/// <param name="size">The original size object</param>
|
||||
/// <param name="width">A new fixed width, if desired</param>
|
||||
/// <param name="height">A new fixed neight, if desired</param>
|
||||
/// <param name="maxWidth">A max fixed width, if desired</param>
|
||||
/// <param name="maxHeight">A max fixed height, if desired</param>
|
||||
/// <returns>A new size object</returns>
|
||||
public static Size Resize(Size size, int? width, int? height, int? maxWidth, int? maxHeight)
|
||||
{
|
||||
decimal newWidth = size.Width;
|
||||
decimal newHeight = size.Height;
|
||||
|
||||
if (width.HasValue && height.HasValue)
|
||||
{
|
||||
newWidth = width.Value;
|
||||
newHeight = height.Value;
|
||||
}
|
||||
|
||||
else if (height.HasValue)
|
||||
{
|
||||
newWidth = GetNewWidth(newHeight, newWidth, height.Value);
|
||||
newHeight = height.Value;
|
||||
}
|
||||
|
||||
else if (width.HasValue)
|
||||
{
|
||||
newHeight = GetNewHeight(newHeight, newWidth, width.Value);
|
||||
newWidth = width.Value;
|
||||
}
|
||||
|
||||
if (maxHeight.HasValue && maxHeight < newHeight)
|
||||
{
|
||||
newWidth = GetNewWidth(newHeight, newWidth, maxHeight.Value);
|
||||
newHeight = maxHeight.Value;
|
||||
}
|
||||
|
||||
if (maxWidth.HasValue && maxWidth < newWidth)
|
||||
{
|
||||
newHeight = GetNewHeight(newHeight, newWidth, maxWidth.Value);
|
||||
newWidth = maxWidth.Value;
|
||||
}
|
||||
|
||||
return new Size(Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
|
||||
}
|
||||
|
||||
private static decimal GetNewWidth(decimal currentHeight, decimal currentWidth, int newHeight)
|
||||
{
|
||||
decimal scaleFactor = newHeight;
|
||||
scaleFactor /= currentHeight;
|
||||
scaleFactor *= currentWidth;
|
||||
|
||||
return scaleFactor;
|
||||
}
|
||||
|
||||
private static decimal GetNewHeight(decimal currentHeight, decimal currentWidth, int newWidth)
|
||||
{
|
||||
decimal scaleFactor = newWidth;
|
||||
scaleFactor /= currentWidth;
|
||||
scaleFactor *= currentHeight;
|
||||
|
||||
return scaleFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
600
MediaBrowser.Controller/Drawing/ImageManager.cs
Normal file
600
MediaBrowser.Controller/Drawing/ImageManager.cs
Normal file
@@ -0,0 +1,600 @@
|
||||
using MediaBrowser.Common.Drawing;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Kernel;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ImageManager
|
||||
/// </summary>
|
||||
public class ImageManager : BaseManager<Kernel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the image size cache.
|
||||
/// </summary>
|
||||
/// <value>The image size cache.</value>
|
||||
private FileSystemRepository ImageSizeCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resized image cache.
|
||||
/// </summary>
|
||||
/// <value>The resized image cache.</value>
|
||||
private FileSystemRepository ResizedImageCache { get; set; }
|
||||
/// <summary>
|
||||
/// Gets the cropped image cache.
|
||||
/// </summary>
|
||||
/// <value>The cropped image cache.</value>
|
||||
private FileSystemRepository CroppedImageCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cropped image cache.
|
||||
/// </summary>
|
||||
/// <value>The cropped image cache.</value>
|
||||
private FileSystemRepository EnhancedImageCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cached imaged sizes
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, Task<ImageSize>> _cachedImagedSizes = new ConcurrentDictionary<string, Task<ImageSize>>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageManager" /> class.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The kernel.</param>
|
||||
public ImageManager(Kernel kernel)
|
||||
: base(kernel)
|
||||
{
|
||||
ImageSizeCache = new FileSystemRepository(Path.Combine(Kernel.ApplicationPaths.ImageCachePath, "image-sizes"));
|
||||
ResizedImageCache = new FileSystemRepository(Path.Combine(Kernel.ApplicationPaths.ImageCachePath, "resized-images"));
|
||||
CroppedImageCache = new FileSystemRepository(Path.Combine(Kernel.ApplicationPaths.ImageCachePath, "cropped-images"));
|
||||
EnhancedImageCache = new FileSystemRepository(Path.Combine(Kernel.ApplicationPaths.ImageCachePath, "enhanced-images"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes an image by resizing to target dimensions
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity that owns the image</param>
|
||||
/// <param name="imageType">The image type</param>
|
||||
/// <param name="imageIndex">The image index (currently only used with backdrops)</param>
|
||||
/// <param name="cropWhitespace">if set to <c>true</c> [crop whitespace].</param>
|
||||
/// <param name="dateModified">The last date modified of the original image file</param>
|
||||
/// <param name="toStream">The stream to save the new image to</param>
|
||||
/// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
|
||||
/// <param name="height">Use if a fixed height 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="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <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)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
throw new ArgumentNullException("entity");
|
||||
}
|
||||
|
||||
if (toStream == null)
|
||||
{
|
||||
throw new ArgumentNullException("toStream");
|
||||
}
|
||||
|
||||
var originalImagePath = GetImagePath(entity, imageType, imageIndex);
|
||||
|
||||
if (cropWhitespace)
|
||||
{
|
||||
try
|
||||
{
|
||||
originalImagePath = GetCroppedImage(originalImagePath, dateModified);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// We have to have a catch-all here because some of the .net image methods throw a plain old Exception
|
||||
Logger.ErrorException("Error cropping image", ex);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Enhance if we have enhancers
|
||||
var ehnancedImagePath = await GetEnhancedImage(originalImagePath, dateModified, entity, imageType, imageIndex).ConfigureAwait(false);
|
||||
|
||||
// If the path changed update dateModified
|
||||
if (!ehnancedImagePath.Equals(originalImagePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
dateModified = File.GetLastWriteTimeUtc(ehnancedImagePath);
|
||||
originalImagePath = ehnancedImagePath;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Error("Error enhancing image");
|
||||
}
|
||||
|
||||
var originalImageSize = await GetImageSize(originalImagePath, dateModified).ConfigureAwait(false);
|
||||
|
||||
// Determine the output size based on incoming parameters
|
||||
var newSize = DrawingUtils.Resize(originalImageSize, width, height, maxWidth, maxHeight);
|
||||
|
||||
if (!quality.HasValue)
|
||||
{
|
||||
quality = 90;
|
||||
}
|
||||
|
||||
var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality.Value, dateModified);
|
||||
|
||||
// Grab the cache file if it already exists
|
||||
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 (FileNotFoundException)
|
||||
{
|
||||
// Cache file doesn't exist. No biggie.
|
||||
}
|
||||
|
||||
using (var fileStream = File.OpenRead(originalImagePath))
|
||||
{
|
||||
using (var originalImage = Bitmap.FromStream(fileStream, true, false))
|
||||
{
|
||||
var newWidth = Convert.ToInt32(newSize.Width);
|
||||
var newHeight = Convert.ToInt32(newSize.Height);
|
||||
|
||||
// Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
|
||||
var thumbnail = originalImage.PixelFormat.HasFlag(PixelFormat.Indexed) ? new Bitmap(originalImage, newWidth, newHeight) : new Bitmap(newWidth, newHeight, originalImage.PixelFormat);
|
||||
|
||||
// Preserve the original resolution
|
||||
thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
|
||||
|
||||
var thumbnailGraph = Graphics.FromImage(thumbnail);
|
||||
|
||||
thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
|
||||
thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
|
||||
thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
|
||||
|
||||
thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight);
|
||||
|
||||
var outputFormat = originalImage.RawFormat;
|
||||
|
||||
using (var memoryStream = new MemoryStream { })
|
||||
{
|
||||
// Save to the memory stream
|
||||
thumbnail.Save(outputFormat, memoryStream, quality.Value);
|
||||
|
||||
var bytes = memoryStream.ToArray();
|
||||
|
||||
var outputTask = Task.Run(async () => await toStream.WriteAsync(bytes, 0, bytes.Length));
|
||||
|
||||
// Save to the cache location
|
||||
using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
|
||||
{
|
||||
// Save to the filestream
|
||||
await cacheFileStream.WriteAsync(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
await outputTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
thumbnailGraph.Dispose();
|
||||
thumbnail.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cache file path based on a set of parameters
|
||||
/// </summary>
|
||||
/// <param name="originalPath">The path to the original image file</param>
|
||||
/// <param name="outputSize">The size to output the image in</param>
|
||||
/// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
|
||||
/// <param name="dateModified">The last modified date of the image</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified)
|
||||
{
|
||||
var filename = originalPath;
|
||||
|
||||
filename += "width=" + outputSize.Width;
|
||||
|
||||
filename += "height=" + outputSize.Height;
|
||||
|
||||
filename += "quality=" + quality;
|
||||
|
||||
filename += "datemodified=" + dateModified.Ticks;
|
||||
|
||||
return ResizedImageCache.GetResourcePath(filename, Path.GetExtension(originalPath));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets image dimensions
|
||||
/// </summary>
|
||||
/// <param name="imagePath">The image path.</param>
|
||||
/// <param name="dateModified">The date modified.</param>
|
||||
/// <returns>Task{ImageSize}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">imagePath</exception>
|
||||
public Task<ImageSize> GetImageSize(string imagePath, DateTime dateModified)
|
||||
{
|
||||
if (string.IsNullOrEmpty(imagePath))
|
||||
{
|
||||
throw new ArgumentNullException("imagePath");
|
||||
}
|
||||
|
||||
var name = imagePath + "datemodified=" + dateModified.Ticks;
|
||||
|
||||
return _cachedImagedSizes.GetOrAdd(name, keyName => GetImageSizeTask(keyName, imagePath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets cached image dimensions, or results null if non-existant
|
||||
/// </summary>
|
||||
/// <param name="keyName">Name of the key.</param>
|
||||
/// <param name="imagePath">The image path.</param>
|
||||
/// <returns>Task{ImageSize}.</returns>
|
||||
private Task<ImageSize> GetImageSizeTask(string keyName, string imagePath)
|
||||
{
|
||||
return Task.Run(() => GetImageSize(keyName, imagePath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the image.
|
||||
/// </summary>
|
||||
/// <param name="keyName">Name of the key.</param>
|
||||
/// <param name="imagePath">The image path.</param>
|
||||
/// <returns>ImageSize.</returns>
|
||||
private ImageSize GetImageSize(string keyName, string imagePath)
|
||||
{
|
||||
// Now check the file system cache
|
||||
var fullCachePath = ImageSizeCache.GetResourcePath(keyName, ".pb");
|
||||
|
||||
try
|
||||
{
|
||||
var result = Kernel.ProtobufSerializer.DeserializeFromFile<int[]>(fullCachePath);
|
||||
|
||||
return new ImageSize { Width = result[0], Height = result[1] };
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// Cache file doesn't exist no biggie
|
||||
}
|
||||
|
||||
var size = ImageHeader.GetDimensions(imagePath);
|
||||
|
||||
var imageSize = new ImageSize { Width = size.Width, Height = size.Height };
|
||||
|
||||
// Update the file system cache
|
||||
CacheImageSize(fullCachePath, size.Width, size.Height);
|
||||
|
||||
return imageSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches image dimensions
|
||||
/// </summary>
|
||||
/// <param name="cachePath">The cache path.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <param name="height">The height.</param>
|
||||
private void CacheImageSize(string cachePath, int width, int height)
|
||||
{
|
||||
var output = new[] { width, height };
|
||||
|
||||
Kernel.ProtobufSerializer.SerializeToFile(output, cachePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="imageIndex">Index of the image.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
/// <exception cref="System.InvalidOperationException"></exception>
|
||||
public string GetImagePath(BaseItem item, ImageType imageType, int imageIndex)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
if (imageType == ImageType.Backdrop)
|
||||
{
|
||||
if (item.BackdropImagePaths == null)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format("Item {0} does not have any Backdrops.", item.Name));
|
||||
}
|
||||
|
||||
return item.BackdropImagePaths[imageIndex];
|
||||
}
|
||||
|
||||
if (imageType == ImageType.Screenshot)
|
||||
{
|
||||
if (item.ScreenshotImagePaths == null)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format("Item {0} does not have any Screenshots.", item.Name));
|
||||
}
|
||||
|
||||
return item.ScreenshotImagePaths[imageIndex];
|
||||
}
|
||||
|
||||
if (imageType == ImageType.ChapterImage)
|
||||
{
|
||||
var video = (Video)item;
|
||||
|
||||
if (video.Chapters == null)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format("Item {0} does not have any Chapters.", item.Name));
|
||||
}
|
||||
|
||||
return video.Chapters[imageIndex].ImagePath;
|
||||
}
|
||||
|
||||
return item.GetImage(imageType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image date modified.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="imageIndex">Index of the image.</param>
|
||||
/// <returns>DateTime.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
public DateTime GetImageDateModified(BaseItem item, ImageType imageType, int imageIndex)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
var imagePath = GetImagePath(item, imageType, imageIndex);
|
||||
|
||||
return GetImageDateModified(item, imagePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image date modified.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imagePath">The image path.</param>
|
||||
/// <returns>DateTime.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
public DateTime GetImageDateModified(BaseItem item, string imagePath)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(imagePath))
|
||||
{
|
||||
throw new ArgumentNullException("imagePath");
|
||||
}
|
||||
|
||||
var metaFileEntry = item.ResolveArgs.GetMetaFileByPath(imagePath);
|
||||
|
||||
// If we didn't the metafile entry, check the Season
|
||||
if (!metaFileEntry.HasValue)
|
||||
{
|
||||
var episode = item as Episode;
|
||||
|
||||
if (episode != null && episode.Season != null)
|
||||
{
|
||||
episode.Season.ResolveArgs.GetMetaFileByPath(imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
// See if we can avoid a file system lookup by looking for the file in ResolveArgs
|
||||
return metaFileEntry == null ? File.GetLastWriteTimeUtc(imagePath) : metaFileEntry.Value.LastWriteTimeUtc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crops whitespace from an image, caches the result, and returns the cached path
|
||||
/// </summary>
|
||||
/// <param name="originalImagePath">The original image path.</param>
|
||||
/// <param name="dateModified">The date modified.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetCroppedImage(string originalImagePath, DateTime dateModified)
|
||||
{
|
||||
var name = originalImagePath;
|
||||
name += "datemodified=" + dateModified.Ticks;
|
||||
|
||||
var croppedImagePath = CroppedImageCache.GetResourcePath(name, Path.GetExtension(originalImagePath));
|
||||
|
||||
if (!CroppedImageCache.ContainsFilePath(croppedImagePath))
|
||||
{
|
||||
using (var fileStream = File.OpenRead(originalImagePath))
|
||||
{
|
||||
using (var originalImage = (Bitmap)Bitmap.FromStream(fileStream, true, false))
|
||||
{
|
||||
var outputFormat = originalImage.RawFormat;
|
||||
|
||||
using (var croppedImage = originalImage.CropWhitespace())
|
||||
{
|
||||
using (var cacheFileStream = new FileStream(croppedImagePath, FileMode.Create))
|
||||
{
|
||||
croppedImage.Save(outputFormat, cacheFileStream, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return croppedImagePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs an image through the image enhancers, caches the result, and returns the cached path
|
||||
/// </summary>
|
||||
/// <param name="originalImagePath">The original image path.</param>
|
||||
/// <param name="dateModified">The date modified of the original image file.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="imageIndex">Index of the image.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">originalImagePath</exception>
|
||||
public async Task<string> GetEnhancedImage(string originalImagePath, DateTime dateModified, BaseItem item, ImageType imageType, int imageIndex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(originalImagePath))
|
||||
{
|
||||
throw new ArgumentNullException("originalImagePath");
|
||||
}
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
var supportedEnhancers = Kernel.ImageEnhancers.Where(i => i.Supports(item, imageType)).ToList();
|
||||
|
||||
// No enhancement - don't cache
|
||||
if (supportedEnhancers.Count == 0)
|
||||
{
|
||||
return originalImagePath;
|
||||
}
|
||||
|
||||
var cacheGuid = GetImageCacheTag(originalImagePath, dateModified, supportedEnhancers, item, imageType);
|
||||
|
||||
// All enhanced images are saved as png to allow transparency
|
||||
var enhancedImagePath = EnhancedImageCache.GetResourcePath(cacheGuid + ".png");
|
||||
|
||||
if (!EnhancedImageCache.ContainsFilePath(enhancedImagePath))
|
||||
{
|
||||
using (var fileStream = File.OpenRead(originalImagePath))
|
||||
{
|
||||
using (var originalImage = Image.FromStream(fileStream, true, false))
|
||||
{
|
||||
//Pass the image through registered enhancers
|
||||
using (var newImage = await ExecuteImageEnhancers(supportedEnhancers, originalImage, item, imageType, imageIndex).ConfigureAwait(false))
|
||||
{
|
||||
//And then save it in the cache
|
||||
newImage.Save(enhancedImagePath, ImageFormat.Png);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return enhancedImagePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache tag.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="imagePath">The image path.</param>
|
||||
/// <returns>Guid.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(imagePath))
|
||||
{
|
||||
throw new ArgumentNullException("imagePath");
|
||||
}
|
||||
|
||||
var dateModified = GetImageDateModified(item, imagePath);
|
||||
|
||||
var supportedEnhancers = Kernel.ImageEnhancers.Where(i => i.Supports(item, imageType));
|
||||
|
||||
return GetImageCacheTag(imagePath, dateModified, supportedEnhancers, item, imageType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache tag.
|
||||
/// </summary>
|
||||
/// <param name="originalImagePath">The original image path.</param>
|
||||
/// <param name="dateModified">The date modified of the original image file.</param>
|
||||
/// <param name="imageEnhancers">The image enhancers.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <returns>Guid.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
public Guid GetImageCacheTag(string originalImagePath, DateTime dateModified, IEnumerable<BaseImageEnhancer> imageEnhancers, BaseItem item, ImageType imageType)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
if (imageEnhancers == null)
|
||||
{
|
||||
throw new ArgumentNullException("imageEnhancers");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(originalImagePath))
|
||||
{
|
||||
throw new ArgumentNullException("originalImagePath");
|
||||
}
|
||||
|
||||
// Cache name is created with supported enhancers combined with the last config change so we pick up new config changes
|
||||
var cacheKeys = imageEnhancers.Select(i => i.GetType().Name + i.LastConfigurationChange(item, imageType).Ticks).ToList();
|
||||
cacheKeys.Add(originalImagePath + dateModified.Ticks);
|
||||
|
||||
return string.Join("|", cacheKeys.ToArray()).GetMD5();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the image enhancers.
|
||||
/// </summary>
|
||||
/// <param name="imageEnhancers">The image enhancers.</param>
|
||||
/// <param name="originalImage">The original image.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="imageIndex">Index of the image.</param>
|
||||
/// <returns>Task{EnhancedImage}.</returns>
|
||||
private async Task<Image> ExecuteImageEnhancers(IEnumerable<BaseImageEnhancer> imageEnhancers, Image originalImage, BaseItem item, ImageType imageType, int imageIndex)
|
||||
{
|
||||
var result = originalImage;
|
||||
|
||||
// Run the enhancers sequentially in order of priority
|
||||
foreach (var enhancer in imageEnhancers)
|
||||
{
|
||||
result = await enhancer.EnhanceImageAsync(item, result, imageType, imageIndex).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected override void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
ImageSizeCache.Dispose();
|
||||
ResizedImageCache.Dispose();
|
||||
CroppedImageCache.Dispose();
|
||||
EnhancedImageCache.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(dispose);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
public static class ImageProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes an image by resizing to target dimensions
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity that owns the image</param>
|
||||
/// <param name="imageType">The image type</param>
|
||||
/// <param name="imageIndex">The image index (currently only used with backdrops)</param>
|
||||
/// <param name="toStream">The stream to save the new image to</param>
|
||||
/// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
|
||||
/// <param name="height">Use if a fixed height 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="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
|
||||
public static void ProcessImage(BaseEntity entity, ImageType imageType, int imageIndex, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality)
|
||||
{
|
||||
Image originalImage = Image.FromFile(GetImagePath(entity, imageType, imageIndex));
|
||||
|
||||
// Determine the output size based on incoming parameters
|
||||
Size newSize = DrawingUtils.Resize(originalImage.Size, width, height, maxWidth, maxHeight);
|
||||
|
||||
Bitmap thumbnail;
|
||||
|
||||
// Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
|
||||
if (originalImage.PixelFormat.HasFlag(PixelFormat.Indexed))
|
||||
{
|
||||
thumbnail = new Bitmap(originalImage, newSize.Width, newSize.Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
thumbnail = new Bitmap(newSize.Width, newSize.Height, originalImage.PixelFormat);
|
||||
}
|
||||
|
||||
thumbnail.MakeTransparent();
|
||||
|
||||
// Preserve the original resolution
|
||||
thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
|
||||
|
||||
Graphics thumbnailGraph = Graphics.FromImage(thumbnail);
|
||||
|
||||
thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
|
||||
thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
|
||||
thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
|
||||
|
||||
thumbnailGraph.DrawImage(originalImage, 0, 0, newSize.Width, newSize.Height);
|
||||
|
||||
ImageFormat outputFormat = originalImage.RawFormat;
|
||||
|
||||
// Write to the output stream
|
||||
SaveImage(outputFormat, thumbnail, toStream, quality);
|
||||
|
||||
thumbnailGraph.Dispose();
|
||||
thumbnail.Dispose();
|
||||
originalImage.Dispose();
|
||||
}
|
||||
|
||||
public static string GetImagePath(BaseEntity entity, ImageType imageType, int imageIndex)
|
||||
{
|
||||
var item = entity as BaseItem;
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
if (imageType == ImageType.Logo)
|
||||
{
|
||||
return item.LogoImagePath;
|
||||
}
|
||||
if (imageType == ImageType.Backdrop)
|
||||
{
|
||||
return item.BackdropImagePaths.ElementAt(imageIndex);
|
||||
}
|
||||
if (imageType == ImageType.Banner)
|
||||
{
|
||||
return item.BannerImagePath;
|
||||
}
|
||||
if (imageType == ImageType.Art)
|
||||
{
|
||||
return item.ArtImagePath;
|
||||
}
|
||||
if (imageType == ImageType.Thumbnail)
|
||||
{
|
||||
return item.ThumbnailImagePath;
|
||||
}
|
||||
}
|
||||
|
||||
return entity.PrimaryImagePath;
|
||||
}
|
||||
|
||||
public static void SaveImage(ImageFormat outputFormat, Image newImage, Stream toStream, int? quality)
|
||||
{
|
||||
// Use special save methods for jpeg and png that will result in a much higher quality image
|
||||
// All other formats use the generic Image.Save
|
||||
if (ImageFormat.Jpeg.Equals(outputFormat))
|
||||
{
|
||||
SaveJpeg(newImage, toStream, quality);
|
||||
}
|
||||
else if (ImageFormat.Png.Equals(outputFormat))
|
||||
{
|
||||
newImage.Save(toStream, ImageFormat.Png);
|
||||
}
|
||||
else
|
||||
{
|
||||
newImage.Save(toStream, outputFormat);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SaveJpeg(Image image, Stream target, int? quality)
|
||||
{
|
||||
if (!quality.HasValue)
|
||||
{
|
||||
quality = 90;
|
||||
}
|
||||
|
||||
using (var encoderParameters = new EncoderParameters(1))
|
||||
{
|
||||
encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality.Value);
|
||||
image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
|
||||
}
|
||||
}
|
||||
|
||||
public static ImageCodecInfo GetImageCodecInfo(string mimeType)
|
||||
{
|
||||
ImageCodecInfo[] info = ImageCodecInfo.GetImageEncoders();
|
||||
|
||||
for (int i = 0; i < info.Length; i++)
|
||||
{
|
||||
ImageCodecInfo ici = info[i];
|
||||
if (ici.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ici;
|
||||
}
|
||||
}
|
||||
return info[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
68
MediaBrowser.Controller/Entities/AggregateFolder.cs
Normal file
68
MediaBrowser.Controller/Entities/AggregateFolder.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Specialized folder that can have items added to it's children by external entities.
|
||||
/// Used for our RootFolder so plug-ins can add items.
|
||||
/// </summary>
|
||||
public class AggregateFolder : Folder
|
||||
{
|
||||
/// <summary>
|
||||
/// The _virtual children
|
||||
/// </summary>
|
||||
private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the virtual children.
|
||||
/// </summary>
|
||||
/// <value>The virtual children.</value>
|
||||
public ConcurrentBag<BaseItem> VirtualChildren
|
||||
{
|
||||
get { return _virtualChildren; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the virtual child.
|
||||
/// </summary>
|
||||
/// <param name="child">The child.</param>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public void AddVirtualChild(BaseItem child)
|
||||
{
|
||||
if (child == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
_virtualChildren.Add(child);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the children of this folder from the actual file system
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{BaseItem}.</returns>
|
||||
protected override IEnumerable<BaseItem> GetNonCachedChildren()
|
||||
{
|
||||
return base.GetNonCachedChildren().Concat(_virtualChildren);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the virtual child.
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">id</exception>
|
||||
public BaseItem FindVirtualChild(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentNullException("id");
|
||||
}
|
||||
|
||||
return _virtualChildren.FirstOrDefault(i => i.Id == id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class Audio : BaseItem
|
||||
{
|
||||
public int BitRate { get; set; }
|
||||
public int Channels { get; set; }
|
||||
public int SampleRate { get; set; }
|
||||
|
||||
public string Artist { get; set; }
|
||||
public string Album { get; set; }
|
||||
public string AlbumArtist { get; set; }
|
||||
}
|
||||
}
|
||||
78
MediaBrowser.Controller/Entities/Audio/Audio.cs
Normal file
78
MediaBrowser.Controller/Entities/Audio/Audio.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Audio
|
||||
/// </summary>
|
||||
public class Audio : BaseItem, IHasMediaStreams
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the media streams.
|
||||
/// </summary>
|
||||
/// <value>The media streams.</value>
|
||||
public List<MediaStream> MediaStreams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Override this to true if class should be grouped under a container in indicies
|
||||
/// The container class should be defined via IndexContainer
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public override bool GroupInIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The unknown album
|
||||
/// </summary>
|
||||
private static readonly MusicAlbum UnknownAlbum = new MusicAlbum {Name = "<Unknown>"};
|
||||
/// <summary>
|
||||
/// Override this to return the folder that should be used to construct a container
|
||||
/// for this item in an index. GroupInIndex should be true as well.
|
||||
/// </summary>
|
||||
/// <value>The index container.</value>
|
||||
[IgnoreDataMember]
|
||||
public override Folder IndexContainer
|
||||
{
|
||||
get
|
||||
{
|
||||
return Parent is MusicAlbum ? Parent : Album != null ? new MusicAlbum {Name = Album, PrimaryImagePath = PrimaryImagePath } : UnknownAlbum;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artist.
|
||||
/// </summary>
|
||||
/// <value>The artist.</value>
|
||||
public string Artist { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the album.
|
||||
/// </summary>
|
||||
/// <value>The album.</value>
|
||||
public string Album { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the album artist.
|
||||
/// </summary>
|
||||
/// <value>The album artist.</value>
|
||||
public string AlbumArtist { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the media.
|
||||
/// </summary>
|
||||
/// <value>The type of the media.</value>
|
||||
public override string MediaType
|
||||
{
|
||||
get
|
||||
{
|
||||
return Model.Entities.MediaType.Audio;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
Normal file
133
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MusicAlbum
|
||||
/// </summary>
|
||||
public class MusicAlbum : Folder
|
||||
{
|
||||
/// <summary>
|
||||
/// Songs will group into us so don't also include us in the index
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [include in index]; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public override bool IncludeInIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to true if class should be grouped under a container in indicies
|
||||
/// The container class should be defined via IndexContainer
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public override bool GroupInIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The unknwon artist
|
||||
/// </summary>
|
||||
private static readonly MusicArtist UnknwonArtist = new MusicArtist {Name = "<Unknown>"};
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return the folder that should be used to construct a container
|
||||
/// for this item in an index. GroupInIndex should be true as well.
|
||||
/// </summary>
|
||||
/// <value>The index container.</value>
|
||||
[IgnoreDataMember]
|
||||
public override Folder IndexContainer
|
||||
{
|
||||
get { return Parent as MusicArtist ?? UnknwonArtist; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to point to first child (song) if not explicitly defined
|
||||
/// </summary>
|
||||
/// <value>The primary image path.</value>
|
||||
[IgnoreDataMember]
|
||||
public override string PrimaryImagePath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (base.PrimaryImagePath == null)
|
||||
{
|
||||
var child = Children.FirstOrDefault();
|
||||
|
||||
return child == null ? base.PrimaryImagePath : child.PrimaryImagePath;
|
||||
}
|
||||
|
||||
return base.PrimaryImagePath;
|
||||
}
|
||||
set
|
||||
{
|
||||
base.PrimaryImagePath = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to point to first child (song)
|
||||
/// </summary>
|
||||
/// <value>The production year.</value>
|
||||
public override int? ProductionYear
|
||||
{
|
||||
get
|
||||
{
|
||||
var child = Children.FirstOrDefault();
|
||||
|
||||
return child == null ? base.ProductionYear : child.ProductionYear;
|
||||
}
|
||||
set
|
||||
{
|
||||
base.ProductionYear = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to point to first child (song)
|
||||
/// </summary>
|
||||
/// <value>The genres.</value>
|
||||
public override List<string> Genres
|
||||
{
|
||||
get
|
||||
{
|
||||
var child = Children.FirstOrDefault();
|
||||
|
||||
return child == null ? base.Genres : child.Genres;
|
||||
}
|
||||
set
|
||||
{
|
||||
base.Genres = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to point to first child (song)
|
||||
/// </summary>
|
||||
/// <value>The studios.</value>
|
||||
public override List<string> Studios
|
||||
{
|
||||
get
|
||||
{
|
||||
var child = Children.FirstOrDefault();
|
||||
|
||||
return child == null ? base.Studios : child.Studios;
|
||||
}
|
||||
set
|
||||
{
|
||||
base.Studios = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
Normal file
10
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MusicArtist
|
||||
/// </summary>
|
||||
public class MusicArtist : Folder
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a base entity for all of our types
|
||||
/// </summary>
|
||||
public abstract class BaseEntity
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string Path { get; set; }
|
||||
|
||||
public Folder Parent { get; set; }
|
||||
|
||||
public string PrimaryImagePath { get; set; }
|
||||
|
||||
public DateTime DateCreated { get; set; }
|
||||
|
||||
public DateTime DateModified { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
protected Dictionary<Guid, BaseProviderInfo> _providerData;
|
||||
/// <summary>
|
||||
/// Holds persistent data for providers like last refresh date.
|
||||
/// Providers can use this to determine if they need to refresh.
|
||||
/// The BaseProviderInfo class can be extended to hold anything a provider may need.
|
||||
///
|
||||
/// Keyed by a unique provider ID.
|
||||
/// </summary>
|
||||
public Dictionary<Guid, BaseProviderInfo> ProviderData
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_providerData == null) _providerData = new Dictionary<Guid, BaseProviderInfo>();
|
||||
return _providerData;
|
||||
}
|
||||
set
|
||||
{
|
||||
_providerData = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected ItemResolveEventArgs _resolveArgs;
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// Use ResolveArgs.FileSystemChildren to check for the existence of files instead of File.Exists
|
||||
/// </summary>
|
||||
public ItemResolveEventArgs ResolveArgs
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_resolveArgs == null)
|
||||
{
|
||||
_resolveArgs = new ItemResolveEventArgs()
|
||||
{
|
||||
FileInfo = FileData.GetFileData(this.Path),
|
||||
Parent = this.Parent,
|
||||
Cancel = false,
|
||||
Path = this.Path
|
||||
};
|
||||
_resolveArgs = FileSystemHelper.FilterChildFileSystemEntries(_resolveArgs, (this.Parent != null && this.Parent.IsRoot));
|
||||
}
|
||||
return _resolveArgs;
|
||||
}
|
||||
set
|
||||
{
|
||||
_resolveArgs = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh metadata on us by execution our provider chain
|
||||
/// </summary>
|
||||
/// <returns>true if a provider reports we changed</returns>
|
||||
public bool RefreshMetadata()
|
||||
{
|
||||
Kernel.Instance.ExecuteMetadataProviders(this).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
49
MediaBrowser.Controller/Entities/BasePluginFolder.cs
Normal file
49
MediaBrowser.Controller/Entities/BasePluginFolder.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugins derive from and export this class to create a folder that will appear in the root along
|
||||
/// with all the other actual physical folders in the system.
|
||||
/// </summary>
|
||||
public abstract class BasePluginFolder : Folder, ICollectionFolder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
public override Guid Id
|
||||
{
|
||||
get
|
||||
{
|
||||
// This doesn't get populated through the normal resolving process
|
||||
if (base.Id == Guid.Empty)
|
||||
{
|
||||
base.Id = (Path ?? Name).GetMBId(GetType());
|
||||
}
|
||||
return base.Id;
|
||||
}
|
||||
set
|
||||
{
|
||||
base.Id = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We don't resolve normally so need to fill this in
|
||||
/// </summary>
|
||||
public override string DisplayMediaType
|
||||
{
|
||||
get
|
||||
{
|
||||
return "CollectionFolder"; // Plug-in folders are collection folders
|
||||
}
|
||||
set
|
||||
{
|
||||
base.DisplayMediaType = value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
100
MediaBrowser.Controller/Entities/CollectionFolder.cs
Normal file
100
MediaBrowser.Controller/Entities/CollectionFolder.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Specialized Folder class that points to a subset of the physical folders in the system.
|
||||
/// It is created from the user-specific folders within the system root
|
||||
/// </summary>
|
||||
public class CollectionFolder : Folder, ICollectionFolder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is virtual folder.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is virtual folder; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public override bool IsVirtualFolder
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allow different display preferences for each collection folder
|
||||
/// </summary>
|
||||
/// <value>The display prefs id.</value>
|
||||
public override Guid DisplayPrefsId
|
||||
{
|
||||
get
|
||||
{
|
||||
return Id;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache this since it will be used a lot
|
||||
/// <summary>
|
||||
/// The null task result
|
||||
/// </summary>
|
||||
private static readonly Task NullTaskResult = Task.FromResult<object>(null);
|
||||
|
||||
/// <summary>
|
||||
/// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
|
||||
/// ***Currently does not contain logic to maintain items that are unavailable in the file system***
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
|
||||
/// <returns>Task.</returns>
|
||||
protected override Task ValidateChildrenInternal(IProgress<TaskProgress> progress, CancellationToken cancellationToken, bool? recursive = null)
|
||||
{
|
||||
//we don't directly validate our children
|
||||
//but we do need to clear out the index cache...
|
||||
IndexCache = new ConcurrentDictionary<string, List<BaseItem>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return NullTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Our children are actually just references to the ones in the physical root...
|
||||
/// </summary>
|
||||
/// <value>The actual children.</value>
|
||||
protected override ConcurrentBag<BaseItem> ActualChildren
|
||||
{
|
||||
get
|
||||
{
|
||||
IEnumerable<Guid> folderIds;
|
||||
|
||||
try
|
||||
{
|
||||
// Accessing ResolveArgs could involve file system access
|
||||
folderIds = ResolveArgs.PhysicalLocations.Select(f => (f.GetMBId(typeof(Folder))));
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.LogException("Error creating FolderIds for {0}", ex, Path);
|
||||
|
||||
folderIds = new Guid[] {};
|
||||
}
|
||||
|
||||
var ourChildren =
|
||||
Kernel.Instance.RootFolder.Children.OfType<Folder>()
|
||||
.Where(i => folderIds.Contains(i.Id))
|
||||
.SelectMany(c => c.Children);
|
||||
|
||||
return new ConcurrentBag<BaseItem>(ourChildren);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,10 @@
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class Genre : BaseEntity
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Genre
|
||||
/// </summary>
|
||||
public class Genre : BaseItem
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
10
MediaBrowser.Controller/Entities/ICollectionFolder.cs
Normal file
10
MediaBrowser.Controller/Entities/ICollectionFolder.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// This is just a marker interface to denote top level folders
|
||||
/// </summary>
|
||||
public interface ICollectionFolder
|
||||
{
|
||||
}
|
||||
}
|
||||
105
MediaBrowser.Controller/Entities/ISupportsSpecialFeatures.cs
Normal file
105
MediaBrowser.Controller/Entities/ISupportsSpecialFeatures.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Common.Win32;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows some code sharing between entities that support special features
|
||||
/// </summary>
|
||||
public interface ISupportsSpecialFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resolve args.
|
||||
/// </summary>
|
||||
/// <value>The resolve args.</value>
|
||||
ItemResolveArgs ResolveArgs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the special features.
|
||||
/// </summary>
|
||||
/// <value>The special features.</value>
|
||||
List<Video> SpecialFeatures { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class SpecialFeatures
|
||||
/// </summary>
|
||||
public static class SpecialFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads special features from the file system
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity.</param>
|
||||
/// <returns>List{Video}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public static IEnumerable<Video> LoadSpecialFeatures(ISupportsSpecialFeatures entity)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
WIN32_FIND_DATA? folder;
|
||||
|
||||
try
|
||||
{
|
||||
folder = entity.ResolveArgs.GetFileSystemEntryByName("specials");
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.LogException("Error getting ResolveArgs for {0}", ex, entity.Path);
|
||||
return new List<Video> { };
|
||||
}
|
||||
|
||||
// Path doesn't exist. No biggie
|
||||
if (folder == null)
|
||||
{
|
||||
return new List<Video> {};
|
||||
}
|
||||
|
||||
IEnumerable<WIN32_FIND_DATA> files;
|
||||
|
||||
try
|
||||
{
|
||||
files = FileSystem.GetFiles(folder.Value.Path);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.LogException("Error loading trailers for {0}", ex, entity.Name);
|
||||
return new List<Video> { };
|
||||
}
|
||||
|
||||
return Kernel.Instance.LibraryManager.GetItems<Video>(files, null).Select(video =>
|
||||
{
|
||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||
var dbItem = Kernel.Instance.ItemRepository.RetrieveItem(video.Id) as Video;
|
||||
|
||||
if (dbItem != null)
|
||||
{
|
||||
dbItem.ResolveArgs = video.ResolveArgs;
|
||||
video = dbItem;
|
||||
}
|
||||
|
||||
return video;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
204
MediaBrowser.Controller/Entities/IndexFolder.cs
Normal file
204
MediaBrowser.Controller/Entities/IndexFolder.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class IndexFolder
|
||||
/// </summary>
|
||||
public class IndexFolder : Folder
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IndexFolder" /> class.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent.</param>
|
||||
/// <param name="shadow">The shadow.</param>
|
||||
/// <param name="children">The children.</param>
|
||||
/// <param name="indexName">Name of the index.</param>
|
||||
/// <param name="groupContents">if set to <c>true</c> [group contents].</param>
|
||||
public IndexFolder(Folder parent, BaseItem shadow, IEnumerable<BaseItem> children, string indexName, bool groupContents = true)
|
||||
{
|
||||
ChildSource = children;
|
||||
ShadowItem = shadow;
|
||||
GroupContents = groupContents;
|
||||
if (shadow == null)
|
||||
{
|
||||
Name = SortName = "<Unknown>";
|
||||
}
|
||||
else
|
||||
{
|
||||
SetShadowValues();
|
||||
}
|
||||
Id = (parent.Id.ToString() + Name).GetMBId(typeof(IndexFolder));
|
||||
|
||||
IndexName = indexName;
|
||||
Parent = parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the parent.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent.</param>
|
||||
public void ResetParent(Folder parent)
|
||||
{
|
||||
Parent = parent;
|
||||
Id = (parent.Id.ToString() + Name).GetMBId(typeof(IndexFolder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to true if class should be grouped under a container in indicies
|
||||
/// The container class should be defined via IndexContainer
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public override bool GroupInIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return ShadowItem != null && ShadowItem.GroupInIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return the folder that should be used to construct a container
|
||||
/// for this item in an index. GroupInIndex should be true as well.
|
||||
/// </summary>
|
||||
/// <value>The index container.</value>
|
||||
[IgnoreDataMember]
|
||||
public override Folder IndexContainer
|
||||
{
|
||||
get { return ShadowItem != null ? ShadowItem.IndexContainer : new IndexFolder(this, null, null, "<Unknown>", false); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [group contents].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [group contents]; otherwise, <c>false</c>.</value>
|
||||
protected bool GroupContents { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the child source.
|
||||
/// </summary>
|
||||
/// <value>The child source.</value>
|
||||
protected IEnumerable<BaseItem> ChildSource { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets our children.
|
||||
/// </summary>
|
||||
/// <value>Our children.</value>
|
||||
protected ConcurrentBag<BaseItem> OurChildren { get; set; }
|
||||
/// <summary>
|
||||
/// Gets the name of the index.
|
||||
/// </summary>
|
||||
/// <value>The name of the index.</value>
|
||||
public string IndexName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Override to return the children defined to us when we were created
|
||||
/// </summary>
|
||||
/// <value>The actual children.</value>
|
||||
protected override ConcurrentBag<BaseItem> LoadChildren()
|
||||
{
|
||||
var originalChildSource = ChildSource.ToList();
|
||||
|
||||
var kids = originalChildSource;
|
||||
if (GroupContents)
|
||||
{
|
||||
// Recursively group up the chain
|
||||
var group = true;
|
||||
var isSubsequentLoop = false;
|
||||
|
||||
while (group)
|
||||
{
|
||||
kids = isSubsequentLoop || kids.Any(i => i.GroupInIndex)
|
||||
? GroupedSource(kids).ToList()
|
||||
: originalChildSource;
|
||||
|
||||
group = kids.Any(i => i.GroupInIndex);
|
||||
isSubsequentLoop = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Now - since we built the index grouping from the bottom up - we now need to properly set Parents from the top down
|
||||
SetParents(this, kids.OfType<IndexFolder>());
|
||||
|
||||
return new ConcurrentBag<BaseItem>(kids);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the parents.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent.</param>
|
||||
/// <param name="kids">The kids.</param>
|
||||
private void SetParents(Folder parent, IEnumerable<IndexFolder> kids)
|
||||
{
|
||||
foreach (var child in kids)
|
||||
{
|
||||
child.ResetParent(parent);
|
||||
child.SetParents(child, child.Children.OfType<IndexFolder>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groupeds the source.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <returns>IEnumerable{BaseItem}.</returns>
|
||||
protected IEnumerable<BaseItem> GroupedSource(IEnumerable<BaseItem> source)
|
||||
{
|
||||
return source.GroupBy(i => i.IndexContainer).Select(container => new IndexFolder(this, container.Key, container, null, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The item we are shadowing as a folder (Genre, Actor, etc.)
|
||||
/// We inherit the images and other meta from this item
|
||||
/// </summary>
|
||||
/// <value>The shadow item.</value>
|
||||
protected BaseItem ShadowItem { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the shadow values.
|
||||
/// </summary>
|
||||
protected void SetShadowValues()
|
||||
{
|
||||
if (ShadowItem != null)
|
||||
{
|
||||
Name = ShadowItem.Name;
|
||||
SortName = ShadowItem.SortName;
|
||||
Genres = ShadowItem.Genres;
|
||||
Studios = ShadowItem.Studios;
|
||||
OfficialRating = ShadowItem.OfficialRating;
|
||||
BackdropImagePaths = ShadowItem.BackdropImagePaths;
|
||||
Images = ShadowItem.Images;
|
||||
Overview = ShadowItem.Overview;
|
||||
DisplayMediaType = ShadowItem.GetType().Name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the base implementation to refresh metadata for local trailers
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="forceSave">if set to <c>true</c> [is new item].</param>
|
||||
/// <param name="forceRefresh">if set to <c>true</c> [force].</param>
|
||||
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
|
||||
/// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
|
||||
{
|
||||
if (ShadowItem != null)
|
||||
{
|
||||
var changed = await ShadowItem.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
SetShadowValues();
|
||||
return changed;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Movies
|
||||
{
|
||||
public class BoxSet : Folder
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Movies
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BoxSet
|
||||
/// </summary>
|
||||
public class BoxSet : Folder
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Movies
|
||||
{
|
||||
public class Movie : Video
|
||||
{
|
||||
public IEnumerable<Video> SpecialFeatures { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Finds an item by ID, recursively
|
||||
/// </summary>
|
||||
public override BaseItem FindItemById(Guid id)
|
||||
{
|
||||
var item = base.FindItemById(id);
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
||||
if (SpecialFeatures != null)
|
||||
{
|
||||
return SpecialFeatures.FirstOrDefault(i => i.Id == id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Movies
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Movie
|
||||
/// </summary>
|
||||
public class Movie : Video, ISupportsSpecialFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Should be overridden to return the proper folder where metadata lives
|
||||
/// </summary>
|
||||
/// <value>The meta location.</value>
|
||||
[IgnoreDataMember]
|
||||
public override string MetaLocation
|
||||
{
|
||||
get
|
||||
{
|
||||
return VideoType == VideoType.VideoFile || VideoType == VideoType.Iso ? System.IO.Path.GetDirectoryName(Path) : Path;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to use tmdb or imdb id so it will stick if the item moves physical locations
|
||||
/// </summary>
|
||||
/// <value>The user data id.</value>
|
||||
[IgnoreDataMember]
|
||||
public override Guid UserDataId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_userDataId == Guid.Empty)
|
||||
{
|
||||
var baseId = this.GetProviderId(MetadataProviders.Tmdb) ?? this.GetProviderId(MetadataProviders.Imdb);
|
||||
_userDataId = baseId != null ? baseId.GetMD5() : Id;
|
||||
}
|
||||
return _userDataId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _special features
|
||||
/// </summary>
|
||||
private List<Video> _specialFeatures;
|
||||
/// <summary>
|
||||
/// The _special features initialized
|
||||
/// </summary>
|
||||
private bool _specialFeaturesInitialized;
|
||||
/// <summary>
|
||||
/// The _special features sync lock
|
||||
/// </summary>
|
||||
private object _specialFeaturesSyncLock = new object();
|
||||
/// <summary>
|
||||
/// Gets the special features.
|
||||
/// </summary>
|
||||
/// <value>The special features.</value>
|
||||
[IgnoreDataMember]
|
||||
public List<Video> SpecialFeatures
|
||||
{
|
||||
get
|
||||
{
|
||||
LazyInitializer.EnsureInitialized(ref _specialFeatures, ref _specialFeaturesInitialized, ref _specialFeaturesSyncLock, () => Entities.SpecialFeatures.LoadSpecialFeatures(this).ToList());
|
||||
return _specialFeatures;
|
||||
}
|
||||
private set
|
||||
{
|
||||
_specialFeatures = value;
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
_specialFeaturesInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Needed because the resolver stops at the movie folder and we find the video inside.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [use parent path to create resolve args]; otherwise, <c>false</c>.</value>
|
||||
protected override bool UseParentPathToCreateResolveArgs
|
||||
{
|
||||
get
|
||||
{
|
||||
return VideoType == VideoType.VideoFile || VideoType == VideoType.Iso;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the base implementation to refresh metadata for special features
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="forceSave">if set to <c>true</c> [is new item].</param>
|
||||
/// <param name="forceRefresh">if set to <c>true</c> [force].</param>
|
||||
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
|
||||
/// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
|
||||
{
|
||||
// Lazy load these again
|
||||
SpecialFeatures = null;
|
||||
|
||||
// Kick off a task to refresh the main item
|
||||
var result = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false);
|
||||
|
||||
var tasks = SpecialFeatures.Select(item => item.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders));
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an item by ID, recursively
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
public override BaseItem FindItemById(Guid id, User user)
|
||||
{
|
||||
var item = base.FindItemById(id, user);
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
||||
if (SpecialFeatures != null)
|
||||
{
|
||||
return SpecialFeatures.FirstOrDefault(i => i.Id == id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,41 @@
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the full Person object that can be retrieved with all of it's data.
|
||||
/// </summary>
|
||||
public class Person : BaseEntity
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the small Person stub that is attached to BaseItems
|
||||
/// </summary>
|
||||
public class PersonInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public string Type { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the full Person object that can be retrieved with all of it's data.
|
||||
/// </summary>
|
||||
public class Person : BaseItem
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the small Person stub that is attached to BaseItems
|
||||
/// </summary>
|
||||
public class PersonInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the role.
|
||||
/// </summary>
|
||||
/// <value>The role.</value>
|
||||
public string Role { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the type.
|
||||
/// </summary>
|
||||
/// <value>The type.</value>
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="System.String" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="System.String" /> that represents this instance.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using MediaBrowser.Common.Events;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds information about a playback progress event
|
||||
/// </summary>
|
||||
public class PlaybackProgressEventArgs : GenericEventArgs<BaseItem>
|
||||
{
|
||||
public User User { get; set; }
|
||||
public long? PlaybackPositionTicks { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class Studio : BaseEntity
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Studio
|
||||
/// </summary>
|
||||
public class Studio : BaseItem
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,163 @@
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
public class Episode : Video
|
||||
{
|
||||
}
|
||||
}
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Episode
|
||||
/// </summary>
|
||||
public class Episode : Video
|
||||
{
|
||||
/// <summary>
|
||||
/// Episodes have a special Metadata folder
|
||||
/// </summary>
|
||||
/// <value>The meta location.</value>
|
||||
[IgnoreDataMember]
|
||||
public override string MetaLocation
|
||||
{
|
||||
get
|
||||
{
|
||||
return System.IO.Path.Combine(Parent.Path, "metadata");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We want to group into series not show individually in an index
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public override bool GroupInIndex
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We roll up into series
|
||||
/// </summary>
|
||||
/// <value>The index container.</value>
|
||||
[IgnoreDataMember]
|
||||
public override Folder IndexContainer
|
||||
{
|
||||
get
|
||||
{
|
||||
return Season;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to use the provider Ids + season and episode number so it will be portable
|
||||
/// </summary>
|
||||
/// <value>The user data id.</value>
|
||||
[IgnoreDataMember]
|
||||
public override Guid UserDataId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_userDataId == Guid.Empty)
|
||||
{
|
||||
var baseId = Series != null ? Series.GetProviderId(MetadataProviders.Tvdb) ?? Series.GetProviderId(MetadataProviders.Tvcom) : null;
|
||||
if (baseId != null)
|
||||
{
|
||||
var seasonNo = Season != null ? Season.IndexNumber ?? 0 : 0;
|
||||
var epNo = IndexNumber ?? 0;
|
||||
baseId = baseId + seasonNo.ToString("000") + epNo.ToString("000");
|
||||
}
|
||||
_userDataId = baseId != null ? baseId.GetMD5() : Id;
|
||||
}
|
||||
return _userDataId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this if you need to combine/collapse person information
|
||||
/// </summary>
|
||||
/// <value>All people.</value>
|
||||
[IgnoreDataMember]
|
||||
public override IEnumerable<PersonInfo> AllPeople
|
||||
{
|
||||
get
|
||||
{
|
||||
if (People == null) return Series != null ? Series.People : People;
|
||||
return Series != null && Series.People != null ? People.Concat(Series.People) : base.AllPeople;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the studios.
|
||||
/// </summary>
|
||||
/// <value>The studios.</value>
|
||||
[IgnoreDataMember]
|
||||
public override List<string> Studios
|
||||
{
|
||||
get
|
||||
{
|
||||
return Series != null ? Series.Studios : null;
|
||||
}
|
||||
set
|
||||
{
|
||||
base.Studios = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the genres.
|
||||
/// </summary>
|
||||
/// <value>The genres.</value>
|
||||
[IgnoreDataMember]
|
||||
public override List<string> Genres
|
||||
{
|
||||
get { return Series != null ? Series.Genres : null; }
|
||||
set
|
||||
{
|
||||
base.Genres = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We persist the MB Id of our series object so we can always find it no matter
|
||||
/// what context we happen to be loaded from.
|
||||
/// </summary>
|
||||
/// <value>The series item id.</value>
|
||||
public Guid SeriesItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// We persist the MB Id of our season object so we can always find it no matter
|
||||
/// what context we happen to be loaded from.
|
||||
/// </summary>
|
||||
/// <value>The season item id.</value>
|
||||
public Guid SeasonItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _series
|
||||
/// </summary>
|
||||
private Series _series;
|
||||
/// <summary>
|
||||
/// This Episode's Series Instance
|
||||
/// </summary>
|
||||
/// <value>The series.</value>
|
||||
[IgnoreDataMember]
|
||||
public Series Series
|
||||
{
|
||||
get { return _series ?? (_series = FindParent<Series>()); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _season
|
||||
/// </summary>
|
||||
private Season _season;
|
||||
/// <summary>
|
||||
/// This Episode's Season Instance
|
||||
/// </summary>
|
||||
/// <value>The season.</value>
|
||||
[IgnoreDataMember]
|
||||
public Season Season
|
||||
{
|
||||
get { return _season ?? (_season = FindParent<Season>()); }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,143 @@
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
public class Season : Folder
|
||||
{
|
||||
/// <summary>
|
||||
/// Store these to reduce disk access in Episode Resolver
|
||||
/// </summary>
|
||||
public string[] MetadataFiles
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResolveArgs.MetadataFiles ?? new string[] { };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the metafolder contains a given file
|
||||
/// </summary>
|
||||
public bool ContainsMetadataFile(string file)
|
||||
{
|
||||
for (int i = 0; i < MetadataFiles.Length; i++)
|
||||
{
|
||||
if (MetadataFiles[i].Equals(file, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Win32;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Localization;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Season
|
||||
/// </summary>
|
||||
public class Season : Folder
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Seasons are just containers
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [include in index]; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public override bool IncludeInIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We want to group into our Series
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [group in index]; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public override bool GroupInIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return the folder that should be used to construct a container
|
||||
/// for this item in an index. GroupInIndex should be true as well.
|
||||
/// </summary>
|
||||
/// <value>The index container.</value>
|
||||
[IgnoreDataMember]
|
||||
public override Folder IndexContainer
|
||||
{
|
||||
get
|
||||
{
|
||||
return Series;
|
||||
}
|
||||
}
|
||||
|
||||
// Genre, Rating and Stuido will all be the same
|
||||
protected override Dictionary<string, Func<User, IEnumerable<BaseItem>>> GetIndexByOptions()
|
||||
{
|
||||
return new Dictionary<string, Func<User, IEnumerable<BaseItem>>> {
|
||||
{LocalizedStrings.Instance.GetString("NoneDispPref"), null},
|
||||
{LocalizedStrings.Instance.GetString("PerformerDispPref"), GetIndexByPerformer},
|
||||
{LocalizedStrings.Instance.GetString("DirectorDispPref"), GetIndexByDirector},
|
||||
{LocalizedStrings.Instance.GetString("YearDispPref"), GetIndexByYear},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to use the provider Ids + season number so it will be portable
|
||||
/// </summary>
|
||||
/// <value>The user data id.</value>
|
||||
[IgnoreDataMember]
|
||||
public override Guid UserDataId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_userDataId == Guid.Empty)
|
||||
{
|
||||
var baseId = Series != null ? Series.GetProviderId(MetadataProviders.Tvdb) ?? Series.GetProviderId(MetadataProviders.Tvcom) : null;
|
||||
if (baseId != null)
|
||||
{
|
||||
var seasonNo = IndexNumber ?? 0;
|
||||
baseId = baseId + seasonNo.ToString("000");
|
||||
}
|
||||
|
||||
_userDataId = baseId != null ? baseId.GetMD5() : Id;
|
||||
}
|
||||
return _userDataId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We persist the MB Id of our series object so we can always find it no matter
|
||||
/// what context we happen to be loaded from.
|
||||
/// </summary>
|
||||
/// <value>The series item id.</value>
|
||||
public Guid SeriesItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _series
|
||||
/// </summary>
|
||||
private Series _series;
|
||||
/// <summary>
|
||||
/// This Episode's Series Instance
|
||||
/// </summary>
|
||||
/// <value>The series.</value>
|
||||
[IgnoreDataMember]
|
||||
public Series Series
|
||||
{
|
||||
get { return _series ?? (_series = FindParent<Series>()); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add files from the metadata folder to ResolveArgs
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
internal static void AddMetadataFiles(ItemResolveArgs args)
|
||||
{
|
||||
var folder = args.GetFileSystemEntryByName("metadata");
|
||||
|
||||
if (folder.HasValue)
|
||||
{
|
||||
args.AddMetadataFiles(FileSystem.GetFiles(folder.Value.Path));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates ResolveArgs on demand
|
||||
/// </summary>
|
||||
/// <param name="pathInfo">The path info.</param>
|
||||
/// <returns>ItemResolveArgs.</returns>
|
||||
protected internal override ItemResolveArgs CreateResolveArgs(WIN32_FIND_DATA? pathInfo = null)
|
||||
{
|
||||
var args = base.CreateResolveArgs(pathInfo);
|
||||
|
||||
AddMetadataFiles(args);
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
public class Series : Folder
|
||||
{
|
||||
public string Status { get; set; }
|
||||
public IEnumerable<DayOfWeek> AirDays { get; set; }
|
||||
public string AirTime { get; set; }
|
||||
}
|
||||
}
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Win32;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Localization;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Series
|
||||
/// </summary>
|
||||
public class Series : Folder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the status.
|
||||
/// </summary>
|
||||
/// <value>The status.</value>
|
||||
public SeriesStatus? Status { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the air days.
|
||||
/// </summary>
|
||||
/// <value>The air days.</value>
|
||||
public List<DayOfWeek> AirDays { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the air time.
|
||||
/// </summary>
|
||||
/// <value>The air time.</value>
|
||||
public string AirTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Series aren't included directly in indices - Their Episodes will roll up to them
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [include in index]; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public override bool IncludeInIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to use the provider Ids so it will be portable
|
||||
/// </summary>
|
||||
/// <value>The user data id.</value>
|
||||
[IgnoreDataMember]
|
||||
public override Guid UserDataId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_userDataId == Guid.Empty)
|
||||
{
|
||||
var baseId = this.GetProviderId(MetadataProviders.Tvdb) ?? this.GetProviderId(MetadataProviders.Tvcom);
|
||||
_userDataId = baseId != null ? baseId.GetMD5() : Id;
|
||||
}
|
||||
return _userDataId;
|
||||
}
|
||||
}
|
||||
|
||||
// Studio, Genre and Rating will all be the same so makes no sense to index by these
|
||||
protected override Dictionary<string, Func<User, IEnumerable<BaseItem>>> GetIndexByOptions()
|
||||
{
|
||||
return new Dictionary<string, Func<User, IEnumerable<BaseItem>>> {
|
||||
{LocalizedStrings.Instance.GetString("NoneDispPref"), null},
|
||||
{LocalizedStrings.Instance.GetString("PerformerDispPref"), GetIndexByPerformer},
|
||||
{LocalizedStrings.Instance.GetString("DirectorDispPref"), GetIndexByDirector},
|
||||
{LocalizedStrings.Instance.GetString("YearDispPref"), GetIndexByYear},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates ResolveArgs on demand
|
||||
/// </summary>
|
||||
/// <param name="pathInfo">The path info.</param>
|
||||
/// <returns>ItemResolveArgs.</returns>
|
||||
protected internal override ItemResolveArgs CreateResolveArgs(WIN32_FIND_DATA? pathInfo = null)
|
||||
{
|
||||
var args = base.CreateResolveArgs(pathInfo);
|
||||
|
||||
Season.AddMetadataFiles(args);
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
MediaBrowser.Controller/Entities/Trailer.cs
Normal file
51
MediaBrowser.Controller/Entities/Trailer.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Trailer
|
||||
/// </summary>
|
||||
public class Trailer : Video
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is local trailer.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is local trailer; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public bool IsLocalTrailer
|
||||
{
|
||||
get
|
||||
{
|
||||
// Local trailers are not part of children
|
||||
return Parent == null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should be overridden to return the proper folder where metadata lives
|
||||
/// </summary>
|
||||
/// <value>The meta location.</value>
|
||||
[IgnoreDataMember]
|
||||
public override string MetaLocation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsLocalTrailer)
|
||||
{
|
||||
return System.IO.Path.GetDirectoryName(Path);
|
||||
}
|
||||
|
||||
return base.MetaLocation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Needed because the resolver stops at the trailer folder and we find the video inside.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [use parent path to create resolve args]; otherwise, <c>false</c>.</value>
|
||||
protected override bool UseParentPathToCreateResolveArgs
|
||||
{
|
||||
get { return !IsLocalTrailer; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,446 @@
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class User : BaseEntity
|
||||
{
|
||||
public string Password { get; set; }
|
||||
|
||||
public string MaxParentalRating { get; set; }
|
||||
|
||||
public int RecentItemDays { get; set; }
|
||||
|
||||
public User()
|
||||
{
|
||||
RecentItemDays = 14;
|
||||
}
|
||||
|
||||
public DateTime? LastLoginDate { get; set; }
|
||||
public DateTime? LastActivityDate { get; set; }
|
||||
}
|
||||
}
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Common.Serialization;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class User
|
||||
/// </summary>
|
||||
public class User : BaseItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The _root folder path
|
||||
/// </summary>
|
||||
private string _rootFolderPath;
|
||||
/// <summary>
|
||||
/// Gets the root folder path.
|
||||
/// </summary>
|
||||
/// <value>The root folder path.</value>
|
||||
[IgnoreDataMember]
|
||||
public string RootFolderPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_rootFolderPath == null)
|
||||
{
|
||||
if (Configuration.UseCustomLibrary)
|
||||
{
|
||||
_rootFolderPath = GetRootFolderPath(Name);
|
||||
|
||||
if (!Directory.Exists(_rootFolderPath))
|
||||
{
|
||||
Directory.CreateDirectory(_rootFolderPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_rootFolderPath = Kernel.Instance.ApplicationPaths.DefaultUserViewsPath;
|
||||
}
|
||||
}
|
||||
return _rootFolderPath;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root folder path based on a given username
|
||||
/// </summary>
|
||||
/// <param name="username">The username.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetRootFolderPath(string username)
|
||||
{
|
||||
var safeFolderName = FileSystem.GetValidFilename(username);
|
||||
|
||||
return System.IO.Path.Combine(Kernel.Instance.ApplicationPaths.RootFolderPath, safeFolderName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
/// <value>The password.</value>
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public override string Path
|
||||
{
|
||||
get
|
||||
{
|
||||
// Return this so that metadata providers will look in here
|
||||
return ConfigurationDirectoryPath;
|
||||
}
|
||||
set
|
||||
{
|
||||
base.Path = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure this has a value
|
||||
/// </summary>
|
||||
/// <value>The display type of the media.</value>
|
||||
public override string DisplayMediaType
|
||||
{
|
||||
get
|
||||
{
|
||||
return base.DisplayMediaType ?? GetType().Name;
|
||||
}
|
||||
set
|
||||
{
|
||||
base.DisplayMediaType = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder
|
||||
/// </summary>
|
||||
private UserRootFolder _rootFolder;
|
||||
/// <summary>
|
||||
/// The _user root folder initialized
|
||||
/// </summary>
|
||||
private bool _userRootFolderInitialized;
|
||||
/// <summary>
|
||||
/// The _user root folder sync lock
|
||||
/// </summary>
|
||||
private object _userRootFolderSyncLock = new object();
|
||||
/// <summary>
|
||||
/// Gets the root folder.
|
||||
/// </summary>
|
||||
/// <value>The root folder.</value>
|
||||
[IgnoreDataMember]
|
||||
public UserRootFolder RootFolder
|
||||
{
|
||||
get
|
||||
{
|
||||
LazyInitializer.EnsureInitialized(ref _rootFolder, ref _userRootFolderInitialized, ref _userRootFolderSyncLock, () => (UserRootFolder)Kernel.Instance.LibraryManager.GetItem(RootFolderPath));
|
||||
return _rootFolder;
|
||||
}
|
||||
private set
|
||||
{
|
||||
_rootFolder = value;
|
||||
|
||||
if (_rootFolder == null)
|
||||
{
|
||||
_userRootFolderInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last login date.
|
||||
/// </summary>
|
||||
/// <value>The last login date.</value>
|
||||
public DateTime? LastLoginDate { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the last activity date.
|
||||
/// </summary>
|
||||
/// <value>The last activity date.</value>
|
||||
public DateTime? LastActivityDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _configuration
|
||||
/// </summary>
|
||||
private UserConfiguration _configuration;
|
||||
/// <summary>
|
||||
/// The _configuration initialized
|
||||
/// </summary>
|
||||
private bool _configurationInitialized;
|
||||
/// <summary>
|
||||
/// The _configuration sync lock
|
||||
/// </summary>
|
||||
private object _configurationSyncLock = new object();
|
||||
/// <summary>
|
||||
/// Gets the user's configuration
|
||||
/// </summary>
|
||||
/// <value>The configuration.</value>
|
||||
[IgnoreDataMember]
|
||||
public UserConfiguration Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
// Lazy load
|
||||
LazyInitializer.EnsureInitialized(ref _configuration, ref _configurationInitialized, ref _configurationSyncLock, () => XmlSerializer.GetXmlConfiguration<UserConfiguration>(ConfigurationFilePath));
|
||||
return _configuration;
|
||||
}
|
||||
private set
|
||||
{
|
||||
_configuration = value;
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
_configurationInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last date modified of the configuration
|
||||
/// </summary>
|
||||
/// <value>The configuration date last modified.</value>
|
||||
[IgnoreDataMember]
|
||||
public DateTime ConfigurationDateLastModified
|
||||
{
|
||||
get
|
||||
{
|
||||
// Ensure it's been lazy loaded
|
||||
var config = Configuration;
|
||||
|
||||
return File.GetLastWriteTimeUtc(ConfigurationFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the root media folder
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task ValidateMediaLibrary(IProgress<TaskProgress> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInfo("Validating media library for {0}", Name);
|
||||
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await RootFolder.ValidateChildren(progress, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates only the collection folders for a User and goes no further
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task ValidateCollectionFolders(IProgress<TaskProgress> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogInfo("Validating collection folders for {0}", Name);
|
||||
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await RootFolder.ValidateChildren(progress, cancellationToken, recursive: false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renames the user.
|
||||
/// </summary>
|
||||
/// <param name="newName">The new name.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
internal Task Rename(string newName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(newName))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
// If only the casing is changing, leave the file system alone
|
||||
if (!newName.Equals(Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Move configuration
|
||||
var newConfigDirectory = GetConfigurationDirectoryPath(newName);
|
||||
|
||||
// Exceptions will be thrown if these paths already exist
|
||||
if (Directory.Exists(newConfigDirectory))
|
||||
{
|
||||
Directory.Delete(newConfigDirectory, true);
|
||||
}
|
||||
Directory.Move(ConfigurationDirectoryPath, newConfigDirectory);
|
||||
|
||||
var customLibraryPath = GetRootFolderPath(Name);
|
||||
|
||||
// Move the root folder path if using a custom library
|
||||
if (Directory.Exists(customLibraryPath))
|
||||
{
|
||||
var newRootFolderPath = GetRootFolderPath(newName);
|
||||
if (Directory.Exists(newRootFolderPath))
|
||||
{
|
||||
Directory.Delete(newRootFolderPath, true);
|
||||
}
|
||||
Directory.Move(customLibraryPath, newRootFolderPath);
|
||||
}
|
||||
}
|
||||
|
||||
Name = newName;
|
||||
|
||||
// Force these to be lazy loaded again
|
||||
_configurationDirectoryPath = null;
|
||||
_rootFolderPath = null;
|
||||
RootFolder = null;
|
||||
|
||||
// Kick off a task to validate the media library
|
||||
Task.Run(() => ValidateMediaLibrary(new Progress<TaskProgress> { }, CancellationToken.None));
|
||||
|
||||
return RefreshMetadata(CancellationToken.None, forceSave: true, forceRefresh: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _configuration directory path
|
||||
/// </summary>
|
||||
private string _configurationDirectoryPath;
|
||||
/// <summary>
|
||||
/// Gets the path to the user's configuration directory
|
||||
/// </summary>
|
||||
/// <value>The configuration directory path.</value>
|
||||
private string ConfigurationDirectoryPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_configurationDirectoryPath == null)
|
||||
{
|
||||
_configurationDirectoryPath = GetConfigurationDirectoryPath(Name);
|
||||
|
||||
if (!Directory.Exists(_configurationDirectoryPath))
|
||||
{
|
||||
Directory.CreateDirectory(_configurationDirectoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
return _configurationDirectoryPath;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration directory path.
|
||||
/// </summary>
|
||||
/// <param name="username">The username.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetConfigurationDirectoryPath(string username)
|
||||
{
|
||||
var safeFolderName = FileSystem.GetValidFilename(username);
|
||||
|
||||
return System.IO.Path.Combine(Kernel.Instance.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the user's configuration file
|
||||
/// </summary>
|
||||
/// <value>The configuration file path.</value>
|
||||
private string ConfigurationFilePath
|
||||
{
|
||||
get
|
||||
{
|
||||
return System.IO.Path.Combine(ConfigurationDirectoryPath, "config.xml");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current configuration to the file system
|
||||
/// </summary>
|
||||
public void SaveConfiguration()
|
||||
{
|
||||
XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh metadata on us by execution our provider chain
|
||||
/// The item will be persisted if a change is made by a provider, or if it's new or changed.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="forceSave">if set to <c>true</c> [is new item].</param>
|
||||
/// <param name="forceRefresh">if set to <c>true</c> [force].</param>
|
||||
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
|
||||
/// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
|
||||
/// <returns>true if a provider reports we changed</returns>
|
||||
public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
|
||||
{
|
||||
if (resetResolveArgs)
|
||||
{
|
||||
ResolveArgs = null;
|
||||
}
|
||||
|
||||
var changed = await Kernel.Instance.ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders).ConfigureAwait(false);
|
||||
|
||||
if (changed || forceSave)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await Kernel.Instance.UserManager.UpdateUser(this).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">The config.</param>
|
||||
/// <exception cref="System.ArgumentNullException">config</exception>
|
||||
public void UpdateConfiguration(UserConfiguration config)
|
||||
{
|
||||
if (config == null)
|
||||
{
|
||||
throw new ArgumentNullException("config");
|
||||
}
|
||||
|
||||
var customLibraryChanged = config.UseCustomLibrary != Configuration.UseCustomLibrary;
|
||||
|
||||
Configuration = config;
|
||||
SaveConfiguration();
|
||||
|
||||
// Force these to be lazy loaded again
|
||||
if (customLibraryChanged)
|
||||
{
|
||||
_rootFolderPath = null;
|
||||
RootFolder = null;
|
||||
|
||||
if (config.UseCustomLibrary)
|
||||
{
|
||||
CopyDefaultLibraryPathsIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the default library paths if needed.
|
||||
/// </summary>
|
||||
private void CopyDefaultLibraryPathsIfNeeded()
|
||||
{
|
||||
var userPath = RootFolderPath;
|
||||
|
||||
var defaultPath = Kernel.Instance.ApplicationPaths.DefaultUserViewsPath;
|
||||
|
||||
if (userPath.Equals(defaultPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.EnumerateFileSystemEntries(userPath, "*.lnk", SearchOption.AllDirectories).Any())
|
||||
{
|
||||
FileSystem.CopyAll(defaultPath, userPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the password by clearing it.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
public Task ResetPassword()
|
||||
{
|
||||
return ChangePassword(string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the password.
|
||||
/// </summary>
|
||||
/// <param name="newPassword">The new password.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task ChangePassword(string newPassword)
|
||||
{
|
||||
Password = string.IsNullOrEmpty(newPassword) ? string.Empty : newPassword.GetMD5().ToString();
|
||||
|
||||
return Kernel.Instance.UserManager.UpdateUser(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,116 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class UserItemData
|
||||
{
|
||||
private float? _rating;
|
||||
/// <summary>
|
||||
/// Gets or sets the users 0-10 rating
|
||||
/// </summary>
|
||||
public float? Rating
|
||||
{
|
||||
get
|
||||
{
|
||||
return _rating;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
if (value.Value < 0 || value.Value > 10)
|
||||
{
|
||||
throw new InvalidOperationException("A 0-10 rating is required for UserItemData.");
|
||||
}
|
||||
}
|
||||
|
||||
_rating = value;
|
||||
}
|
||||
}
|
||||
|
||||
public long PlaybackPositionTicks { get; set; }
|
||||
|
||||
public int PlayCount { get; set; }
|
||||
|
||||
public bool IsFavorite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is an interpreted property to indicate likes or dislikes
|
||||
/// This should never be serialized.
|
||||
/// </summary>
|
||||
[IgnoreDataMember]
|
||||
public bool? Likes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Rating != null)
|
||||
{
|
||||
return Rating >= 6.5;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
Rating = value.Value ? 10 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Rating = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class UserItemData
|
||||
/// </summary>
|
||||
[ProtoContract]
|
||||
public class UserItemData
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
[ProtoMember(1)]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _rating
|
||||
/// </summary>
|
||||
private float? _rating;
|
||||
/// <summary>
|
||||
/// Gets or sets the users 0-10 rating
|
||||
/// </summary>
|
||||
/// <value>The rating.</value>
|
||||
/// <exception cref="System.ArgumentOutOfRangeException">A 0-10 rating is required for UserItemData.</exception>
|
||||
/// <exception cref="System.InvalidOperationException">A 0-10 rating is required for UserItemData.</exception>
|
||||
[ProtoMember(2)]
|
||||
public float? Rating
|
||||
{
|
||||
get
|
||||
{
|
||||
return _rating;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
if (value.Value < 0 || value.Value > 10)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("A 0-10 rating is required for UserItemData.");
|
||||
}
|
||||
}
|
||||
|
||||
_rating = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playback position ticks.
|
||||
/// </summary>
|
||||
/// <value>The playback position ticks.</value>
|
||||
[ProtoMember(3)]
|
||||
public long PlaybackPositionTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the play count.
|
||||
/// </summary>
|
||||
/// <value>The play count.</value>
|
||||
[ProtoMember(4)]
|
||||
public int PlayCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is favorite.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
|
||||
[ProtoMember(5)]
|
||||
public bool IsFavorite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last played date.
|
||||
/// </summary>
|
||||
/// <value>The last played date.</value>
|
||||
[ProtoMember(6)]
|
||||
public DateTime? LastPlayedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="UserItemData" /> is played.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
|
||||
[ProtoMember(7)]
|
||||
public bool Played { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is an interpreted property to indicate likes or dislikes
|
||||
/// This should never be serialized.
|
||||
/// </summary>
|
||||
/// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public bool? Likes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Rating != null)
|
||||
{
|
||||
return Rating >= 6.5;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
Rating = value.Value ? 10 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Rating = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
MediaBrowser.Controller/Entities/UserRootFolder.cs
Normal file
21
MediaBrowser.Controller/Entities/UserRootFolder.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Special class used for User Roots. Children contain actual ones defined for this user
|
||||
/// PLUS the virtual folders from the physical root (added by plug-ins).
|
||||
/// </summary>
|
||||
public class UserRootFolder : Folder
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the children of this folder from the actual file system
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{BaseItem}.</returns>
|
||||
protected override IEnumerable<BaseItem> GetNonCachedChildren()
|
||||
{
|
||||
return base.GetNonCachedChildren().Concat(Kernel.Instance.RootFolder.VirtualChildren);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,109 @@
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class Video : BaseItem
|
||||
{
|
||||
public VideoType VideoType { get; set; }
|
||||
|
||||
public List<SubtitleStream> Subtitles { get; set; }
|
||||
public List<AudioStream> AudioStreams { get; set; }
|
||||
|
||||
public int Height { get; set; }
|
||||
public int Width { get; set; }
|
||||
public string ScanType { get; set; }
|
||||
public float FrameRate { get; set; }
|
||||
public int BitRate { get; set; }
|
||||
public string Codec { get; set; }
|
||||
}
|
||||
}
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Video
|
||||
/// </summary>
|
||||
public class Video : BaseItem, IHasMediaStreams
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the video.
|
||||
/// </summary>
|
||||
/// <value>The type of the video.</value>
|
||||
public VideoType VideoType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the iso.
|
||||
/// </summary>
|
||||
/// <value>The type of the iso.</value>
|
||||
public IsoType? IsoType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format of the video.
|
||||
/// </summary>
|
||||
/// <value>The format of the video.</value>
|
||||
public VideoFormat VideoFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the media streams.
|
||||
/// </summary>
|
||||
/// <value>The media streams.</value>
|
||||
public List<MediaStream> MediaStreams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the chapters.
|
||||
/// </summary>
|
||||
/// <value>The chapters.</value>
|
||||
public List<ChapterInfo> Chapters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the video is a folder-rip, this will hold the file list for the largest playlist
|
||||
/// </summary>
|
||||
public List<string> PlayableStreamFileNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playable stream files.
|
||||
/// </summary>
|
||||
/// <returns>List{System.String}.</returns>
|
||||
public List<string> GetPlayableStreamFiles()
|
||||
{
|
||||
return GetPlayableStreamFiles(Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playable stream files.
|
||||
/// </summary>
|
||||
/// <param name="rootPath">The root path.</param>
|
||||
/// <returns>List{System.String}.</returns>
|
||||
public List<string> GetPlayableStreamFiles(string rootPath)
|
||||
{
|
||||
if (PlayableStreamFileNames == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var allFiles = Directory.EnumerateFiles(rootPath, "*", SearchOption.AllDirectories).ToList();
|
||||
|
||||
return PlayableStreamFileNames.Select(name => allFiles.FirstOrDefault(f => string.Equals(System.IO.Path.GetFileName(f), name, System.StringComparison.OrdinalIgnoreCase)))
|
||||
.Where(f => !string.IsNullOrEmpty(f))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default video stream for this video. Use this to determine media info for this item.
|
||||
/// </summary>
|
||||
/// <value>The default video stream.</value>
|
||||
[IgnoreDataMember]
|
||||
public MediaStream DefaultVideoStream
|
||||
{
|
||||
get { return MediaStreams != null ? MediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video) : null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [is3 D].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value>
|
||||
[IgnoreDataMember]
|
||||
public bool Is3D
|
||||
{
|
||||
get { return VideoFormat > 0; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the media.
|
||||
/// </summary>
|
||||
/// <value>The type of the media.</value>
|
||||
public override string MediaType
|
||||
{
|
||||
get
|
||||
{
|
||||
return Model.Entities.MediaType.Video;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class Year : BaseEntity
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Year
|
||||
/// </summary>
|
||||
public class Year : BaseItem
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Common.Serialization;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.FFMpeg
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs FFProbe against a media file and returns metadata.
|
||||
/// </summary>
|
||||
public static class FFProbe
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs FFProbe against an Audio file, caches the result and returns the output
|
||||
/// </summary>
|
||||
public static FFProbeResult Run(BaseItem item, string cacheDirectory)
|
||||
{
|
||||
string cachePath = GetFfProbeCachePath(item, cacheDirectory);
|
||||
|
||||
// Use try catch to avoid having to use File.Exists
|
||||
try
|
||||
{
|
||||
return GetCachedResult(cachePath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogException(ex);
|
||||
}
|
||||
|
||||
FFProbeResult result = Run(item.Path);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
// Fire and forget
|
||||
CacheResult(result, cachePath);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cached result of an FFProbe operation
|
||||
/// </summary>
|
||||
private static FFProbeResult GetCachedResult(string path)
|
||||
{
|
||||
return ProtobufSerializer.DeserializeFromFile<FFProbeResult>(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches the result of an FFProbe operation
|
||||
/// </summary>
|
||||
private static async void CacheResult(FFProbeResult result, string outputCachePath)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ProtobufSerializer.SerializeToFile(result, outputCachePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogException(ex);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static FFProbeResult Run(string input)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo { };
|
||||
|
||||
startInfo.CreateNoWindow = true;
|
||||
|
||||
startInfo.UseShellExecute = false;
|
||||
|
||||
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
||||
startInfo.RedirectStandardOutput = true;
|
||||
startInfo.RedirectStandardError = true;
|
||||
|
||||
startInfo.FileName = Kernel.Instance.ApplicationPaths.FFProbePath;
|
||||
startInfo.WorkingDirectory = Kernel.Instance.ApplicationPaths.FFMpegDirectory;
|
||||
startInfo.Arguments = string.Format("\"{0}\" -v quiet -print_format json -show_streams -show_format", input);
|
||||
|
||||
//Logger.LogInfo(startInfo.FileName + " " + startInfo.Arguments);
|
||||
|
||||
var process = new Process { };
|
||||
process.StartInfo = startInfo;
|
||||
|
||||
process.EnableRaisingEvents = true;
|
||||
|
||||
process.Exited += ProcessExited;
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
|
||||
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
|
||||
// If we ever decide to disable the ffmpeg log then you must uncomment the below line.
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
return JsonSerializer.DeserializeFromStream<FFProbeResult>(process.StandardOutput.BaseStream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogException(ex);
|
||||
|
||||
// Hate having to do this
|
||||
try
|
||||
{
|
||||
process.Kill();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void ProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
(sender as Process).Dispose();
|
||||
}
|
||||
|
||||
private static string GetFfProbeCachePath(BaseItem item, string cacheDirectory)
|
||||
{
|
||||
string outputDirectory = Path.Combine(cacheDirectory, item.Id.ToString().Substring(0, 1));
|
||||
|
||||
return Path.Combine(outputDirectory, item.Id + "-" + item.DateModified.Ticks + ".pb");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using ProtoBuf;
|
||||
|
||||
namespace MediaBrowser.Controller.FFMpeg
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a class that we can use to deserialize the ffprobe json output
|
||||
/// Sample output:
|
||||
/// http://stackoverflow.com/questions/7708373/get-ffmpeg-information-in-friendly-way
|
||||
/// </summary>
|
||||
[ProtoContract]
|
||||
public class FFProbeResult
|
||||
{
|
||||
[ProtoMember(1)]
|
||||
public MediaStream[] streams { get; set; }
|
||||
|
||||
[ProtoMember(2)]
|
||||
public MediaFormat format { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stream within the output
|
||||
/// A number of properties are commented out to improve deserialization performance
|
||||
/// Enable them as needed.
|
||||
/// </summary>
|
||||
[ProtoContract]
|
||||
public class MediaStream
|
||||
{
|
||||
[ProtoMember(1)]
|
||||
public int index { get; set; }
|
||||
|
||||
[ProtoMember(2)]
|
||||
public string profile { get; set; }
|
||||
|
||||
[ProtoMember(3)]
|
||||
public string codec_name { get; set; }
|
||||
|
||||
[ProtoMember(4)]
|
||||
public string codec_long_name { get; set; }
|
||||
|
||||
[ProtoMember(5)]
|
||||
public string codec_type { get; set; }
|
||||
|
||||
//public string codec_time_base { get; set; }
|
||||
//public string codec_tag { get; set; }
|
||||
//public string codec_tag_string { get; set; }
|
||||
//public string sample_fmt { get; set; }
|
||||
|
||||
[ProtoMember(6)]
|
||||
public string sample_rate { get; set; }
|
||||
|
||||
[ProtoMember(7)]
|
||||
public int channels { get; set; }
|
||||
|
||||
//public int bits_per_sample { get; set; }
|
||||
//public string r_frame_rate { get; set; }
|
||||
|
||||
[ProtoMember(8)]
|
||||
public string avg_frame_rate { get; set; }
|
||||
|
||||
//public string time_base { get; set; }
|
||||
//public string start_time { get; set; }
|
||||
|
||||
[ProtoMember(9)]
|
||||
public string duration { get; set; }
|
||||
|
||||
[ProtoMember(10)]
|
||||
public string bit_rate { get; set; }
|
||||
|
||||
[ProtoMember(11)]
|
||||
public int width { get; set; }
|
||||
|
||||
[ProtoMember(12)]
|
||||
public int height { get; set; }
|
||||
|
||||
//public int has_b_frames { get; set; }
|
||||
//public string sample_aspect_ratio { get; set; }
|
||||
|
||||
[ProtoMember(13)]
|
||||
public string display_aspect_ratio { get; set; }
|
||||
|
||||
//public string pix_fmt { get; set; }
|
||||
//public int level { get; set; }
|
||||
|
||||
[ProtoMember(14)]
|
||||
public Dictionary<string, string> tags { get; set; }
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
public class MediaFormat
|
||||
{
|
||||
[ProtoMember(1)]
|
||||
public string filename { get; set; }
|
||||
|
||||
[ProtoMember(2)]
|
||||
public int nb_streams { get; set; }
|
||||
|
||||
[ProtoMember(3)]
|
||||
public string format_name { get; set; }
|
||||
|
||||
[ProtoMember(4)]
|
||||
public string format_long_name { get; set; }
|
||||
|
||||
[ProtoMember(5)]
|
||||
public string start_time { get; set; }
|
||||
|
||||
[ProtoMember(6)]
|
||||
public string duration { get; set; }
|
||||
|
||||
[ProtoMember(7)]
|
||||
public string size { get; set; }
|
||||
|
||||
[ProtoMember(8)]
|
||||
public string bit_rate { get; set; }
|
||||
|
||||
[ProtoMember(9)]
|
||||
public Dictionary<string, string> tags { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
84ac1c51e84cfbfb20e7b96c9f1a4442a8cfadf2
|
||||
@@ -1 +0,0 @@
|
||||
331e241e29f1b015e303b301c17c37883e39f39d
|
||||
@@ -1,3 +0,0 @@
|
||||
This is the 32-bit static build of ffmpeg, located at:
|
||||
|
||||
http://ffmpeg.zeranoe.com/builds/
|
||||
@@ -1,172 +1,525 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.IO
|
||||
{
|
||||
public class DirectoryWatchers
|
||||
{
|
||||
private readonly List<FileSystemWatcher> FileSystemWatchers = new List<FileSystemWatcher>();
|
||||
private Timer updateTimer;
|
||||
private List<string> affectedPaths = new List<string>();
|
||||
|
||||
private const int TimerDelayInSeconds = 30;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
var pathsToWatch = new List<string>();
|
||||
|
||||
var rootFolder = Kernel.Instance.RootFolder;
|
||||
|
||||
pathsToWatch.Add(rootFolder.Path);
|
||||
|
||||
foreach (Folder folder in rootFolder.Children.OfType<Folder>())
|
||||
{
|
||||
foreach (string path in folder.PhysicalLocations)
|
||||
{
|
||||
if (Path.IsPathRooted(path) && !pathsToWatch.ContainsParentFolder(path))
|
||||
{
|
||||
pathsToWatch.Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string path in pathsToWatch)
|
||||
{
|
||||
Logger.LogInfo("Watching directory " + path + " for changes.");
|
||||
|
||||
var watcher = new FileSystemWatcher(path, "*") { };
|
||||
watcher.IncludeSubdirectories = true;
|
||||
|
||||
//watcher.Changed += watcher_Changed;
|
||||
|
||||
// All the others seem to trigger change events on the parent, so let's keep it simple for now.
|
||||
// Actually, we really need to only watch created, deleted and renamed as changed fires too much -ebr
|
||||
watcher.Created += watcher_Changed;
|
||||
watcher.Deleted += watcher_Changed;
|
||||
watcher.Renamed += watcher_Changed;
|
||||
|
||||
watcher.EnableRaisingEvents = true;
|
||||
FileSystemWatchers.Add(watcher);
|
||||
}
|
||||
}
|
||||
|
||||
void watcher_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
Logger.LogDebugInfo("****** Watcher sees change of type " + e.ChangeType.ToString() + " to " + e.FullPath);
|
||||
lock (affectedPaths)
|
||||
{
|
||||
//Since we're watching created, deleted and renamed we always want the parent of the item to be the affected path
|
||||
var affectedPath = Path.GetDirectoryName(e.FullPath);
|
||||
|
||||
if (e.ChangeType == WatcherChangeTypes.Renamed)
|
||||
{
|
||||
var renamedArgs = e as RenamedEventArgs;
|
||||
if (affectedPaths.Contains(renamedArgs.OldFullPath))
|
||||
{
|
||||
Logger.LogDebugInfo("****** Removing " + renamedArgs.OldFullPath + " from affected paths.");
|
||||
affectedPaths.Remove(renamedArgs.OldFullPath);
|
||||
}
|
||||
}
|
||||
|
||||
//If anything underneath this path was already marked as affected - remove it as it will now get captured by this one
|
||||
affectedPaths.RemoveAll(p => p.StartsWith(e.FullPath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!affectedPaths.ContainsParentFolder(affectedPath))
|
||||
{
|
||||
Logger.LogDebugInfo("****** Adding " + affectedPath + " to affected paths.");
|
||||
affectedPaths.Add(affectedPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateTimer == null)
|
||||
{
|
||||
updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(TimerDelayInSeconds), TimeSpan.FromMilliseconds(-1));
|
||||
}
|
||||
else
|
||||
{
|
||||
updateTimer.Change(TimeSpan.FromSeconds(TimerDelayInSeconds), TimeSpan.FromMilliseconds(-1));
|
||||
}
|
||||
}
|
||||
|
||||
private async void TimerStopped(object stateInfo)
|
||||
{
|
||||
updateTimer.Dispose();
|
||||
updateTimer = null;
|
||||
List<string> paths;
|
||||
lock (affectedPaths)
|
||||
{
|
||||
paths = affectedPaths;
|
||||
affectedPaths = new List<string>();
|
||||
}
|
||||
|
||||
await ProcessPathChanges(paths).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task ProcessPathChanges(IEnumerable<string> paths)
|
||||
{
|
||||
var itemsToRefresh = new List<BaseItem>();
|
||||
|
||||
foreach (BaseItem item in paths.Select(p => GetAffectedBaseItem(p)))
|
||||
{
|
||||
if (item != null && !itemsToRefresh.Contains(item))
|
||||
{
|
||||
itemsToRefresh.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsToRefresh.Any(i =>
|
||||
{
|
||||
var folder = i as Folder;
|
||||
|
||||
return folder != null && folder.IsRoot;
|
||||
}))
|
||||
{
|
||||
return Kernel.Instance.ReloadRoot();
|
||||
}
|
||||
|
||||
foreach (var p in paths) Logger.LogDebugInfo("********* "+ p + " reports change.");
|
||||
foreach (var i in itemsToRefresh) Logger.LogDebugInfo("********* "+i.Name + " ("+ i.Path + ") will be refreshed.");
|
||||
return Task.WhenAll(itemsToRefresh.Select(i => i.ChangedExternally()));
|
||||
}
|
||||
|
||||
private BaseItem GetAffectedBaseItem(string path)
|
||||
{
|
||||
BaseItem item = null;
|
||||
|
||||
while (item == null && !string.IsNullOrEmpty(path))
|
||||
{
|
||||
item = Kernel.Instance.RootFolder.FindByPath(path);
|
||||
|
||||
path = Path.GetDirectoryName(path);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
foreach (FileSystemWatcher watcher in FileSystemWatchers)
|
||||
{
|
||||
watcher.Changed -= watcher_Changed;
|
||||
watcher.EnableRaisingEvents = false;
|
||||
watcher.Dispose();
|
||||
}
|
||||
|
||||
if (updateTimer != null)
|
||||
{
|
||||
updateTimer.Dispose();
|
||||
updateTimer = null;
|
||||
}
|
||||
|
||||
FileSystemWatchers.Clear();
|
||||
affectedPaths.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.ScheduledTasks;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Class DirectoryWatchers
|
||||
/// </summary>
|
||||
public class DirectoryWatchers : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The file system watchers
|
||||
/// </summary>
|
||||
private ConcurrentBag<FileSystemWatcher> FileSystemWatchers = new ConcurrentBag<FileSystemWatcher>();
|
||||
/// <summary>
|
||||
/// The update timer
|
||||
/// </summary>
|
||||
private Timer updateTimer;
|
||||
/// <summary>
|
||||
/// The affected paths
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, string> affectedPaths = new ConcurrentDictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string,string> TempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// The timer lock
|
||||
/// </summary>
|
||||
private readonly object timerLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
public void TemporarilyIgnore(string path)
|
||||
{
|
||||
TempIgnoredPaths[path] = path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the temp ignore.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
public void RemoveTempIgnore(string path)
|
||||
{
|
||||
string val;
|
||||
TempIgnoredPaths.TryRemove(path, out val);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logger.
|
||||
/// </summary>
|
||||
/// <value>The logger.</value>
|
||||
private ILogger Logger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DirectoryWatchers" /> class.
|
||||
/// </summary>
|
||||
public DirectoryWatchers()
|
||||
{
|
||||
Logger = LogManager.GetLogger(GetType().Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts this instance.
|
||||
/// </summary>
|
||||
internal void Start()
|
||||
{
|
||||
Kernel.Instance.LibraryManager.LibraryChanged += Instance_LibraryChanged;
|
||||
|
||||
var pathsToWatch = new List<string> { Kernel.Instance.RootFolder.Path };
|
||||
|
||||
var paths = Kernel.Instance.RootFolder.Children.OfType<Folder>()
|
||||
.SelectMany(f =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Accessing ResolveArgs could involve file system access
|
||||
return f.ResolveArgs.PhysicalLocations;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return new string[] {};
|
||||
}
|
||||
|
||||
})
|
||||
.Where(Path.IsPathRooted);
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (!ContainsParentFolder(pathsToWatch, path))
|
||||
{
|
||||
pathsToWatch.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var path in pathsToWatch)
|
||||
{
|
||||
StartWatchingPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Examine a list of strings assumed to be file paths to see if it contains a parent of
|
||||
/// the provided path.
|
||||
/// </summary>
|
||||
/// <param name="lst">The LST.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">path</exception>
|
||||
private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
throw new ArgumentNullException("path");
|
||||
}
|
||||
|
||||
path = path.TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
return lst.Any(str =>
|
||||
{
|
||||
//this should be a little quicker than examining each actual parent folder...
|
||||
var compare = str.TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
return (path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the watching path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
private void StartWatchingPath(string path)
|
||||
{
|
||||
// Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel
|
||||
Task.Run(() =>
|
||||
{
|
||||
var newWatcher = new FileSystemWatcher(path, "*") { IncludeSubdirectories = true, InternalBufferSize = 32767 };
|
||||
|
||||
newWatcher.Created += watcher_Changed;
|
||||
newWatcher.Deleted += watcher_Changed;
|
||||
newWatcher.Renamed += watcher_Changed;
|
||||
newWatcher.Changed += watcher_Changed;
|
||||
|
||||
newWatcher.Error += watcher_Error;
|
||||
|
||||
try
|
||||
{
|
||||
newWatcher.EnableRaisingEvents = true;
|
||||
FileSystemWatchers.Add(newWatcher);
|
||||
|
||||
Logger.Info("Watching directory " + path);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.ErrorException("Error watching path: {0}", ex, path);
|
||||
}
|
||||
catch (PlatformNotSupportedException ex)
|
||||
{
|
||||
Logger.ErrorException("Error watching path: {0}", ex, path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the watching path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
private void StopWatchingPath(string path)
|
||||
{
|
||||
var watcher = FileSystemWatchers.FirstOrDefault(f => f.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (watcher != null)
|
||||
{
|
||||
DisposeWatcher(watcher);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the watcher.
|
||||
/// </summary>
|
||||
/// <param name="watcher">The watcher.</param>
|
||||
private void DisposeWatcher(FileSystemWatcher watcher)
|
||||
{
|
||||
Logger.Info("Stopping directory watching for path {0}", watcher.Path);
|
||||
|
||||
watcher.EnableRaisingEvents = false;
|
||||
watcher.Dispose();
|
||||
|
||||
var watchers = FileSystemWatchers.ToList();
|
||||
|
||||
watchers.Remove(watcher);
|
||||
|
||||
FileSystemWatchers = new ConcurrentBag<FileSystemWatcher>(watchers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the LibraryChanged event of the Kernel
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="Library.ChildrenChangedEventArgs" /> instance containing the event data.</param>
|
||||
void Instance_LibraryChanged(object sender, ChildrenChangedEventArgs e)
|
||||
{
|
||||
if (e.Folder is AggregateFolder && e.HasAddOrRemoveChange)
|
||||
{
|
||||
if (e.ItemsRemoved != null)
|
||||
{
|
||||
foreach (var item in e.ItemsRemoved.OfType<Folder>())
|
||||
{
|
||||
StopWatchingPath(item.Path);
|
||||
}
|
||||
}
|
||||
if (e.ItemsAdded != null)
|
||||
{
|
||||
foreach (var item in e.ItemsAdded.OfType<Folder>())
|
||||
{
|
||||
StartWatchingPath(item.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Error event of the watcher control.
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
|
||||
async void watcher_Error(object sender, ErrorEventArgs e)
|
||||
{
|
||||
var ex = e.GetException();
|
||||
var dw = (FileSystemWatcher) sender;
|
||||
|
||||
Logger.ErrorException("Error in Directory watcher for: "+dw.Path, ex);
|
||||
|
||||
if (ex.Message.Contains("network name is no longer available"))
|
||||
{
|
||||
//Network either dropped or, we are coming out of sleep and it hasn't reconnected yet - wait and retry
|
||||
Logger.Warn("Network connection lost - will retry...");
|
||||
var retries = 0;
|
||||
var success = false;
|
||||
while (!success && retries < 10)
|
||||
{
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
dw.EnableRaisingEvents = false;
|
||||
dw.EnableRaisingEvents = true;
|
||||
success = true;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
Logger.Warn("Network still unavailable...");
|
||||
retries++;
|
||||
}
|
||||
}
|
||||
if (!success)
|
||||
{
|
||||
Logger.Warn("Unable to access network. Giving up.");
|
||||
DisposeWatcher(dw);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ex.Message.Contains("BIOS command limit"))
|
||||
{
|
||||
Logger.Info("Attempting to re-start watcher.");
|
||||
|
||||
dw.EnableRaisingEvents = false;
|
||||
dw.EnableRaisingEvents = true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Changed event of the watcher control.
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
|
||||
void watcher_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (e.ChangeType == WatcherChangeTypes.Created && e.Name == "New folder")
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (TempIgnoredPaths.ContainsKey(e.FullPath))
|
||||
{
|
||||
Logger.Info("Watcher requested to ignore change to " + e.FullPath);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Info("Watcher sees change of type " + e.ChangeType.ToString() + " to " + e.FullPath);
|
||||
|
||||
//Since we're watching created, deleted and renamed we always want the parent of the item to be the affected path
|
||||
var affectedPath = e.FullPath;
|
||||
|
||||
affectedPaths.AddOrUpdate(affectedPath, affectedPath, (key, oldValue) => affectedPath);
|
||||
|
||||
lock (timerLock)
|
||||
{
|
||||
if (updateTimer == null)
|
||||
{
|
||||
updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(Kernel.Instance.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1));
|
||||
}
|
||||
else
|
||||
{
|
||||
updateTimer.Change(TimeSpan.FromSeconds(Kernel.Instance.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timers the stopped.
|
||||
/// </summary>
|
||||
/// <param name="stateInfo">The state info.</param>
|
||||
private async void TimerStopped(object stateInfo)
|
||||
{
|
||||
lock (timerLock)
|
||||
{
|
||||
// Extend the timer as long as any of the paths are still being written to.
|
||||
if (affectedPaths.Any(p => IsFileLocked(p.Key)))
|
||||
{
|
||||
Logger.Info("Timer extended.");
|
||||
updateTimer.Change(TimeSpan.FromSeconds(Kernel.Instance.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1));
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Info("Timer stopped.");
|
||||
|
||||
updateTimer.Dispose();
|
||||
updateTimer = null;
|
||||
}
|
||||
|
||||
var paths = affectedPaths.Keys.ToList();
|
||||
affectedPaths.Clear();
|
||||
|
||||
await ProcessPathChanges(paths).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try and determine if a file is locked
|
||||
/// This is not perfect, and is subject to race conditions, so I'd rather not make this a re-usable library method.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns><c>true</c> if [is file locked] [the specified path]; otherwise, <c>false</c>.</returns>
|
||||
private bool IsFileLocked(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = FileSystem.GetFileData(path);
|
||||
|
||||
if (!data.HasValue || data.Value.IsDirectory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FileStream stream = null;
|
||||
|
||||
try
|
||||
{
|
||||
stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
|
||||
}
|
||||
catch
|
||||
{
|
||||
//the file is unavailable because it is:
|
||||
//still being written to
|
||||
//or being processed by another thread
|
||||
//or does not exist (has already been processed)
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (stream != null)
|
||||
stream.Close();
|
||||
}
|
||||
|
||||
//file is not locked
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the path changes.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task ProcessPathChanges(List<string> paths)
|
||||
{
|
||||
var itemsToRefresh = paths.Select(Path.GetDirectoryName)
|
||||
.Select(GetAffectedBaseItem)
|
||||
.Where(item => item != null)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
foreach (var p in paths) Logger.Info(p + " reports change.");
|
||||
|
||||
// If the root folder changed, run the library task so the user can see it
|
||||
if (itemsToRefresh.Any(i => i is AggregateFolder))
|
||||
{
|
||||
Kernel.Instance.TaskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.WhenAll(itemsToRefresh.Select(i => Task.Run(async () =>
|
||||
{
|
||||
Logger.Info(i.Name + " (" + i.Path + ") will be refreshed.");
|
||||
|
||||
try
|
||||
{
|
||||
await i.ChangedExternally().ConfigureAwait(false);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
// For now swallow and log.
|
||||
// Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
|
||||
// Should we remove it from it's parent?
|
||||
Logger.ErrorException("Error refreshing {0}", ex, i.Name);
|
||||
}
|
||||
|
||||
}))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the affected base item.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
private BaseItem GetAffectedBaseItem(string path)
|
||||
{
|
||||
BaseItem item = null;
|
||||
|
||||
while (item == null && !string.IsNullOrEmpty(path))
|
||||
{
|
||||
item = Kernel.Instance.RootFolder.FindByPath(path);
|
||||
|
||||
path = Path.GetDirectoryName(path);
|
||||
}
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
// If the item has been deleted find the first valid parent that still exists
|
||||
while (!Directory.Exists(item.Path) && !File.Exists(item.Path))
|
||||
{
|
||||
item = item.Parent;
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops this instance.
|
||||
/// </summary>
|
||||
private void Stop()
|
||||
{
|
||||
Kernel.Instance.LibraryManager.LibraryChanged -= Instance_LibraryChanged;
|
||||
|
||||
FileSystemWatcher watcher;
|
||||
|
||||
while (FileSystemWatchers.TryTake(out watcher))
|
||||
{
|
||||
watcher.Changed -= watcher_Changed;
|
||||
watcher.EnableRaisingEvents = false;
|
||||
watcher.Dispose();
|
||||
}
|
||||
|
||||
lock (timerLock)
|
||||
{
|
||||
if (updateTimer != null)
|
||||
{
|
||||
updateTimer.Dispose();
|
||||
updateTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
affectedPaths.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,251 +1,130 @@
|
||||
using MediaBrowser.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MediaBrowser.Controller.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides low level File access that is much faster than the File/Directory api's
|
||||
/// </summary>
|
||||
public static class FileData
|
||||
{
|
||||
public const int MAX_PATH = 260;
|
||||
public const int MAX_ALTERNATE = 14;
|
||||
public static IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets information about a path
|
||||
/// </summary>
|
||||
public static WIN32_FIND_DATA GetFileData(string path)
|
||||
{
|
||||
WIN32_FIND_DATA data;
|
||||
IntPtr handle = FindFirstFile(path, out data);
|
||||
bool getFilename = false;
|
||||
|
||||
if (handle == INVALID_HANDLE_VALUE && !Path.HasExtension(path))
|
||||
{
|
||||
if (!path.EndsWith("*"))
|
||||
{
|
||||
Logger.LogInfo("Handle came back invalid for {0}. Since this is a directory we'll try appending \\*.", path);
|
||||
|
||||
FindClose(handle);
|
||||
|
||||
handle = FindFirstFile(Path.Combine(path, "*"), out data);
|
||||
|
||||
getFilename = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
throw new IOException("FindFirstFile failed");
|
||||
}
|
||||
|
||||
if (getFilename)
|
||||
{
|
||||
data.cFileName = Path.GetFileName(path);
|
||||
}
|
||||
|
||||
FindClose(handle);
|
||||
|
||||
data.Path = path;
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all file system entries within a foler
|
||||
/// </summary>
|
||||
public static IEnumerable<WIN32_FIND_DATA> GetFileSystemEntries(string path, string searchPattern)
|
||||
{
|
||||
return GetFileSystemEntries(path, searchPattern, true, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all files within a folder
|
||||
/// </summary>
|
||||
public static IEnumerable<WIN32_FIND_DATA> GetFiles(string path, string searchPattern)
|
||||
{
|
||||
return GetFileSystemEntries(path, searchPattern, true, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all sub-directories within a folder
|
||||
/// </summary>
|
||||
public static IEnumerable<WIN32_FIND_DATA> GetDirectories(string path, string searchPattern)
|
||||
{
|
||||
return GetFileSystemEntries(path, searchPattern, false, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all file system entries within a foler
|
||||
/// </summary>
|
||||
public static IEnumerable<WIN32_FIND_DATA> GetFileSystemEntries(string path, string searchPattern, bool includeFiles, bool includeDirectories)
|
||||
{
|
||||
string lpFileName = Path.Combine(path, searchPattern);
|
||||
|
||||
WIN32_FIND_DATA lpFindFileData;
|
||||
var handle = FindFirstFile(lpFileName, out lpFindFileData);
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
int hr = Marshal.GetLastWin32Error();
|
||||
if (hr != 2 && hr != 0x12)
|
||||
{
|
||||
throw new IOException("GetFileSystemEntries failed");
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (IncludeInOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories))
|
||||
{
|
||||
yield return lpFindFileData;
|
||||
}
|
||||
|
||||
while (FindNextFile(handle, out lpFindFileData) != IntPtr.Zero)
|
||||
{
|
||||
if (IncludeInOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories))
|
||||
{
|
||||
lpFindFileData.Path = Path.Combine(path, lpFindFileData.cFileName);
|
||||
yield return lpFindFileData;
|
||||
}
|
||||
}
|
||||
|
||||
FindClose(handle);
|
||||
}
|
||||
|
||||
private static bool IncludeInOutput(string cFileName, FileAttributes attributes, bool includeFiles, bool includeDirectories)
|
||||
{
|
||||
if (cFileName.Equals(".", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (cFileName.Equals("..", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!includeFiles && !attributes.HasFlag(FileAttributes.Directory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!includeDirectories && attributes.HasFlag(FileAttributes.Directory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern IntPtr FindFirstFile(string fileName, out WIN32_FIND_DATA data);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
private static extern IntPtr FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA data);
|
||||
|
||||
[DllImport("kernel32")]
|
||||
private static extern bool FindClose(IntPtr hFindFile);
|
||||
|
||||
private const char SpaceChar = ' ';
|
||||
private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars();
|
||||
|
||||
/// <summary>
|
||||
/// Takes a filename and removes invalid characters
|
||||
/// </summary>
|
||||
public static string GetValidFilename(string filename)
|
||||
{
|
||||
foreach (char c in InvalidFileNameChars)
|
||||
{
|
||||
filename = filename.Replace(c, SpaceChar);
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FILETIME
|
||||
{
|
||||
public uint dwLowDateTime;
|
||||
public uint dwHighDateTime;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public struct WIN32_FIND_DATA
|
||||
{
|
||||
public FileAttributes dwFileAttributes;
|
||||
public FILETIME ftCreationTime;
|
||||
public FILETIME ftLastAccessTime;
|
||||
public FILETIME ftLastWriteTime;
|
||||
public int nFileSizeHigh;
|
||||
public int nFileSizeLow;
|
||||
public int dwReserved0;
|
||||
public int dwReserved1;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = FileData.MAX_PATH)]
|
||||
public string cFileName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = FileData.MAX_ALTERNATE)]
|
||||
public string cAlternate;
|
||||
|
||||
public bool IsHidden
|
||||
{
|
||||
get
|
||||
{
|
||||
return dwFileAttributes.HasFlag(FileAttributes.Hidden);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSystemFile
|
||||
{
|
||||
get
|
||||
{
|
||||
return dwFileAttributes.HasFlag(FileAttributes.System);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return dwFileAttributes.HasFlag(FileAttributes.Directory);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime CreationTimeUtc
|
||||
{
|
||||
get
|
||||
{
|
||||
return ParseFileTime(ftCreationTime);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime LastAccessTimeUtc
|
||||
{
|
||||
get
|
||||
{
|
||||
return ParseFileTime(ftLastAccessTime);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime LastWriteTimeUtc
|
||||
{
|
||||
get
|
||||
{
|
||||
return ParseFileTime(ftLastWriteTime);
|
||||
}
|
||||
}
|
||||
|
||||
private DateTime ParseFileTime(FILETIME filetime)
|
||||
{
|
||||
long highBits = filetime.dwHighDateTime;
|
||||
highBits = highBits << 32;
|
||||
return DateTime.FromFileTimeUtc(highBits + (long)filetime.dwLowDateTime);
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Common.Win32;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MediaBrowser.Controller.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides low level File access that is much faster than the File/Directory api's
|
||||
/// </summary>
|
||||
public static class FileData
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all file system entries within a foler
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="searchPattern">The search pattern.</param>
|
||||
/// <param name="includeFiles">if set to <c>true</c> [include files].</param>
|
||||
/// <param name="includeDirectories">if set to <c>true</c> [include directories].</param>
|
||||
/// <param name="flattenFolderDepth">The flatten folder depth.</param>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Dictionary{System.StringWIN32_FIND_DATA}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
/// <exception cref="System.IO.IOException">GetFileSystemEntries failed</exception>
|
||||
public static Dictionary<string, WIN32_FIND_DATA> GetFilteredFileSystemEntries(string path, string searchPattern = "*", bool includeFiles = true, bool includeDirectories = true, int flattenFolderDepth = 0, ItemResolveArgs args = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
var lpFileName = Path.Combine(path, searchPattern);
|
||||
|
||||
WIN32_FIND_DATA lpFindFileData;
|
||||
var handle = NativeMethods.FindFirstFileEx(lpFileName, FINDEX_INFO_LEVELS.FindExInfoBasic, out lpFindFileData,
|
||||
FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, FindFirstFileExFlags.FIND_FIRST_EX_LARGE_FETCH);
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
int hr = Marshal.GetLastWin32Error();
|
||||
if (hr != 2 && hr != 0x12)
|
||||
{
|
||||
throw new IOException("GetFileSystemEntries failed");
|
||||
}
|
||||
return new Dictionary<string, WIN32_FIND_DATA>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var dict = new Dictionary<string, WIN32_FIND_DATA>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (FileSystem.IncludeInFindFileOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(lpFindFileData.cFileName))
|
||||
{
|
||||
lpFindFileData.Path = Path.Combine(path, lpFindFileData.cFileName);
|
||||
|
||||
dict[lpFindFileData.Path] = lpFindFileData;
|
||||
}
|
||||
}
|
||||
|
||||
while (NativeMethods.FindNextFile(handle, out lpFindFileData) != IntPtr.Zero)
|
||||
{
|
||||
// This is the one circumstance where we can completely disregard a file
|
||||
if (lpFindFileData.IsSystemFile)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter out invalid entries
|
||||
if (lpFindFileData.cFileName.Equals(".", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (lpFindFileData.cFileName.Equals("..", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lpFindFileData.Path = Path.Combine(path, lpFindFileData.cFileName);
|
||||
|
||||
if (FileSystem.IsShortcut(lpFindFileData.Path))
|
||||
{
|
||||
var newPath = FileSystem.ResolveShortcut(lpFindFileData.Path);
|
||||
if (string.IsNullOrWhiteSpace(newPath))
|
||||
{
|
||||
//invalid shortcut - could be old or target could just be unavailable
|
||||
Logger.LogWarning("Encountered invalid shortuct: "+lpFindFileData.Path);
|
||||
continue;
|
||||
}
|
||||
var data = FileSystem.GetFileData(newPath);
|
||||
|
||||
if (data.HasValue)
|
||||
{
|
||||
lpFindFileData = data.Value;
|
||||
|
||||
// Find out if the shortcut is pointing to a directory or file
|
||||
if (lpFindFileData.IsDirectory)
|
||||
{
|
||||
// add to our physical locations
|
||||
if (args != null)
|
||||
{
|
||||
args.AddAdditionalLocation(newPath);
|
||||
}
|
||||
}
|
||||
|
||||
dict[lpFindFileData.Path] = lpFindFileData;
|
||||
}
|
||||
}
|
||||
else if (flattenFolderDepth > 0 && lpFindFileData.IsDirectory)
|
||||
{
|
||||
foreach (var child in GetFilteredFileSystemEntries(lpFindFileData.Path, flattenFolderDepth: flattenFolderDepth - 1))
|
||||
{
|
||||
dict[child.Key] = child.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
dict[lpFindFileData.Path] = lpFindFileData;
|
||||
}
|
||||
}
|
||||
|
||||
NativeMethods.FindClose(handle);
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
namespace MediaBrowser.Controller.IO
|
||||
{
|
||||
public static class FileSystemHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Transforms shortcuts into their actual paths and filters out items that should be ignored
|
||||
/// </summary>
|
||||
public static ItemResolveEventArgs FilterChildFileSystemEntries(ItemResolveEventArgs args, bool flattenShortcuts)
|
||||
{
|
||||
|
||||
List<WIN32_FIND_DATA> returnChildren = new List<WIN32_FIND_DATA>();
|
||||
List<WIN32_FIND_DATA> resolvedShortcuts = new List<WIN32_FIND_DATA>();
|
||||
|
||||
foreach (var file in args.FileSystemChildren)
|
||||
{
|
||||
// If it's a shortcut, resolve it
|
||||
if (Shortcut.IsShortcut(file.Path))
|
||||
{
|
||||
string newPath = Shortcut.ResolveShortcut(file.Path);
|
||||
WIN32_FIND_DATA newPathData = FileData.GetFileData(newPath);
|
||||
|
||||
// Find out if the shortcut is pointing to a directory or file
|
||||
if (newPathData.IsDirectory)
|
||||
{
|
||||
// add to our physical locations
|
||||
args.AdditionalLocations.Add(newPath);
|
||||
|
||||
// If we're flattening then get the shortcut's children
|
||||
if (flattenShortcuts)
|
||||
{
|
||||
returnChildren.Add(file);
|
||||
ItemResolveEventArgs newArgs = new ItemResolveEventArgs()
|
||||
{
|
||||
FileSystemChildren = FileData.GetFileSystemEntries(newPath, "*").ToArray()
|
||||
};
|
||||
|
||||
resolvedShortcuts.AddRange(FilterChildFileSystemEntries(newArgs, false).FileSystemChildren);
|
||||
}
|
||||
else
|
||||
{
|
||||
returnChildren.Add(newPathData);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
returnChildren.Add(newPathData);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//not a shortcut check to see if we should filter it out
|
||||
if (EntityResolutionHelper.ShouldResolvePath(file))
|
||||
{
|
||||
returnChildren.Add(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
//filtered - see if it is one of our "indicator" folders and mark it now - no reason to search for it again
|
||||
args.IsBDFolder |= file.cFileName.Equals("bdmv", StringComparison.OrdinalIgnoreCase);
|
||||
args.IsDVDFolder |= file.cFileName.Equals("video_ts", StringComparison.OrdinalIgnoreCase);
|
||||
args.IsHDDVDFolder |= file.cFileName.Equals("hvdvd_ts", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
//and check to see if it is a metadata folder and collect contents now if so
|
||||
if (IsMetadataFolder(file.cFileName))
|
||||
{
|
||||
args.MetadataFiles = Directory.GetFiles(Path.Combine(args.Path, "metadata"), "*", SearchOption.TopDirectoryOnly);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedShortcuts.Count > 0)
|
||||
{
|
||||
resolvedShortcuts.InsertRange(0, returnChildren);
|
||||
args.FileSystemChildren = resolvedShortcuts.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
args.FileSystemChildren = returnChildren.ToArray();
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
public static bool IsMetadataFolder(string path)
|
||||
{
|
||||
return path.TrimEnd('\\').EndsWith("metadata", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsVideoFile(string path)
|
||||
{
|
||||
string extension = System.IO.Path.GetExtension(path).ToLower();
|
||||
|
||||
switch (extension)
|
||||
{
|
||||
case ".mkv":
|
||||
case ".m2ts":
|
||||
case ".iso":
|
||||
case ".ts":
|
||||
case ".rmvb":
|
||||
case ".mov":
|
||||
case ".avi":
|
||||
case ".mpg":
|
||||
case ".mpeg":
|
||||
case ".wmv":
|
||||
case ".mp4":
|
||||
case ".divx":
|
||||
case ".dvr-ms":
|
||||
case ".wtv":
|
||||
case ".ogm":
|
||||
case ".ogv":
|
||||
case ".asf":
|
||||
case ".m4v":
|
||||
case ".flv":
|
||||
case ".f4v":
|
||||
case ".3gp":
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
MediaBrowser.Controller/IO/FileSystemManager.cs
Normal file
112
MediaBrowser.Controller/IO/FileSystemManager.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Kernel;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// This class will manage our file system watching and modifications. Any process that needs to
|
||||
/// modify the directories that the system is watching for changes should use the methods of
|
||||
/// this class to do so. This way we can have the watchers correctly respond to only external changes.
|
||||
/// </summary>
|
||||
public class FileSystemManager : BaseManager<Kernel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the directory watchers.
|
||||
/// </summary>
|
||||
/// <value>The directory watchers.</value>
|
||||
private DirectoryWatchers DirectoryWatchers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileSystemManager" /> class.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The kernel.</param>
|
||||
public FileSystemManager(Kernel kernel)
|
||||
: base(kernel)
|
||||
{
|
||||
DirectoryWatchers = new DirectoryWatchers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the directory watchers on our library folders
|
||||
/// </summary>
|
||||
public void StartWatchers()
|
||||
{
|
||||
DirectoryWatchers.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves to library filesystem.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="dataToSave">The data to save.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public async Task SaveToLibraryFilesystem(BaseItem item, string path, Stream dataToSave, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
if (dataToSave == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
if (cancellationToken == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
//Tell the watchers to ignore
|
||||
DirectoryWatchers.TemporarilyIgnore(path);
|
||||
|
||||
//Make the mod
|
||||
|
||||
dataToSave.Position = 0;
|
||||
|
||||
try
|
||||
{
|
||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
|
||||
{
|
||||
await dataToSave.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
dataToSave.Dispose();
|
||||
|
||||
// If this is ever used for something other than metadata we can add a file type param
|
||||
item.ResolveArgs.AddMetadataFile(path);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
//Remove the ignore
|
||||
DirectoryWatchers.RemoveTempIgnore(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected override void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
DirectoryWatchers.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(dispose);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.Controller.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains helpers to interact with shortcut files (.lnk)
|
||||
/// </summary>
|
||||
public static class Shortcut
|
||||
{
|
||||
#region Signitures were imported from http://pinvoke.net
|
||||
[Flags()]
|
||||
enum SLGP_FLAGS
|
||||
{
|
||||
/// <summary>Retrieves the standard short (8.3 format) file name</summary>
|
||||
SLGP_SHORTPATH = 0x1,
|
||||
/// <summary>Retrieves the Universal Naming Convention (UNC) path name of the file</summary>
|
||||
SLGP_UNCPRIORITY = 0x2,
|
||||
/// <summary>Retrieves the raw path name. A raw path is something that might not exist and may include environment variables that need to be expanded</summary>
|
||||
SLGP_RAWPATH = 0x4
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
||||
struct WIN32_FIND_DATAW
|
||||
{
|
||||
public uint dwFileAttributes;
|
||||
public long ftCreationTime;
|
||||
public long ftLastAccessTime;
|
||||
public long ftLastWriteTime;
|
||||
public uint nFileSizeHigh;
|
||||
public uint nFileSizeLow;
|
||||
public uint dwReserved0;
|
||||
public uint dwReserved1;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
||||
public string cFileName;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
|
||||
public string cAlternateFileName;
|
||||
}
|
||||
|
||||
[Flags()]
|
||||
|
||||
enum SLR_FLAGS
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not display a dialog box if the link cannot be resolved. When SLR_NO_UI is set,
|
||||
/// the high-order word of fFlags can be set to a time-out value that specifies the
|
||||
/// maximum amount of time to be spent resolving the link. The function returns if the
|
||||
/// link cannot be resolved within the time-out duration. If the high-order word is set
|
||||
/// to zero, the time-out duration will be set to the default value of 3,000 milliseconds
|
||||
/// (3 seconds). To specify a value, set the high word of fFlags to the desired time-out
|
||||
/// duration, in milliseconds.
|
||||
/// </summary>
|
||||
SLR_NO_UI = 0x1,
|
||||
/// <summary>Obsolete and no longer used</summary>
|
||||
SLR_ANY_MATCH = 0x2,
|
||||
/// <summary>If the link object has changed, update its path and list of identifiers.
|
||||
/// If SLR_UPDATE is set, you do not need to call IPersistFile::IsDirty to determine
|
||||
/// whether or not the link object has changed.</summary>
|
||||
SLR_UPDATE = 0x4,
|
||||
/// <summary>Do not update the link information</summary>
|
||||
SLR_NOUPDATE = 0x8,
|
||||
/// <summary>Do not execute the search heuristics</summary>
|
||||
SLR_NOSEARCH = 0x10,
|
||||
/// <summary>Do not use distributed link tracking</summary>
|
||||
SLR_NOTRACK = 0x20,
|
||||
/// <summary>Disable distributed link tracking. By default, distributed link tracking tracks
|
||||
/// removable media across multiple devices based on the volume name. It also uses the
|
||||
/// Universal Naming Convention (UNC) path to track remote file systems whose drive letter
|
||||
/// has changed. Setting SLR_NOLINKINFO disables both types of tracking.</summary>
|
||||
SLR_NOLINKINFO = 0x40,
|
||||
/// <summary>Call the Microsoft Windows Installer</summary>
|
||||
SLR_INVOKE_MSI = 0x80
|
||||
}
|
||||
|
||||
|
||||
/// <summary>The IShellLink interface allows Shell links to be created, modified, and resolved</summary>
|
||||
[ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
|
||||
interface IShellLinkW
|
||||
{
|
||||
/// <summary>Retrieves the path and file name of a Shell link object</summary>
|
||||
void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
|
||||
/// <summary>Retrieves the list of item identifiers for a Shell link object</summary>
|
||||
void GetIDList(out IntPtr ppidl);
|
||||
/// <summary>Sets the pointer to an item identifier list (PIDL) for a Shell link object.</summary>
|
||||
void SetIDList(IntPtr pidl);
|
||||
/// <summary>Retrieves the description string for a Shell link object</summary>
|
||||
void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
|
||||
/// <summary>Sets the description for a Shell link object. The description can be any application-defined string</summary>
|
||||
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
||||
/// <summary>Retrieves the name of the working directory for a Shell link object</summary>
|
||||
void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
|
||||
/// <summary>Sets the name of the working directory for a Shell link object</summary>
|
||||
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
|
||||
/// <summary>Retrieves the command-line arguments associated with a Shell link object</summary>
|
||||
void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
|
||||
/// <summary>Sets the command-line arguments for a Shell link object</summary>
|
||||
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
|
||||
/// <summary>Retrieves the hot key for a Shell link object</summary>
|
||||
void GetHotkey(out short pwHotkey);
|
||||
/// <summary>Sets a hot key for a Shell link object</summary>
|
||||
void SetHotkey(short wHotkey);
|
||||
/// <summary>Retrieves the show command for a Shell link object</summary>
|
||||
void GetShowCmd(out int piShowCmd);
|
||||
/// <summary>Sets the show command for a Shell link object. The show command sets the initial show state of the window.</summary>
|
||||
void SetShowCmd(int iShowCmd);
|
||||
/// <summary>Retrieves the location (path and index) of the icon for a Shell link object</summary>
|
||||
void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
|
||||
int cchIconPath, out int piIcon);
|
||||
/// <summary>Sets the location (path and index) of the icon for a Shell link object</summary>
|
||||
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
|
||||
/// <summary>Sets the relative path to the Shell link object</summary>
|
||||
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
|
||||
/// <summary>Attempts to find the target of a Shell link, even if it has been moved or renamed</summary>
|
||||
void Resolve(IntPtr hwnd, SLR_FLAGS fFlags);
|
||||
/// <summary>Sets the path and file name of a Shell link object</summary>
|
||||
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
|
||||
|
||||
}
|
||||
|
||||
[ComImport, Guid("0000010c-0000-0000-c000-000000000046"),
|
||||
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IPersist
|
||||
{
|
||||
[PreserveSig]
|
||||
void GetClassID(out Guid pClassID);
|
||||
}
|
||||
|
||||
|
||||
[ComImport, Guid("0000010b-0000-0000-C000-000000000046"),
|
||||
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IPersistFile : IPersist
|
||||
{
|
||||
new void GetClassID(out Guid pClassID);
|
||||
[PreserveSig]
|
||||
int IsDirty();
|
||||
|
||||
[PreserveSig]
|
||||
void Load([In, MarshalAs(UnmanagedType.LPWStr)]
|
||||
string pszFileName, uint dwMode);
|
||||
|
||||
[PreserveSig]
|
||||
void Save([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName,
|
||||
[In, MarshalAs(UnmanagedType.Bool)] bool remember);
|
||||
|
||||
[PreserveSig]
|
||||
void SaveCompleted([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
|
||||
|
||||
[PreserveSig]
|
||||
void GetCurFile([In, MarshalAs(UnmanagedType.LPWStr)] string ppszFileName);
|
||||
}
|
||||
|
||||
const uint STGM_READ = 0;
|
||||
const int MAX_PATH = 260;
|
||||
|
||||
// CLSID_ShellLink from ShlGuid.h
|
||||
[
|
||||
ComImport(),
|
||||
Guid("00021401-0000-0000-C000-000000000046")
|
||||
]
|
||||
public class ShellLink
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static string ResolveShortcut(string filename)
|
||||
{
|
||||
var link = new ShellLink();
|
||||
((IPersistFile)link).Load(filename, STGM_READ);
|
||||
// TODO: if I can get hold of the hwnd call resolve first. This handles moved and renamed files.
|
||||
// ((IShellLinkW)link).Resolve(hwnd, 0)
|
||||
var sb = new StringBuilder(MAX_PATH);
|
||||
var data = new WIN32_FIND_DATAW();
|
||||
((IShellLinkW)link).GetPath(sb, sb.Capacity, out data, 0);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static bool IsShortcut(string filename)
|
||||
{
|
||||
return filename != null ? Path.GetExtension(filename).EndsWith("lnk", StringComparison.OrdinalIgnoreCase) : false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,386 +1,599 @@
|
||||
using MediaBrowser.Common.Kernel;
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Controller.Weather;
|
||||
using MediaBrowser.Model.Authentication;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Progress;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller
|
||||
{
|
||||
public class Kernel : BaseKernel<ServerConfiguration, ServerApplicationPaths>
|
||||
{
|
||||
#region Events
|
||||
/// <summary>
|
||||
/// Fires whenever any validation routine adds or removes items. The added and removed items are properties of the args.
|
||||
/// *** Will fire asynchronously. ***
|
||||
/// </summary>
|
||||
public event EventHandler<ChildrenChangedEventArgs> LibraryChanged;
|
||||
public void OnLibraryChanged(ChildrenChangedEventArgs args)
|
||||
{
|
||||
if (LibraryChanged != null)
|
||||
{
|
||||
Task.Run(() => LibraryChanged(this, args));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
public static Kernel Instance { get; private set; }
|
||||
|
||||
public ItemController ItemController { get; private set; }
|
||||
|
||||
public IEnumerable<User> Users { get; private set; }
|
||||
public Folder RootFolder { get; private set; }
|
||||
|
||||
private DirectoryWatchers DirectoryWatchers { get; set; }
|
||||
|
||||
private string MediaRootFolderPath
|
||||
{
|
||||
get
|
||||
{
|
||||
return ApplicationPaths.RootFolderPath;
|
||||
}
|
||||
}
|
||||
|
||||
public override KernelContext KernelContext
|
||||
{
|
||||
get { return KernelContext.Server; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of currently registered weather prvoiders
|
||||
/// </summary>
|
||||
[ImportMany(typeof(BaseWeatherProvider))]
|
||||
public IEnumerable<BaseWeatherProvider> WeatherProviders { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of currently registered metadata prvoiders
|
||||
/// </summary>
|
||||
[ImportMany(typeof(BaseMetadataProvider))]
|
||||
private IEnumerable<BaseMetadataProvider> MetadataProvidersEnumerable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Once MEF has loaded the resolvers, sort them by priority and store them in this array
|
||||
/// Given the sheer number of times they'll be iterated over it'll be faster to loop through an array
|
||||
/// </summary>
|
||||
private BaseMetadataProvider[] MetadataProviders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of currently registered entity resolvers
|
||||
/// </summary>
|
||||
[ImportMany(typeof(IBaseItemResolver))]
|
||||
private IEnumerable<IBaseItemResolver> EntityResolversEnumerable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Once MEF has loaded the resolvers, sort them by priority and store them in this array
|
||||
/// Given the sheer number of times they'll be iterated over it'll be faster to loop through an array
|
||||
/// </summary>
|
||||
internal IBaseItemResolver[] EntityResolvers { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a kernel based on a Data path, which is akin to our current programdata path
|
||||
/// </summary>
|
||||
public Kernel()
|
||||
: base()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs initializations that only occur once
|
||||
/// </summary>
|
||||
protected override void InitializeInternal(IProgress<TaskProgress> progress)
|
||||
{
|
||||
base.InitializeInternal(progress);
|
||||
|
||||
ItemController = new ItemController();
|
||||
DirectoryWatchers = new DirectoryWatchers();
|
||||
|
||||
|
||||
ExtractFFMpeg();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs initializations that can be reloaded at anytime
|
||||
/// </summary>
|
||||
protected override async Task ReloadInternal(IProgress<TaskProgress> progress)
|
||||
{
|
||||
await base.ReloadInternal(progress).ConfigureAwait(false);
|
||||
|
||||
ReportProgress(progress, "Loading Users");
|
||||
ReloadUsers();
|
||||
|
||||
ReportProgress(progress, "Loading Media Library");
|
||||
|
||||
await ReloadRoot(allowInternetProviders: false).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completely disposes the Kernel
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
|
||||
DirectoryWatchers.Stop();
|
||||
|
||||
}
|
||||
|
||||
protected override void OnComposablePartsLoaded()
|
||||
{
|
||||
// The base class will start up all the plugins
|
||||
base.OnComposablePartsLoaded();
|
||||
|
||||
// Sort the resolvers by priority
|
||||
EntityResolvers = EntityResolversEnumerable.OrderBy(e => e.Priority).ToArray();
|
||||
|
||||
// Sort the providers by priority
|
||||
MetadataProviders = MetadataProvidersEnumerable.OrderBy(e => e.Priority).ToArray();
|
||||
}
|
||||
|
||||
public BaseItem ResolveItem(ItemResolveEventArgs args)
|
||||
{
|
||||
// Try first priority resolvers
|
||||
for (int i = 0; i < EntityResolvers.Length; i++)
|
||||
{
|
||||
var item = EntityResolvers[i].ResolvePath(args);
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
item.ResolveArgs = args;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ReloadUsers()
|
||||
{
|
||||
Users = GetAllUsers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the root media folder
|
||||
/// </summary>
|
||||
public async Task ReloadRoot(bool allowInternetProviders = true)
|
||||
{
|
||||
if (!Directory.Exists(MediaRootFolderPath))
|
||||
{
|
||||
Directory.CreateDirectory(MediaRootFolderPath);
|
||||
}
|
||||
|
||||
DirectoryWatchers.Stop();
|
||||
|
||||
RootFolder = await ItemController.GetItem(MediaRootFolderPath, allowInternetProviders: allowInternetProviders).ConfigureAwait(false) as Folder;
|
||||
RootFolder.ChildrenChanged += RootFolder_ChildrenChanged;
|
||||
|
||||
DirectoryWatchers.Start();
|
||||
}
|
||||
|
||||
void RootFolder_ChildrenChanged(object sender, ChildrenChangedEventArgs e)
|
||||
{
|
||||
Logger.LogDebugInfo("Root Folder Children Changed. Added: " + e.ItemsAdded.Count + " Removed: " + e.ItemsRemoved.Count());
|
||||
//re-start the directory watchers
|
||||
DirectoryWatchers.Stop();
|
||||
DirectoryWatchers.Start();
|
||||
//Task.Delay(30000); //let's wait and see if more data gets filled in...
|
||||
var allChildren = RootFolder.RecursiveChildren;
|
||||
Logger.LogDebugInfo(string.Format("Loading complete. Movies: {0} Episodes: {1} Folders: {2}", allChildren.OfType<Entities.Movies.Movie>().Count(), allChildren.OfType<Entities.TV.Episode>().Count(), allChildren.Where(i => i is Folder && !(i is Series || i is Season)).Count()));
|
||||
//foreach (var child in allChildren)
|
||||
//{
|
||||
// Logger.LogDebugInfo("(" + child.GetType().Name + ") " + child.Name + " (" + child.Path + ")");
|
||||
//}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default user to use when EnableUserProfiles is false
|
||||
/// </summary>
|
||||
public User GetDefaultUser()
|
||||
{
|
||||
User user = Users.FirstOrDefault();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists a User
|
||||
/// </summary>
|
||||
public void SaveUser(User user)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates a User and returns a result indicating whether or not it succeeded
|
||||
/// </summary>
|
||||
public AuthenticationResult AuthenticateUser(User user, string password)
|
||||
{
|
||||
var result = new AuthenticationResult();
|
||||
|
||||
// When EnableUserProfiles is false, only the default User can login
|
||||
if (!Configuration.EnableUserProfiles)
|
||||
{
|
||||
result.Success = user.Id == GetDefaultUser().Id;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(user.Password))
|
||||
{
|
||||
result.Success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
password = password ?? string.Empty;
|
||||
result.Success = password.GetMD5().ToString().Equals(user.Password);
|
||||
}
|
||||
|
||||
// Update LastActivityDate and LastLoginDate, then save
|
||||
if (result.Success)
|
||||
{
|
||||
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
|
||||
SaveUser(user);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a library item by Id
|
||||
/// </summary>
|
||||
public BaseItem GetItemById(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
return RootFolder;
|
||||
}
|
||||
|
||||
return RootFolder.FindItemById(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all users within the system
|
||||
/// </summary>
|
||||
private IEnumerable<User> GetAllUsers()
|
||||
{
|
||||
var list = new List<User>();
|
||||
|
||||
// Return a dummy user for now since all calls to get items requre a userId
|
||||
var user = new User { };
|
||||
|
||||
user.Name = "Default User";
|
||||
user.Id = Guid.Parse("5d1cf7fce25943b790d140095457a42b");
|
||||
user.PrimaryImagePath = "D:\\Video\\TV\\Archer (2009)\\backdrop.jpg";
|
||||
list.Add(user);
|
||||
|
||||
user = new User { };
|
||||
user.Name = "Abobader";
|
||||
user.Id = Guid.NewGuid();
|
||||
user.LastLoginDate = DateTime.UtcNow.AddDays(-1);
|
||||
user.LastActivityDate = DateTime.UtcNow.AddHours(-3);
|
||||
user.Password = ("1234").GetMD5().ToString();
|
||||
list.Add(user);
|
||||
|
||||
user = new User { };
|
||||
user.Name = "Scottisafool";
|
||||
user.Id = Guid.NewGuid();
|
||||
list.Add(user);
|
||||
|
||||
user = new User { };
|
||||
user.Name = "Redshirt";
|
||||
user.Id = Guid.NewGuid();
|
||||
list.Add(user);
|
||||
|
||||
/*user = new User();
|
||||
user.Name = "Test User 4";
|
||||
user.Id = Guid.NewGuid();
|
||||
list.Add(user);
|
||||
|
||||
user = new User();
|
||||
user.Name = "Test User 5";
|
||||
user.Id = Guid.NewGuid();
|
||||
list.Add(user);
|
||||
|
||||
user = new User();
|
||||
user.Name = "Test User 6";
|
||||
user.Id = Guid.NewGuid();
|
||||
list.Add(user);*/
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs all metadata providers for an entity
|
||||
/// </summary>
|
||||
internal async Task ExecuteMetadataProviders(BaseEntity item, bool allowInternetProviders = true)
|
||||
{
|
||||
// Run them sequentially in order of priority
|
||||
for (int i = 0; i < MetadataProviders.Length; i++)
|
||||
{
|
||||
var provider = MetadataProviders[i];
|
||||
|
||||
// Skip if internet providers are currently disabled
|
||||
if (provider.RequiresInternet && (!Configuration.EnableInternetProviders || !allowInternetProviders))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if the provider doesn't support the current item
|
||||
if (!provider.Supports(item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await provider.FetchIfNeededAsync(item).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractFFMpeg()
|
||||
{
|
||||
ExtractFFMpeg(ApplicationPaths.FFMpegPath);
|
||||
ExtractFFMpeg(ApplicationPaths.FFProbePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run these during Init.
|
||||
/// Can't run do this on-demand because there will be multiple workers accessing them at once and we'd have to lock them
|
||||
/// </summary>
|
||||
private void ExtractFFMpeg(string exe)
|
||||
{
|
||||
if (File.Exists(exe))
|
||||
{
|
||||
File.Delete(exe);
|
||||
}
|
||||
|
||||
// Extract exe
|
||||
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MediaBrowser.Controller.FFMpeg." + Path.GetFileName(exe)))
|
||||
{
|
||||
using (var fileStream = new FileStream(exe, FileMode.Create))
|
||||
{
|
||||
stream.CopyTo(fileStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using MediaBrowser.Common.Kernel;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaInfo;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Persistence.SQLite;
|
||||
using MediaBrowser.Controller.Playback;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Controller.ScheduledTasks;
|
||||
using MediaBrowser.Controller.Updates;
|
||||
using MediaBrowser.Controller.Weather;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Kernel
|
||||
/// </summary>
|
||||
public class Kernel : BaseKernel<ServerConfiguration, ServerApplicationPaths>
|
||||
{
|
||||
/// <summary>
|
||||
/// The MB admin URL
|
||||
/// </summary>
|
||||
public const string MBAdminUrl = "http://mb3admin.com/admin/";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance.
|
||||
/// </summary>
|
||||
/// <value>The instance.</value>
|
||||
public static Kernel Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the library manager.
|
||||
/// </summary>
|
||||
/// <value>The library manager.</value>
|
||||
public LibraryManager LibraryManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image manager.
|
||||
/// </summary>
|
||||
/// <value>The image manager.</value>
|
||||
public ImageManager ImageManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user manager.
|
||||
/// </summary>
|
||||
/// <value>The user manager.</value>
|
||||
public UserManager UserManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FFMPEG controller.
|
||||
/// </summary>
|
||||
/// <value>The FFMPEG controller.</value>
|
||||
public FFMpegManager FFMpegManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the installation manager.
|
||||
/// </summary>
|
||||
/// <value>The installation manager.</value>
|
||||
public InstallationManager InstallationManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the file system manager.
|
||||
/// </summary>
|
||||
/// <value>The file system manager.</value>
|
||||
public FileSystemManager FileSystemManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the provider manager.
|
||||
/// </summary>
|
||||
/// <value>The provider manager.</value>
|
||||
public ProviderManager ProviderManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user data manager.
|
||||
/// </summary>
|
||||
/// <value>The user data manager.</value>
|
||||
public UserDataManager UserDataManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plug-in security manager.
|
||||
/// </summary>
|
||||
/// <value>The plug-in security manager.</value>
|
||||
public PluginSecurityManager PluginSecurityManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _users
|
||||
/// </summary>
|
||||
private IEnumerable<User> _users;
|
||||
/// <summary>
|
||||
/// The _user lock
|
||||
/// </summary>
|
||||
private object _usersSyncLock = new object();
|
||||
/// <summary>
|
||||
/// The _users initialized
|
||||
/// </summary>
|
||||
private bool _usersInitialized;
|
||||
/// <summary>
|
||||
/// Gets the users.
|
||||
/// </summary>
|
||||
/// <value>The users.</value>
|
||||
public IEnumerable<User> Users
|
||||
{
|
||||
get
|
||||
{
|
||||
// Call ToList to exhaust the stream because we'll be iterating over this multiple times
|
||||
LazyInitializer.EnsureInitialized(ref _users, ref _usersInitialized, ref _usersSyncLock, UserManager.LoadUsers);
|
||||
return _users;
|
||||
}
|
||||
internal set
|
||||
{
|
||||
_users = value;
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
_usersInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder
|
||||
/// </summary>
|
||||
private AggregateFolder _rootFolder;
|
||||
/// <summary>
|
||||
/// The _root folder sync lock
|
||||
/// </summary>
|
||||
private object _rootFolderSyncLock = new object();
|
||||
/// <summary>
|
||||
/// The _root folder initialized
|
||||
/// </summary>
|
||||
private bool _rootFolderInitialized;
|
||||
/// <summary>
|
||||
/// Gets the root folder.
|
||||
/// </summary>
|
||||
/// <value>The root folder.</value>
|
||||
public AggregateFolder RootFolder
|
||||
{
|
||||
get
|
||||
{
|
||||
LazyInitializer.EnsureInitialized(ref _rootFolder, ref _rootFolderInitialized, ref _rootFolderSyncLock, LibraryManager.CreateRootFolder);
|
||||
return _rootFolder;
|
||||
}
|
||||
private set
|
||||
{
|
||||
_rootFolder = value;
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
_rootFolderInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kernel context.
|
||||
/// </summary>
|
||||
/// <value>The kernel context.</value>
|
||||
public override KernelContext KernelContext
|
||||
{
|
||||
get { return KernelContext.Server; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of plugin configuration pages
|
||||
/// </summary>
|
||||
/// <value>The configuration pages.</value>
|
||||
[ImportMany(typeof(BaseConfigurationPage))]
|
||||
public IEnumerable<BaseConfigurationPage> PluginConfigurationPages { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the intro providers.
|
||||
/// </summary>
|
||||
/// <value>The intro providers.</value>
|
||||
[ImportMany(typeof(BaseIntroProvider))]
|
||||
public IEnumerable<BaseIntroProvider> IntroProviders { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of currently registered weather prvoiders
|
||||
/// </summary>
|
||||
/// <value>The weather providers.</value>
|
||||
[ImportMany(typeof(BaseWeatherProvider))]
|
||||
public IEnumerable<BaseWeatherProvider> WeatherProviders { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of currently registered metadata prvoiders
|
||||
/// </summary>
|
||||
/// <value>The metadata providers enumerable.</value>
|
||||
[ImportMany(typeof(BaseMetadataProvider))]
|
||||
public BaseMetadataProvider[] MetadataProviders { get; private set; }
|
||||
|
||||
/// <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>
|
||||
[ImportMany(typeof(BaseImageEnhancer))]
|
||||
public BaseImageEnhancer[] ImageEnhancers { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of currently registered entity resolvers
|
||||
/// </summary>
|
||||
/// <value>The entity resolvers enumerable.</value>
|
||||
[ImportMany(typeof(IBaseItemResolver))]
|
||||
internal IBaseItemResolver[] EntityResolvers { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of BasePluginFolders added by plugins
|
||||
/// </summary>
|
||||
/// <value>The plugin folders.</value>
|
||||
[ImportMany(typeof(BasePluginFolder))]
|
||||
internal IEnumerable<BasePluginFolder> PluginFolders { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available user repositories
|
||||
/// </summary>
|
||||
/// <value>The user repositories.</value>
|
||||
[ImportMany(typeof(IUserRepository))]
|
||||
private IEnumerable<IUserRepository> UserRepositories { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active user repository
|
||||
/// </summary>
|
||||
/// <value>The user repository.</value>
|
||||
public IUserRepository UserRepository { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active user repository
|
||||
/// </summary>
|
||||
/// <value>The display preferences repository.</value>
|
||||
public IDisplayPreferencesRepository DisplayPreferencesRepository { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available item repositories
|
||||
/// </summary>
|
||||
/// <value>The item repositories.</value>
|
||||
[ImportMany(typeof(IItemRepository))]
|
||||
private IEnumerable<IItemRepository> ItemRepositories { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active item repository
|
||||
/// </summary>
|
||||
/// <value>The item repository.</value>
|
||||
public IItemRepository ItemRepository { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available item repositories
|
||||
/// </summary>
|
||||
/// <value>The user data repositories.</value>
|
||||
[ImportMany(typeof(IUserDataRepository))]
|
||||
private IEnumerable<IUserDataRepository> UserDataRepositories { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available DisplayPreferencesRepositories
|
||||
/// </summary>
|
||||
/// <value>The display preferences repositories.</value>
|
||||
[ImportMany(typeof(IDisplayPreferencesRepository))]
|
||||
private IEnumerable<IDisplayPreferencesRepository> DisplayPreferencesRepositories { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of entity resolution ignore rules
|
||||
/// </summary>
|
||||
/// <value>The entity resolution ignore rules.</value>
|
||||
[ImportMany(typeof(BaseResolutionIgnoreRule))]
|
||||
internal IEnumerable<BaseResolutionIgnoreRule> EntityResolutionIgnoreRules { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active user data repository
|
||||
/// </summary>
|
||||
/// <value>The user data repository.</value>
|
||||
public IUserDataRepository UserDataRepository { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Limits simultaneous access to various resources
|
||||
/// </summary>
|
||||
/// <value>The resource pools.</value>
|
||||
public ResourcePool ResourcePools { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UDP server port number.
|
||||
/// </summary>
|
||||
/// <value>The UDP server port number.</value>
|
||||
public override int UdpServerPortNumber
|
||||
{
|
||||
get { return 7359; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a kernel based on a Data path, which is akin to our current programdata path
|
||||
/// </summary>
|
||||
public Kernel()
|
||||
: base()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs initializations that can be reloaded at anytime
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
protected override async Task ReloadInternal()
|
||||
{
|
||||
Logger.Info("Extracting tools");
|
||||
|
||||
// Reset these so that they can be lazy loaded again
|
||||
Users = null;
|
||||
RootFolder = null;
|
||||
|
||||
ReloadResourcePools();
|
||||
InstallationManager = new InstallationManager(this);
|
||||
LibraryManager = new LibraryManager(this);
|
||||
UserManager = new UserManager(this);
|
||||
FFMpegManager = new FFMpegManager(this);
|
||||
ImageManager = new ImageManager(this);
|
||||
ProviderManager = new ProviderManager(this);
|
||||
UserDataManager = new UserDataManager(this);
|
||||
PluginSecurityManager = new PluginSecurityManager(this);
|
||||
|
||||
await base.ReloadInternal().ConfigureAwait(false);
|
||||
|
||||
ReloadFileSystemManager();
|
||||
|
||||
await UserManager.RefreshUsersMetadata(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected override void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
DisposeResourcePools();
|
||||
|
||||
DisposeFileSystemManager();
|
||||
}
|
||||
|
||||
base.Dispose(dispose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the resource pools.
|
||||
/// </summary>
|
||||
private void DisposeResourcePools()
|
||||
{
|
||||
if (ResourcePools != null)
|
||||
{
|
||||
ResourcePools.Dispose();
|
||||
ResourcePools = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the resource pools.
|
||||
/// </summary>
|
||||
private void ReloadResourcePools()
|
||||
{
|
||||
DisposeResourcePools();
|
||||
ResourcePools = new ResourcePool();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when [composable parts loaded].
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
protected override async Task OnComposablePartsLoaded()
|
||||
{
|
||||
// The base class will start up all the plugins
|
||||
await base.OnComposablePartsLoaded().ConfigureAwait(false);
|
||||
|
||||
// Get the current item repository
|
||||
ItemRepository = GetRepository(ItemRepositories, Configuration.ItemRepository, SQLiteItemRepository.RepositoryName);
|
||||
var itemRepoTask = ItemRepository.Initialize();
|
||||
|
||||
// Get the current user repository
|
||||
UserRepository = GetRepository(UserRepositories, Configuration.UserRepository, SQLiteUserRepository.RepositoryName);
|
||||
var userRepoTask = UserRepository.Initialize();
|
||||
|
||||
// Get the current item repository
|
||||
UserDataRepository = GetRepository(UserDataRepositories, Configuration.UserDataRepository, SQLiteUserDataRepository.RepositoryName);
|
||||
var userDataRepoTask = UserDataRepository.Initialize();
|
||||
|
||||
// Get the current display preferences repository
|
||||
DisplayPreferencesRepository = GetRepository(DisplayPreferencesRepositories, Configuration.DisplayPreferencesRepository, SQLiteDisplayPreferencesRepository.RepositoryName);
|
||||
var displayPreferencesRepoTask = DisplayPreferencesRepository.Initialize();
|
||||
|
||||
// Sort the resolvers by priority
|
||||
EntityResolvers = EntityResolvers.OrderBy(e => e.Priority).ToArray();
|
||||
|
||||
// Sort the providers by priority
|
||||
MetadataProviders = MetadataProviders.OrderBy(e => e.Priority).ToArray();
|
||||
|
||||
// Sort the image processors by priority
|
||||
ImageEnhancers = ImageEnhancers.OrderBy(e => e.Priority).ToArray();
|
||||
|
||||
await Task.WhenAll(itemRepoTask, userRepoTask, userDataRepoTask, displayPreferencesRepoTask).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override IEnumerable<Assembly> GetComposablePartAssemblies()
|
||||
{
|
||||
var runningDirectory = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
|
||||
|
||||
return base.GetComposablePartAssemblies().Concat(new[] {
|
||||
|
||||
Assembly.Load(File.ReadAllBytes(Path.Combine(runningDirectory, "MediaBrowser.Api.dll"))),
|
||||
Assembly.Load(File.ReadAllBytes(Path.Combine(runningDirectory, "MediaBrowser.ApiInteraction.Javascript.dll"))),
|
||||
Assembly.Load(File.ReadAllBytes(Path.Combine(runningDirectory, "MediaBrowser.WebDashboard.dll")))
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a repository by name from a list, and returns the default if not found
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="repositories">The repositories.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="defaultName">The default name.</param>
|
||||
/// <returns>``0.</returns>
|
||||
private T GetRepository<T>(IEnumerable<T> repositories, string name, string defaultName)
|
||||
where T : class, IRepository
|
||||
{
|
||||
var enumerable = repositories as T[] ?? repositories.ToArray();
|
||||
|
||||
return enumerable.FirstOrDefault(r => r.Name.Equals(name ?? defaultName, StringComparison.OrdinalIgnoreCase)) ??
|
||||
enumerable.First(r => r.Name.Equals(defaultName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the file system manager.
|
||||
/// </summary>
|
||||
private void DisposeFileSystemManager()
|
||||
{
|
||||
if (FileSystemManager != null)
|
||||
{
|
||||
FileSystemManager.Dispose();
|
||||
FileSystemManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the file system manager.
|
||||
/// </summary>
|
||||
private void ReloadFileSystemManager()
|
||||
{
|
||||
DisposeFileSystemManager();
|
||||
|
||||
FileSystemManager = new FileSystemManager(this);
|
||||
FileSystemManager.StartWatchers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a User by Id
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <returns>User.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public User GetUserById(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
return Users.FirstOrDefault(u => u.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a library item by Id and UserId.
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">id</exception>
|
||||
public BaseItem GetItemById(Guid id, Guid userId)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentNullException("id");
|
||||
}
|
||||
|
||||
if (userId == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentNullException("userId");
|
||||
}
|
||||
|
||||
var user = GetUserById(userId);
|
||||
var userRoot = user.RootFolder;
|
||||
|
||||
return userRoot.FindItemById(id, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item by id.
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">id</exception>
|
||||
public BaseItem GetItemById(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentNullException("id");
|
||||
}
|
||||
|
||||
return RootFolder.FindItemById(id, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completely overwrites the current configuration with a new copy
|
||||
/// </summary>
|
||||
/// <param name="config">The config.</param>
|
||||
public void UpdateConfiguration(ServerConfiguration config)
|
||||
{
|
||||
var oldConfiguration = Configuration;
|
||||
|
||||
var reloadLogger = config.ShowLogWindow != oldConfiguration.ShowLogWindow;
|
||||
|
||||
// Figure out whether or not we should refresh people after the update is finished
|
||||
var refreshPeopleAfterUpdate = !oldConfiguration.EnableInternetProviders && config.EnableInternetProviders;
|
||||
|
||||
// This is true if internet providers has just been turned on, or if People have just been removed from InternetProviderExcludeTypes
|
||||
if (!refreshPeopleAfterUpdate)
|
||||
{
|
||||
var oldConfigurationFetchesPeopleImages = oldConfiguration.InternetProviderExcludeTypes == null || !oldConfiguration.InternetProviderExcludeTypes.Contains(typeof(Person).Name, StringComparer.OrdinalIgnoreCase);
|
||||
var newConfigurationFetchesPeopleImages = config.InternetProviderExcludeTypes == null || !config.InternetProviderExcludeTypes.Contains(typeof(Person).Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
refreshPeopleAfterUpdate = newConfigurationFetchesPeopleImages && !oldConfigurationFetchesPeopleImages;
|
||||
}
|
||||
|
||||
Configuration = config;
|
||||
SaveConfiguration();
|
||||
|
||||
if (reloadLogger)
|
||||
{
|
||||
ReloadLogger();
|
||||
}
|
||||
|
||||
TcpManager.OnApplicationConfigurationChanged(oldConfiguration, config);
|
||||
|
||||
// Validate currently executing providers, in the background
|
||||
Task.Run(() =>
|
||||
{
|
||||
ProviderManager.ValidateCurrentlyRunningProviders();
|
||||
|
||||
// Any number of configuration settings could change the way the library is refreshed, so do that now
|
||||
TaskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
|
||||
|
||||
if (refreshPeopleAfterUpdate)
|
||||
{
|
||||
TaskManager.CancelIfRunningAndQueue<PeopleValidationTask>();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the plugin.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin.</param>
|
||||
internal void RemovePlugin(IPlugin plugin)
|
||||
{
|
||||
var list = Plugins.ToList();
|
||||
list.Remove(plugin);
|
||||
Plugins = list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system info.
|
||||
/// </summary>
|
||||
/// <returns>SystemInfo.</returns>
|
||||
public override SystemInfo GetSystemInfo()
|
||||
{
|
||||
var info = base.GetSystemInfo();
|
||||
|
||||
if (InstallationManager != null)
|
||||
{
|
||||
info.InProgressInstallations = InstallationManager.CurrentInstallations.Select(i => i.Item1).ToArray();
|
||||
info.CompletedInstallations = InstallationManager.CompletedInstallations.ToArray();
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
public class ChildrenChangedEventArgs : EventArgs
|
||||
{
|
||||
public Folder Folder { get; set; }
|
||||
public List<BaseItem> ItemsAdded { get; set; }
|
||||
public IEnumerable<BaseItem> ItemsRemoved { get; set; }
|
||||
|
||||
public ChildrenChangedEventArgs()
|
||||
{
|
||||
//initialize the list
|
||||
ItemsAdded = new List<BaseItem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the args and set the folder property
|
||||
/// </summary>
|
||||
/// <param name="folder"></param>
|
||||
public ChildrenChangedEventArgs(Folder folder)
|
||||
{
|
||||
//init the folder property
|
||||
this.Folder = folder;
|
||||
//init the list
|
||||
ItemsAdded = new List<BaseItem>();
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Concurrent;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ChildrenChangedEventArgs
|
||||
/// </summary>
|
||||
public class ChildrenChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the folder.
|
||||
/// </summary>
|
||||
/// <value>The folder.</value>
|
||||
public Folder Folder { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the items added.
|
||||
/// </summary>
|
||||
/// <value>The items added.</value>
|
||||
public ConcurrentBag<BaseItem> ItemsAdded { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the items removed.
|
||||
/// </summary>
|
||||
/// <value>The items removed.</value>
|
||||
public List<BaseItem> ItemsRemoved { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the items updated.
|
||||
/// </summary>
|
||||
/// <value>The items updated.</value>
|
||||
public ConcurrentBag<BaseItem> ItemsUpdated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create the args and set the folder property
|
||||
/// </summary>
|
||||
/// <param name="folder">The folder.</param>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public ChildrenChangedEventArgs(Folder folder)
|
||||
{
|
||||
if (folder == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
//init the folder property
|
||||
Folder = folder;
|
||||
//init the list
|
||||
ItemsAdded = new ConcurrentBag<BaseItem>();
|
||||
ItemsRemoved = new List<BaseItem>();
|
||||
ItemsUpdated = new ConcurrentBag<BaseItem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the new item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public void AddNewItem(BaseItem item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
ItemsAdded.Add(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the updated item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public void AddUpdatedItem(BaseItem item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
ItemsUpdated.Add(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the removed item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public void AddRemovedItem(BaseItem item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
ItemsRemoved.Add(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists the has change.
|
||||
/// </summary>
|
||||
/// <param name="list">The list.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
private bool ListHasChange(List<BaseItem> list)
|
||||
{
|
||||
return list != null && list.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists the has change.
|
||||
/// </summary>
|
||||
/// <param name="list">The list.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
private bool ListHasChange(ConcurrentBag<BaseItem> list)
|
||||
{
|
||||
return list != null && !list.IsEmpty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has change.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance has change; otherwise, <c>false</c>.</value>
|
||||
public bool HasChange
|
||||
{
|
||||
get { return HasAddOrRemoveChange || ListHasChange(ItemsUpdated); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has add or remove change.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance has add or remove change; otherwise, <c>false</c>.</value>
|
||||
public bool HasAddOrRemoveChange
|
||||
{
|
||||
get { return ListHasChange(ItemsAdded) || ListHasChange(ItemsRemoved); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
934
MediaBrowser.Controller/Library/DtoBuilder.cs
Normal file
934
MediaBrowser.Controller/Library/DtoBuilder.cs
Normal file
@@ -0,0 +1,934 @@
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.DTO;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates DTO's from domain entities
|
||||
/// </summary>
|
||||
public static class DtoBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// The index folder delimeter
|
||||
/// </summary>
|
||||
const string IndexFolderDelimeter = "-index-";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dto base item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="fields">The fields.</param>
|
||||
/// <returns>Task{DtoBaseItem}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
public async static Task<DtoBaseItem> GetDtoBaseItem(BaseItem item, List<ItemFields> fields)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
if (fields == null)
|
||||
{
|
||||
throw new ArgumentNullException("fields");
|
||||
}
|
||||
|
||||
var dto = new DtoBaseItem();
|
||||
|
||||
var tasks = new List<Task>();
|
||||
|
||||
if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
|
||||
{
|
||||
try
|
||||
{
|
||||
tasks.Add(AttachPrimaryImageAspectRatio(dto, item));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
|
||||
Logger.LogException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.Studios))
|
||||
{
|
||||
dto.Studios = item.Studios;
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.People))
|
||||
{
|
||||
tasks.Add(AttachPeople(dto, item));
|
||||
}
|
||||
|
||||
AttachBasicFields(dto, item, fields);
|
||||
|
||||
// Make sure all the tasks we kicked off have completed.
|
||||
if (tasks.Count > 0)
|
||||
{
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a BaseItem to a DTOBaseItem
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="fields">The fields.</param>
|
||||
/// <returns>Task{DtoBaseItem}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public async static Task<DtoBaseItem> GetDtoBaseItem(BaseItem item, User user, List<ItemFields> fields)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
if (fields == null)
|
||||
{
|
||||
throw new ArgumentNullException("fields");
|
||||
}
|
||||
|
||||
var dto = new DtoBaseItem();
|
||||
|
||||
var tasks = new List<Task>();
|
||||
|
||||
if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
|
||||
{
|
||||
try
|
||||
{
|
||||
tasks.Add(AttachPrimaryImageAspectRatio(dto, item));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
|
||||
Logger.LogException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.Studios))
|
||||
{
|
||||
dto.Studios = item.Studios;
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.People))
|
||||
{
|
||||
tasks.Add(AttachPeople(dto, item));
|
||||
}
|
||||
|
||||
AttachBasicFields(dto, item, fields);
|
||||
|
||||
AttachUserSpecificInfo(dto, item, user, fields);
|
||||
|
||||
// Make sure all the tasks we kicked off have completed.
|
||||
if (tasks.Count > 0)
|
||||
{
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the user specific info.
|
||||
/// </summary>
|
||||
/// <param name="dto">The dto.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="fields">The fields.</param>
|
||||
private static void AttachUserSpecificInfo(DtoBaseItem dto, BaseItem item, User user, List<ItemFields> fields)
|
||||
{
|
||||
dto.IsNew = item.IsRecentlyAdded(user);
|
||||
|
||||
if (fields.Contains(ItemFields.UserData))
|
||||
{
|
||||
var userData = item.GetUserData(user, false);
|
||||
|
||||
if (userData != null)
|
||||
{
|
||||
dto.UserData = GetDtoUserItemData(userData);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.IsFolder && fields.Contains(ItemFields.DisplayPreferences))
|
||||
{
|
||||
dto.DisplayPreferences = ((Folder)item).GetDisplayPrefs(user, false) ?? new DisplayPreferences { UserId = user.Id };
|
||||
}
|
||||
|
||||
if (item.IsFolder)
|
||||
{
|
||||
if (fields.Contains(ItemFields.ItemCounts))
|
||||
{
|
||||
var folder = (Folder)item;
|
||||
|
||||
// Skip sorting since all we want is a count
|
||||
dto.ChildCount = folder.GetChildren(user).Count();
|
||||
|
||||
SetSpecialCounts(folder, user, dto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the primary image aspect ratio.
|
||||
/// </summary>
|
||||
/// <param name="dto">The dto.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private static async Task AttachPrimaryImageAspectRatio(DtoBaseItem dto, BaseItem item)
|
||||
{
|
||||
var path = item.PrimaryImagePath;
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var metaFileEntry = item.ResolveArgs.GetMetaFileByPath(path);
|
||||
|
||||
// See if we can avoid a file system lookup by looking for the file in ResolveArgs
|
||||
var dateModified = metaFileEntry == null ? File.GetLastWriteTimeUtc(path) : metaFileEntry.Value.LastWriteTimeUtc;
|
||||
|
||||
ImageSize size;
|
||||
|
||||
try
|
||||
{
|
||||
size = await Kernel.Instance.ImageManager.GetImageSize(path, dateModified).ConfigureAwait(false);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
Logger.LogError("Image file does not exist: {0}", path);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var enhancer in Kernel.Instance.ImageEnhancers
|
||||
.Where(i => i.Supports(item, ImageType.Primary)))
|
||||
{
|
||||
|
||||
size = enhancer.GetEnhancedImageSize(item, ImageType.Primary, 0, size);
|
||||
}
|
||||
|
||||
dto.PrimaryImageAspectRatio = size.Width / size.Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets simple property values on a DTOBaseItem
|
||||
/// </summary>
|
||||
/// <param name="dto">The dto.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="fields">The fields.</param>
|
||||
private static void AttachBasicFields(DtoBaseItem dto, BaseItem item, List<ItemFields> fields)
|
||||
{
|
||||
if (fields.Contains(ItemFields.DateCreated))
|
||||
{
|
||||
dto.DateCreated = item.DateCreated;
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.DisplayMediaType))
|
||||
{
|
||||
dto.DisplayMediaType = item.DisplayMediaType;
|
||||
}
|
||||
|
||||
dto.AspectRatio = item.AspectRatio;
|
||||
|
||||
dto.BackdropImageTags = GetBackdropImageTags(item);
|
||||
|
||||
if (fields.Contains(ItemFields.Genres))
|
||||
{
|
||||
dto.Genres = item.Genres;
|
||||
}
|
||||
|
||||
if (item.Images != null)
|
||||
{
|
||||
dto.ImageTags = new Dictionary<ImageType, Guid>();
|
||||
|
||||
foreach (var image in item.Images)
|
||||
{
|
||||
ImageType type;
|
||||
|
||||
if (Enum.TryParse(image.Key, true, out type))
|
||||
{
|
||||
dto.ImageTags[type] = Kernel.Instance.ImageManager.GetImageCacheTag(item, type, image.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dto.Id = GetClientItemId(item);
|
||||
dto.IndexNumber = item.IndexNumber;
|
||||
dto.IsFolder = item.IsFolder;
|
||||
dto.Language = item.Language;
|
||||
dto.MediaType = item.MediaType;
|
||||
dto.LocationType = item.LocationType;
|
||||
|
||||
var localTrailerCount = item.LocalTrailers == null ? 0 : item.LocalTrailers.Count;
|
||||
|
||||
if (localTrailerCount > 0)
|
||||
{
|
||||
dto.LocalTrailerCount = localTrailerCount;
|
||||
}
|
||||
|
||||
dto.Name = item.Name;
|
||||
dto.OfficialRating = item.OfficialRating;
|
||||
|
||||
if (fields.Contains(ItemFields.Overview))
|
||||
{
|
||||
dto.Overview = item.Overview;
|
||||
}
|
||||
|
||||
// If there are no backdrops, indicate what parent has them in case the Ui wants to allow inheritance
|
||||
if (dto.BackdropImageTags.Count == 0)
|
||||
{
|
||||
var parentWithBackdrop = GetParentBackdropItem(item);
|
||||
|
||||
if (parentWithBackdrop != null)
|
||||
{
|
||||
dto.ParentBackdropItemId = GetClientItemId(parentWithBackdrop);
|
||||
dto.ParentBackdropImageTags = GetBackdropImageTags(parentWithBackdrop);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Parent != null && fields.Contains(ItemFields.ParentId))
|
||||
{
|
||||
dto.ParentId = GetClientItemId(item.Parent);
|
||||
}
|
||||
|
||||
dto.ParentIndexNumber = item.ParentIndexNumber;
|
||||
|
||||
// If there is no logo, indicate what parent has one in case the Ui wants to allow inheritance
|
||||
if (!dto.HasLogo)
|
||||
{
|
||||
var parentWithLogo = GetParentLogoItem(item);
|
||||
|
||||
if (parentWithLogo != null)
|
||||
{
|
||||
dto.ParentLogoItemId = GetClientItemId(parentWithLogo);
|
||||
|
||||
dto.ParentLogoImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(parentWithLogo, ImageType.Logo, parentWithLogo.GetImage(ImageType.Logo));
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.Path))
|
||||
{
|
||||
dto.Path = item.Path;
|
||||
}
|
||||
|
||||
dto.PremiereDate = item.PremiereDate;
|
||||
dto.ProductionYear = item.ProductionYear;
|
||||
|
||||
if (fields.Contains(ItemFields.ProviderIds))
|
||||
{
|
||||
dto.ProviderIds = item.ProviderIds;
|
||||
}
|
||||
|
||||
dto.RunTimeTicks = item.RunTimeTicks;
|
||||
|
||||
if (fields.Contains(ItemFields.SortName))
|
||||
{
|
||||
dto.SortName = item.SortName;
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.Taglines))
|
||||
{
|
||||
dto.Taglines = item.Taglines;
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.TrailerUrls))
|
||||
{
|
||||
dto.TrailerUrls = item.TrailerUrls;
|
||||
}
|
||||
|
||||
dto.Type = item.GetType().Name;
|
||||
dto.CommunityRating = item.CommunityRating;
|
||||
|
||||
if (item.IsFolder)
|
||||
{
|
||||
var folder = (Folder)item;
|
||||
|
||||
dto.IsRoot = folder.IsRoot;
|
||||
dto.IsVirtualFolder = folder.IsVirtualFolder;
|
||||
|
||||
if (fields.Contains(ItemFields.IndexOptions))
|
||||
{
|
||||
dto.IndexOptions = folder.IndexByOptionStrings.ToArray();
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.SortOptions))
|
||||
{
|
||||
dto.SortOptions = folder.SortByOptionStrings.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Add audio info
|
||||
var audio = item as Audio;
|
||||
if (audio != null)
|
||||
{
|
||||
if (fields.Contains(ItemFields.AudioInfo))
|
||||
{
|
||||
dto.Album = audio.Album;
|
||||
dto.AlbumArtist = audio.AlbumArtist;
|
||||
dto.Artist = audio.Artist;
|
||||
}
|
||||
}
|
||||
|
||||
// Add video info
|
||||
var video = item as Video;
|
||||
if (video != null)
|
||||
{
|
||||
dto.VideoType = video.VideoType;
|
||||
dto.VideoFormat = video.VideoFormat;
|
||||
dto.IsoType = video.IsoType;
|
||||
|
||||
if (fields.Contains(ItemFields.Chapters) && video.Chapters != null)
|
||||
{
|
||||
dto.Chapters = video.Chapters.Select(c => GetChapterInfoDto(c, item)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.MediaStreams))
|
||||
{
|
||||
// Add VideoInfo
|
||||
var iHasMediaStreams = item as IHasMediaStreams;
|
||||
|
||||
if (iHasMediaStreams != null)
|
||||
{
|
||||
dto.MediaStreams = iHasMediaStreams.MediaStreams;
|
||||
}
|
||||
}
|
||||
|
||||
// Add MovieInfo
|
||||
var movie = item as Movie;
|
||||
|
||||
if (movie != null)
|
||||
{
|
||||
var specialFeatureCount = movie.SpecialFeatures == null ? 0 : movie.SpecialFeatures.Count;
|
||||
|
||||
if (specialFeatureCount > 0)
|
||||
{
|
||||
dto.SpecialFeatureCount = specialFeatureCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.SeriesInfo))
|
||||
{
|
||||
// Add SeriesInfo
|
||||
var series = item as Series;
|
||||
|
||||
if (series != null)
|
||||
{
|
||||
dto.AirDays = series.AirDays;
|
||||
dto.AirTime = series.AirTime;
|
||||
dto.Status = series.Status;
|
||||
}
|
||||
|
||||
// Add EpisodeInfo
|
||||
var episode = item as Episode;
|
||||
|
||||
if (episode != null)
|
||||
{
|
||||
series = item.FindParent<Series>();
|
||||
|
||||
dto.SeriesId = GetClientItemId(series);
|
||||
dto.SeriesName = series.Name;
|
||||
}
|
||||
|
||||
// Add SeasonInfo
|
||||
var season = item as Season;
|
||||
|
||||
if (season != null)
|
||||
{
|
||||
series = item.FindParent<Series>();
|
||||
|
||||
dto.SeriesId = GetClientItemId(series);
|
||||
dto.SeriesName = series.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Since it can be slow to make all of these calculations independently, this method will provide a way to do them all at once
|
||||
/// </summary>
|
||||
/// <param name="folder">The folder.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="dto">The dto.</param>
|
||||
private static void SetSpecialCounts(Folder folder, User user, DtoBaseItem dto)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
var rcentlyAddedItemCount = 0;
|
||||
var recursiveItemCount = 0;
|
||||
var favoriteItemsCount = 0;
|
||||
var recentlyAddedUnPlayedItemCount = 0;
|
||||
var resumableItemCount = 0;
|
||||
var recentlyPlayedItemCount = 0;
|
||||
|
||||
double totalPercentPlayed = 0;
|
||||
|
||||
// Loop through each recursive child
|
||||
foreach (var child in folder.GetRecursiveChildren(user))
|
||||
{
|
||||
var userdata = child.GetUserData(user, false);
|
||||
|
||||
if (!child.IsFolder)
|
||||
{
|
||||
recursiveItemCount++;
|
||||
|
||||
// Check is recently added
|
||||
if (child.IsRecentlyAdded(user))
|
||||
{
|
||||
rcentlyAddedItemCount++;
|
||||
|
||||
// Check recently added unplayed
|
||||
if (userdata == null || userdata.PlayCount == 0)
|
||||
{
|
||||
recentlyAddedUnPlayedItemCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Incrememt totalPercentPlayed
|
||||
if (userdata != null)
|
||||
{
|
||||
if (userdata.PlayCount > 0)
|
||||
{
|
||||
totalPercentPlayed += 100;
|
||||
}
|
||||
else if (userdata.PlaybackPositionTicks > 0 && child.RunTimeTicks.HasValue && child.RunTimeTicks.Value > 0)
|
||||
{
|
||||
double itemPercent = userdata.PlaybackPositionTicks;
|
||||
itemPercent /= child.RunTimeTicks.Value;
|
||||
totalPercentPlayed += itemPercent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userdata != null)
|
||||
{
|
||||
if (userdata.IsFavorite)
|
||||
{
|
||||
favoriteItemsCount++;
|
||||
}
|
||||
|
||||
if (userdata.PlaybackPositionTicks > 0)
|
||||
{
|
||||
resumableItemCount++;
|
||||
}
|
||||
|
||||
if (userdata.LastPlayedDate.HasValue && (utcNow - userdata.LastPlayedDate.Value).TotalDays < Kernel.Instance.Configuration.RecentlyPlayedDays)
|
||||
{
|
||||
recentlyPlayedItemCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dto.RecursiveItemCount = recursiveItemCount;
|
||||
dto.RecentlyAddedItemCount = rcentlyAddedItemCount;
|
||||
dto.RecentlyAddedUnPlayedItemCount = recentlyAddedUnPlayedItemCount;
|
||||
dto.ResumableItemCount = resumableItemCount;
|
||||
dto.FavoriteItemCount = favoriteItemsCount;
|
||||
dto.RecentlyPlayedItemCount = recentlyPlayedItemCount;
|
||||
|
||||
if (recursiveItemCount > 0)
|
||||
{
|
||||
dto.PlayedPercentage = totalPercentPlayed / recursiveItemCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches People DTO's to a DTOBaseItem
|
||||
/// </summary>
|
||||
/// <param name="dto">The dto.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private static async Task AttachPeople(DtoBaseItem dto, BaseItem item)
|
||||
{
|
||||
if (item.People == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach People by transforming them into BaseItemPerson (DTO)
|
||||
dto.People = new BaseItemPerson[item.People.Count];
|
||||
|
||||
var entities = await Task.WhenAll(item.People.Select(c =>
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Kernel.Instance.LibraryManager.GetPerson(c.Name).ConfigureAwait(false);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.LogException("Error getting person {0}", ex, c.Name);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
||||
)).ConfigureAwait(false);
|
||||
|
||||
for (var i = 0; i < item.People.Count; i++)
|
||||
{
|
||||
var person = item.People[i];
|
||||
|
||||
var baseItemPerson = new BaseItemPerson
|
||||
{
|
||||
Name = person.Name,
|
||||
Role = person.Role,
|
||||
Type = person.Type
|
||||
};
|
||||
|
||||
var ibnObject = entities[i];
|
||||
|
||||
if (ibnObject != null)
|
||||
{
|
||||
var primaryImagePath = ibnObject.PrimaryImagePath;
|
||||
|
||||
if (!string.IsNullOrEmpty(primaryImagePath))
|
||||
{
|
||||
baseItemPerson.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(ibnObject, ImageType.Primary, primaryImagePath);
|
||||
}
|
||||
}
|
||||
|
||||
dto.People[i] = baseItemPerson;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If an item does not any backdrops, this can be used to find the first parent that does have one
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
private static BaseItem GetParentBackdropItem(BaseItem item)
|
||||
{
|
||||
var parent = item.Parent;
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent.BackdropImagePaths != null && parent.BackdropImagePaths.Count > 0)
|
||||
{
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent = parent.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If an item does not have a logo, this can be used to find the first parent that does have one
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
private static BaseItem GetParentLogoItem(BaseItem item)
|
||||
{
|
||||
var parent = item.Parent;
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent.HasImage(ImageType.Logo))
|
||||
{
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent = parent.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the library update info.
|
||||
/// </summary>
|
||||
/// <param name="changeEvent">The <see cref="ChildrenChangedEventArgs" /> instance containing the event data.</param>
|
||||
/// <returns>LibraryUpdateInfo.</returns>
|
||||
internal static LibraryUpdateInfo GetLibraryUpdateInfo(ChildrenChangedEventArgs changeEvent)
|
||||
{
|
||||
return new LibraryUpdateInfo
|
||||
{
|
||||
Folder = GetBaseItemInfo(changeEvent.Folder),
|
||||
ItemsAdded = changeEvent.ItemsAdded.Select(GetBaseItemInfo),
|
||||
ItemsRemoved = changeEvent.ItemsRemoved.Select(i => i.Id),
|
||||
ItemsUpdated = changeEvent.ItemsUpdated.Select(i => i.Id)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a UserItemData to a DTOUserItemData
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>DtoUserItemData.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public static DtoUserItemData GetDtoUserItemData(UserItemData data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
return new DtoUserItemData
|
||||
{
|
||||
IsFavorite = data.IsFavorite,
|
||||
Likes = data.Likes,
|
||||
PlaybackPositionTicks = data.PlaybackPositionTicks,
|
||||
PlayCount = data.PlayCount,
|
||||
Rating = data.Rating,
|
||||
Played = data.Played
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chapter info dto.
|
||||
/// </summary>
|
||||
/// <param name="chapterInfo">The chapter info.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>ChapterInfoDto.</returns>
|
||||
private static ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item)
|
||||
{
|
||||
var dto = new ChapterInfoDto
|
||||
{
|
||||
Name = chapterInfo.Name,
|
||||
StartPositionTicks = chapterInfo.StartPositionTicks
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(chapterInfo.ImagePath))
|
||||
{
|
||||
dto.ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.ChapterImage, chapterInfo.ImagePath);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a BaseItem to a BaseItemInfo
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>BaseItemInfo.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
public static BaseItemInfo GetBaseItemInfo(BaseItem item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
var info = new BaseItemInfo
|
||||
{
|
||||
Id = GetClientItemId(item),
|
||||
Name = item.Name,
|
||||
Type = item.GetType().Name,
|
||||
IsFolder = item.IsFolder,
|
||||
RunTimeTicks = item.RunTimeTicks
|
||||
};
|
||||
|
||||
var imagePath = item.PrimaryImagePath;
|
||||
|
||||
if (!string.IsNullOrEmpty(imagePath))
|
||||
{
|
||||
info.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Primary, imagePath);
|
||||
}
|
||||
|
||||
if (item.BackdropImagePaths != null && item.BackdropImagePaths.Count > 0)
|
||||
{
|
||||
imagePath = item.BackdropImagePaths[0];
|
||||
|
||||
if (!string.IsNullOrEmpty(imagePath))
|
||||
{
|
||||
info.BackdropImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets client-side Id of a server-side BaseItem
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
public static string GetClientItemId(BaseItem item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
var indexFolder = item as IndexFolder;
|
||||
|
||||
if (indexFolder != null)
|
||||
{
|
||||
return GetClientItemId(indexFolder.Parent) + IndexFolderDelimeter + (indexFolder.IndexName ?? string.Empty) + IndexFolderDelimeter + indexFolder.Id;
|
||||
}
|
||||
|
||||
return item.Id.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a User to a DTOUser
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>DtoUser.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">user</exception>
|
||||
public static DtoUser GetDtoUser(User user)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
|
||||
var dto = new DtoUser
|
||||
{
|
||||
Id = user.Id,
|
||||
Name = user.Name,
|
||||
HasPassword = !String.IsNullOrEmpty(user.Password),
|
||||
LastActivityDate = user.LastActivityDate,
|
||||
LastLoginDate = user.LastLoginDate,
|
||||
Configuration = user.Configuration
|
||||
};
|
||||
|
||||
var image = user.PrimaryImagePath;
|
||||
|
||||
if (!string.IsNullOrEmpty(image))
|
||||
{
|
||||
dto.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(user, ImageType.Primary, image);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a BaseItem based upon it's client-side item id
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
public static BaseItem GetItemByClientId(string id, Guid? userId = null)
|
||||
{
|
||||
var isIdEmpty = string.IsNullOrEmpty(id);
|
||||
|
||||
// If the item is an indexed folder we have to do a special routine to get it
|
||||
var isIndexFolder = !isIdEmpty &&
|
||||
id.IndexOf(IndexFolderDelimeter, StringComparison.OrdinalIgnoreCase) != -1;
|
||||
|
||||
if (isIndexFolder)
|
||||
{
|
||||
if (userId.HasValue)
|
||||
{
|
||||
return GetIndexFolder(id, userId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
BaseItem item = null;
|
||||
|
||||
if (userId.HasValue)
|
||||
{
|
||||
item = isIdEmpty
|
||||
? Kernel.Instance.GetUserById(userId.Value).RootFolder
|
||||
: Kernel.Instance.GetItemById(new Guid(id), userId.Value);
|
||||
}
|
||||
else if (!isIndexFolder)
|
||||
{
|
||||
item = Kernel.Instance.GetItemById(new Guid(id));
|
||||
}
|
||||
|
||||
// If we still don't find it, look within individual user views
|
||||
if (item == null && !userId.HasValue)
|
||||
{
|
||||
foreach (var user in Kernel.Instance.Users)
|
||||
{
|
||||
item = GetItemByClientId(id, user.Id);
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an index folder based on an Id and userId
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
private static BaseItem GetIndexFolder(string id, Guid userId)
|
||||
{
|
||||
var user = Kernel.Instance.GetUserById(userId);
|
||||
|
||||
var stringSeparators = new[] { IndexFolderDelimeter };
|
||||
|
||||
// Split using the delimeter
|
||||
var values = id.Split(stringSeparators, StringSplitOptions.None).ToList();
|
||||
|
||||
// Get the top folder normally using the first id
|
||||
var folder = GetItemByClientId(values[0], userId) as Folder;
|
||||
|
||||
values.RemoveAt(0);
|
||||
|
||||
// Get indexed folders using the remaining values in the id string
|
||||
return GetIndexFolder(values, folder, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets indexed folders based on a list of index names and folder id's
|
||||
/// </summary>
|
||||
/// <param name="values">The values.</param>
|
||||
/// <param name="parentFolder">The parent folder.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
private static BaseItem GetIndexFolder(List<string> values, Folder parentFolder, User user)
|
||||
{
|
||||
// The index name is first
|
||||
var indexBy = values[0];
|
||||
|
||||
// The index folder id is next
|
||||
var indexFolderId = new Guid(values[1]);
|
||||
|
||||
// Remove them from the lst
|
||||
values.RemoveRange(0, 2);
|
||||
|
||||
// Get the IndexFolder
|
||||
var indexFolder = parentFolder.GetChildren(user, indexBy).FirstOrDefault(i => i.Id == indexFolderId) as Folder;
|
||||
|
||||
// Nested index folder
|
||||
if (values.Count > 0)
|
||||
{
|
||||
return GetIndexFolder(values, indexFolder, user);
|
||||
}
|
||||
|
||||
return indexFolder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the backdrop image tags.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>List{System.String}.</returns>
|
||||
private static List<Guid> GetBackdropImageTags(BaseItem item)
|
||||
{
|
||||
if (item.BackdropImagePaths == null)
|
||||
{
|
||||
return new List<Guid>();
|
||||
}
|
||||
|
||||
return item.BackdropImagePaths.Select(p => Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, p)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
public class ItemController
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a path into a BaseItem
|
||||
/// </summary>
|
||||
public async Task<BaseItem> GetItem(string path, Folder parent = null, WIN32_FIND_DATA? fileInfo = null, bool allowInternetProviders = true)
|
||||
{
|
||||
var args = new ItemResolveEventArgs
|
||||
{
|
||||
FileInfo = fileInfo ?? FileData.GetFileData(path),
|
||||
Parent = parent,
|
||||
Cancel = false,
|
||||
Path = path
|
||||
};
|
||||
|
||||
// Gather child folder and files
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
|
||||
|
||||
bool isVirtualFolder = parent != null && parent.IsRoot;
|
||||
args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
args.FileSystemChildren = new WIN32_FIND_DATA[] { };
|
||||
}
|
||||
|
||||
|
||||
// Check to see if we should resolve based on our contents
|
||||
if (!EntityResolutionHelper.ShouldResolvePathContents(args))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
BaseItem item = Kernel.Instance.ResolveItem(args);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Person
|
||||
/// </summary>
|
||||
public Task<Person> GetPerson(string name)
|
||||
{
|
||||
return GetImagesByNameItem<Person>(Kernel.Instance.ApplicationPaths.PeoplePath, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Studio
|
||||
/// </summary>
|
||||
public Task<Studio> GetStudio(string name)
|
||||
{
|
||||
return GetImagesByNameItem<Studio>(Kernel.Instance.ApplicationPaths.StudioPath, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Genre
|
||||
/// </summary>
|
||||
public Task<Genre> GetGenre(string name)
|
||||
{
|
||||
return GetImagesByNameItem<Genre>(Kernel.Instance.ApplicationPaths.GenrePath, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Year
|
||||
/// </summary>
|
||||
public Task<Year> GetYear(int value)
|
||||
{
|
||||
return GetImagesByNameItem<Year>(Kernel.Instance.ApplicationPaths.YearPath, value.ToString());
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, object> ImagesByNameItemCache = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Generically retrieves an IBN item
|
||||
/// </summary>
|
||||
private Task<T> GetImagesByNameItem<T>(string path, string name)
|
||||
where T : BaseEntity, new()
|
||||
{
|
||||
name = FileData.GetValidFilename(name);
|
||||
|
||||
path = Path.Combine(path, name);
|
||||
|
||||
// Look for it in the cache, if it's not there, create it
|
||||
if (!ImagesByNameItemCache.ContainsKey(path))
|
||||
{
|
||||
ImagesByNameItemCache[path] = CreateImagesByNameItem<T>(path, name);
|
||||
}
|
||||
|
||||
return ImagesByNameItemCache[path] as Task<T>;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an IBN item based on a given path
|
||||
/// </summary>
|
||||
private async Task<T> CreateImagesByNameItem<T>(string path, string name)
|
||||
where T : BaseEntity, new()
|
||||
{
|
||||
var item = new T { };
|
||||
|
||||
item.Name = name;
|
||||
item.Id = path.GetMD5();
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
item.DateCreated = Directory.GetCreationTimeUtc(path);
|
||||
item.DateModified = Directory.GetLastWriteTimeUtc(path);
|
||||
|
||||
var args = new ItemResolveEventArgs { };
|
||||
args.FileInfo = FileData.GetFileData(path);
|
||||
args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
|
||||
|
||||
await Kernel.Instance.ExecuteMetadataProviders(item).ConfigureAwait(false);
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
397
MediaBrowser.Controller/Library/ItemResolveArgs.cs
Normal file
397
MediaBrowser.Controller/Library/ItemResolveArgs.cs
Normal file
@@ -0,0 +1,397 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Win32;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// These are arguments relating to the file system that are collected once and then referred to
|
||||
/// whenever needed. Primarily for entity resolution.
|
||||
/// </summary>
|
||||
public class ItemResolveArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the file system children.
|
||||
/// </summary>
|
||||
/// <value>The file system children.</value>
|
||||
public IEnumerable<WIN32_FIND_DATA> FileSystemChildren
|
||||
{
|
||||
get { return FileSystemDictionary.Values; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the file system dictionary.
|
||||
/// </summary>
|
||||
/// <value>The file system dictionary.</value>
|
||||
public Dictionary<string, WIN32_FIND_DATA> FileSystemDictionary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent.
|
||||
/// </summary>
|
||||
/// <value>The parent.</value>
|
||||
public Folder Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the file info.
|
||||
/// </summary>
|
||||
/// <value>The file info.</value>
|
||||
public WIN32_FIND_DATA FileInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is directory.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is directory; otherwise, <c>false</c>.</value>
|
||||
public bool IsDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return FileInfo.dwFileAttributes.HasFlag(FileAttributes.Directory);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is hidden.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
|
||||
public bool IsHidden
|
||||
{
|
||||
get
|
||||
{
|
||||
return FileInfo.IsHidden;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is system file.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is system file; otherwise, <c>false</c>.</value>
|
||||
public bool IsSystemFile
|
||||
{
|
||||
get
|
||||
{
|
||||
return FileInfo.IsSystemFile;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is vf.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is vf; otherwise, <c>false</c>.</value>
|
||||
public bool IsVf
|
||||
{
|
||||
// we should be considered a virtual folder if we are a child of one of the children of the system root folder.
|
||||
// this is a bit of a trick to determine that... the directory name of a sub-child of the root will start with
|
||||
// the root but not be equal to it
|
||||
get
|
||||
{
|
||||
if (!IsDirectory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var parentDir = FileInfo.Path != null ? System.IO.Path.GetDirectoryName(FileInfo.Path) ?? string.Empty : string.Empty;
|
||||
|
||||
return (parentDir.Length > Kernel.Instance.ApplicationPaths.RootFolderPath.Length
|
||||
&& parentDir.StartsWith(Kernel.Instance.ApplicationPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is physical root.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is physical root; otherwise, <c>false</c>.</value>
|
||||
public bool IsPhysicalRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
return IsDirectory && Path.Equals(Kernel.Instance.ApplicationPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is root.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
|
||||
public bool IsRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
return Parent == null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the additional locations.
|
||||
/// </summary>
|
||||
/// <value>The additional locations.</value>
|
||||
private List<string> AdditionalLocations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the additional location.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public void AddAdditionalLocation(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
if (AdditionalLocations == null)
|
||||
{
|
||||
AdditionalLocations = new List<string>();
|
||||
}
|
||||
|
||||
AdditionalLocations.Add(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the physical locations.
|
||||
/// </summary>
|
||||
/// <value>The physical locations.</value>
|
||||
public IEnumerable<string> PhysicalLocations
|
||||
{
|
||||
get
|
||||
{
|
||||
var paths = string.IsNullOrWhiteSpace(Path) ? new string[] {} : new[] {Path};
|
||||
return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store these to reduce disk access in Resolvers
|
||||
/// </summary>
|
||||
/// <value>The metadata file dictionary.</value>
|
||||
private Dictionary<string, WIN32_FIND_DATA> MetadataFileDictionary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metadata files.
|
||||
/// </summary>
|
||||
/// <value>The metadata files.</value>
|
||||
public IEnumerable<WIN32_FIND_DATA> MetadataFiles
|
||||
{
|
||||
get
|
||||
{
|
||||
if (MetadataFileDictionary != null)
|
||||
{
|
||||
return MetadataFileDictionary.Values;
|
||||
}
|
||||
|
||||
return new WIN32_FIND_DATA[] {};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the metadata file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <exception cref="System.IO.FileNotFoundException"></exception>
|
||||
public void AddMetadataFile(string path)
|
||||
{
|
||||
var file = FileSystem.GetFileData(path);
|
||||
|
||||
if (!file.HasValue)
|
||||
{
|
||||
throw new FileNotFoundException(path);
|
||||
}
|
||||
|
||||
AddMetadataFile(file.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the metadata file.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo">The file info.</param>
|
||||
public void AddMetadataFile(WIN32_FIND_DATA fileInfo)
|
||||
{
|
||||
AddMetadataFiles(new[] { fileInfo });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the metadata files.
|
||||
/// </summary>
|
||||
/// <param name="files">The files.</param>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public void AddMetadataFiles(IEnumerable<WIN32_FIND_DATA> files)
|
||||
{
|
||||
if (files == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
if (MetadataFileDictionary == null)
|
||||
{
|
||||
MetadataFileDictionary = new Dictionary<string, WIN32_FIND_DATA>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
foreach (var file in files)
|
||||
{
|
||||
MetadataFileDictionary[file.cFileName] = file;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the file system entry by.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public WIN32_FIND_DATA? GetFileSystemEntryByName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
return GetFileSystemEntryByPath(System.IO.Path.Combine(Path, name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file system entry by path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public WIN32_FIND_DATA? GetFileSystemEntryByPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
if (FileSystemDictionary != null)
|
||||
{
|
||||
WIN32_FIND_DATA entry;
|
||||
|
||||
if (FileSystemDictionary.TryGetValue(path, out entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the meta file by path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public WIN32_FIND_DATA? GetMetaFileByPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
if (MetadataFileDictionary != null)
|
||||
{
|
||||
WIN32_FIND_DATA entry;
|
||||
|
||||
if (MetadataFileDictionary.TryGetValue(System.IO.Path.GetFileName(path), out entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return GetFileSystemEntryByPath(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the meta file by.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public WIN32_FIND_DATA? GetMetaFileByName(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
if (MetadataFileDictionary != null)
|
||||
{
|
||||
WIN32_FIND_DATA entry;
|
||||
|
||||
if (MetadataFileDictionary.TryGetValue(name, out entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return GetFileSystemEntryByName(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [contains meta file by name] [the specified name].
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns><c>true</c> if [contains meta file by name] [the specified name]; otherwise, <c>false</c>.</returns>
|
||||
public bool ContainsMetaFileByName(string name)
|
||||
{
|
||||
return GetMetaFileByName(name).HasValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [contains file system entry by name] [the specified name].
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns>
|
||||
public bool ContainsFileSystemEntryByName(string name)
|
||||
{
|
||||
return GetFileSystemEntryByName(name).HasValue;
|
||||
}
|
||||
|
||||
#region Equality Overrides
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="System.Object" /> is equal to this instance.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current object.</param>
|
||||
/// <returns><c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return (Equals(obj as ItemResolveArgs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Path.GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equalses the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
protected bool Equals(ItemResolveArgs args)
|
||||
{
|
||||
if (args != null)
|
||||
{
|
||||
if (args.Path == null && Path == null) return true;
|
||||
return args.Path != null && args.Path.Equals(Path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// This is an EventArgs object used when resolving a Path into a BaseItem
|
||||
/// </summary>
|
||||
public class ItemResolveEventArgs : PreBeginResolveEventArgs
|
||||
{
|
||||
public WIN32_FIND_DATA[] FileSystemChildren { get; set; }
|
||||
|
||||
protected List<string> _additionalLocations = new List<string>();
|
||||
public List<string> AdditionalLocations
|
||||
{
|
||||
get
|
||||
{
|
||||
return _additionalLocations;
|
||||
}
|
||||
set
|
||||
{
|
||||
_additionalLocations = value;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> PhysicalLocations
|
||||
{
|
||||
get
|
||||
{
|
||||
return (new List<string>() {this.Path}).Concat(AdditionalLocations);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsBDFolder { get; set; }
|
||||
public bool IsDVDFolder { get; set; }
|
||||
public bool IsHDDVDFolder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Store these to reduce disk access in Resolvers
|
||||
/// </summary>
|
||||
public string[] MetadataFiles { get; set; }
|
||||
|
||||
public WIN32_FIND_DATA? GetFileSystemEntry(string path)
|
||||
{
|
||||
WIN32_FIND_DATA entry = FileSystemChildren.FirstOrDefault(f => f.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
|
||||
return entry.cFileName != null ? (WIN32_FIND_DATA?)entry : null;
|
||||
}
|
||||
|
||||
public bool ContainsFile(string name)
|
||||
{
|
||||
return FileSystemChildren.FirstOrDefault(f => f.cFileName.Equals(name, StringComparison.OrdinalIgnoreCase)).cFileName != null;
|
||||
}
|
||||
|
||||
public bool ContainsFolder(string name)
|
||||
{
|
||||
return ContainsFile(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is an EventArgs object used before we begin resolving a Path into a BaseItem
|
||||
/// File system children have not been collected yet, but consuming events will
|
||||
/// have a chance to cancel resolution based on the Path, Parent and FileAttributes
|
||||
/// </summary>
|
||||
public class PreBeginResolveEventArgs : EventArgs
|
||||
{
|
||||
public Folder Parent { get; set; }
|
||||
|
||||
public bool Cancel { get; set; }
|
||||
|
||||
public WIN32_FIND_DATA FileInfo { get; set; }
|
||||
|
||||
public string Path { get; set; }
|
||||
|
||||
public bool IsDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return FileInfo.dwFileAttributes.HasFlag(FileAttributes.Directory);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsHidden
|
||||
{
|
||||
get
|
||||
{
|
||||
return FileInfo.IsHidden;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSystemFile
|
||||
{
|
||||
get
|
||||
{
|
||||
return FileInfo.IsSystemFile;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
511
MediaBrowser.Controller/Library/LibraryManager.cs
Normal file
511
MediaBrowser.Controller/Library/LibraryManager.cs
Normal file
@@ -0,0 +1,511 @@
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Kernel;
|
||||
using MediaBrowser.Common.Win32;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using MoreLinq;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Class LibraryManager
|
||||
/// </summary>
|
||||
public class LibraryManager : BaseManager<Kernel>
|
||||
{
|
||||
#region LibraryChanged Event
|
||||
/// <summary>
|
||||
/// Fires whenever any validation routine adds or removes items. The added and removed items are properties of the args.
|
||||
/// *** Will fire asynchronously. ***
|
||||
/// </summary>
|
||||
public event EventHandler<ChildrenChangedEventArgs> LibraryChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="E:LibraryChanged" /> event.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="ChildrenChangedEventArgs" /> instance containing the event data.</param>
|
||||
internal void OnLibraryChanged(ChildrenChangedEventArgs args)
|
||||
{
|
||||
EventHelper.QueueEventIfNotNull(LibraryChanged, this, args);
|
||||
|
||||
// Had to put this in a separate method to avoid an implicitly captured closure
|
||||
SendLibraryChangedWebSocketMessage(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the library changed web socket message.
|
||||
/// </summary>
|
||||
/// <param name="args">The <see cref="ChildrenChangedEventArgs" /> instance containing the event data.</param>
|
||||
private void SendLibraryChangedWebSocketMessage(ChildrenChangedEventArgs args)
|
||||
{
|
||||
// Notify connected ui's
|
||||
Kernel.TcpManager.SendWebSocketMessage("LibraryChanged", () => DtoBuilder.GetLibraryUpdateInfo(args));
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The kernel.</param>
|
||||
public LibraryManager(Kernel kernel)
|
||||
: base(kernel)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the item.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
public BaseItem ResolveItem(ItemResolveArgs args)
|
||||
{
|
||||
return Kernel.EntityResolvers.Select(r => r.ResolvePath(args)).FirstOrDefault(i => i != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a path into a BaseItem
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="parent">The parent.</param>
|
||||
/// <param name="fileInfo">The file info.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public BaseItem GetItem(string path, Folder parent = null, WIN32_FIND_DATA? fileInfo = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
fileInfo = fileInfo ?? FileSystem.GetFileData(path);
|
||||
|
||||
if (!fileInfo.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var args = new ItemResolveArgs
|
||||
{
|
||||
Parent = parent,
|
||||
Path = path,
|
||||
FileInfo = fileInfo.Value
|
||||
};
|
||||
|
||||
// Return null if ignore rules deem that we should do so
|
||||
if (Kernel.EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(args)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Gather child folder and files
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
// When resolving the root, we need it's grandchildren (children of user views)
|
||||
var flattenFolderDepth = args.IsPhysicalRoot ? 2 : 0;
|
||||
|
||||
args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, flattenFolderDepth: flattenFolderDepth, args: args);
|
||||
}
|
||||
|
||||
// Check to see if we should resolve based on our contents
|
||||
if (args.IsDirectory && !EntityResolutionHelper.ShouldResolvePathContents(args))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResolveItem(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a set of files into a list of BaseItem
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="files">The files.</param>
|
||||
/// <param name="parent">The parent.</param>
|
||||
/// <returns>List{``0}.</returns>
|
||||
public List<T> GetItems<T>(IEnumerable<WIN32_FIND_DATA> files, Folder parent)
|
||||
where T : BaseItem
|
||||
{
|
||||
var list = new List<T>();
|
||||
|
||||
Parallel.ForEach(files, f =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = GetItem(f.Path, parent, f) as T;
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
lock (list)
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error resolving path {0}", ex, f.Path);
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the root media folder
|
||||
/// </summary>
|
||||
/// <returns>AggregateFolder.</returns>
|
||||
/// <exception cref="System.InvalidOperationException">Cannot create the root folder until plugins have loaded</exception>
|
||||
internal AggregateFolder CreateRootFolder()
|
||||
{
|
||||
if (Kernel.Plugins == null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot create the root folder until plugins have loaded");
|
||||
}
|
||||
|
||||
var rootFolderPath = Kernel.ApplicationPaths.RootFolderPath;
|
||||
var rootFolder = Kernel.ItemRepository.RetrieveItem(rootFolderPath.GetMBId(typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)GetItem(rootFolderPath);
|
||||
|
||||
// Add in the plug-in folders
|
||||
foreach (var child in Kernel.PluginFolders)
|
||||
{
|
||||
rootFolder.AddVirtualChild(child);
|
||||
}
|
||||
|
||||
return rootFolder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Person
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
|
||||
/// <returns>Task{Person}.</returns>
|
||||
public Task<Person> GetPerson(string name, bool allowSlowProviders = false)
|
||||
{
|
||||
return GetPerson(name, CancellationToken.None, allowSlowProviders);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Person
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
|
||||
/// <returns>Task{Person}.</returns>
|
||||
private Task<Person> GetPerson(string name, CancellationToken cancellationToken, bool allowSlowProviders = false)
|
||||
{
|
||||
return GetImagesByNameItem<Person>(Kernel.ApplicationPaths.PeoplePath, name, cancellationToken, allowSlowProviders);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Studio
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
|
||||
/// <returns>Task{Studio}.</returns>
|
||||
public Task<Studio> GetStudio(string name, bool allowSlowProviders = false)
|
||||
{
|
||||
return GetImagesByNameItem<Studio>(Kernel.ApplicationPaths.StudioPath, name, CancellationToken.None, allowSlowProviders);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Genre
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
|
||||
/// <returns>Task{Genre}.</returns>
|
||||
public Task<Genre> GetGenre(string name, bool allowSlowProviders = false)
|
||||
{
|
||||
return GetImagesByNameItem<Genre>(Kernel.ApplicationPaths.GenrePath, name, CancellationToken.None, allowSlowProviders);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The us culture
|
||||
/// </summary>
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Year
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
|
||||
/// <returns>Task{Year}.</returns>
|
||||
/// <exception cref="System.ArgumentOutOfRangeException"></exception>
|
||||
public Task<Year> GetYear(int value, bool allowSlowProviders = false)
|
||||
{
|
||||
if (value <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return GetImagesByNameItem<Year>(Kernel.ApplicationPaths.YearPath, value.ToString(UsCulture), CancellationToken.None, allowSlowProviders);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The images by name item cache
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, object> ImagesByNameItemCache = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Generically retrieves an IBN item
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
|
||||
/// <returns>Task{``0}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
private Task<T> GetImagesByNameItem<T>(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true)
|
||||
where T : BaseItem, new()
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
var key = Path.Combine(path, FileSystem.GetValidFilename(name));
|
||||
|
||||
var obj = ImagesByNameItemCache.GetOrAdd(key, keyname => CreateImagesByNameItem<T>(path, name, cancellationToken, allowSlowProviders));
|
||||
|
||||
return obj as Task<T>;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an IBN item based on a given path
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
|
||||
/// <returns>Task{``0}.</returns>
|
||||
/// <exception cref="System.IO.IOException">Path not created: + path</exception>
|
||||
private async Task<T> CreateImagesByNameItem<T>(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true)
|
||||
where T : BaseItem, new()
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Logger.Debug("Creating {0}: {1}", typeof(T).Name, name);
|
||||
|
||||
path = Path.Combine(path, FileSystem.GetValidFilename(name));
|
||||
|
||||
var fileInfo = FileSystem.GetFileData(path);
|
||||
|
||||
var isNew = false;
|
||||
|
||||
if (!fileInfo.HasValue)
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
fileInfo = FileSystem.GetFileData(path);
|
||||
|
||||
if (!fileInfo.HasValue)
|
||||
{
|
||||
throw new IOException("Path not created: " + path);
|
||||
}
|
||||
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var id = path.GetMBId(typeof(T));
|
||||
|
||||
var item = Kernel.ItemRepository.RetrieveItem(id) as T;
|
||||
if (item == null)
|
||||
{
|
||||
item = new T
|
||||
{
|
||||
Name = name,
|
||||
Id = id,
|
||||
DateCreated = fileInfo.Value.CreationTimeUtc,
|
||||
DateModified = fileInfo.Value.LastWriteTimeUtc,
|
||||
Path = path
|
||||
};
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Set this now so we don't cause additional file system access during provider executions
|
||||
item.ResetResolveArgs(fileInfo);
|
||||
|
||||
await item.RefreshMetadata(cancellationToken, isNew, allowSlowProviders: allowSlowProviders).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate and refresh the People sub-set of the IBN.
|
||||
/// The items are stored in the db but not loaded into memory until actually requested by an operation.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <returns>Task.</returns>
|
||||
internal async Task ValidatePeople(CancellationToken cancellationToken, IProgress<TaskProgress> progress)
|
||||
{
|
||||
// Clear the IBN cache
|
||||
ImagesByNameItemCache.Clear();
|
||||
|
||||
const int maxTasks = 250;
|
||||
|
||||
var tasks = new List<Task>();
|
||||
|
||||
var includedPersonTypes = new[] { PersonType.Actor, PersonType.Director };
|
||||
|
||||
var people = Kernel.RootFolder.RecursiveChildren
|
||||
.Where(c => c.People != null)
|
||||
.SelectMany(c => c.People.Where(p => includedPersonTypes.Contains(p.Type)))
|
||||
.DistinctBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var numComplete = 0;
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
if (tasks.Count > maxTasks)
|
||||
{
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
tasks.Clear();
|
||||
|
||||
// Safe cancellation point, when there are no pending tasks
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
// Avoid accessing the foreach variable within the closure
|
||||
var currentPerson = person;
|
||||
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
await GetPerson(currentPerson.Name, cancellationToken, allowSlowProviders: true).ConfigureAwait(false);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.ErrorException("Error validating IBN entry {0}", ex, currentPerson.Name);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
lock (progress)
|
||||
{
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= people.Count;
|
||||
|
||||
progress.Report(new TaskProgress { PercentComplete = 100 * percent });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
progress.Report(new TaskProgress { PercentComplete = 100 });
|
||||
|
||||
Logger.Info("People validation complete");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the root media folder
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
internal async Task ValidateMediaLibrary(IProgress<TaskProgress> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.Info("Validating media library");
|
||||
|
||||
await Kernel.RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Start by just validating the children of the root, but go no further
|
||||
await Kernel.RootFolder.ValidateChildren(new Progress<TaskProgress> { }, cancellationToken, recursive: false);
|
||||
|
||||
// Validate only the collection folders for each user, just to make them available as quickly as possible
|
||||
var userCollectionFolderTasks = Kernel.Users.AsParallel().Select(user => user.ValidateCollectionFolders(new Progress<TaskProgress> { }, cancellationToken));
|
||||
await Task.WhenAll(userCollectionFolderTasks).ConfigureAwait(false);
|
||||
|
||||
// Now validate the entire media library
|
||||
await Kernel.RootFolder.ValidateChildren(progress, cancellationToken, recursive: true).ConfigureAwait(false);
|
||||
|
||||
foreach (var user in Kernel.Users)
|
||||
{
|
||||
await user.ValidateMediaLibrary(new Progress<TaskProgress> { }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves display preferences for a Folder
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="folder">The folder.</param>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SaveDisplayPreferencesForFolder(User user, Folder folder, DisplayPreferences data)
|
||||
{
|
||||
// Need to update all items with the same DisplayPrefsId
|
||||
foreach (var child in Kernel.RootFolder.GetRecursiveChildren(user)
|
||||
.OfType<Folder>()
|
||||
.Where(i => i.DisplayPrefsId == folder.DisplayPrefsId))
|
||||
{
|
||||
child.AddOrUpdateDisplayPrefs(user, data);
|
||||
}
|
||||
|
||||
return Kernel.DisplayPreferencesRepository.SaveDisplayPrefs(folder, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default view.
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{VirtualFolderInfo}.</returns>
|
||||
public IEnumerable<VirtualFolderInfo> GetDefaultVirtualFolders()
|
||||
{
|
||||
return GetView(Kernel.ApplicationPaths.DefaultUserViewsPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the view.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>IEnumerable{VirtualFolderInfo}.</returns>
|
||||
public IEnumerable<VirtualFolderInfo> GetVirtualFolders(User user)
|
||||
{
|
||||
return GetView(user.RootFolderPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the view.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>IEnumerable{VirtualFolderInfo}.</returns>
|
||||
private IEnumerable<VirtualFolderInfo> GetView(string path)
|
||||
{
|
||||
return Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)
|
||||
.Select(dir => new VirtualFolderInfo
|
||||
{
|
||||
Name = Path.GetFileName(dir),
|
||||
Locations = Directory.EnumerateFiles(dir, "*.lnk", SearchOption.TopDirectoryOnly).Select(FileSystem.ResolveShortcut).ToList()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
69
MediaBrowser.Controller/Library/Profiler.cs
Normal file
69
MediaBrowser.Controller/Library/Profiler.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using MediaBrowser.Common.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Profiler
|
||||
/// </summary>
|
||||
public class Profiler : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The name
|
||||
/// </summary>
|
||||
readonly string name;
|
||||
/// <summary>
|
||||
/// The stopwatch
|
||||
/// </summary>
|
||||
readonly Stopwatch stopwatch;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Profiler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
public Profiler(string name)
|
||||
{
|
||||
this.name = name;
|
||||
|
||||
stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
}
|
||||
#region IDisposable Members
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
string message;
|
||||
if (stopwatch.ElapsedMilliseconds > 300000)
|
||||
{
|
||||
message = string.Format("{0} took {1} minutes.",
|
||||
name, ((float)stopwatch.ElapsedMilliseconds / 60000).ToString("F"));
|
||||
}
|
||||
else
|
||||
{
|
||||
message = string.Format("{0} took {1} seconds.",
|
||||
name, ((float)stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000"));
|
||||
}
|
||||
Logger.LogInfo(message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
79
MediaBrowser.Controller/Library/ResourcePool.cs
Normal file
79
MediaBrowser.Controller/Library/ResourcePool.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// This is just a collection of semaphores to control the number of concurrent executions of various resources
|
||||
/// </summary>
|
||||
public class ResourcePool : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// You tube
|
||||
/// </summary>
|
||||
public readonly SemaphoreSlim YouTube = new SemaphoreSlim(5, 5);
|
||||
|
||||
/// <summary>
|
||||
/// The trakt
|
||||
/// </summary>
|
||||
public readonly SemaphoreSlim Trakt = new SemaphoreSlim(5, 5);
|
||||
|
||||
/// <summary>
|
||||
/// The tv db
|
||||
/// </summary>
|
||||
public readonly SemaphoreSlim TvDb = new SemaphoreSlim(5, 5);
|
||||
|
||||
/// <summary>
|
||||
/// The movie db
|
||||
/// </summary>
|
||||
public readonly SemaphoreSlim MovieDb = new SemaphoreSlim(5, 5);
|
||||
|
||||
/// <summary>
|
||||
/// The fan art
|
||||
/// </summary>
|
||||
public readonly SemaphoreSlim FanArt = new SemaphoreSlim(5, 5);
|
||||
|
||||
/// <summary>
|
||||
/// The mb
|
||||
/// </summary>
|
||||
public readonly SemaphoreSlim Mb = new SemaphoreSlim(5, 5);
|
||||
|
||||
/// <summary>
|
||||
/// Apple doesn't seem to like too many simulataneous requests.
|
||||
/// </summary>
|
||||
public readonly SemaphoreSlim AppleTrailerVideos = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// The apple trailer images
|
||||
/// </summary>
|
||||
public readonly SemaphoreSlim AppleTrailerImages = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
YouTube.Dispose();
|
||||
Trakt.Dispose();
|
||||
TvDb.Dispose();
|
||||
MovieDb.Dispose();
|
||||
FanArt.Dispose();
|
||||
Mb.Dispose();
|
||||
AppleTrailerVideos.Dispose();
|
||||
AppleTrailerImages.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
219
MediaBrowser.Controller/Library/UserDataManager.cs
Normal file
219
MediaBrowser.Controller/Library/UserDataManager.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Common.Kernel;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Connectivity;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Class UserDataManager
|
||||
/// </summary>
|
||||
public class UserDataManager : BaseManager<Kernel>
|
||||
{
|
||||
#region Events
|
||||
/// <summary>
|
||||
/// Occurs when [playback start].
|
||||
/// </summary>
|
||||
public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
|
||||
/// <summary>
|
||||
/// Occurs when [playback progress].
|
||||
/// </summary>
|
||||
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
|
||||
/// <summary>
|
||||
/// Occurs when [playback stopped].
|
||||
/// </summary>
|
||||
public event EventHandler<PlaybackProgressEventArgs> PlaybackStopped;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserDataManager" /> class.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The kernel.</param>
|
||||
public UserDataManager(Kernel kernel)
|
||||
: base(kernel)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to report that playback has started for an item
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="clientType">Type of the client.</param>
|
||||
/// <param name="deviceName">Name of the device.</param>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public void OnPlaybackStart(User user, BaseItem item, ClientType clientType, string deviceName)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
Kernel.UserManager.UpdateNowPlayingItemId(user, clientType, deviceName, item);
|
||||
|
||||
// Nothing to save here
|
||||
// Fire events to inform plugins
|
||||
EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs
|
||||
{
|
||||
Argument = item,
|
||||
User = user
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to report playback progress for an item
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="positionTicks">The position ticks.</param>
|
||||
/// <param name="clientType">Type of the client.</param>
|
||||
/// <param name="deviceName">Name of the device.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public async Task OnPlaybackProgress(User user, BaseItem item, long? positionTicks, ClientType clientType, string deviceName)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
Kernel.UserManager.UpdateNowPlayingItemId(user, clientType, deviceName, item, positionTicks);
|
||||
|
||||
if (positionTicks.HasValue)
|
||||
{
|
||||
var data = item.GetUserData(user, true);
|
||||
|
||||
UpdatePlayState(item, data, positionTicks.Value, false);
|
||||
await SaveUserDataForItem(user, item, data).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
EventHelper.QueueEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs
|
||||
{
|
||||
Argument = item,
|
||||
User = user,
|
||||
PlaybackPositionTicks = positionTicks
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to report that playback has ended for an item
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="positionTicks">The position ticks.</param>
|
||||
/// <param name="clientType">Type of the client.</param>
|
||||
/// <param name="deviceName">Name of the device.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public async Task OnPlaybackStopped(User user, BaseItem item, long? positionTicks, ClientType clientType, string deviceName)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
Kernel.UserManager.RemoveNowPlayingItemId(user, clientType, deviceName, item);
|
||||
|
||||
var data = item.GetUserData(user, true);
|
||||
|
||||
if (positionTicks.HasValue)
|
||||
{
|
||||
UpdatePlayState(item, data, positionTicks.Value, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the client isn't able to report this, then we'll just have to make an assumption
|
||||
data.PlayCount++;
|
||||
data.Played = true;
|
||||
}
|
||||
|
||||
await SaveUserDataForItem(user, item, data).ConfigureAwait(false);
|
||||
|
||||
EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackProgressEventArgs
|
||||
{
|
||||
Argument = item,
|
||||
User = user,
|
||||
PlaybackPositionTicks = positionTicks
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates playstate position for an item but does not save
|
||||
/// </summary>
|
||||
/// <param name="item">The item</param>
|
||||
/// <param name="data">User data for the item</param>
|
||||
/// <param name="positionTicks">The current playback position</param>
|
||||
/// <param name="incrementPlayCount">Whether or not to increment playcount</param>
|
||||
private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks, bool incrementPlayCount)
|
||||
{
|
||||
// If a position has been reported, and if we know the duration
|
||||
if (positionTicks > 0 && item.RunTimeTicks.HasValue && item.RunTimeTicks > 0)
|
||||
{
|
||||
var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100;
|
||||
|
||||
// Don't track in very beginning
|
||||
if (pctIn < Kernel.Configuration.MinResumePct)
|
||||
{
|
||||
positionTicks = 0;
|
||||
incrementPlayCount = false;
|
||||
}
|
||||
|
||||
// If we're at the end, assume completed
|
||||
else if (pctIn > Kernel.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value)
|
||||
{
|
||||
positionTicks = 0;
|
||||
data.Played = true;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// Enforce MinResumeDuration
|
||||
var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds;
|
||||
|
||||
if (durationSeconds < Kernel.Configuration.MinResumeDurationSeconds)
|
||||
{
|
||||
positionTicks = 0;
|
||||
data.Played = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.PlaybackPositionTicks = positionTicks;
|
||||
|
||||
if (incrementPlayCount)
|
||||
{
|
||||
data.PlayCount++;
|
||||
data.LastPlayedDate = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves user data for an item
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="data">The data.</param>
|
||||
public Task SaveUserDataForItem(User user, BaseItem item, UserItemData data)
|
||||
{
|
||||
item.AddOrUpdateUserData(user, data);
|
||||
|
||||
return Kernel.UserDataRepository.SaveUserData(item, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
395
MediaBrowser.Controller/Library/UserManager.cs
Normal file
395
MediaBrowser.Controller/Library/UserManager.cs
Normal file
@@ -0,0 +1,395 @@
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Kernel;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Connectivity;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Class UserManager
|
||||
/// </summary>
|
||||
public class UserManager : BaseManager<Kernel>
|
||||
{
|
||||
/// <summary>
|
||||
/// The _active connections
|
||||
/// </summary>
|
||||
private readonly ConcurrentBag<ClientConnectionInfo> _activeConnections =
|
||||
new ConcurrentBag<ClientConnectionInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets all connections.
|
||||
/// </summary>
|
||||
/// <value>All connections.</value>
|
||||
public IEnumerable<ClientConnectionInfo> AllConnections
|
||||
{
|
||||
get { return _activeConnections.Where(c => Kernel.GetUserById(c.UserId) != null).OrderByDescending(c => c.LastActivityDate); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active connections.
|
||||
/// </summary>
|
||||
/// <value>The active connections.</value>
|
||||
public IEnumerable<ClientConnectionInfo> ActiveConnections
|
||||
{
|
||||
get { return AllConnections.Where(c => (DateTime.UtcNow - c.LastActivityDate).TotalMinutes <= 10); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserManager" /> class.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The kernel.</param>
|
||||
public UserManager(Kernel kernel)
|
||||
: base(kernel)
|
||||
{
|
||||
}
|
||||
|
||||
#region UserUpdated Event
|
||||
/// <summary>
|
||||
/// Occurs when [user updated].
|
||||
/// </summary>
|
||||
public event EventHandler<GenericEventArgs<User>> UserUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Called when [user updated].
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
internal void OnUserUpdated(User user)
|
||||
{
|
||||
EventHelper.QueueEventIfNotNull(UserUpdated, this, new GenericEventArgs<User> { Argument = user });
|
||||
|
||||
// Notify connected ui's
|
||||
Kernel.TcpManager.SendWebSocketMessage("UserUpdated", DtoBuilder.GetDtoUser(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>
|
||||
internal void OnUserDeleted(User user)
|
||||
{
|
||||
EventHelper.QueueEventIfNotNull(UserDeleted, this, new GenericEventArgs<User> { Argument = user });
|
||||
|
||||
// Notify connected ui's
|
||||
Kernel.TcpManager.SendWebSocketMessage("UserDeleted", user.Id.ToString());
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates a User and returns a result indicating whether or not it succeeded
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">user</exception>
|
||||
public async Task<bool> AuthenticateUser(User user, string password)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
|
||||
password = password ?? string.Empty;
|
||||
var existingPassword = string.IsNullOrEmpty(user.Password) ? string.Empty.GetMD5().ToString() : user.Password;
|
||||
|
||||
var success = password.GetMD5().ToString().Equals(existingPassword);
|
||||
|
||||
// Update LastActivityDate and LastLoginDate, then save
|
||||
if (success)
|
||||
{
|
||||
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
|
||||
await UpdateUser(user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.Info("Authentication request for {0} {1}.", user.Name, (success ? "has succeeded" : "has been denied"));
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the user activity.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="clientType">Type of the client.</param>
|
||||
/// <param name="deviceName">Name of the device.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">user</exception>
|
||||
public Task LogUserActivity(User user, ClientType clientType, string deviceName)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
|
||||
var activityDate = DateTime.UtcNow;
|
||||
|
||||
user.LastActivityDate = activityDate;
|
||||
|
||||
LogConnection(user.Id, clientType, deviceName, activityDate);
|
||||
|
||||
// Save this directly. No need to fire off all the events for this.
|
||||
return Kernel.UserRepository.SaveUser(user, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the now playing item id.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="clientType">Type of the client.</param>
|
||||
/// <param name="deviceName">Name of the device.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="currentPositionTicks">The current position ticks.</param>
|
||||
public void UpdateNowPlayingItemId(User user, ClientType clientType, string deviceName, BaseItem item, long? currentPositionTicks = null)
|
||||
{
|
||||
var conn = GetConnection(user.Id, clientType, deviceName);
|
||||
|
||||
conn.NowPlayingPositionTicks = currentPositionTicks;
|
||||
conn.NowPlayingItem = DtoBuilder.GetBaseItemInfo(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the now playing item id.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="clientType">Type of the client.</param>
|
||||
/// <param name="deviceName">Name of the device.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
public void RemoveNowPlayingItemId(User user, ClientType clientType, string deviceName, BaseItem item)
|
||||
{
|
||||
var conn = GetConnection(user.Id, clientType, deviceName);
|
||||
|
||||
if (conn.NowPlayingItem != null && conn.NowPlayingItem.Id.Equals(item.Id.ToString()))
|
||||
{
|
||||
conn.NowPlayingItem = null;
|
||||
conn.NowPlayingPositionTicks = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs the connection.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="clientType">Type of the client.</param>
|
||||
/// <param name="deviceName">Name of the device.</param>
|
||||
/// <param name="lastActivityDate">The last activity date.</param>
|
||||
private void LogConnection(Guid userId, ClientType clientType, string deviceName, DateTime lastActivityDate)
|
||||
{
|
||||
GetConnection(userId, clientType, deviceName).LastActivityDate = lastActivityDate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="clientType">Type of the client.</param>
|
||||
/// <param name="deviceName">Name of the device.</param>
|
||||
/// <returns>ClientConnectionInfo.</returns>
|
||||
private ClientConnectionInfo GetConnection(Guid userId, ClientType clientType, string deviceName)
|
||||
{
|
||||
var conn = _activeConnections.FirstOrDefault(c => c.UserId == userId && c.ClientType == clientType && string.Equals(deviceName, c.DeviceName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (conn == null)
|
||||
{
|
||||
conn = new ClientConnectionInfo
|
||||
{
|
||||
UserId = userId,
|
||||
ClientType = clientType,
|
||||
DeviceName = deviceName
|
||||
};
|
||||
|
||||
_activeConnections.Add(conn);
|
||||
}
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the users from the repository
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{User}.</returns>
|
||||
internal IEnumerable<User> LoadUsers()
|
||||
{
|
||||
var users = Kernel.UserRepository.RetrieveAllUsers().ToList();
|
||||
|
||||
// There always has to be at least one user.
|
||||
if (users.Count == 0)
|
||||
{
|
||||
var name = Environment.UserName;
|
||||
|
||||
var user = InstantiateNewUser(name);
|
||||
|
||||
var task = Kernel.UserRepository.SaveUser(user, CancellationToken.None);
|
||||
|
||||
// Hate having to block threads
|
||||
Task.WaitAll(task);
|
||||
|
||||
users.Add(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes metadata for each user
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task RefreshUsersMetadata(CancellationToken cancellationToken, bool force = false)
|
||||
{
|
||||
var tasks = Kernel.Users.Select(user => user.RefreshMetadata(cancellationToken, forceRefresh: force)).ToList();
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renames the user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="newName">The new name.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">user</exception>
|
||||
/// <exception cref="System.ArgumentException"></exception>
|
||||
public async Task RenameUser(User user, string newName)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(newName))
|
||||
{
|
||||
throw new ArgumentNullException("newName");
|
||||
}
|
||||
|
||||
if (Kernel.Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName));
|
||||
}
|
||||
|
||||
if (user.Name.Equals(newName, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("The new and old names must be different.");
|
||||
}
|
||||
|
||||
await user.Rename(newName);
|
||||
|
||||
OnUserUpdated(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <exception cref="System.ArgumentNullException">user</exception>
|
||||
/// <exception cref="System.ArgumentException"></exception>
|
||||
public async Task UpdateUser(User user)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
|
||||
if (user.Id == Guid.Empty || !Kernel.Users.Any(u => u.Id.Equals(user.Id)))
|
||||
{
|
||||
throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id));
|
||||
}
|
||||
|
||||
user.DateModified = DateTime.UtcNow;
|
||||
|
||||
await Kernel.UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
OnUserUpdated(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the user.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns>User.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">name</exception>
|
||||
/// <exception cref="System.ArgumentException"></exception>
|
||||
public async Task<User> CreateUser(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentNullException("name");
|
||||
}
|
||||
|
||||
if (Kernel.Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name));
|
||||
}
|
||||
|
||||
var user = InstantiateNewUser(name);
|
||||
|
||||
var list = Kernel.Users.ToList();
|
||||
list.Add(user);
|
||||
Kernel.Users = list;
|
||||
|
||||
await Kernel.UserRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">user</exception>
|
||||
/// <exception cref="System.ArgumentException"></exception>
|
||||
public async Task DeleteUser(User user)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
|
||||
if (Kernel.Users.FirstOrDefault(u => u.Id == user.Id) == null)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
if (Kernel.Users.Count() == 1)
|
||||
{
|
||||
throw new ArgumentException(string.Format("The user '{0}' be deleted because there must be at least one user in the system.", user.Name));
|
||||
}
|
||||
|
||||
await Kernel.UserRepository.DeleteUser(user, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
OnUserDeleted(user);
|
||||
|
||||
// Force this to be lazy loaded again
|
||||
Kernel.Users = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates the new user.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns>User.</returns>
|
||||
private User InstantiateNewUser(string name)
|
||||
{
|
||||
return new User
|
||||
{
|
||||
Name = name,
|
||||
Id = ("MBUser" + name).GetMD5(),
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
23
MediaBrowser.Controller/Localization/AURatingsDictionary.cs
Normal file
23
MediaBrowser.Controller/Localization/AURatingsDictionary.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// Class AURatingsDictionary
|
||||
/// </summary>
|
||||
public class AURatingsDictionary : Dictionary<string, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AURatingsDictionary" /> class.
|
||||
/// </summary>
|
||||
public AURatingsDictionary()
|
||||
{
|
||||
Add("AU-G", 1);
|
||||
Add("AU-PG", 5);
|
||||
Add("AU-M", 6);
|
||||
Add("AU-M15+", 7);
|
||||
Add("AU-R18+", 9);
|
||||
Add("AU-X18+", 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
290
MediaBrowser.Controller/Localization/BaseStrings.cs
Normal file
290
MediaBrowser.Controller/Localization/BaseStrings.cs
Normal file
@@ -0,0 +1,290 @@
|
||||
using MediaBrowser.Common.Localization;
|
||||
using System.ComponentModel.Composition;
|
||||
|
||||
namespace MediaBrowser.Controller.Localization
|
||||
{
|
||||
[Export(typeof(LocalizedStringData))]
|
||||
public class BaseStrings : LocalizedStringData
|
||||
{
|
||||
public BaseStrings()
|
||||
{
|
||||
ThisVersion = "1.0002";
|
||||
Prefix = LocalizedStrings.BasePrefix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Config Panel
|
||||
public string ConfigConfig = "Configuration";
|
||||
public string VersionConfig = "Version";
|
||||
public string MediaOptionsConfig = "Media Options";
|
||||
public string ThemesConfig = "Theme Options";
|
||||
public string ParentalControlConfig = "Parental Control";
|
||||
public string ContinueConfig = "Continue";
|
||||
public string ResetDefaultsConfig = "Reset Defaults";
|
||||
public string ClearCacheConfig = "Clear Cache";
|
||||
public string UnlockConfig = "Unlock";
|
||||
public string GeneralConfig = "General";
|
||||
public string EnableScreenSaverConfig = "Screen Saver";
|
||||
public string SSTimeOutConfig = "Timeout (mins)";
|
||||
public string TrackingConfig = "Tracking";
|
||||
public string AssumeWatchedIfOlderThanConfig = "Assume Played If Older Than";
|
||||
public string MetadataConfig = "Metadata";
|
||||
public string EnableInternetProvidersConfig = "Allow Internet Providers";
|
||||
public string UpdatesConfig = "Updates";
|
||||
public string AutomaticUpdatesConfig = "Check For Updates";
|
||||
public string LoggingConfig = "Logging";
|
||||
public string BetaUpdatesConfig = "Beta Updates";
|
||||
public string GlobalConfig = "Global";
|
||||
public string EnableEHSConfig = "Enable EHS";
|
||||
public string ShowClockConfig = "Show Clock";
|
||||
public string DimUnselectedPostersConfig = "Dim Unselected Posters";
|
||||
public string HideFocusFrameConfig = "Hide Focus Frame";
|
||||
public string AlwaysShowDetailsConfig = "Always Show Details";
|
||||
public string ExcludeRemoteContentInSearchesConfig = "Exclude Remote Content In Searches";
|
||||
public string EnhancedMouseSupportConfig = "Enhanced Mouse Support";
|
||||
public string ViewsConfig = "Views";
|
||||
public string PosterGridSpacingConfig = "Poster Grid Spacing";
|
||||
public string ThumbWidthSplitConfig = "Thumb Width Split";
|
||||
public string BreadcrumbCountConfig = "Breadcrumb Count";
|
||||
public string ShowFanArtonViewsConfig = "Show Fan Art on Views";
|
||||
public string ShowInitialFolderBackgroundConfig = "Show Initial Folder Background";
|
||||
public string ShowThemeBackgroundConfig = "Show Theme Background";
|
||||
public string ShowHDOverlayonPostersConfig = "Show HD Overlay on Posters";
|
||||
public string ShowIcononRemoteContentConfig = "Show Icon on Remote Content";
|
||||
public string EnableAdvancedCmdsConfig = "Enable Advanced Commands";
|
||||
public string MediaTrackingConfig = "Media Tracking";
|
||||
public string RememberFolderIndexingConfig = "Remember Folder Indexing";
|
||||
public string ShowUnwatchedCountConfig = "Show Unplayed Count";
|
||||
public string WatchedIndicatoronFoldersConfig = "Played Indicator on Folders";
|
||||
public string HighlightUnwatchedItemsConfig = "Highlight Unplayed Items";
|
||||
public string WatchedIndicatoronVideosConfig = "Played Indicator on Items";
|
||||
public string WatchedIndicatorinDetailViewConfig = "Played Indicator in Detail View";
|
||||
public string DefaultToFirstUnwatchedItemConfig = "Default To First Unplayed Item";
|
||||
public string GeneralBehaviorConfig = "General Behavior";
|
||||
public string AllowNestedMovieFoldersConfig = "Allow Nested Movie Folders";
|
||||
public string AutoEnterSingleFolderItemsConfig = "Auto Enter Single Folder Items";
|
||||
public string MultipleFileBehaviorConfig = "Multiple File Behavior";
|
||||
public string TreatMultipleFilesAsSingleMovieConfig = "Treat Multiple Files As Single Movie";
|
||||
public string MultipleFileSizeLimitConfig = "Multiple File Size Limit";
|
||||
public string MBThemeConfig = "Media Browser Theme";
|
||||
public string VisualThemeConfig = "Visual Theme";
|
||||
public string ColorSchemeConfig = "Color Scheme *";
|
||||
public string FontSizeConfig = "Font Size *";
|
||||
public string RequiresRestartConfig = "* Requires a restart to take effect.";
|
||||
public string ThemeSettingsConfig = "Theme Specific Settings";
|
||||
public string ShowConfigButtonConfig = "Show Config Button";
|
||||
public string AlphaBlendingConfig = "Alpha Blending";
|
||||
public string SecurityPINConfig = "Security PIN";
|
||||
public string PCUnlockedTxtConfig = "Parental Controls are Temporarily Unlocked. You cannot change values unless you re-lock.";
|
||||
public string RelockBtnConfig = "Re-Lock";
|
||||
public string EnableParentalBlocksConfig = "Enable Parental Blocks";
|
||||
public string MaxAllowedRatingConfig = "Max Allowed Rating ";
|
||||
public string BlockUnratedContentConfig = "Block Unrated Content";
|
||||
public string HideBlockedContentConfig = "Hide Blocked Content";
|
||||
public string UnlockonPINEntryConfig = "Unlock on PIN Entry";
|
||||
public string UnlockPeriodHoursConfig = "Unlock Period (Hours)";
|
||||
public string EnterNewPINConfig = "Enter New PIN";
|
||||
public string RandomizeBackdropConfig = "Randomize";
|
||||
public string RotateBackdropConfig = "Rotate";
|
||||
public string UpdateLibraryConfig = "Update Library";
|
||||
public string BackdropSettingsConfig = "Backdrop Settings";
|
||||
public string BackdropRotationIntervalConfig = "Rotation Time";
|
||||
public string BackdropTransitionIntervalConfig = "Transition Time";
|
||||
public string BackdropLoadDelayConfig = "Load Delay";
|
||||
public string AutoScrollTextConfig = "Auto Scroll Overview";
|
||||
public string SortYearsAscConfig = "Sort by Year in Ascending Order";
|
||||
public string AutoValidateConfig = "Automatically Validate Items";
|
||||
public string SaveLocalMetaConfig = "Save Locally";
|
||||
public string HideEmptyFoldersConfig = "Hide Empty TV Folders";
|
||||
|
||||
|
||||
//EHS
|
||||
public string RecentlyWatchedEHS = "last played";
|
||||
public string RecentlyAddedEHS = "last added";
|
||||
public string RecentlyAddedUnwatchedEHS = "last added unplayed";
|
||||
public string WatchedEHS = "Played";
|
||||
public string AddedEHS = "Added";
|
||||
public string UnwatchedEHS = "Unplayed";
|
||||
public string AddedOnEHS = "Added on";
|
||||
public string OnEHS = "on";
|
||||
public string OfEHS = "of";
|
||||
public string NoItemsEHS = "No Items To Show";
|
||||
public string VariousEHS = "(various)";
|
||||
|
||||
//Context menu
|
||||
public string CloseCMenu = "Close";
|
||||
public string PlayMenuCMenu = "Play Menu";
|
||||
public string ItemMenuCMenu = "Item Menu";
|
||||
public string PlayAllCMenu = "Play All";
|
||||
public string PlayAllFromHereCMenu = "Play All From Here";
|
||||
public string ResumeCMenu = "Resume";
|
||||
public string MarkUnwatchedCMenu = "Mark Unplayed";
|
||||
public string MarkWatchedCMenu = "Mark Played";
|
||||
public string ShufflePlayCMenu = "Shuffle Play";
|
||||
|
||||
//Media Detail Page
|
||||
public string GeneralDetail = "General";
|
||||
public string ActorsDetail = "Actors";
|
||||
public string ArtistsDetail = "Artists";
|
||||
public string PlayDetail = "Play";
|
||||
public string ResumeDetail = "Resume";
|
||||
public string RefreshDetail = "Refresh";
|
||||
public string PlayTrailersDetail = "Trailer";
|
||||
public string CacheDetail = "Cache 2 xml";
|
||||
public string DeleteDetail = "Delete";
|
||||
public string TMDBRatingDetail = "TMDb Rating";
|
||||
public string OutOfDetail = "out of";
|
||||
public string DirectorDetail = "Director";
|
||||
public string ComposerDetail = "Composer";
|
||||
public string HostDetail = "Host";
|
||||
public string RuntimeDetail = "Runtime";
|
||||
public string NextItemDetail = "Next";
|
||||
public string PreviousItemDetail = "Previous";
|
||||
public string FirstAiredDetail = "First aired";
|
||||
public string LastPlayedDetail = "Last played";
|
||||
public string TrackNumberDetail = "Track";
|
||||
|
||||
public string DirectedByDetail = "Directed By: ";
|
||||
public string WrittenByDetail = "Written By: ";
|
||||
public string ComposedByDetail = "Composed By: ";
|
||||
|
||||
//Display Prefs
|
||||
public string ViewDispPref = "View";
|
||||
public string ViewSearch = "Search";
|
||||
public string CoverFlowDispPref = "Cover Flow";
|
||||
public string DetailDispPref = "Detail";
|
||||
public string PosterDispPref = "Poster";
|
||||
public string ThumbDispPref = "Thumb";
|
||||
public string ThumbStripDispPref = "Thumb Strip";
|
||||
public string ShowLabelsDispPref = "Show Labels";
|
||||
public string VerticalScrollDispPref = "Vertical Scroll";
|
||||
public string UseBannersDispPref = "Use Banners";
|
||||
public string UseCoverflowDispPref = "Use Coverflow Style";
|
||||
public string ThumbSizeDispPref = "Thumb Size";
|
||||
public string NameDispPref = "Name";
|
||||
public string DateDispPref = "Date";
|
||||
public string RatingDispPref = "User Rating";
|
||||
public string OfficialRatingDispPref = "Rating";
|
||||
public string RuntimeDispPref = "Runtime";
|
||||
public string UnWatchedDispPref = "Unplayed";
|
||||
public string YearDispPref = "Year";
|
||||
public string NoneDispPref = "None";
|
||||
public string PerformerDispPref = "Performer";
|
||||
public string ActorDispPref = "Actor";
|
||||
public string GenreDispPref = "Genre";
|
||||
public string DirectorDispPref = "Director";
|
||||
public string StudioDispPref = "Studio";
|
||||
|
||||
//Dialog boxes
|
||||
//public string BrokenEnvironmentDial = "Application will now close due to broken MediaCenterEnvironment object, possibly due to 5 minutes of idle time and/or running with TVPack installed.";
|
||||
//public string InitialConfigDial = "Initial configuration is complete, please restart Media Browser";
|
||||
//public string DeleteMediaDial = "Are you sure you wish to delete this media item?";
|
||||
//public string DeleteMediaCapDial = "Delete Confirmation";
|
||||
//public string NotDeletedDial = "Item NOT Deleted.";
|
||||
//public string NotDeletedCapDial = "Delete Cancelled by User";
|
||||
//public string NotDelInvalidPathDial = "The selected media item cannot be deleted due to an invalid path. Or you may not have sufficient access rights to perform this command.";
|
||||
//public string DelFailedDial = "Delete Failed";
|
||||
//public string NotDelUnknownDial = "The selected media item cannot be deleted due to an unknown error.";
|
||||
//public string NotDelTypeDial = "The selected media item cannot be deleted due to its Item-Type or you have not enabled this feature in the configuration file.";
|
||||
//public string FirstTimeDial = "As this is the first time you have run Media Browser please setup the inital configuration";
|
||||
//public string FirstTimeCapDial = "Configure";
|
||||
//public string EntryPointErrorDial = "Media Browser could not launch directly into ";
|
||||
//public string EntryPointErrorCapDial = "Entrypoint Error";
|
||||
//public string CriticalErrorDial = "Media Browser encountered a critical error and had to shut down: ";
|
||||
//public string CriticalErrorCapDial = "Critical Error";
|
||||
//public string ClearCacheErrorDial = "An error occured during the clearing of the cache, you may wish to manually clear it from {0} before restarting Media Browser";
|
||||
//public string RestartMBDial = "Please restart Media Browser";
|
||||
//public string ClearCacheDial = "Are you sure you wish to clear the cache?\nThis will erase all cached and downloaded information and images.";
|
||||
//public string ClearCacheCapDial = "Clear Cache";
|
||||
//public string CacheClearedDial = "Cache Cleared";
|
||||
//public string ResetConfigDial = "Are you sure you wish to reset all configuration to defaults?";
|
||||
//public string ResetConfigCapDial = "Reset Configuration";
|
||||
//public string ConfigResetDial = "Configuration Reset";
|
||||
//public string UpdateMBDial = "Please visit www.mediabrowser.tv/download to install the new version.";
|
||||
//public string UpdateMBCapDial = "Update Available";
|
||||
//public string UpdateMBExtDial = "There is an update available for Media Browser. Please update Media Browser next time you are at your MediaCenter PC.";
|
||||
//public string DLUpdateFailDial = "Media Browser will operate normally and prompt you again the next time you load it.";
|
||||
//public string DLUpdateFailCapDial = "Update Download Failed";
|
||||
//public string UpdateSuccessDial = "Media Browser must now exit to apply the update. It will restart automatically when it is done";
|
||||
//public string UpdateSuccessCapDial = "Update Downloaded";
|
||||
//public string CustomErrorDial = "Customisation Error";
|
||||
//public string ConfigErrorDial = "Reset to default?";
|
||||
//public string ConfigErrorCapDial = "Error in configuration file";
|
||||
//public string ContentErrorDial = "There was a problem playing the content. Check location exists";
|
||||
//public string ContentErrorCapDial = "Content Error";
|
||||
//public string CannotMaximizeDial = "We can not maximize the window! This is a known bug with Windows 7 and TV Pack, you will have to restart Media Browser!";
|
||||
//public string IncorrectPINDial = "Incorrect PIN Entered";
|
||||
//public string ContentProtected = "Content Protected";
|
||||
//public string CantChangePINDial = "Cannot Change PIN";
|
||||
//public string LibraryUnlockedDial = "Library Temporarily Unlocked. Will Re-Lock in {0} Hour(s) or on Application Re-Start";
|
||||
//public string LibraryUnlockedCapDial = "Unlock";
|
||||
//public string PINChangedDial = "PIN Successfully Changed";
|
||||
//public string PINChangedCapDial = "PIN Change";
|
||||
//public string EnterPINToViewDial = "Please Enter PIN to View Protected Content";
|
||||
//public string EnterPINToPlayDial = "Please Enter PIN to Play Protected Content";
|
||||
//public string EnterCurrentPINDial = "Please Enter CURRENT PIN.";
|
||||
//public string EnterNewPINDial = "Please Enter NEW PIN (exactly 4 digits).";
|
||||
//public string EnterPINDial = "Please Enter PIN to Unlock Library";
|
||||
//public string NoContentDial = "No Content that can be played in this context.";
|
||||
//public string FontsMissingDial = "CustomFonts.mcml as been patched with missing values";
|
||||
//public string StyleMissingDial = "{0} has been patched with missing values";
|
||||
//public string ManualRefreshDial = "Library Update Started. Will proceed in the background.";
|
||||
//public string ForcedRebuildDial = "Your library is currently being migrated by the service. The service will re-start when it is finished and you may then run Media Browser.";
|
||||
//public string ForcedRebuildCapDial = "Library Migration";
|
||||
//public string RefreshFailedDial = "The last service refresh process failed. Please run a manual refresh from the service.";
|
||||
//public string RefreshFailedCapDial = "Service Refresh Failed";
|
||||
//public string RebuildNecDial = "This version of Media Browser requires a re-build of your library. It has started automatically in the service. Some information may be incomplete until this process finishes.";
|
||||
//public string MigrateNecDial = "This version of Media Browser requires a migration of your library. It has started automatically in the service. The service will restart when it is complete and you may then run Media Browser.";
|
||||
//public string RebuildFailedDial = "There was an error attempting to tell the service to re-build your library. Please run the service and do a manual refresh with the cache clear options selected.";
|
||||
//public string MigrateFailedDial = "There was an error attempting to tell the service to re-build your library. Please run the service and do a manual refresh with the cache clear options selected.";
|
||||
//public string RefreshFolderDial = "Refresh all contents too?";
|
||||
//public string RefreshFolderCapDial = "Refresh Folder";
|
||||
|
||||
//Generic
|
||||
public string Restartstr = "Restart";
|
||||
public string Errorstr = "Error";
|
||||
public string Playstr = "Play";
|
||||
public string MinutesStr = "mins"; //Minutes abbreviation
|
||||
public string HoursStr = "hrs"; //Hours abbreviation
|
||||
public string EndsStr = "Ends";
|
||||
public string KBsStr = "Kbps"; //Kilobytes per second
|
||||
public string FrameRateStr = "fps"; //Frames per second
|
||||
public string AtStr = "at"; //x at y, e.g. 1920x1080 at 25 fps
|
||||
public string Rated = "Rated";
|
||||
public string Or = "Or ";
|
||||
public string Lower = "Lower";
|
||||
public string Higher = "Higher";
|
||||
public string Search = "Search";
|
||||
public string Cancel = "Cancel";
|
||||
public string TitleContains = "Title Contains ";
|
||||
public string Any = "Any";
|
||||
|
||||
//Search
|
||||
public string IncludeNested = "Include Subfolders";
|
||||
public string UnwatchedOnly = "Include Only Unwatched";
|
||||
public string FilterByRated = "Filter by Rating";
|
||||
|
||||
//Profiler
|
||||
public string WelcomeProf = "Welcome to Media Browser";
|
||||
public string ProfilerTimeProf = "{1} took {2} seconds.";
|
||||
public string RefreshProf = "Refresh";
|
||||
public string SetWatchedProf = "Set Played {0}";
|
||||
public string RefreshFolderProf = "Refresh Folder and all Contents of";
|
||||
public string ClearWatchedProf = "Clear Played {0}";
|
||||
public string FullRefreshProf = "Full Library Refresh";
|
||||
public string FullValidationProf = "Full Library Validation";
|
||||
public string FastRefreshProf = "Fast Metadata refresh";
|
||||
public string SlowRefresh = "Slow Metadata refresh";
|
||||
public string ImageRefresh = "Image refresh";
|
||||
public string PluginUpdateProf = "An update is available for plug-in {0}";
|
||||
public string NoPluginUpdateProf = "No Plugin Updates Currently Available.";
|
||||
public string LibraryUnLockedProf = "Library Temporarily UnLocked. Will Re-Lock in {0} Hour(s)";
|
||||
public string LibraryReLockedProf = "Library Re-Locked";
|
||||
|
||||
//Messages
|
||||
public string FullRefreshMsg = "Updating Media Library...";
|
||||
public string FullRefreshFinishedMsg = "Library update complete";
|
||||
|
||||
}
|
||||
}
|
||||
24
MediaBrowser.Controller/Localization/GBRatingsDictionary.cs
Normal file
24
MediaBrowser.Controller/Localization/GBRatingsDictionary.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GBRatingsDictionary
|
||||
/// </summary>
|
||||
public class GBRatingsDictionary : Dictionary<string, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GBRatingsDictionary" /> class.
|
||||
/// </summary>
|
||||
public GBRatingsDictionary()
|
||||
{
|
||||
Add("GB-U", 1);
|
||||
Add("GB-PG", 5);
|
||||
Add("GB-12", 6);
|
||||
Add("GB-12A", 7);
|
||||
Add("GB-15", 8);
|
||||
Add("GB-18", 9);
|
||||
Add("GB-R18", 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
155
MediaBrowser.Controller/Localization/LocalizedStrings.cs
Normal file
155
MediaBrowser.Controller/Localization/LocalizedStrings.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using MediaBrowser.Common.Localization;
|
||||
using MediaBrowser.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// Class LocalizedStrings
|
||||
/// </summary>
|
||||
public class LocalizedStrings
|
||||
{
|
||||
/// <summary>
|
||||
/// The base prefix
|
||||
/// </summary>
|
||||
public const string BasePrefix = "base-";
|
||||
/// <summary>
|
||||
/// The local strings
|
||||
/// </summary>
|
||||
protected ConcurrentDictionary<string, string> LocalStrings = new ConcurrentDictionary<string, string>();
|
||||
/// <summary>
|
||||
/// The _instance
|
||||
/// </summary>
|
||||
private static LocalizedStrings _instance;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance.
|
||||
/// </summary>
|
||||
/// <value>The instance.</value>
|
||||
public static LocalizedStrings Instance { get { return _instance ?? (_instance = new LocalizedStrings()); } }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalizedStrings" /> class.
|
||||
/// </summary>
|
||||
public LocalizedStrings()
|
||||
{
|
||||
foreach (var stringObject in Kernel.Instance.StringFiles)
|
||||
{
|
||||
AddStringData(LoadFromFile(GetFileName(stringObject),stringObject.GetType()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the file.
|
||||
/// </summary>
|
||||
/// <param name="stringObject">The string object.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected string GetFileName(LocalizedStringData stringObject)
|
||||
{
|
||||
var path = Kernel.Instance.ApplicationPaths.LocalizationPath;
|
||||
var name = Path.Combine(path, stringObject.Prefix + "strings-" + CultureInfo.CurrentCulture + ".xml");
|
||||
if (File.Exists(name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
name = Path.Combine(path, stringObject.Prefix + "strings-" + CultureInfo.CurrentCulture.Parent + ".xml");
|
||||
if (File.Exists(name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
//just return default
|
||||
return Path.Combine(path, stringObject.Prefix + "strings-en.xml");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads from file.
|
||||
/// </summary>
|
||||
/// <param name="file">The file.</param>
|
||||
/// <param name="t">The t.</param>
|
||||
/// <returns>LocalizedStringData.</returns>
|
||||
protected LocalizedStringData LoadFromFile(string file, Type t)
|
||||
{
|
||||
var xs = new XmlSerializer(t);
|
||||
var strings = (LocalizedStringData)Activator.CreateInstance(t);
|
||||
strings.FileName = file;
|
||||
Logger.LogInfo("Using String Data from {0}", file);
|
||||
if (File.Exists(file))
|
||||
{
|
||||
using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
strings = (LocalizedStringData)xs.Deserialize(fs);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
strings.Save(); //brand new - save it
|
||||
}
|
||||
|
||||
if (strings.ThisVersion != strings.Version && file.ToLower().Contains("-en.xml"))
|
||||
{
|
||||
//only re-save the english version as that is the one defined internally
|
||||
strings = new BaseStrings {FileName = file};
|
||||
strings.Save();
|
||||
}
|
||||
return strings;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the string data.
|
||||
/// </summary>
|
||||
/// <param name="stringData">The string data.</param>
|
||||
public void AddStringData(object stringData )
|
||||
{
|
||||
//translate our object definition into a dictionary for lookups
|
||||
// and a reverse dictionary so we can lookup keys by value
|
||||
foreach (var field in stringData.GetType().GetFields().Where(f => f != null && f.FieldType == typeof(string)))
|
||||
{
|
||||
string value;
|
||||
|
||||
try
|
||||
{
|
||||
value = field.GetValue(stringData) as string;
|
||||
}
|
||||
catch (TargetException ex)
|
||||
{
|
||||
Logger.LogException("Error getting value for field: {0}", ex, field.Name);
|
||||
continue;
|
||||
}
|
||||
catch (FieldAccessException ex)
|
||||
{
|
||||
Logger.LogException("Error getting value for field: {0}", ex, field.Name);
|
||||
continue;
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
Logger.LogException("Error getting value for field: {0}", ex, field.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalStrings.TryAdd(field.Name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public string GetString(string key)
|
||||
{
|
||||
string value;
|
||||
|
||||
LocalStrings.TryGetValue(key, out value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
MediaBrowser.Controller/Localization/NLRatingsDictionary.cs
Normal file
23
MediaBrowser.Controller/Localization/NLRatingsDictionary.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// Class NLRatingsDictionary
|
||||
/// </summary>
|
||||
public class NLRatingsDictionary : Dictionary<string, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NLRatingsDictionary" /> class.
|
||||
/// </summary>
|
||||
public NLRatingsDictionary()
|
||||
{
|
||||
Add("NL-AL", 1);
|
||||
Add("NL-MG6", 2);
|
||||
Add("NL-6", 3);
|
||||
Add("NL-9", 5);
|
||||
Add("NL-12", 6);
|
||||
Add("NL-16", 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
162
MediaBrowser.Controller/Localization/Ratings.cs
Normal file
162
MediaBrowser.Controller/Localization/Ratings.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Ratings
|
||||
/// </summary>
|
||||
public static class Ratings
|
||||
{
|
||||
/// <summary>
|
||||
/// The ratings def
|
||||
/// </summary>
|
||||
private static RatingsDefinition ratingsDef;
|
||||
/// <summary>
|
||||
/// The _ratings dict
|
||||
/// </summary>
|
||||
private static Dictionary<string, int> _ratingsDict;
|
||||
/// <summary>
|
||||
/// Gets the ratings dict.
|
||||
/// </summary>
|
||||
/// <value>The ratings dict.</value>
|
||||
public static Dictionary<string, int> RatingsDict
|
||||
{
|
||||
get { return _ratingsDict ?? (_ratingsDict = Initialize(false)); }
|
||||
}
|
||||
/// <summary>
|
||||
/// The ratings strings
|
||||
/// </summary>
|
||||
private static readonly Dictionary<int, string> ratingsStrings = new Dictionary<int, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the specified block unrated.
|
||||
/// </summary>
|
||||
/// <param name="blockUnrated">if set to <c>true</c> [block unrated].</param>
|
||||
/// <returns>Dictionary{System.StringSystem.Int32}.</returns>
|
||||
public static Dictionary<string, int> Initialize(bool blockUnrated)
|
||||
{
|
||||
//build our ratings dictionary from the combined local one and us one
|
||||
ratingsDef = new RatingsDefinition(Path.Combine(Kernel.Instance.ApplicationPaths.LocalizationPath, "Ratings-" + Kernel.Instance.Configuration.MetadataCountryCode+".txt"));
|
||||
//global value of None
|
||||
var dict = new Dictionary<string, int> {{"None", -1}};
|
||||
foreach (var pair in ratingsDef.RatingsDict)
|
||||
{
|
||||
dict.TryAdd(pair.Key, pair.Value);
|
||||
}
|
||||
if (Kernel.Instance.Configuration.MetadataCountryCode.ToUpper() != "US")
|
||||
{
|
||||
foreach (var pair in new USRatingsDictionary())
|
||||
{
|
||||
dict.TryAdd(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
//global values of CS
|
||||
dict.TryAdd("CS", 1000);
|
||||
|
||||
dict.TryAdd("", blockUnrated ? 1000 : 0);
|
||||
|
||||
//and rating reverse lookup dictionary (non-redundant ones)
|
||||
ratingsStrings.Clear();
|
||||
var lastLevel = -10;
|
||||
ratingsStrings.Add(-1,LocalizedStrings.Instance.GetString("Any"));
|
||||
foreach (var pair in ratingsDef.RatingsDict.OrderBy(p => p.Value))
|
||||
{
|
||||
if (pair.Value > lastLevel)
|
||||
{
|
||||
lastLevel = pair.Value;
|
||||
ratingsStrings.TryAdd(pair.Value, pair.Key);
|
||||
}
|
||||
}
|
||||
|
||||
ratingsStrings.TryAdd(999, "CS");
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches the unrated.
|
||||
/// </summary>
|
||||
/// <param name="block">if set to <c>true</c> [block].</param>
|
||||
public static void SwitchUnrated(bool block)
|
||||
{
|
||||
RatingsDict.Remove("");
|
||||
RatingsDict.Add("", block ? 1000 : 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Levels the specified rating STR.
|
||||
/// </summary>
|
||||
/// <param name="ratingStr">The rating STR.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
public static int Level(string ratingStr)
|
||||
{
|
||||
if (ratingStr == null) ratingStr = "";
|
||||
if (RatingsDict.ContainsKey(ratingStr))
|
||||
return RatingsDict[ratingStr];
|
||||
|
||||
string stripped = StripCountry(ratingStr);
|
||||
if (RatingsDict.ContainsKey(stripped))
|
||||
return RatingsDict[stripped];
|
||||
|
||||
return RatingsDict[""]; //return "unknown" level
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips the country.
|
||||
/// </summary>
|
||||
/// <param name="rating">The rating.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string StripCountry(string rating)
|
||||
{
|
||||
int start = rating.IndexOf('-');
|
||||
return start > 0 ? rating.Substring(start + 1) : rating;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="System.String" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <param name="level">The level.</param>
|
||||
/// <returns>A <see cref="System.String" /> that represents this instance.</returns>
|
||||
public static string ToString(int level)
|
||||
{
|
||||
//return the closest one
|
||||
while (level > 0)
|
||||
{
|
||||
if (ratingsStrings.ContainsKey(level))
|
||||
return ratingsStrings[level];
|
||||
|
||||
level--;
|
||||
}
|
||||
return ratingsStrings.Values.FirstOrDefault(); //default to first one
|
||||
}
|
||||
/// <summary>
|
||||
/// To the strings.
|
||||
/// </summary>
|
||||
/// <returns>List{System.String}.</returns>
|
||||
public static List<string> ToStrings()
|
||||
{
|
||||
//return the whole list of ratings strings
|
||||
return ratingsStrings.Values.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To the values.
|
||||
/// </summary>
|
||||
/// <returns>List{System.Int32}.</returns>
|
||||
public static List<int> ToValues()
|
||||
{
|
||||
//return the whole list of ratings values
|
||||
return ratingsStrings.Keys.ToList();
|
||||
}
|
||||
|
||||
//public Microsoft.MediaCenter.UI.Image RatingImage(string rating)
|
||||
//{
|
||||
// return Helper.GetMediaInfoImage("Rated_" + rating);
|
||||
//}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
122
MediaBrowser.Controller/Localization/RatingsDefinition.cs
Normal file
122
MediaBrowser.Controller/Localization/RatingsDefinition.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MediaBrowser.Common.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// Class RatingsDefinition
|
||||
/// </summary>
|
||||
public class RatingsDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RatingsDefinition" /> class.
|
||||
/// </summary>
|
||||
/// <param name="file">The file.</param>
|
||||
public RatingsDefinition(string file)
|
||||
{
|
||||
Logger.LogInfo("Loading Certification Ratings from file " + file);
|
||||
this.file = file;
|
||||
if (!Load())
|
||||
{
|
||||
Init(Kernel.Instance.Configuration.MetadataCountryCode.ToUpper());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inits the specified country.
|
||||
/// </summary>
|
||||
/// <param name="country">The country.</param>
|
||||
protected void Init(string country)
|
||||
{
|
||||
//intitialze based on country
|
||||
switch (country)
|
||||
{
|
||||
case "US":
|
||||
RatingsDict = new USRatingsDictionary();
|
||||
break;
|
||||
case "GB":
|
||||
RatingsDict = new GBRatingsDictionary();
|
||||
break;
|
||||
case "NL":
|
||||
RatingsDict = new NLRatingsDictionary();
|
||||
break;
|
||||
case "AU":
|
||||
RatingsDict = new AURatingsDictionary();
|
||||
break;
|
||||
default:
|
||||
RatingsDict = new USRatingsDictionary();
|
||||
break;
|
||||
}
|
||||
Save();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The file
|
||||
/// </summary>
|
||||
readonly string file;
|
||||
|
||||
/// <summary>
|
||||
/// Save to file
|
||||
/// </summary>
|
||||
public void Save()
|
||||
{
|
||||
// Use simple text serialization - no need for xml
|
||||
using (var fs = new StreamWriter(file))
|
||||
{
|
||||
foreach (var pair in RatingsDict)
|
||||
{
|
||||
fs.WriteLine(pair.Key + "," + pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load from file
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
protected bool Load()
|
||||
{
|
||||
// Read back in our simple serialized format
|
||||
RatingsDict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
using (var fs = new StreamReader(file))
|
||||
{
|
||||
while (!fs.EndOfStream)
|
||||
{
|
||||
var line = fs.ReadLine() ?? "";
|
||||
var values = line.Split(',');
|
||||
if (values.Length == 2)
|
||||
{
|
||||
|
||||
int value;
|
||||
|
||||
if (int.TryParse(values[1], out value))
|
||||
{
|
||||
RatingsDict[values[0].Trim()] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError("Invalid line in ratings file " + file + "(" + line + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Couldn't load - probably just not there yet
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ratings dict
|
||||
/// </summary>
|
||||
public Dictionary<string, int> RatingsDict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
}
|
||||
}
|
||||
39
MediaBrowser.Controller/Localization/USRatingsDictionary.cs
Normal file
39
MediaBrowser.Controller/Localization/USRatingsDictionary.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// Class USRatingsDictionary
|
||||
/// </summary>
|
||||
public class USRatingsDictionary : Dictionary<string,int>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="USRatingsDictionary" /> class.
|
||||
/// </summary>
|
||||
public USRatingsDictionary()
|
||||
{
|
||||
Add("G", 1);
|
||||
Add("E", 1);
|
||||
Add("EC", 1);
|
||||
Add("TV-G", 1);
|
||||
Add("TV-Y", 2);
|
||||
Add("TV-Y7", 3);
|
||||
Add("TV-Y7-FV", 4);
|
||||
Add("PG", 5);
|
||||
Add("TV-PG", 5);
|
||||
Add("PG-13", 7);
|
||||
Add("T", 7);
|
||||
Add("TV-14", 8);
|
||||
Add("R", 9);
|
||||
Add("M", 9);
|
||||
Add("TV-MA", 9);
|
||||
Add("NC-17", 10);
|
||||
Add("AO", 15);
|
||||
Add("RP", 15);
|
||||
Add("UR", 15);
|
||||
Add("NR", 15);
|
||||
Add("X", 15);
|
||||
Add("XXX", 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,150 +1,279 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>MediaBrowser.Controller</RootNamespace>
|
||||
<AssemblyName>MediaBrowser.Controller</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="protobuf-net">
|
||||
<HintPath>..\protobuf-net\Full\net30\protobuf-net.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.ComponentModel.Composition" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Net.Http.WebRequest" />
|
||||
<Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Rx-Core.2.0.20823\lib\Net45\System.Reactive.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Rx-Interfaces.2.0.20823\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Rx-Linq.2.0.20823\lib\Net45\System.Reactive.Linq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Drawing\DrawingUtils.cs" />
|
||||
<Compile Include="Drawing\ImageProcessor.cs" />
|
||||
<Compile Include="Entities\Audio.cs" />
|
||||
<Compile Include="Entities\BaseEntity.cs" />
|
||||
<Compile Include="Entities\BaseItem.cs" />
|
||||
<Compile Include="Entities\Folder.cs" />
|
||||
<Compile Include="Entities\Genre.cs" />
|
||||
<Compile Include="Entities\Movies\BoxSet.cs" />
|
||||
<Compile Include="Entities\Movies\Movie.cs" />
|
||||
<Compile Include="Entities\Person.cs" />
|
||||
<Compile Include="Entities\Studio.cs" />
|
||||
<Compile Include="Entities\TV\Episode.cs" />
|
||||
<Compile Include="Entities\TV\Season.cs" />
|
||||
<Compile Include="Entities\TV\Series.cs" />
|
||||
<Compile Include="Entities\User.cs" />
|
||||
<Compile Include="Entities\UserItemData.cs" />
|
||||
<Compile Include="Entities\Video.cs" />
|
||||
<Compile Include="Entities\Year.cs" />
|
||||
<Compile Include="IO\FileSystemHelper.cs" />
|
||||
<Compile Include="Library\ChildrenChangedEventArgs.cs" />
|
||||
<Compile Include="Providers\BaseProviderInfo.cs" />
|
||||
<Compile Include="Providers\Movies\MovieProviderFromXml.cs" />
|
||||
<Compile Include="Providers\Movies\MovieSpecialFeaturesProvider.cs" />
|
||||
<Compile Include="Providers\TV\EpisodeImageFromMediaLocationProvider.cs" />
|
||||
<Compile Include="Providers\TV\EpisodeProviderFromXml.cs" />
|
||||
<Compile Include="Providers\TV\EpisodeXmlParser.cs" />
|
||||
<Compile Include="Providers\TV\SeriesProviderFromXml.cs" />
|
||||
<Compile Include="Providers\TV\SeriesXmlParser.cs" />
|
||||
<Compile Include="Resolvers\EntityResolutionHelper.cs" />
|
||||
<Compile Include="Resolvers\Movies\BoxSetResolver.cs" />
|
||||
<Compile Include="Resolvers\Movies\MovieResolver.cs" />
|
||||
<Compile Include="Resolvers\TV\EpisodeResolver.cs" />
|
||||
<Compile Include="Resolvers\TV\SeasonResolver.cs" />
|
||||
<Compile Include="Resolvers\TV\SeriesResolver.cs" />
|
||||
<Compile Include="Resolvers\TV\TVUtils.cs" />
|
||||
<Compile Include="ServerApplicationPaths.cs" />
|
||||
<Compile Include="Library\ItemResolveEventArgs.cs" />
|
||||
<Compile Include="FFMpeg\FFProbe.cs" />
|
||||
<Compile Include="FFMpeg\FFProbeResult.cs" />
|
||||
<Compile Include="IO\DirectoryWatchers.cs" />
|
||||
<Compile Include="IO\FileData.cs" />
|
||||
<Compile Include="IO\Shortcut.cs" />
|
||||
<Compile Include="Library\ItemController.cs" />
|
||||
<Compile Include="Kernel.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Providers\BaseMetadataProvider.cs" />
|
||||
<Compile Include="Providers\AudioInfoProvider.cs" />
|
||||
<Compile Include="Providers\FolderProviderFromXml.cs" />
|
||||
<Compile Include="Providers\ImageFromMediaLocationProvider.cs" />
|
||||
<Compile Include="Providers\LocalTrailerProvider.cs" />
|
||||
<Compile Include="Providers\VideoInfoProvider.cs" />
|
||||
<Compile Include="Resolvers\AudioResolver.cs" />
|
||||
<Compile Include="Resolvers\BaseItemResolver.cs" />
|
||||
<Compile Include="Resolvers\FolderResolver.cs" />
|
||||
<Compile Include="Resolvers\VideoResolver.cs" />
|
||||
<Compile Include="Weather\BaseWeatherProvider.cs" />
|
||||
<Compile Include="Weather\WeatherProvider.cs" />
|
||||
<Compile Include="Providers\BaseItemXmlParser.cs" />
|
||||
<Compile Include="Xml\XmlExtensions.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
|
||||
<Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
|
||||
<Name>MediaBrowser.Common</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
|
||||
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
|
||||
<Name>MediaBrowser.Model</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="FFMpeg\ffmpeg.exe" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="FFMpeg\ffprobe.exe" />
|
||||
<Content Include="FFMpeg\readme.txt" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>MediaBrowser.Controller</RootNamespace>
|
||||
<AssemblyName>MediaBrowser.Controller</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
|
||||
<RestorePackages>true</RestorePackages>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<RunPostBuildEvent>Always</RunPostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Ionic.Zip">
|
||||
<HintPath>..\packages\DotNetZip.1.9.1.8\lib\net20\Ionic.Zip.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Mediabrowser.PluginSecurity">
|
||||
<HintPath>Plugins\Mediabrowser.PluginSecurity.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MoreLinq">
|
||||
<HintPath>..\packages\morelinq.1.0.15631-beta\lib\net35\MoreLinq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="protobuf-net, Version=2.0.0.621, Culture=neutral, PublicKeyToken=257b51d87d2e4d67, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\protobuf-net.2.0.0.621\lib\net40\protobuf-net.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.ComponentModel.Composition" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Data.SQLite, Version=1.0.84.0, Culture=neutral, PublicKeyToken=db937bc2d44ff139, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\System.Data.SQLite.1.0.84.0\lib\net45\System.Data.SQLite.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Data.SQLite.Linq, Version=1.0.84.0, Culture=neutral, PublicKeyToken=db937bc2d44ff139, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\System.Data.SQLite.1.0.84.0\lib\net45\System.Data.SQLite.Linq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Deployment" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Net" />
|
||||
<Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Rx-Core.2.0.21114\lib\Net45\System.Reactive.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Rx-Interfaces.2.0.21114\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Rx-Linq.2.0.21114\lib\Net45\System.Reactive.Linq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Drawing\ImageManager.cs" />
|
||||
<Compile Include="Entities\AggregateFolder.cs" />
|
||||
<Compile Include="Entities\Audio\Audio.cs" />
|
||||
<Compile Include="Entities\Audio\MusicAlbum.cs" />
|
||||
<Compile Include="Entities\Audio\MusicArtist.cs" />
|
||||
<Compile Include="Entities\BaseItem.cs" />
|
||||
<Compile Include="Entities\BasePluginFolder.cs" />
|
||||
<Compile Include="Entities\Folder.cs" />
|
||||
<Compile Include="Entities\Genre.cs" />
|
||||
<Compile Include="Entities\ICollectionFolder.cs" />
|
||||
<Compile Include="Entities\IndexFolder.cs" />
|
||||
<Compile Include="Entities\Movies\BoxSet.cs" />
|
||||
<Compile Include="Entities\ISupportsSpecialFeatures.cs" />
|
||||
<Compile Include="Entities\Movies\Movie.cs" />
|
||||
<Compile Include="Entities\Person.cs" />
|
||||
<Compile Include="Entities\PlaybackProgressEventArgs.cs" />
|
||||
<Compile Include="Entities\Studio.cs" />
|
||||
<Compile Include="Entities\Trailer.cs" />
|
||||
<Compile Include="Entities\TV\Episode.cs" />
|
||||
<Compile Include="Entities\TV\Season.cs" />
|
||||
<Compile Include="Entities\TV\Series.cs" />
|
||||
<Compile Include="Entities\User.cs" />
|
||||
<Compile Include="Entities\UserItemData.cs" />
|
||||
<Compile Include="Entities\UserRootFolder.cs" />
|
||||
<Compile Include="Entities\Video.cs" />
|
||||
<Compile Include="Entities\CollectionFolder.cs" />
|
||||
<Compile Include="Entities\Year.cs" />
|
||||
<Compile Include="IO\FileSystemManager.cs" />
|
||||
<Compile Include="Library\ChildrenChangedEventArgs.cs" />
|
||||
<Compile Include="Library\DtoBuilder.cs" />
|
||||
<Compile Include="Library\Profiler.cs" />
|
||||
<Compile Include="Library\UserDataManager.cs" />
|
||||
<Compile Include="Library\UserManager.cs" />
|
||||
<Compile Include="Localization\AURatingsDictionary.cs" />
|
||||
<Compile Include="Localization\BaseStrings.cs" />
|
||||
<Compile Include="Localization\GBRatingsDictionary.cs" />
|
||||
<Compile Include="Localization\LocalizedStrings.cs" />
|
||||
<Compile Include="Localization\NLRatingsDictionary.cs" />
|
||||
<Compile Include="Localization\Ratings.cs" />
|
||||
<Compile Include="Localization\RatingsDefinition.cs" />
|
||||
<Compile Include="Localization\USRatingsDictionary.cs" />
|
||||
<Compile Include="MediaInfo\BDInfoResult.cs" />
|
||||
<Compile Include="MediaInfo\FFMpegManager.cs" />
|
||||
<Compile Include="MediaInfo\FFProbeResult.cs" />
|
||||
<Compile Include="Persistence\IDisplayPreferencesRepository.cs" />
|
||||
<Compile Include="Persistence\IItemRepository.cs" />
|
||||
<Compile Include="Persistence\IRepository.cs" />
|
||||
<Compile Include="Persistence\IUserDataRepository.cs" />
|
||||
<Compile Include="Persistence\IUserRepository.cs" />
|
||||
<Compile Include="Persistence\SQLite\SQLiteDisplayPreferencesRepository.cs" />
|
||||
<Compile Include="Persistence\SQLite\SQLiteExtensions.cs" />
|
||||
<Compile Include="Persistence\SQLite\SQLiteItemRepository.cs" />
|
||||
<Compile Include="Persistence\SQLite\SQLiteRepository.cs" />
|
||||
<Compile Include="Persistence\SQLite\SQLiteUserDataRepository.cs" />
|
||||
<Compile Include="Persistence\SQLite\SQLiteUserRepository.cs" />
|
||||
<Compile Include="Persistence\TypeMapper.cs" />
|
||||
<Compile Include="Playback\BaseIntroProvider.cs" />
|
||||
<Compile Include="Plugins\BaseConfigurationPage.cs" />
|
||||
<Compile Include="Plugins\PluginSecurityManager.cs" />
|
||||
<Compile Include="Providers\BaseImageEnhancer.cs" />
|
||||
<Compile Include="Providers\FanartBaseProvider.cs" />
|
||||
<Compile Include="Providers\ImagesByNameProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\BaseFFMpegImageProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\BaseFFMpegProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\BDInfoProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\FFMpegAudioImageProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\BaseFFProbeProvider.cs" />
|
||||
<Compile Include="Providers\BaseProviderInfo.cs" />
|
||||
<Compile Include="Providers\Movies\FanArtMovieProvider.cs" />
|
||||
<Compile Include="Providers\Movies\MovieDbProvider.cs" />
|
||||
<Compile Include="Providers\Movies\MovieProviderFromJson.cs" />
|
||||
<Compile Include="Providers\Movies\MovieProviderFromXml.cs" />
|
||||
<Compile Include="Providers\Movies\PersonProviderFromJson.cs" />
|
||||
<Compile Include="Providers\Movies\TmdbPersonProvider.cs" />
|
||||
<Compile Include="Providers\ProviderManager.cs" />
|
||||
<Compile Include="Providers\SortNameProvider.cs" />
|
||||
<Compile Include="Providers\TV\EpisodeImageFromMediaLocationProvider.cs" />
|
||||
<Compile Include="Providers\TV\EpisodeProviderFromXml.cs" />
|
||||
<Compile Include="Providers\TV\EpisodeXmlParser.cs" />
|
||||
<Compile Include="Providers\TV\FanArtTVProvider.cs" />
|
||||
<Compile Include="Providers\TV\RemoteEpisodeProvider.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Providers\TV\RemoteSeasonProvider.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Providers\TV\RemoteSeriesProvider.cs" />
|
||||
<Compile Include="Providers\TV\SeriesProviderFromXml.cs" />
|
||||
<Compile Include="Providers\TV\SeriesXmlParser.cs" />
|
||||
<Compile Include="Providers\MediaInfo\FFMpegVideoImageProvider.cs" />
|
||||
<Compile Include="Resolvers\Audio\MusicAlbumResolver.cs" />
|
||||
<Compile Include="Resolvers\Audio\MusicArtistResolver.cs" />
|
||||
<Compile Include="Resolvers\BaseResolutionIgnoreRule.cs" />
|
||||
<Compile Include="Resolvers\CoreResolutionIgnoreRule.cs" />
|
||||
<Compile Include="Resolvers\EntityResolutionHelper.cs" />
|
||||
<Compile Include="Resolvers\LocalTrailerResolver.cs" />
|
||||
<Compile Include="Resolvers\Movies\BoxSetResolver.cs" />
|
||||
<Compile Include="Resolvers\Movies\MovieResolver.cs" />
|
||||
<Compile Include="Resolvers\TV\EpisodeResolver.cs" />
|
||||
<Compile Include="Resolvers\TV\SeasonResolver.cs" />
|
||||
<Compile Include="Resolvers\TV\SeriesResolver.cs" />
|
||||
<Compile Include="Resolvers\TV\TVUtils.cs" />
|
||||
<Compile Include="Library\ResourcePool.cs" />
|
||||
<Compile Include="ScheduledTasks\ChapterImagesTask.cs" />
|
||||
<Compile Include="ScheduledTasks\ImageCleanupTask.cs" />
|
||||
<Compile Include="ScheduledTasks\PeopleValidationTask.cs" />
|
||||
<Compile Include="ScheduledTasks\PluginUpdateTask.cs" />
|
||||
<Compile Include="ScheduledTasks\RefreshMediaLibraryTask.cs" />
|
||||
<Compile Include="ServerApplicationPaths.cs" />
|
||||
<Compile Include="Library\ItemResolveArgs.cs" />
|
||||
<Compile Include="IO\DirectoryWatchers.cs" />
|
||||
<Compile Include="IO\FileData.cs" />
|
||||
<Compile Include="Library\LibraryManager.cs" />
|
||||
<Compile Include="Kernel.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Providers\BaseMetadataProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\FFProbeAudioInfoProvider.cs" />
|
||||
<Compile Include="Providers\FolderProviderFromXml.cs" />
|
||||
<Compile Include="Providers\ImageFromMediaLocationProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\FFProbeVideoInfoProvider.cs" />
|
||||
<Compile Include="Resolvers\Audio\AudioResolver.cs" />
|
||||
<Compile Include="Resolvers\BaseItemResolver.cs" />
|
||||
<Compile Include="Resolvers\FolderResolver.cs" />
|
||||
<Compile Include="Resolvers\VideoResolver.cs" />
|
||||
<Compile Include="Sorting\BaseItemComparer.cs" />
|
||||
<Compile Include="Sorting\SortOrder.cs" />
|
||||
<Compile Include="Updates\InstallationManager.cs" />
|
||||
<Compile Include="Weather\BaseWeatherProvider.cs" />
|
||||
<Compile Include="Weather\WeatherProvider.cs" />
|
||||
<Compile Include="Providers\BaseItemXmlParser.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BDInfo\BDInfo.csproj">
|
||||
<Project>{07b509c0-0c28-4f3f-8963-5263281f7e3d}</Project>
|
||||
<Name>BDInfo</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
|
||||
<Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
|
||||
<Name>MediaBrowser.Common</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
|
||||
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
|
||||
<Name>MediaBrowser.Model</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="MediaInfo\ffmpeg20130209.zip" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="MediaInfo\readme.txt" />
|
||||
<Content Include="x64\SQLite.Interop.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="x86\SQLite.Interop.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<PropertyGroup>
|
||||
<PostBuildEvent>
|
||||
</PostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PreBuildEvent>
|
||||
</PreBuildEvent>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(SolutionDir)\.nuget\nuget.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
41
MediaBrowser.Controller/MediaInfo/BDInfoResult.cs
Normal file
41
MediaBrowser.Controller/MediaInfo/BDInfoResult.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using MediaBrowser.Model.Entities;
|
||||
using ProtoBuf;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the result of BDInfo output
|
||||
/// </summary>
|
||||
[ProtoContract]
|
||||
public class BDInfoResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the media streams.
|
||||
/// </summary>
|
||||
/// <value>The media streams.</value>
|
||||
[ProtoMember(1)]
|
||||
public List<MediaStream> MediaStreams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the run time ticks.
|
||||
/// </summary>
|
||||
/// <value>The run time ticks.</value>
|
||||
[ProtoMember(2)]
|
||||
public long? RunTimeTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the files.
|
||||
/// </summary>
|
||||
/// <value>The files.</value>
|
||||
[ProtoMember(3)]
|
||||
public List<string> Files { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the chapters.
|
||||
/// </summary>
|
||||
/// <value>The chapters.</value>
|
||||
[ProtoMember(4)]
|
||||
public List<double> Chapters { get; set; }
|
||||
}
|
||||
}
|
||||
1078
MediaBrowser.Controller/MediaInfo/FFMpegManager.cs
Normal file
1078
MediaBrowser.Controller/MediaInfo/FFMpegManager.cs
Normal file
File diff suppressed because it is too large
Load Diff
354
MediaBrowser.Controller/MediaInfo/FFProbeResult.cs
Normal file
354
MediaBrowser.Controller/MediaInfo/FFProbeResult.cs
Normal file
@@ -0,0 +1,354 @@
|
||||
using MediaBrowser.Model.Entities;
|
||||
using ProtoBuf;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a class that we can use to deserialize the ffprobe json output
|
||||
/// Sample output:
|
||||
/// http://stackoverflow.com/questions/7708373/get-ffmpeg-information-in-friendly-way
|
||||
/// </summary>
|
||||
[ProtoContract]
|
||||
public class FFProbeResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the streams.
|
||||
/// </summary>
|
||||
/// <value>The streams.</value>
|
||||
[ProtoMember(1)]
|
||||
public FFProbeMediaStreamInfo[] streams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format.
|
||||
/// </summary>
|
||||
/// <value>The format.</value>
|
||||
[ProtoMember(2)]
|
||||
public FFProbeMediaFormatInfo format { get; set; }
|
||||
|
||||
[ProtoMember(3)]
|
||||
public List<ChapterInfo> Chapters { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stream within the output
|
||||
/// </summary>
|
||||
[ProtoContract]
|
||||
public class FFProbeMediaStreamInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the index.
|
||||
/// </summary>
|
||||
/// <value>The index.</value>
|
||||
[ProtoMember(1)]
|
||||
public int index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile.
|
||||
/// </summary>
|
||||
/// <value>The profile.</value>
|
||||
[ProtoMember(2)]
|
||||
public string profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_name.</value>
|
||||
[ProtoMember(3)]
|
||||
public string codec_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_long_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_long_name.</value>
|
||||
[ProtoMember(4)]
|
||||
public string codec_long_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_type.
|
||||
/// </summary>
|
||||
/// <value>The codec_type.</value>
|
||||
[ProtoMember(5)]
|
||||
public string codec_type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_rate.
|
||||
/// </summary>
|
||||
/// <value>The sample_rate.</value>
|
||||
[ProtoMember(6)]
|
||||
public string sample_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channels.
|
||||
/// </summary>
|
||||
/// <value>The channels.</value>
|
||||
[ProtoMember(7)]
|
||||
public int channels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the avg_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The avg_frame_rate.</value>
|
||||
[ProtoMember(8)]
|
||||
public string avg_frame_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
[ProtoMember(9)]
|
||||
public string duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
[ProtoMember(10)]
|
||||
public string bit_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
/// <value>The width.</value>
|
||||
[ProtoMember(11)]
|
||||
public int width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
/// <value>The height.</value>
|
||||
[ProtoMember(12)]
|
||||
public int height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The display_aspect_ratio.</value>
|
||||
[ProtoMember(13)]
|
||||
public string display_aspect_ratio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
[ProtoMember(14)]
|
||||
public Dictionary<string, string> tags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bits_per_sample.
|
||||
/// </summary>
|
||||
/// <value>The bits_per_sample.</value>
|
||||
[ProtoMember(17)]
|
||||
public int bits_per_sample { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the r_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The r_frame_rate.</value>
|
||||
[ProtoMember(18)]
|
||||
public string r_frame_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the has_b_frames.
|
||||
/// </summary>
|
||||
/// <value>The has_b_frames.</value>
|
||||
[ProtoMember(19)]
|
||||
public int has_b_frames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The sample_aspect_ratio.</value>
|
||||
[ProtoMember(20)]
|
||||
public string sample_aspect_ratio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pix_fmt.
|
||||
/// </summary>
|
||||
/// <value>The pix_fmt.</value>
|
||||
[ProtoMember(21)]
|
||||
public string pix_fmt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// </summary>
|
||||
/// <value>The level.</value>
|
||||
[ProtoMember(22)]
|
||||
public int level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time_base.
|
||||
/// </summary>
|
||||
/// <value>The time_base.</value>
|
||||
[ProtoMember(23)]
|
||||
public string time_base { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
[ProtoMember(24)]
|
||||
public string start_time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_time_base.
|
||||
/// </summary>
|
||||
/// <value>The codec_time_base.</value>
|
||||
[ProtoMember(25)]
|
||||
public string codec_time_base { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag.</value>
|
||||
[ProtoMember(26)]
|
||||
public string codec_tag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag_string.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag_string.</value>
|
||||
[ProtoMember(27)]
|
||||
public string codec_tag_string { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_fmt.
|
||||
/// </summary>
|
||||
/// <value>The sample_fmt.</value>
|
||||
[ProtoMember(28)]
|
||||
public string sample_fmt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the dmix_mode.
|
||||
/// </summary>
|
||||
/// <value>The dmix_mode.</value>
|
||||
[ProtoMember(29)]
|
||||
public string dmix_mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_pts.
|
||||
/// </summary>
|
||||
/// <value>The start_pts.</value>
|
||||
[ProtoMember(30)]
|
||||
public string start_pts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the is_avc.
|
||||
/// </summary>
|
||||
/// <value>The is_avc.</value>
|
||||
[ProtoMember(31)]
|
||||
public string is_avc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nal_length_size.
|
||||
/// </summary>
|
||||
/// <value>The nal_length_size.</value>
|
||||
[ProtoMember(32)]
|
||||
public string nal_length_size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_cmixlev.</value>
|
||||
[ProtoMember(33)]
|
||||
public string ltrt_cmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_surmixlev.</value>
|
||||
[ProtoMember(34)]
|
||||
public string ltrt_surmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_cmixlev.</value>
|
||||
[ProtoMember(35)]
|
||||
public string loro_cmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_surmixlev.</value>
|
||||
[ProtoMember(36)]
|
||||
public string loro_surmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the disposition.
|
||||
/// </summary>
|
||||
/// <value>The disposition.</value>
|
||||
[ProtoMember(37)]
|
||||
public Dictionary<string, string> disposition { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class MediaFormat
|
||||
/// </summary>
|
||||
[ProtoContract]
|
||||
public class FFProbeMediaFormatInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the filename.
|
||||
/// </summary>
|
||||
/// <value>The filename.</value>
|
||||
[ProtoMember(1)]
|
||||
public string filename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nb_streams.
|
||||
/// </summary>
|
||||
/// <value>The nb_streams.</value>
|
||||
[ProtoMember(2)]
|
||||
public int nb_streams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format_name.
|
||||
/// </summary>
|
||||
/// <value>The format_name.</value>
|
||||
[ProtoMember(3)]
|
||||
public string format_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format_long_name.
|
||||
/// </summary>
|
||||
/// <value>The format_long_name.</value>
|
||||
[ProtoMember(4)]
|
||||
public string format_long_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
[ProtoMember(5)]
|
||||
public string start_time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
[ProtoMember(6)]
|
||||
public string duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size.
|
||||
/// </summary>
|
||||
/// <value>The size.</value>
|
||||
[ProtoMember(7)]
|
||||
public string size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
[ProtoMember(8)]
|
||||
public string bit_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
[ProtoMember(9)]
|
||||
public Dictionary<string, string> tags { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
985770c0d2633a13719be2e5cf19554262415f62
|
||||
5
MediaBrowser.Controller/MediaInfo/readme.txt
Normal file
5
MediaBrowser.Controller/MediaInfo/readme.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
This is the 32-bit static build of ffmpeg, located at:
|
||||
|
||||
http://ffmpeg.zeranoe.com/builds/
|
||||
|
||||
The zip file contains both ffmpeg and ffprobe, and is suffixed with the date of the build.
|
||||
@@ -0,0 +1,29 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IDisplayPreferencesRepository
|
||||
/// </summary>
|
||||
public interface IDisplayPreferencesRepository : IRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves display preferences for an item
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SaveDisplayPrefs(Folder item, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets display preferences for an item
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>IEnumerable{DisplayPreferences}.</returns>
|
||||
IEnumerable<DisplayPreferences> RetrieveDisplayPrefs(Folder item);
|
||||
}
|
||||
}
|
||||
45
MediaBrowser.Controller/Persistence/IItemRepository.cs
Normal file
45
MediaBrowser.Controller/Persistence/IItemRepository.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an interface to implement an Item repository
|
||||
/// </summary>
|
||||
public interface IItemRepository : IRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves an item
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SaveItem(BaseItem item, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an item
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
BaseItem RetrieveItem(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets children of a given Folder
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent.</param>
|
||||
/// <returns>IEnumerable{BaseItem}.</returns>
|
||||
IEnumerable<BaseItem> RetrieveChildren(Folder parent);
|
||||
|
||||
/// <summary>
|
||||
/// Saves children of a given Folder
|
||||
/// </summary>
|
||||
/// <param name="parentId">The parent id.</param>
|
||||
/// <param name="children">The children.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SaveChildren(Guid parentId, IEnumerable<BaseItem> children, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
23
MediaBrowser.Controller/Persistence/IRepository.cs
Normal file
23
MediaBrowser.Controller/Persistence/IRepository.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a base interface for all the repository interfaces
|
||||
/// </summary>
|
||||
public interface IRepository : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens the connection to the repository
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
Task Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the repository
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
string Name { get; }
|
||||
}
|
||||
}
|
||||
28
MediaBrowser.Controller/Persistence/IUserDataRepository.cs
Normal file
28
MediaBrowser.Controller/Persistence/IUserDataRepository.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an interface to implement a UserData repository
|
||||
/// </summary>
|
||||
public interface IUserDataRepository : IRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves user data for an item
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SaveUserData(BaseItem item, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets user data for an item
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>IEnumerable{UserItemData}.</returns>
|
||||
IEnumerable<UserItemData> RetrieveUserData(BaseItem item);
|
||||
}
|
||||
}
|
||||
35
MediaBrowser.Controller/Persistence/IUserRepository.cs
Normal file
35
MediaBrowser.Controller/Persistence/IUserRepository.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an interface to implement a User repository
|
||||
/// </summary>
|
||||
public interface IUserRepository : IRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Deletes the user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteUser(User user, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SaveUser(User user, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all users.
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{User}.</returns>
|
||||
IEnumerable<User> RetrieveAllUsers();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence.SQLite
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SQLiteDisplayPreferencesRepository
|
||||
/// </summary>
|
||||
[Export(typeof(IDisplayPreferencesRepository))]
|
||||
class SQLiteDisplayPreferencesRepository : SqliteRepository, IDisplayPreferencesRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// The repository name
|
||||
/// </summary>
|
||||
public const string RepositoryName = "SQLite";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the repository
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return RepositoryName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Initialize()
|
||||
{
|
||||
var dbFile = Path.Combine(Kernel.Instance.ApplicationPaths.DataPath, "displaypreferences.db");
|
||||
|
||||
await ConnectToDB(dbFile).ConfigureAwait(false);
|
||||
|
||||
string[] queries = {
|
||||
|
||||
"create table if not exists display_prefs (item_id GUID, user_id GUID, data BLOB)",
|
||||
"create unique index if not exists idx_display_prefs on display_prefs (item_id, user_id)",
|
||||
"create table if not exists schema_version (table_name primary key, version)",
|
||||
//pragmas
|
||||
"pragma temp_store = memory"
|
||||
};
|
||||
|
||||
RunQueries(queries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save the display preferences associated with an item in the repo
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
public Task SaveDisplayPrefs(Folder item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
if (cancellationToken == null)
|
||||
{
|
||||
throw new ArgumentNullException("cancellationToken");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var cmd = connection.CreateCommand();
|
||||
|
||||
cmd.CommandText = "delete from display_prefs where item_id = @guid";
|
||||
cmd.AddParam("@guid", item.DisplayPrefsId);
|
||||
|
||||
QueueCommand(cmd);
|
||||
|
||||
if (item.DisplayPrefs != null)
|
||||
{
|
||||
foreach (var data in item.DisplayPrefs)
|
||||
{
|
||||
cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "insert into display_prefs (item_id, user_id, data) values (@1, @2, @3)";
|
||||
cmd.AddParam("@1", item.DisplayPrefsId);
|
||||
cmd.AddParam("@2", data.UserId);
|
||||
|
||||
cmd.AddParam("@3", Kernel.Instance.ProtobufSerializer.SerializeToBytes(data));
|
||||
|
||||
QueueCommand(cmd);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets display preferences for an item
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>IEnumerable{DisplayPreferences}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public IEnumerable<DisplayPreferences> RetrieveDisplayPrefs(Folder item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "select data from display_prefs where item_id = @guid";
|
||||
var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
|
||||
guidParam.Value = item.DisplayPrefsId;
|
||||
|
||||
using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
using (var stream = GetStream(reader, 0))
|
||||
{
|
||||
var data = Kernel.Instance.ProtobufSerializer.DeserializeFromStream<DisplayPreferences>(stream);
|
||||
if (data != null)
|
||||
{
|
||||
yield return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.SQLite;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence.SQLite
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SQLiteExtensions
|
||||
/// </summary>
|
||||
static class SQLiteExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the param.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The CMD.</param>
|
||||
/// <param name="param">The param.</param>
|
||||
/// <returns>SQLiteParameter.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public static SQLiteParameter AddParam(this SQLiteCommand cmd, string param)
|
||||
{
|
||||
if (string.IsNullOrEmpty(param))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
var sqliteParam = new SQLiteParameter(param);
|
||||
cmd.Parameters.Add(sqliteParam);
|
||||
return sqliteParam;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the param.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The CMD.</param>
|
||||
/// <param name="param">The param.</param>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>SQLiteParameter.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public static SQLiteParameter AddParam(this SQLiteCommand cmd, string param, object data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(param))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
var sqliteParam = AddParam(cmd, param);
|
||||
sqliteParam.Value = data;
|
||||
return sqliteParam;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified conn is open.
|
||||
/// </summary>
|
||||
/// <param name="conn">The conn.</param>
|
||||
/// <returns><c>true</c> if the specified conn is open; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsOpen(this SQLiteConnection conn)
|
||||
{
|
||||
return conn.State == ConnectionState.Open;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
using MediaBrowser.Common.Serialization;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence.SQLite
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SQLiteItemRepository
|
||||
/// </summary>
|
||||
[Export(typeof(IItemRepository))]
|
||||
public class SQLiteItemRepository : SqliteRepository, IItemRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// The _type mapper
|
||||
/// </summary>
|
||||
private readonly TypeMapper _typeMapper = new TypeMapper();
|
||||
|
||||
/// <summary>
|
||||
/// The repository name
|
||||
/// </summary>
|
||||
public const string RepositoryName = "SQLite";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the repository
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return RepositoryName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Initialize()
|
||||
{
|
||||
var dbFile = Path.Combine(Kernel.Instance.ApplicationPaths.DataPath, "library.db");
|
||||
|
||||
await ConnectToDB(dbFile).ConfigureAwait(false);
|
||||
|
||||
string[] queries = {
|
||||
|
||||
"create table if not exists items (guid GUID primary key, obj_type, data BLOB)",
|
||||
"create index if not exists idx_items on items(guid)",
|
||||
"create table if not exists children (guid GUID, child GUID)",
|
||||
"create unique index if not exists idx_children on children(guid, child)",
|
||||
"create table if not exists schema_version (table_name primary key, version)",
|
||||
//triggers
|
||||
TriggerSql,
|
||||
//pragmas
|
||||
"pragma temp_store = memory"
|
||||
};
|
||||
|
||||
RunQueries(queries);
|
||||
}
|
||||
|
||||
//cascade delete triggers
|
||||
/// <summary>
|
||||
/// The trigger SQL
|
||||
/// </summary>
|
||||
protected string TriggerSql =
|
||||
@"CREATE TRIGGER if not exists delete_item
|
||||
AFTER DELETE
|
||||
ON items
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
DELETE FROM children WHERE children.guid = old.child;
|
||||
DELETE FROM children WHERE children.child = old.child;
|
||||
END";
|
||||
|
||||
/// <summary>
|
||||
/// Save a standard item in the repo
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
public Task SaveItem(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
if (cancellationToken == null)
|
||||
{
|
||||
throw new ArgumentNullException("cancellationToken");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var serialized = JsonSerializer.SerializeToBytes(item);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "replace into items (guid, obj_type, data) values (@1, @2, @3)";
|
||||
cmd.AddParam("@1", item.Id);
|
||||
cmd.AddParam("@2", item.GetType().FullName);
|
||||
cmd.AddParam("@3", serialized);
|
||||
QueueCommand(cmd);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a standard item from the repo
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
/// <exception cref="System.ArgumentException"></exception>
|
||||
public BaseItem RetrieveItem(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
return RetrieveItemInternal(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal retrieve from items or users table
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
/// <exception cref="System.ArgumentException"></exception>
|
||||
protected BaseItem RetrieveItemInternal(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "select obj_type,data from items where guid = @guid";
|
||||
var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
|
||||
guidParam.Value = id;
|
||||
|
||||
using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow))
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
var type = reader.GetString(0);
|
||||
using (var stream = GetStream(reader, 1))
|
||||
{
|
||||
var itemType = _typeMapper.GetType(type);
|
||||
|
||||
if (itemType == null)
|
||||
{
|
||||
Logger.Error("Cannot find type {0}. Probably belongs to plug-in that is no longer loaded.", type);
|
||||
return null;
|
||||
}
|
||||
|
||||
var item = JsonSerializer.DeserializeFromStream(stream, itemType);
|
||||
return item as BaseItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all the children of the given folder
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent.</param>
|
||||
/// <returns>IEnumerable{BaseItem}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public IEnumerable<BaseItem> RetrieveChildren(Folder parent)
|
||||
{
|
||||
if (parent == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "select obj_type,data from items where guid in (select child from children where guid = @guid)";
|
||||
var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
|
||||
guidParam.Value = parent.Id;
|
||||
|
||||
using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var type = reader.GetString(0);
|
||||
|
||||
using (var stream = GetStream(reader, 1))
|
||||
{
|
||||
var itemType = _typeMapper.GetType(type);
|
||||
if (itemType == null)
|
||||
{
|
||||
Logger.Error("Cannot find type {0}. Probably belongs to plug-in that is no longer loaded.",type);
|
||||
continue;
|
||||
}
|
||||
var item = JsonSerializer.DeserializeFromStream(stream, itemType) as BaseItem;
|
||||
if (item != null)
|
||||
{
|
||||
item.Parent = parent;
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save references to all the children for the given folder
|
||||
/// (Doesn't actually save the child entities)
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <param name="children">The children.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">id</exception>
|
||||
public Task SaveChildren(Guid id, IEnumerable<BaseItem> children, CancellationToken cancellationToken)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentNullException("id");
|
||||
}
|
||||
|
||||
if (children == null)
|
||||
{
|
||||
throw new ArgumentNullException("children");
|
||||
}
|
||||
|
||||
if (cancellationToken == null)
|
||||
{
|
||||
throw new ArgumentNullException("cancellationToken");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var cmd = connection.CreateCommand();
|
||||
|
||||
cmd.CommandText = "delete from children where guid = @guid";
|
||||
cmd.AddParam("@guid", id);
|
||||
|
||||
QueueCommand(cmd);
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
var guid = child.Id;
|
||||
cmd = connection.CreateCommand();
|
||||
cmd.AddParam("@guid", id);
|
||||
cmd.CommandText = "replace into children (guid, child) values (@guid, @child)";
|
||||
var childParam = cmd.Parameters.Add("@child", DbType.Guid);
|
||||
|
||||
childParam.Value = guid;
|
||||
QueueCommand(cmd);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
301
MediaBrowser.Controller/Persistence/SQLite/SQLiteRepository.cs
Normal file
301
MediaBrowser.Controller/Persistence/SQLite/SQLiteRepository.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence.SQLite
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SqliteRepository
|
||||
/// </summary>
|
||||
public abstract class SqliteRepository : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The db file name
|
||||
/// </summary>
|
||||
protected string dbFileName;
|
||||
/// <summary>
|
||||
/// The connection
|
||||
/// </summary>
|
||||
protected SQLiteConnection connection;
|
||||
/// <summary>
|
||||
/// The delayed commands
|
||||
/// </summary>
|
||||
protected ConcurrentQueue<SQLiteCommand> delayedCommands = new ConcurrentQueue<SQLiteCommand>();
|
||||
/// <summary>
|
||||
/// The flush interval
|
||||
/// </summary>
|
||||
private const int FlushInterval = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// The flush timer
|
||||
/// </summary>
|
||||
private Timer FlushTimer;
|
||||
|
||||
protected ILogger Logger { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Connects to DB.
|
||||
/// </summary>
|
||||
/// <param name="dbPath">The db path.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
protected async Task ConnectToDB(string dbPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dbPath))
|
||||
{
|
||||
throw new ArgumentNullException("dbPath");
|
||||
}
|
||||
|
||||
Logger = LogManager.GetLogger(GetType().Name);
|
||||
|
||||
dbFileName = dbPath;
|
||||
var connectionstr = new SQLiteConnectionStringBuilder
|
||||
{
|
||||
PageSize = 4096,
|
||||
CacheSize = 40960,
|
||||
SyncMode = SynchronizationModes.Off,
|
||||
DataSource = dbPath,
|
||||
JournalMode = SQLiteJournalModeEnum.Memory
|
||||
};
|
||||
|
||||
connection = new SQLiteConnection(connectionstr.ConnectionString);
|
||||
|
||||
await connection.OpenAsync().ConfigureAwait(false);
|
||||
|
||||
// Run once
|
||||
FlushTimer = new Timer(Flush, null, TimeSpan.FromMilliseconds(FlushInterval), TimeSpan.FromMilliseconds(-1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the queries.
|
||||
/// </summary>
|
||||
/// <param name="queries">The queries.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
protected void RunQueries(string[] queries)
|
||||
{
|
||||
if (queries == null)
|
||||
{
|
||||
throw new ArgumentNullException("queries");
|
||||
}
|
||||
|
||||
using (var tran = connection.BeginTransaction())
|
||||
{
|
||||
try
|
||||
{
|
||||
var cmd = connection.CreateCommand();
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
cmd.Transaction = tran;
|
||||
cmd.CommandText = query;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
tran.Commit();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorException("Error running queries", e);
|
||||
tran.Rollback();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
Logger.Info("Disposing " + GetType().Name);
|
||||
|
||||
try
|
||||
{
|
||||
// If we're not already flushing, do it now
|
||||
if (!IsFlushing)
|
||||
{
|
||||
Flush(null);
|
||||
}
|
||||
|
||||
// Don't dispose in the middle of a flush
|
||||
while (IsFlushing)
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
|
||||
if (FlushTimer != null)
|
||||
{
|
||||
FlushTimer.Dispose();
|
||||
FlushTimer = null;
|
||||
}
|
||||
|
||||
if (connection.IsOpen())
|
||||
{
|
||||
connection.Close();
|
||||
}
|
||||
|
||||
connection.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error disposing database", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues the command.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The CMD.</param>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
protected void QueueCommand(SQLiteCommand cmd)
|
||||
{
|
||||
if (cmd == null)
|
||||
{
|
||||
throw new ArgumentNullException("cmd");
|
||||
}
|
||||
|
||||
delayedCommands.Enqueue(cmd);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The is flushing
|
||||
/// </summary>
|
||||
private bool IsFlushing;
|
||||
|
||||
/// <summary>
|
||||
/// Flushes the specified sender.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
private void Flush(object sender)
|
||||
{
|
||||
// Cannot call Count on a ConcurrentQueue since it's an O(n) operation
|
||||
// Use IsEmpty instead
|
||||
if (delayedCommands.IsEmpty)
|
||||
{
|
||||
FlushTimer.Change(TimeSpan.FromMilliseconds(FlushInterval), TimeSpan.FromMilliseconds(-1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsFlushing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsFlushing = true;
|
||||
var numCommands = 0;
|
||||
|
||||
using (var tran = connection.BeginTransaction())
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!delayedCommands.IsEmpty)
|
||||
{
|
||||
SQLiteCommand command;
|
||||
|
||||
delayedCommands.TryDequeue(out command);
|
||||
|
||||
command.Connection = connection;
|
||||
command.Transaction = tran;
|
||||
|
||||
command.ExecuteNonQuery();
|
||||
numCommands++;
|
||||
}
|
||||
|
||||
tran.Commit();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorException("Failed to commit transaction.", e);
|
||||
tran.Rollback();
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Info("SQL Delayed writer executed " + numCommands + " commands");
|
||||
|
||||
FlushTimer.Change(TimeSpan.FromMilliseconds(FlushInterval), TimeSpan.FromMilliseconds(-1));
|
||||
IsFlushing = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The CMD.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public async Task ExecuteCommand(DbCommand cmd)
|
||||
{
|
||||
if (cmd == null)
|
||||
{
|
||||
throw new ArgumentNullException("cmd");
|
||||
}
|
||||
|
||||
using (var tran = connection.BeginTransaction())
|
||||
{
|
||||
try
|
||||
{
|
||||
cmd.Connection = connection;
|
||||
cmd.Transaction = tran;
|
||||
|
||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||
|
||||
tran.Commit();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorException("Failed to commit transaction.", e);
|
||||
tran.Rollback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a stream from a DataReader at a given ordinal
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <param name="ordinal">The ordinal.</param>
|
||||
/// <returns>Stream.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
protected static Stream GetStream(IDataReader reader, int ordinal)
|
||||
{
|
||||
if (reader == null)
|
||||
{
|
||||
throw new ArgumentNullException("reader");
|
||||
}
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
var num = 0L;
|
||||
var array = new byte[4096];
|
||||
long bytes;
|
||||
do
|
||||
{
|
||||
bytes = reader.GetBytes(ordinal, num, array, 0, array.Length);
|
||||
memoryStream.Write(array, 0, (int)bytes);
|
||||
num += bytes;
|
||||
}
|
||||
while (bytes > 0L);
|
||||
memoryStream.Position = 0;
|
||||
return memoryStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence.SQLite
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SQLiteUserDataRepository
|
||||
/// </summary>
|
||||
[Export(typeof(IUserDataRepository))]
|
||||
public class SQLiteUserDataRepository : SqliteRepository, IUserDataRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// The repository name
|
||||
/// </summary>
|
||||
public const string RepositoryName = "SQLite";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the repository
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return RepositoryName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Initialize()
|
||||
{
|
||||
var dbFile = Path.Combine(Kernel.Instance.ApplicationPaths.DataPath, "userdata.db");
|
||||
|
||||
await ConnectToDB(dbFile).ConfigureAwait(false);
|
||||
|
||||
string[] queries = {
|
||||
|
||||
"create table if not exists user_data (item_id GUID, user_id GUID, data BLOB)",
|
||||
"create unique index if not exists idx_user_data on user_data (item_id, user_id)",
|
||||
"create table if not exists schema_version (table_name primary key, version)",
|
||||
//pragmas
|
||||
"pragma temp_store = memory"
|
||||
};
|
||||
|
||||
RunQueries(queries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save the user specific data associated with an item in the repo
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
public Task SaveUserData(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
if (cancellationToken == null)
|
||||
{
|
||||
throw new ArgumentNullException("cancellationToken");
|
||||
}
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var cmd = connection.CreateCommand();
|
||||
|
||||
cmd.CommandText = "delete from user_data where item_id = @guid";
|
||||
cmd.AddParam("@guid", item.UserDataId);
|
||||
|
||||
QueueCommand(cmd);
|
||||
|
||||
if (item.UserData != null)
|
||||
{
|
||||
foreach (var data in item.UserData)
|
||||
{
|
||||
cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "insert into user_data (item_id, user_id, data) values (@1, @2, @3)";
|
||||
cmd.AddParam("@1", item.UserDataId);
|
||||
cmd.AddParam("@2", data.UserId);
|
||||
|
||||
cmd.AddParam("@3", Kernel.Instance.ProtobufSerializer.SerializeToBytes(data));
|
||||
|
||||
QueueCommand(cmd);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets user data for an item
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>IEnumerable{UserItemData}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public IEnumerable<UserItemData> RetrieveUserData(BaseItem item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "select data from user_data where item_id = @guid";
|
||||
var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
|
||||
guidParam.Value = item.UserDataId;
|
||||
|
||||
using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
using (var stream = GetStream(reader, 0))
|
||||
{
|
||||
var data = Kernel.Instance.ProtobufSerializer.DeserializeFromStream<UserItemData>(stream);
|
||||
if (data != null)
|
||||
{
|
||||
yield return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.Threading;
|
||||
using MediaBrowser.Common.Serialization;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence.SQLite
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SQLiteUserRepository
|
||||
/// </summary>
|
||||
[Export(typeof(IUserRepository))]
|
||||
public class SQLiteUserRepository : SqliteRepository, IUserRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// The repository name
|
||||
/// </summary>
|
||||
public const string RepositoryName = "SQLite";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the repository
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return RepositoryName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Initialize()
|
||||
{
|
||||
var dbFile = Path.Combine(Kernel.Instance.ApplicationPaths.DataPath, "users.db");
|
||||
|
||||
await ConnectToDB(dbFile).ConfigureAwait(false);
|
||||
|
||||
string[] queries = {
|
||||
|
||||
"create table if not exists users (guid GUID primary key, data BLOB)",
|
||||
"create index if not exists idx_users on users(guid)",
|
||||
"create table if not exists schema_version (table_name primary key, version)",
|
||||
//pragmas
|
||||
"pragma temp_store = memory"
|
||||
};
|
||||
|
||||
RunQueries(queries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a user in the repo
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">user</exception>
|
||||
public Task SaveUser(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
|
||||
if (cancellationToken == null)
|
||||
{
|
||||
throw new ArgumentNullException("cancellationToken");
|
||||
}
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var serialized = JsonSerializer.SerializeToBytes(user);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "replace into users (guid, data) values (@1, @2)";
|
||||
cmd.AddParam("@1", user.Id);
|
||||
cmd.AddParam("@2", serialized);
|
||||
QueueCommand(cmd);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all users from the database
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{User}.</returns>
|
||||
public IEnumerable<User> RetrieveAllUsers()
|
||||
{
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "select data from users";
|
||||
|
||||
using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
using (var stream = GetStream(reader, 0))
|
||||
{
|
||||
var user = JsonSerializer.DeserializeFromStream<User>(stream);
|
||||
yield return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">user</exception>
|
||||
public Task DeleteUser(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
|
||||
if (cancellationToken == null)
|
||||
{
|
||||
throw new ArgumentNullException("cancellationToken");
|
||||
}
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "delete from users where guid=@guid";
|
||||
var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
|
||||
guidParam.Value = user.Id;
|
||||
|
||||
return ExecuteCommand(cmd);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
47
MediaBrowser.Controller/Persistence/TypeMapper.cs
Normal file
47
MediaBrowser.Controller/Persistence/TypeMapper.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence
|
||||
{
|
||||
/// <summary>
|
||||
/// Class TypeMapper
|
||||
/// </summary>
|
||||
public class TypeMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// This holds all the types in the running assemblies so that we can de-serialize properly when we don't have strong types
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type.
|
||||
/// </summary>
|
||||
/// <param name="typeName">Name of the type.</param>
|
||||
/// <returns>Type.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public Type GetType(string typeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(typeName))
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
return _typeMap.GetOrAdd(typeName, LookupType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lookups the type.
|
||||
/// </summary>
|
||||
/// <param name="typeName">Name of the type.</param>
|
||||
/// <returns>Type.</returns>
|
||||
private Type LookupType(string typeName)
|
||||
{
|
||||
return AppDomain
|
||||
.CurrentDomain
|
||||
.GetAssemblies()
|
||||
.Select(a => a.GetType(typeName, false))
|
||||
.FirstOrDefault(t => t != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
MediaBrowser.Controller/Playback/BaseIntroProvider.cs
Normal file
19
MediaBrowser.Controller/Playback/BaseIntroProvider.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Playback
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BaseIntroProvider
|
||||
/// </summary>
|
||||
public abstract class BaseIntroProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the intros.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>IEnumerable{System.String}.</returns>
|
||||
public abstract IEnumerable<string> GetIntros(BaseItem item, User user);
|
||||
}
|
||||
}
|
||||
81
MediaBrowser.Controller/Plugins/BaseConfigurationPage.cs
Normal file
81
MediaBrowser.Controller/Plugins/BaseConfigurationPage.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using System.IO;
|
||||
|
||||
namespace MediaBrowser.Controller.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BaseConfigurationPage
|
||||
/// </summary>
|
||||
public abstract class BaseConfigurationPage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public abstract string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description.
|
||||
/// </summary>
|
||||
/// <value>The description.</value>
|
||||
public virtual string Description
|
||||
{
|
||||
get { return string.Empty; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the configuration page.
|
||||
/// </summary>
|
||||
/// <value>The type of the configuration page.</value>
|
||||
public virtual ConfigurationPageType ConfigurationPageType
|
||||
{
|
||||
get { return ConfigurationPageType.PluginConfiguration; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the HTML stream from manifest resource.
|
||||
/// </summary>
|
||||
/// <param name="resource">The resource.</param>
|
||||
/// <returns>Stream.</returns>
|
||||
protected Stream GetHtmlStreamFromManifestResource(string resource)
|
||||
{
|
||||
return GetType().Assembly.GetManifestResourceStream(resource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the HTML stream.
|
||||
/// </summary>
|
||||
/// <returns>Stream.</returns>
|
||||
public abstract Stream GetHtmlStream();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the plugin.
|
||||
/// </summary>
|
||||
/// <value>The name of the plugin.</value>
|
||||
public virtual string OwnerPluginName
|
||||
{
|
||||
get { return GetOwnerPlugin().Name; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the owner plugin.
|
||||
/// </summary>
|
||||
/// <returns>BasePlugin.</returns>
|
||||
public abstract IPlugin GetOwnerPlugin();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum ConfigurationPageType
|
||||
/// </summary>
|
||||
public enum ConfigurationPageType
|
||||
{
|
||||
/// <summary>
|
||||
/// The plugin configuration
|
||||
/// </summary>
|
||||
PluginConfiguration,
|
||||
/// <summary>
|
||||
/// The none
|
||||
/// </summary>
|
||||
None
|
||||
}
|
||||
}
|
||||
65
MediaBrowser.Controller/Plugins/PluginSecurityManager.cs
Normal file
65
MediaBrowser.Controller/Plugins/PluginSecurityManager.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Mediabrowser.Model.Entities;
|
||||
using Mediabrowser.PluginSecurity;
|
||||
using MediaBrowser.Common.Kernel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Plugins
|
||||
{
|
||||
public class PluginSecurityManager : BaseManager<Kernel>
|
||||
{
|
||||
private bool? _isMBSupporter;
|
||||
private bool _isMBSupporterInitialized;
|
||||
private object _isMBSupporterSyncLock = new object();
|
||||
|
||||
public bool IsMBSupporter
|
||||
{
|
||||
get
|
||||
{
|
||||
LazyInitializer.EnsureInitialized(ref _isMBSupporter, ref _isMBSupporterInitialized, ref _isMBSupporterSyncLock, () => GetRegistrationStatus("MBSupporter").Result.IsRegistered);
|
||||
return _isMBSupporter.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public PluginSecurityManager(Kernel kernel) : base(kernel)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<MBRegistrationRecord> GetRegistrationStatus(string feature, string mb2Equivalent = null)
|
||||
{
|
||||
return await MBRegistration.GetRegistrationStatus(feature, mb2Equivalent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string SupporterKey
|
||||
{
|
||||
get { return MBRegistration.SupporterKey; }
|
||||
set {
|
||||
if (value != MBRegistration.SupporterKey)
|
||||
{
|
||||
MBRegistration.SupporterKey = value;
|
||||
// Clear this so it will re-evaluate
|
||||
ResetSupporterInfo();
|
||||
// And we'll need to restart to re-evaluate the status of plug-ins
|
||||
Kernel.NotifyPendingRestart();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string LegacyKey
|
||||
{
|
||||
get { return MBRegistration.LegacyKey; }
|
||||
set {
|
||||
MBRegistration.LegacyKey = value;
|
||||
// And we'll need to restart to re-evaluate the status of plug-ins
|
||||
Kernel.NotifyPendingRestart();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetSupporterInfo()
|
||||
{
|
||||
_isMBSupporter = null;
|
||||
_isMBSupporterInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,34 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MediaBrowser.Controller")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("MediaBrowser.Controller")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2012")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("bc09905a-04ed-497d-b39b-27593401e715")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MediaBrowser.Controller")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("MediaBrowser.Controller")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2012")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("bc09905a-04ed-497d-b39b-27593401e715")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("2.9.*")]
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.FFMpeg;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class AudioInfoProvider : BaseMediaInfoProvider<Audio>
|
||||
{
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.First; }
|
||||
}
|
||||
|
||||
protected override string CacheDirectory
|
||||
{
|
||||
get { return Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory; }
|
||||
}
|
||||
|
||||
protected override void Fetch(Audio audio, FFProbeResult data)
|
||||
{
|
||||
MediaStream stream = data.streams.First(s => s.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
audio.Channels = stream.channels;
|
||||
|
||||
if (!string.IsNullOrEmpty(stream.sample_rate))
|
||||
{
|
||||
audio.SampleRate = int.Parse(stream.sample_rate);
|
||||
}
|
||||
|
||||
string bitrate = stream.bit_rate;
|
||||
string duration = stream.duration;
|
||||
|
||||
if (string.IsNullOrEmpty(bitrate))
|
||||
{
|
||||
bitrate = data.format.bit_rate;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(duration))
|
||||
{
|
||||
duration = data.format.duration;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(bitrate))
|
||||
{
|
||||
audio.BitRate = int.Parse(bitrate);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(duration))
|
||||
{
|
||||
audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration)).Ticks;
|
||||
}
|
||||
|
||||
if (data.format.tags != null)
|
||||
{
|
||||
FetchDataFromTags(audio, data.format.tags);
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
|
||||
{
|
||||
string title = GetDictionaryValue(tags, "title");
|
||||
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
audio.Name = title;
|
||||
}
|
||||
|
||||
string composer = GetDictionaryValue(tags, "composer");
|
||||
|
||||
if (!string.IsNullOrEmpty(composer))
|
||||
{
|
||||
audio.AddPerson(new PersonInfo { Name = composer, Type = "Composer" });
|
||||
}
|
||||
|
||||
audio.Album = GetDictionaryValue(tags, "album");
|
||||
audio.Artist = GetDictionaryValue(tags, "artist");
|
||||
audio.AlbumArtist = GetDictionaryValue(tags, "albumartist") ?? GetDictionaryValue(tags, "album artist") ?? GetDictionaryValue(tags, "album_artist");
|
||||
|
||||
audio.IndexNumber = GetDictionaryNumericValue(tags, "track");
|
||||
audio.ParentIndexNumber = GetDictionaryDiscValue(tags);
|
||||
|
||||
audio.Language = GetDictionaryValue(tags, "language");
|
||||
|
||||
audio.ProductionYear = GetDictionaryNumericValue(tags, "date");
|
||||
|
||||
audio.PremiereDate = GetDictionaryDateTime(tags, "retaildate") ?? GetDictionaryDateTime(tags, "retail date") ?? GetDictionaryDateTime(tags, "retail_date");
|
||||
|
||||
FetchGenres(audio, tags);
|
||||
|
||||
FetchStudios(audio, tags, "organization");
|
||||
FetchStudios(audio, tags, "ensemble");
|
||||
FetchStudios(audio, tags, "publisher");
|
||||
}
|
||||
|
||||
private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
|
||||
{
|
||||
string val = GetDictionaryValue(tags, tagName);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
var list = audio.Studios ?? new List<string>();
|
||||
list.AddRange(val.Split('/'));
|
||||
audio.Studios = list;
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchGenres(Audio audio, Dictionary<string, string> tags)
|
||||
{
|
||||
string val = GetDictionaryValue(tags, "genre");
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
var list = audio.Genres ?? new List<string>();
|
||||
list.AddRange(val.Split('/'));
|
||||
audio.Genres = list;
|
||||
}
|
||||
}
|
||||
|
||||
private int? GetDictionaryDiscValue(Dictionary<string, string> tags)
|
||||
{
|
||||
string disc = GetDictionaryValue(tags, "disc");
|
||||
|
||||
if (!string.IsNullOrEmpty(disc))
|
||||
{
|
||||
disc = disc.Split('/')[0];
|
||||
|
||||
int num;
|
||||
|
||||
if (int.TryParse(disc, out num))
|
||||
{
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BaseMediaInfoProvider<T> : BaseMetadataProvider
|
||||
where T : BaseItem
|
||||
{
|
||||
protected abstract string CacheDirectory { get; }
|
||||
|
||||
public override bool Supports(BaseEntity item)
|
||||
{
|
||||
return item is T;
|
||||
}
|
||||
|
||||
public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
/*T myItem = item as T;
|
||||
|
||||
if (CanSkipFFProbe(myItem))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FFProbeResult result = FFProbe.Run(myItem, CacheDirectory);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
Logger.LogInfo("Null FFProbeResult for {0} {1}", item.Id, item.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.format != null && result.format.tags != null)
|
||||
{
|
||||
result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
|
||||
}
|
||||
|
||||
if (result.streams != null)
|
||||
{
|
||||
foreach (MediaStream stream in result.streams)
|
||||
{
|
||||
if (stream.tags != null)
|
||||
{
|
||||
stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Fetch(myItem, result);*/
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract void Fetch(T item, FFProbeResult result);
|
||||
|
||||
protected virtual bool CanSkipFFProbe(T item)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected string GetDictionaryValue(Dictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!tags.ContainsKey(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return tags[key];
|
||||
}
|
||||
|
||||
protected int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
|
||||
{
|
||||
string val = GetDictionaryValue(tags, key);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
int i;
|
||||
|
||||
if (int.TryParse(val, out i))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
|
||||
{
|
||||
string val = GetDictionaryValue(tags, key);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
DateTime i;
|
||||
|
||||
if (DateTime.TryParse(val, out i))
|
||||
{
|
||||
return i.ToUniversalTime();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
|
||||
{
|
||||
var newDict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (string key in dict.Keys)
|
||||
{
|
||||
newDict[key] = dict[key];
|
||||
}
|
||||
|
||||
return newDict;
|
||||
}
|
||||
}
|
||||
}
|
||||
113
MediaBrowser.Controller/Providers/BaseImageEnhancer.cs
Normal file
113
MediaBrowser.Controller/Providers/BaseImageEnhancer.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BaseImageEnhancer
|
||||
/// </summary>
|
||||
public abstract class BaseImageEnhancer : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Return true only if the given image for the given item will be enhanced by this enhancer.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns>
|
||||
public abstract bool Supports(BaseItem item, ImageType imageType);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority or order in which this enhancer should be run.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public abstract MetadataProviderPriority Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Return the date of the last configuration change affecting the provided baseitem and image type
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <returns>Date of last config change</returns>
|
||||
public virtual DateTime LastConfigurationChange(BaseItem item, ImageType imageType)
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the enhanced image.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="imageIndex">Index of the image.</param>
|
||||
/// <param name="originalImageSize">Size of the original image.</param>
|
||||
/// <returns>ImageSize.</returns>
|
||||
public virtual ImageSize GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageSize originalImageSize)
|
||||
{
|
||||
return originalImageSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enhances the supplied image and returns it
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="originalImage">The original image.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="imageIndex">Index of the image.</param>
|
||||
/// <returns>Task{System.Drawing.Image}.</returns>
|
||||
protected abstract Task<Image> EnhanceImageAsyncInternal(BaseItem item, Image originalImage, ImageType imageType, int imageIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Enhances the image async.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="originalImage">The original image.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="imageIndex">Index of the image.</param>
|
||||
/// <returns>Task{Image}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public async Task<Image> EnhanceImageAsync(BaseItem item, Image originalImage, ImageType imageType, int imageIndex)
|
||||
{
|
||||
if (item == null || originalImage == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
var typeName = GetType().Name;
|
||||
|
||||
Logger.LogDebugInfo("Running {0} for {1}", typeName, item.Path ?? item.Name ?? "--Unknown--");
|
||||
|
||||
try
|
||||
{
|
||||
return await EnhanceImageAsyncInternal(item, originalImage, imageType, imageIndex).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogException("{0} failed enhancing {1}", ex, typeName, item.Name);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,104 +1,408 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
public abstract class BaseMetadataProvider
|
||||
{
|
||||
protected Guid _id;
|
||||
public virtual Guid Id
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_id == null) _id = this.GetType().FullName.GetMD5();
|
||||
return _id;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract bool Supports(BaseEntity item);
|
||||
|
||||
public virtual bool RequiresInternet
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last refresh time of this provider for this item. Providers that care should
|
||||
/// call SetLastRefreshed to update this value.
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual DateTime LastRefreshed(BaseEntity item)
|
||||
{
|
||||
return (item.ProviderData.GetValueOrDefault(this.Id, new BaseProviderInfo())).LastRefreshed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the persisted last refresh date on the item for this provider.
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
/// <param name="value"></param>
|
||||
protected virtual void SetLastRefreshed(BaseEntity item, DateTime value)
|
||||
{
|
||||
var data = item.ProviderData.GetValueOrDefault(this.Id, new BaseProviderInfo());
|
||||
data.LastRefreshed = value;
|
||||
item.ProviderData[this.Id] = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not this provider should be re-fetched. Default functionality can
|
||||
/// compare a provided date with a last refresh time. This can be overridden for more complex
|
||||
/// determinations.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual bool NeedsRefresh(BaseEntity item)
|
||||
{
|
||||
return CompareDate(item) > LastRefreshed(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return the date that should be compared to the last refresh date
|
||||
/// to determine if this provider should be re-fetched.
|
||||
/// </summary>
|
||||
protected virtual DateTime CompareDate(BaseEntity item)
|
||||
{
|
||||
return DateTime.MinValue.AddMinutes(1); // want this to be greater than mindate so new items will refresh
|
||||
}
|
||||
|
||||
public virtual Task FetchIfNeededAsync(BaseEntity item)
|
||||
{
|
||||
if (this.NeedsRefresh(item))
|
||||
return FetchAsync(item, item.ResolveArgs);
|
||||
else
|
||||
return new Task(() => { });
|
||||
}
|
||||
|
||||
public abstract Task FetchAsync(BaseEntity item, ItemResolveEventArgs args);
|
||||
|
||||
public abstract MetadataProviderPriority Priority { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines when a provider should execute, relative to others
|
||||
/// </summary>
|
||||
public enum MetadataProviderPriority
|
||||
{
|
||||
// Run this provider at the beginning
|
||||
First = 1,
|
||||
|
||||
// Run this provider after all first priority providers
|
||||
Second = 2,
|
||||
|
||||
// Run this provider after all second priority providers
|
||||
Third = 3,
|
||||
|
||||
// Run this provider last
|
||||
Last = 4
|
||||
}
|
||||
}
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BaseMetadataProvider
|
||||
/// </summary>
|
||||
public abstract class BaseMetadataProvider : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
/// </summary>
|
||||
/// <value>The logger.</value>
|
||||
protected ILogger Logger { get; private set; }
|
||||
|
||||
// Cache these since they will be used a lot
|
||||
/// <summary>
|
||||
/// The false task result
|
||||
/// </summary>
|
||||
protected static readonly Task<bool> FalseTaskResult = Task.FromResult(false);
|
||||
/// <summary>
|
||||
/// The true task result
|
||||
/// </summary>
|
||||
protected static readonly Task<bool> TrueTaskResult = Task.FromResult(true);
|
||||
|
||||
/// <summary>
|
||||
/// The _id
|
||||
/// </summary>
|
||||
protected Guid _id;
|
||||
/// <summary>
|
||||
/// Gets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
public virtual Guid Id
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_id == Guid.Empty) _id = GetType().FullName.GetMD5();
|
||||
return _id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supportses the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
public abstract bool Supports(BaseItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [requires internet].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
|
||||
public virtual bool RequiresInternet
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the provider version.
|
||||
/// </summary>
|
||||
/// <value>The provider version.</value>
|
||||
protected virtual string ProviderVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [refresh on version change].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
|
||||
protected virtual bool RefreshOnVersionChange
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if this provider is relatively slow and, therefore, should be skipped
|
||||
/// in certain instances. Default is whether or not it requires internet. Can be overridden
|
||||
/// for explicit designation.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is slow; otherwise, <c>false</c>.</value>
|
||||
public virtual bool IsSlow
|
||||
{
|
||||
get { return RequiresInternet; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseMetadataProvider" /> class.
|
||||
/// </summary>
|
||||
protected BaseMetadataProvider()
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this instance.
|
||||
/// </summary>
|
||||
protected virtual void Initialize()
|
||||
{
|
||||
Logger = LogManager.GetLogger(GetType().Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the persisted last refresh date on the item for this provider.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="providerVersion">The provider version.</param>
|
||||
/// <param name="status">The status.</param>
|
||||
/// <exception cref="System.ArgumentNullException">item</exception>
|
||||
protected virtual void SetLastRefreshed(BaseItem item, DateTime value, string providerVersion, ProviderRefreshStatus status = ProviderRefreshStatus.Success)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
var data = item.ProviderData.GetValueOrDefault(Id, new BaseProviderInfo { ProviderId = Id });
|
||||
data.LastRefreshed = value;
|
||||
data.LastRefreshStatus = status;
|
||||
data.ProviderVersion = providerVersion;
|
||||
|
||||
// Save the file system stamp for future comparisons
|
||||
if (RefreshOnFileSystemStampChange)
|
||||
{
|
||||
data.FileSystemStamp = GetCurrentFileSystemStamp(item);
|
||||
}
|
||||
|
||||
item.ProviderData[Id] = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the last refreshed.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="status">The status.</param>
|
||||
protected virtual void SetLastRefreshed(BaseItem item, DateTime value, ProviderRefreshStatus status = ProviderRefreshStatus.Success)
|
||||
{
|
||||
SetLastRefreshed(item, value, ProviderVersion, status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not this provider should be re-fetched. Default functionality can
|
||||
/// compare a provided date with a last refresh time. This can be overridden for more complex
|
||||
/// determinations.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public bool NeedsRefresh(BaseItem item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
var providerInfo = item.ProviderData.GetValueOrDefault(Id, new BaseProviderInfo());
|
||||
|
||||
return NeedsRefreshInternal(item, providerInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Needses the refresh internal.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="providerInfo">The provider info.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
protected virtual bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
if (providerInfo == null)
|
||||
{
|
||||
throw new ArgumentNullException("providerInfo");
|
||||
}
|
||||
|
||||
if (CompareDate(item) > providerInfo.LastRefreshed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (RefreshOnFileSystemStampChange && HasFileSystemStampChanged(item, providerInfo))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (RefreshOnVersionChange && !string.Equals(ProviderVersion, providerInfo.ProviderVersion))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the item's file system stamp has changed from the last time the provider refreshed
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="providerInfo">The provider info.</param>
|
||||
/// <returns><c>true</c> if [has file system stamp changed] [the specified item]; otherwise, <c>false</c>.</returns>
|
||||
protected bool HasFileSystemStampChanged(BaseItem item, BaseProviderInfo providerInfo)
|
||||
{
|
||||
return GetCurrentFileSystemStamp(item) != providerInfo.FileSystemStamp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return the date that should be compared to the last refresh date
|
||||
/// to determine if this provider should be re-fetched.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>DateTime.</returns>
|
||||
protected virtual DateTime CompareDate(BaseItem item)
|
||||
{
|
||||
return DateTime.MinValue.AddMinutes(1); // want this to be greater than mindate so new items will refresh
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public async Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Logger.Info("Running for {0}", item.Path ?? item.Name ?? "--Unknown--");
|
||||
|
||||
// This provides the ability to cancel just this one provider
|
||||
var innerCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
Kernel.Instance.ProviderManager.OnProviderRefreshBeginning(this, item, innerCancellationTokenSource);
|
||||
|
||||
try
|
||||
{
|
||||
var task = FetchAsyncInternal(item, force, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token);
|
||||
|
||||
await task.ConfigureAwait(false);
|
||||
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
// Log the AggregateException
|
||||
if (task.Exception != null)
|
||||
{
|
||||
Logger.ErrorException("AggregateException:", task.Exception);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return task.Result;
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Logger.Info("{0} cancelled for {1}", GetType().Name, item.Name);
|
||||
|
||||
// If the outer cancellation token is the one that caused the cancellation, throw it
|
||||
if (cancellationToken.IsCancellationRequested && ex.CancellationToken == cancellationToken)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("failed refreshing {0}", ex, item.Name);
|
||||
|
||||
SetLastRefreshed(item, DateTime.UtcNow, ProviderRefreshStatus.Failure);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
innerCancellationTokenSource.Dispose();
|
||||
|
||||
Kernel.Instance.ProviderManager.OnProviderRefreshCompleted(this, item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
protected abstract Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public abstract MetadataProviderPriority Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
|
||||
protected virtual bool RefreshOnFileSystemStampChange
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the parent's file system stamp should be used for comparison
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
protected virtual bool UseParentFileSystemStamp(BaseItem item)
|
||||
{
|
||||
// True when the current item is just a file
|
||||
return !item.ResolveArgs.IsDirectory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item's current file system stamp
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>Guid.</returns>
|
||||
private Guid GetCurrentFileSystemStamp(BaseItem item)
|
||||
{
|
||||
if (UseParentFileSystemStamp(item) && item.Parent != null)
|
||||
{
|
||||
return item.Parent.FileSystemStamp;
|
||||
}
|
||||
|
||||
return item.FileSystemStamp;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines when a provider should execute, relative to others
|
||||
/// </summary>
|
||||
public enum MetadataProviderPriority
|
||||
{
|
||||
// Run this provider at the beginning
|
||||
/// <summary>
|
||||
/// The first
|
||||
/// </summary>
|
||||
First = 1,
|
||||
|
||||
// Run this provider after all first priority providers
|
||||
/// <summary>
|
||||
/// The second
|
||||
/// </summary>
|
||||
Second = 2,
|
||||
|
||||
// Run this provider after all second priority providers
|
||||
/// <summary>
|
||||
/// The third
|
||||
/// </summary>
|
||||
Third = 3,
|
||||
|
||||
// Run this provider last
|
||||
/// <summary>
|
||||
/// The last
|
||||
/// </summary>
|
||||
Last = 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
public class BaseProviderInfo
|
||||
{
|
||||
public Guid ProviderId { get; set; }
|
||||
public DateTime LastRefreshed { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BaseProviderInfo
|
||||
/// </summary>
|
||||
public class BaseProviderInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the provider id.
|
||||
/// </summary>
|
||||
/// <value>The provider id.</value>
|
||||
public Guid ProviderId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the last refreshed.
|
||||
/// </summary>
|
||||
/// <value>The last refreshed.</value>
|
||||
public DateTime LastRefreshed { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the file system stamp.
|
||||
/// </summary>
|
||||
/// <value>The file system stamp.</value>
|
||||
public Guid FileSystemStamp { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the last refresh status.
|
||||
/// </summary>
|
||||
/// <value>The last refresh status.</value>
|
||||
public ProviderRefreshStatus LastRefreshStatus { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the provider version.
|
||||
/// </summary>
|
||||
/// <value>The provider version.</value>
|
||||
public string ProviderVersion { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the data hash.
|
||||
/// </summary>
|
||||
/// <value>The data hash.</value>
|
||||
public Guid DataHash { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum ProviderRefreshStatus
|
||||
/// </summary>
|
||||
public enum ProviderRefreshStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The success
|
||||
/// </summary>
|
||||
Success,
|
||||
/// <summary>
|
||||
/// The failure
|
||||
/// </summary>
|
||||
Failure,
|
||||
/// <summary>
|
||||
/// The completed with errors
|
||||
/// </summary>
|
||||
CompletedWithErrors
|
||||
}
|
||||
}
|
||||
|
||||
84
MediaBrowser.Controller/Providers/FanartBaseProvider.cs
Normal file
84
MediaBrowser.Controller/Providers/FanartBaseProvider.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class FanartBaseProvider
|
||||
/// </summary>
|
||||
public abstract class FanartBaseProvider : BaseMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The LOG o_ FILE
|
||||
/// </summary>
|
||||
protected const string LOGO_FILE = "logo.png";
|
||||
/// <summary>
|
||||
/// The AR t_ FILE
|
||||
/// </summary>
|
||||
protected const string ART_FILE = "clearart.png";
|
||||
/// <summary>
|
||||
/// The THUM b_ FILE
|
||||
/// </summary>
|
||||
protected const string THUMB_FILE = "thumb.jpg";
|
||||
/// <summary>
|
||||
/// The DIS c_ FILE
|
||||
/// </summary>
|
||||
protected const string DISC_FILE = "disc.png";
|
||||
/// <summary>
|
||||
/// The BANNE r_ FILE
|
||||
/// </summary>
|
||||
protected const string BANNER_FILE = "banner.png";
|
||||
|
||||
/// <summary>
|
||||
/// The API key
|
||||
/// </summary>
|
||||
protected const string APIKey = "5c6b04c68e904cfed1e6cbc9a9e683d4";
|
||||
|
||||
/// <summary>
|
||||
/// Needses the refresh internal.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="providerInfo">The provider info.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
|
||||
{
|
||||
if (item.DontFetchMeta) return false;
|
||||
|
||||
return DateTime.UtcNow > (providerInfo.LastRefreshed.AddDays(Kernel.Instance.Configuration.MetadataRefreshDays))
|
||||
&& ShouldFetch(item, providerInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [requires internet].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
|
||||
public override bool RequiresInternet
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.Third; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shoulds the fetch.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="providerInfo">The provider info.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
protected virtual bool ShouldFetch(BaseItem item, BaseProviderInfo providerInfo)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,83 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides metadata for Folders and all subclasses by parsing folder.xml
|
||||
/// </summary>
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class FolderProviderFromXml : BaseMetadataProvider
|
||||
{
|
||||
public override bool Supports(BaseEntity item)
|
||||
{
|
||||
return item is Folder;
|
||||
}
|
||||
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.First; }
|
||||
}
|
||||
|
||||
public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
|
||||
{
|
||||
if (args.ContainsFile("folder.xml"))
|
||||
{
|
||||
await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void Fetch(BaseEntity item, ItemResolveEventArgs args)
|
||||
{
|
||||
new BaseItemXmlParser<Folder>().Fetch(item as Folder, Path.Combine(args.Path, "folder.xml"));
|
||||
}
|
||||
}
|
||||
}
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides metadata for Folders and all subclasses by parsing folder.xml
|
||||
/// </summary>
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class FolderProviderFromXml : BaseMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Supportses the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
public override bool Supports(BaseItem item)
|
||||
{
|
||||
return item is Folder && item.LocationType == LocationType.FileSystem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.First; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return the date that should be compared to the last refresh date
|
||||
/// to determine if this provider should be re-fetched.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>DateTime.</returns>
|
||||
protected override DateTime CompareDate(BaseItem item)
|
||||
{
|
||||
var entry = item.MetaLocation != null ? item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "folder.xml")) : null;
|
||||
return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(() => Fetch(item, cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
private bool Fetch(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var metadataFile = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "folder.xml"));
|
||||
|
||||
if (metadataFile.HasValue)
|
||||
{
|
||||
var path = metadataFile.Value.Path;
|
||||
new BaseItemXmlParser<Folder>().Fetch((Folder)item, path, cancellationToken);
|
||||
SetLastRefreshed(item, DateTime.UtcNow);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,128 +1,231 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides images for all types by looking for standard images - folder, backdrop, logo, etc.
|
||||
/// </summary>
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class ImageFromMediaLocationProvider : BaseMetadataProvider
|
||||
{
|
||||
public override bool Supports(BaseEntity item)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.First; }
|
||||
}
|
||||
|
||||
public override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
|
||||
{
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
var baseItem = item as BaseItem;
|
||||
|
||||
if (baseItem != null)
|
||||
{
|
||||
return Task.Run(() => PopulateBaseItemImages(baseItem, args));
|
||||
}
|
||||
|
||||
return Task.Run(() => PopulateImages(item, args));
|
||||
}
|
||||
|
||||
return Task.FromResult<object>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills in image paths based on files win the folder
|
||||
/// </summary>
|
||||
private void PopulateImages(BaseEntity item, ItemResolveEventArgs args)
|
||||
{
|
||||
for (int i = 0; i < args.FileSystemChildren.Length; i++)
|
||||
{
|
||||
var file = args.FileSystemChildren[i];
|
||||
|
||||
string filePath = file.Path;
|
||||
|
||||
string ext = Path.GetExtension(filePath);
|
||||
|
||||
// Only support png and jpg files
|
||||
if (!ext.EndsWith("png", StringComparison.OrdinalIgnoreCase) && !ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = Path.GetFileNameWithoutExtension(filePath);
|
||||
|
||||
if (name.Equals("folder", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.PrimaryImagePath = filePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills in image paths based on files win the folder
|
||||
/// </summary>
|
||||
private void PopulateBaseItemImages(BaseItem item, ItemResolveEventArgs args)
|
||||
{
|
||||
var backdropFiles = new List<string>();
|
||||
|
||||
for (int i = 0; i < args.FileSystemChildren.Length; i++)
|
||||
{
|
||||
var file = args.FileSystemChildren[i];
|
||||
|
||||
string filePath = file.Path;
|
||||
|
||||
string ext = Path.GetExtension(filePath);
|
||||
|
||||
// Only support png and jpg files
|
||||
if (!ext.EndsWith("png", StringComparison.OrdinalIgnoreCase) && !ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = Path.GetFileNameWithoutExtension(filePath);
|
||||
|
||||
if (name.Equals("folder", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.PrimaryImagePath = filePath;
|
||||
}
|
||||
else if (name.StartsWith("backdrop", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
backdropFiles.Add(filePath);
|
||||
}
|
||||
if (name.Equals("logo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.LogoImagePath = filePath;
|
||||
}
|
||||
if (name.Equals("banner", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.BannerImagePath = filePath;
|
||||
}
|
||||
if (name.Equals("clearart", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.ArtImagePath = filePath;
|
||||
}
|
||||
if (name.Equals("thumb", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.ThumbnailImagePath = filePath;
|
||||
}
|
||||
}
|
||||
|
||||
if (backdropFiles.Count > 0)
|
||||
{
|
||||
item.BackdropImagePaths = backdropFiles;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
using MediaBrowser.Common.Win32;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides images for all types by looking for standard images - folder, backdrop, logo, etc.
|
||||
/// </summary>
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class ImageFromMediaLocationProvider : BaseMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Supportses the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
public override bool Supports(BaseItem item)
|
||||
{
|
||||
return item.ResolveArgs.IsDirectory && item.LocationType == LocationType.FileSystem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.First; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
|
||||
protected override bool RefreshOnFileSystemStampChange
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Make sure current image paths still exist
|
||||
ValidateImages(item);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Make sure current backdrop paths still exist
|
||||
ValidateBackdrops(item);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
PopulateBaseItemImages(item);
|
||||
|
||||
SetLastRefreshed(item, DateTime.UtcNow);
|
||||
return TrueTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that images within the item are still on the file system
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
private void ValidateImages(BaseItem item)
|
||||
{
|
||||
if (item.Images == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
|
||||
var deletedKeys = item.Images.Keys.Where(image =>
|
||||
{
|
||||
var path = item.Images[image];
|
||||
|
||||
return IsInSameDirectory(item, path) && !item.ResolveArgs.GetMetaFileByPath(path).HasValue;
|
||||
}).ToList();
|
||||
|
||||
// Now remove them from the dictionary
|
||||
foreach(var key in deletedKeys)
|
||||
{
|
||||
item.Images.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that backdrops within the item are still on the file system
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
private void ValidateBackdrops(BaseItem item)
|
||||
{
|
||||
if (item.BackdropImagePaths == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
|
||||
var deletedImages = item.BackdropImagePaths.Where(path => IsInSameDirectory(item, path) && !item.ResolveArgs.GetMetaFileByPath(path).HasValue).ToList();
|
||||
|
||||
// Now remove them from the dictionary
|
||||
foreach (var path in deletedImages)
|
||||
{
|
||||
item.BackdropImagePaths.Remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is in same directory] [the specified item].
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns><c>true</c> if [is in same directory] [the specified item]; otherwise, <c>false</c>.</returns>
|
||||
private bool IsInSameDirectory(BaseItem item, string path)
|
||||
{
|
||||
return string.Equals(Path.GetDirectoryName(path), item.Path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="filenameWithoutExtension">The filename without extension.</param>
|
||||
/// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
|
||||
protected virtual WIN32_FIND_DATA? GetImage(BaseItem item, string filenameWithoutExtension)
|
||||
{
|
||||
return item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.ResolveArgs.Path, filenameWithoutExtension + ".png")) ?? item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.ResolveArgs.Path, filenameWithoutExtension + ".jpg"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills in image paths based on files win the folder
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
private void PopulateBaseItemImages(BaseItem item)
|
||||
{
|
||||
var backdropFiles = new List<string>();
|
||||
|
||||
// Primary Image
|
||||
var image = GetImage(item, "folder");
|
||||
|
||||
if (image.HasValue)
|
||||
{
|
||||
item.SetImage(ImageType.Primary, image.Value.Path);
|
||||
}
|
||||
|
||||
// Logo Image
|
||||
image = GetImage(item, "logo");
|
||||
|
||||
if (image.HasValue)
|
||||
{
|
||||
item.SetImage(ImageType.Logo, image.Value.Path);
|
||||
}
|
||||
|
||||
// Banner Image
|
||||
image = GetImage(item, "banner");
|
||||
|
||||
if (image.HasValue)
|
||||
{
|
||||
item.SetImage(ImageType.Banner, image.Value.Path);
|
||||
}
|
||||
|
||||
// Clearart
|
||||
image = GetImage(item, "clearart");
|
||||
|
||||
if (image.HasValue)
|
||||
{
|
||||
item.SetImage(ImageType.Art, image.Value.Path);
|
||||
}
|
||||
|
||||
// Thumbnail Image
|
||||
image = GetImage(item, "thumb");
|
||||
|
||||
if (image.HasValue)
|
||||
{
|
||||
item.SetImage(ImageType.Thumb, image.Value.Path);
|
||||
}
|
||||
|
||||
// Backdrop Image
|
||||
image = GetImage(item, "backdrop");
|
||||
|
||||
if (image.HasValue)
|
||||
{
|
||||
backdropFiles.Add(image.Value.Path);
|
||||
}
|
||||
|
||||
var unfound = 0;
|
||||
for (var i = 1; i <= 20; i++)
|
||||
{
|
||||
// Backdrop Image
|
||||
image = GetImage(item, "backdrop" + i);
|
||||
|
||||
if (image.HasValue)
|
||||
{
|
||||
backdropFiles.Add(image.Value.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
unfound++;
|
||||
|
||||
if (unfound >= 3)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (backdropFiles.Count > 0)
|
||||
{
|
||||
item.BackdropImagePaths = backdropFiles;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
103
MediaBrowser.Controller/Providers/ImagesByNameProvider.cs
Normal file
103
MediaBrowser.Controller/Providers/ImagesByNameProvider.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Win32;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides images for generic types by looking for standard images in the IBN
|
||||
/// </summary>
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class ImagesByNameProvider : ImageFromMediaLocationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Supportses the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
public override bool Supports(BaseItem item)
|
||||
{
|
||||
//only run for these generic types since we are expensive in file i/o
|
||||
return item is IndexFolder || item is BasePluginFolder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get
|
||||
{
|
||||
return MetadataProviderPriority.Last;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [refresh on file system stamp change].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
|
||||
protected override bool RefreshOnFileSystemStampChange
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return the date that should be compared to the last refresh date
|
||||
/// to determine if this provider should be re-fetched.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>DateTime.</returns>
|
||||
protected override DateTime CompareDate(BaseItem item)
|
||||
{
|
||||
// If the IBN location exists return the last modified date of any file in it
|
||||
var location = GetLocation(item);
|
||||
return Directory.Exists(location) ? FileSystem.GetFiles(location).Select(f => f.CreationTimeUtc > f.LastWriteTimeUtc ? f.CreationTimeUtc : f.LastWriteTimeUtc).Max() : DateTime.MinValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The us culture
|
||||
/// </summary>
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the location.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected string GetLocation(BaseItem item)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
|
||||
var name = item.Name ?? string.Empty;
|
||||
name = invalid.Aggregate(name, (current, c) => current.Replace(c.ToString(UsCulture), string.Empty));
|
||||
|
||||
return Path.Combine(Kernel.Instance.ApplicationPaths.GeneralPath, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="filenameWithoutExtension">The filename without extension.</param>
|
||||
/// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
|
||||
protected override WIN32_FIND_DATA? GetImage(BaseItem item, string filenameWithoutExtension)
|
||||
{
|
||||
var location = GetLocation(item);
|
||||
|
||||
var result = FileSystem.GetFileData(Path.Combine(location, filenameWithoutExtension + ".png"));
|
||||
if (!result.HasValue)
|
||||
result = FileSystem.GetFileData(Path.Combine(location, filenameWithoutExtension + ".jpg"));
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides local trailers by checking the trailers subfolder
|
||||
/// </summary>
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class LocalTrailerProvider : BaseMetadataProvider
|
||||
{
|
||||
public override bool Supports(BaseEntity item)
|
||||
{
|
||||
return item is BaseItem;
|
||||
}
|
||||
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.First; }
|
||||
}
|
||||
|
||||
public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
|
||||
{
|
||||
if (args.ContainsFolder("trailers"))
|
||||
{
|
||||
var items = new List<Video>();
|
||||
|
||||
foreach (WIN32_FIND_DATA file in FileData.GetFileSystemEntries(Path.Combine(args.Path, "trailers"), "*"))
|
||||
{
|
||||
var video = await Kernel.Instance.ItemController.GetItem(file.Path, fileInfo: file).ConfigureAwait(false) as Video;
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
items.Add(video);
|
||||
}
|
||||
}
|
||||
|
||||
(item as BaseItem).LocalTrailers = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
265
MediaBrowser.Controller/Providers/MediaInfo/BDInfoProvider.cs
Normal file
265
MediaBrowser.Controller/Providers/MediaInfo/BDInfoProvider.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using BDInfo;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.MediaInfo;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts dvd information using VgtMpeg
|
||||
/// </summary>
|
||||
internal static class BDInfoProvider
|
||||
{
|
||||
internal static void FetchBdInfo(BaseItem item, string inputPath, FileSystemRepository bdInfoCache, CancellationToken cancellationToken)
|
||||
{
|
||||
var video = (Video)item;
|
||||
|
||||
// Get the path to the cache file
|
||||
var cacheName = item.Id + "_" + item.DateModified.Ticks;
|
||||
|
||||
var cacheFile = bdInfoCache.GetResourcePath(cacheName, ".pb");
|
||||
|
||||
BDInfoResult result;
|
||||
|
||||
try
|
||||
{
|
||||
result = Kernel.Instance.ProtobufSerializer.DeserializeFromFile<BDInfoResult>(cacheFile);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
result = GetBDInfo(inputPath);
|
||||
|
||||
Kernel.Instance.ProtobufSerializer.SerializeToFile(result, cacheFile);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
int? currentHeight = null;
|
||||
int? currentWidth = null;
|
||||
int? currentBitRate = null;
|
||||
|
||||
var videoStream = video.MediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
|
||||
|
||||
// Grab the values that ffprobe recorded
|
||||
if (videoStream != null)
|
||||
{
|
||||
currentBitRate = videoStream.BitRate;
|
||||
currentWidth = videoStream.Width;
|
||||
currentHeight = videoStream.Height;
|
||||
}
|
||||
|
||||
// Fill video properties from the BDInfo result
|
||||
Fetch(video, inputPath, result);
|
||||
|
||||
videoStream = video.MediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
|
||||
|
||||
// Use the ffprobe values if these are empty
|
||||
if (videoStream != null)
|
||||
{
|
||||
videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate;
|
||||
videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width;
|
||||
videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified num is empty.
|
||||
/// </summary>
|
||||
/// <param name="num">The num.</param>
|
||||
/// <returns><c>true</c> if the specified num is empty; otherwise, <c>false</c>.</returns>
|
||||
private static bool IsEmpty(int? num)
|
||||
{
|
||||
return !num.HasValue || num.Value == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills video properties from the VideoStream of the largest playlist
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
/// <param name="inputPath">The input path.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
private static void Fetch(Video video, string inputPath, BDInfoResult stream)
|
||||
{
|
||||
// Check all input for null/empty/zero
|
||||
|
||||
video.MediaStreams = stream.MediaStreams;
|
||||
|
||||
if (stream.RunTimeTicks.HasValue && stream.RunTimeTicks.Value > 0)
|
||||
{
|
||||
video.RunTimeTicks = stream.RunTimeTicks;
|
||||
}
|
||||
|
||||
video.PlayableStreamFileNames = stream.Files.ToList();
|
||||
|
||||
if (stream.Chapters != null)
|
||||
{
|
||||
video.Chapters = stream.Chapters.Select(c => new ChapterInfo
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(c).Ticks
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets information about the longest playlist on a bdrom
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>VideoStream.</returns>
|
||||
private static BDInfoResult GetBDInfo(string path)
|
||||
{
|
||||
var bdrom = new BDROM(path);
|
||||
|
||||
bdrom.Scan();
|
||||
|
||||
// Get the longest playlist
|
||||
var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid);
|
||||
|
||||
var outputStream = new BDInfoResult
|
||||
{
|
||||
MediaStreams = new List<MediaStream>()
|
||||
};
|
||||
|
||||
if (playlist == null)
|
||||
{
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
outputStream.Chapters = playlist.Chapters;
|
||||
|
||||
outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
|
||||
|
||||
var mediaStreams = new List<MediaStream> {};
|
||||
|
||||
foreach (var stream in playlist.SortedStreams)
|
||||
{
|
||||
var videoStream = stream as TSVideoStream;
|
||||
|
||||
if (videoStream != null)
|
||||
{
|
||||
AddVideoStream(mediaStreams, videoStream);
|
||||
continue;
|
||||
}
|
||||
|
||||
var audioStream = stream as TSAudioStream;
|
||||
|
||||
if (audioStream != null)
|
||||
{
|
||||
AddAudioStream(mediaStreams, audioStream);
|
||||
continue;
|
||||
}
|
||||
|
||||
var textStream = stream as TSTextStream;
|
||||
|
||||
if (textStream != null)
|
||||
{
|
||||
AddSubtitleStream(mediaStreams, textStream);
|
||||
continue;
|
||||
}
|
||||
|
||||
var graphicsStream = stream as TSGraphicsStream;
|
||||
|
||||
if (graphicsStream != null)
|
||||
{
|
||||
AddSubtitleStream(mediaStreams, graphicsStream);
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.MediaStreams = mediaStreams;
|
||||
|
||||
if (playlist.StreamClips != null && playlist.StreamClips.Any())
|
||||
{
|
||||
// Get the files in the playlist
|
||||
outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToList();
|
||||
}
|
||||
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the video stream.
|
||||
/// </summary>
|
||||
/// <param name="streams">The streams.</param>
|
||||
/// <param name="videoStream">The video stream.</param>
|
||||
private static void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream)
|
||||
{
|
||||
var mediaStream = new MediaStream
|
||||
{
|
||||
BitRate = Convert.ToInt32(videoStream.BitRate),
|
||||
Width = videoStream.Width,
|
||||
Height = videoStream.Height,
|
||||
Codec = videoStream.CodecShortName,
|
||||
ScanType = videoStream.IsInterlaced ? "interlaced" : "progressive",
|
||||
Type = MediaStreamType.Video,
|
||||
Index = streams.Count
|
||||
};
|
||||
|
||||
if (videoStream.FrameRateDenominator > 0)
|
||||
{
|
||||
float frameRateEnumerator = videoStream.FrameRateEnumerator;
|
||||
float frameRateDenominator = videoStream.FrameRateDenominator;
|
||||
|
||||
mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator;
|
||||
}
|
||||
|
||||
streams.Add(mediaStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="streams">The streams.</param>
|
||||
/// <param name="audioStream">The audio stream.</param>
|
||||
private static void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream)
|
||||
{
|
||||
streams.Add(new MediaStream
|
||||
{
|
||||
BitRate = Convert.ToInt32(audioStream.BitRate),
|
||||
Codec = audioStream.CodecShortName,
|
||||
Language = audioStream.LanguageCode,
|
||||
Channels = audioStream.ChannelCount,
|
||||
SampleRate = audioStream.SampleRate,
|
||||
Type = MediaStreamType.Audio,
|
||||
Index = streams.Count
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the subtitle stream.
|
||||
/// </summary>
|
||||
/// <param name="streams">The streams.</param>
|
||||
/// <param name="textStream">The text stream.</param>
|
||||
private static void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream)
|
||||
{
|
||||
streams.Add(new MediaStream
|
||||
{
|
||||
Language = textStream.LanguageCode,
|
||||
Codec = textStream.CodecShortName,
|
||||
Type = MediaStreamType.Subtitle,
|
||||
Index = streams.Count
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the subtitle stream.
|
||||
/// </summary>
|
||||
/// <param name="streams">The streams.</param>
|
||||
/// <param name="textStream">The text stream.</param>
|
||||
private static void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream)
|
||||
{
|
||||
streams.Add(new MediaStream
|
||||
{
|
||||
Language = textStream.LanguageCode,
|
||||
Codec = textStream.CodecShortName,
|
||||
Type = MediaStreamType.Subtitle,
|
||||
Index = streams.Count
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
public abstract class BaseFFMpegImageProvider<T> : BaseFFMpegProvider<T>
|
||||
where T : BaseItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.Last; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BaseFFMpegProvider
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public abstract class BaseFFMpegProvider<T> : BaseMetadataProvider
|
||||
where T : BaseItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Supportses the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
public override bool Supports(BaseItem item)
|
||||
{
|
||||
return item.LocationType == LocationType.FileSystem && item is T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return the date that should be compared to the last refresh date
|
||||
/// to determine if this provider should be re-fetched.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>DateTime.</returns>
|
||||
protected override DateTime CompareDate(BaseItem item)
|
||||
{
|
||||
return item.DateModified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The null mount task result
|
||||
/// </summary>
|
||||
protected readonly Task<IIsoMount> NullMountTaskResult = Task.FromResult<IIsoMount>(null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the provider version.
|
||||
/// </summary>
|
||||
/// <value>The provider version.</value>
|
||||
protected override string ProviderVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
return Kernel.Instance.FFMpegManager.FFMpegVersion;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Needses the refresh internal.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="providerInfo">The provider info.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
|
||||
{
|
||||
// If the last run wasn't successful, try again when there's a new version of ffmpeg
|
||||
if (providerInfo.LastRefreshStatus != ProviderRefreshStatus.Success)
|
||||
{
|
||||
if (!string.Equals(ProviderVersion, providerInfo.ProviderVersion))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return base.NeedsRefreshInternal(item, providerInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.MediaInfo;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a base class for extracting media information through ffprobe
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public abstract class BaseFFProbeProvider<T> : BaseFFMpegProvider<T>
|
||||
where T : BaseItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the FF probe cache.
|
||||
/// </summary>
|
||||
/// <value>The FF probe cache.</value>
|
||||
protected FileSystemRepository FFProbeCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this instance.
|
||||
/// </summary>
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
FFProbeCache = new FileSystemRepository(Path.Combine(Kernel.Instance.ApplicationPaths.CachePath, CacheDirectoryName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the cache directory.
|
||||
/// </summary>
|
||||
/// <value>The name of the cache directory.</value>
|
||||
protected virtual string CacheDirectoryName
|
||||
{
|
||||
get
|
||||
{
|
||||
return "ffmpeg-video-info";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
// Give this second priority
|
||||
// Give metadata xml providers a chance to fill in data first, so that we can skip this whenever possible
|
||||
get { return MetadataProviderPriority.Second; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
|
||||
{
|
||||
var myItem = (T)item;
|
||||
|
||||
var isoMount = await MountIsoIfNeeded(myItem, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
OnPreFetch(myItem, isoMount);
|
||||
|
||||
var inputPath = isoMount == null ?
|
||||
Kernel.Instance.FFMpegManager.GetInputArgument(myItem) :
|
||||
Kernel.Instance.FFMpegManager.GetInputArgument((Video)item, isoMount);
|
||||
|
||||
var result = await Kernel.Instance.FFMpegManager.RunFFProbe(item, inputPath, item.DateModified, FFProbeCache, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
NormalizeFFProbeResult(result);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await Fetch(myItem, cancellationToken, result, isoMount).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
SetLastRefreshed(item, DateTime.UtcNow);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (isoMount != null)
|
||||
{
|
||||
isoMount.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [refresh on version change].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
|
||||
protected override bool RefreshOnVersionChange
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mounts the iso if needed.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>IsoMount.</returns>
|
||||
protected virtual Task<IIsoMount> MountIsoIfNeeded(T item, CancellationToken cancellationToken)
|
||||
{
|
||||
return NullMountTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when [pre fetch].
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="mount">The mount.</param>
|
||||
protected virtual void OnPreFetch(T item, IIsoMount mount)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the FF probe result.
|
||||
/// </summary>
|
||||
/// <param name="result">The result.</param>
|
||||
private void NormalizeFFProbeResult(FFProbeResult result)
|
||||
{
|
||||
if (result.format != null && result.format.tags != null)
|
||||
{
|
||||
result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
|
||||
}
|
||||
|
||||
if (result.streams != null)
|
||||
{
|
||||
// Convert all dictionaries to case insensitive
|
||||
foreach (var stream in result.streams)
|
||||
{
|
||||
if (stream.tags != null)
|
||||
{
|
||||
stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
|
||||
}
|
||||
|
||||
if (stream.disposition != null)
|
||||
{
|
||||
stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subclasses must set item values using this
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <param name="isoMount">The iso mount.</param>
|
||||
/// <returns>Task.</returns>
|
||||
protected abstract Task Fetch(T item, CancellationToken cancellationToken, FFProbeResult result, IIsoMount isoMount);
|
||||
|
||||
/// <summary>
|
||||
/// Converts ffprobe stream info to our MediaStream class
|
||||
/// </summary>
|
||||
/// <param name="streamInfo">The stream info.</param>
|
||||
/// <param name="formatInfo">The format info.</param>
|
||||
/// <returns>MediaStream.</returns>
|
||||
protected MediaStream GetMediaStream(FFProbeMediaStreamInfo streamInfo, FFProbeMediaFormatInfo formatInfo)
|
||||
{
|
||||
var stream = new MediaStream
|
||||
{
|
||||
Codec = streamInfo.codec_name,
|
||||
Language = GetDictionaryValue(streamInfo.tags, "language"),
|
||||
Profile = streamInfo.profile,
|
||||
Index = streamInfo.index
|
||||
};
|
||||
|
||||
if (streamInfo.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Audio;
|
||||
|
||||
stream.Channels = streamInfo.channels;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.sample_rate))
|
||||
{
|
||||
stream.SampleRate = int.Parse(streamInfo.sample_rate);
|
||||
}
|
||||
}
|
||||
else if (streamInfo.codec_type.Equals("subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Subtitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
stream.Type = MediaStreamType.Video;
|
||||
|
||||
stream.Width = streamInfo.width;
|
||||
stream.Height = streamInfo.height;
|
||||
stream.AspectRatio = streamInfo.display_aspect_ratio;
|
||||
|
||||
stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
|
||||
stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
|
||||
}
|
||||
|
||||
// Get stream bitrate
|
||||
if (stream.Type != MediaStreamType.Subtitle)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(streamInfo.bit_rate))
|
||||
{
|
||||
stream.BitRate = int.Parse(streamInfo.bit_rate);
|
||||
}
|
||||
else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate))
|
||||
{
|
||||
// If the stream info doesn't have a bitrate get the value from the media format info
|
||||
stream.BitRate = int.Parse(formatInfo.bit_rate);
|
||||
}
|
||||
}
|
||||
|
||||
if (streamInfo.disposition != null)
|
||||
{
|
||||
var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
|
||||
var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
|
||||
|
||||
stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a frame rate from a string value in ffprobe output
|
||||
/// This could be a number or in the format of 2997/125.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>System.Nullable{System.Single}.</returns>
|
||||
private float? GetFrameRate(string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
var parts = value.Split('/');
|
||||
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
return float.Parse(parts[0]) / float.Parse(parts[1]);
|
||||
}
|
||||
return float.Parse(parts[0]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string from an FFProbeResult tags dictionary
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected string GetDictionaryValue(Dictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string val;
|
||||
|
||||
tags.TryGetValue(key, out val);
|
||||
return val;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an int from an FFProbeResult tags dictionary
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
protected int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
|
||||
{
|
||||
var val = GetDictionaryValue(tags, key);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
int i;
|
||||
|
||||
if (int.TryParse(val, out i))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a DateTime from an FFProbeResult tags dictionary
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.Nullable{DateTime}.</returns>
|
||||
protected DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
|
||||
{
|
||||
var val = GetDictionaryValue(tags, key);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
DateTime i;
|
||||
|
||||
if (DateTime.TryParse(val, out i))
|
||||
{
|
||||
return i.ToUniversalTime();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a dictionary to case insensitive
|
||||
/// </summary>
|
||||
/// <param name="dict">The dict.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
|
||||
{
|
||||
return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected override void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
FFProbeCache.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(dispose);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses ffmpeg to create video images
|
||||
/// </summary>
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class FFMpegAudioImageProvider : BaseFFMpegImageProvider<Audio>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
|
||||
{
|
||||
var audio = (Audio)item;
|
||||
|
||||
if (string.IsNullOrEmpty(audio.PrimaryImagePath))
|
||||
{
|
||||
// First try to use the parent's image
|
||||
audio.PrimaryImagePath = audio.ResolveArgs.Parent.PrimaryImagePath;
|
||||
|
||||
// If it's still empty see if there's an embedded image
|
||||
if (string.IsNullOrEmpty(audio.PrimaryImagePath))
|
||||
{
|
||||
if (audio.MediaStreams != null && audio.MediaStreams.Any(s => s.Type == MediaStreamType.Video))
|
||||
{
|
||||
var filename = item.Id + "_" + item.DateModified.Ticks + "_primary";
|
||||
|
||||
var path = Kernel.Instance.FFMpegManager.AudioImageCache.GetResourcePath(filename, ".jpg");
|
||||
|
||||
if (!Kernel.Instance.FFMpegManager.AudioImageCache.ContainsFilePath(path))
|
||||
{
|
||||
return ExtractImage(audio, path, cancellationToken);
|
||||
}
|
||||
|
||||
// Image is already in the cache
|
||||
audio.PrimaryImagePath = path;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
SetLastRefreshed(item, DateTime.UtcNow);
|
||||
return TrueTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the image.
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
private async Task<bool> ExtractImage(Audio audio, string path, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await Kernel.Instance.FFMpegManager.ExtractImage(audio, path, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (success)
|
||||
{
|
||||
audio.PrimaryImagePath = path;
|
||||
SetLastRefreshed(audio, DateTime.UtcNow);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLastRefreshed(audio, DateTime.UtcNow, ProviderRefreshStatus.Failure);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses ffmpeg to create video images
|
||||
/// </summary>
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class FFMpegVideoImageProvider : BaseFFMpegImageProvider<Video>
|
||||
{
|
||||
/// <summary>
|
||||
/// Supportses the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
public override bool Supports(BaseItem item)
|
||||
{
|
||||
if (item.LocationType != LocationType.FileSystem)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var video = item as Video;
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
if (video.VideoType == VideoType.Iso && video.IsoType.HasValue && Kernel.Instance.IsoManager.CanMount(item.Path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// We can only extract images from folder rips if we know the largest stream path
|
||||
return video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.PrimaryImagePath))
|
||||
{
|
||||
var video = (Video)item;
|
||||
|
||||
var filename = item.Id + "_" + item.DateModified.Ticks + "_primary";
|
||||
|
||||
var path = Kernel.Instance.FFMpegManager.VideoImageCache.GetResourcePath(filename, ".jpg");
|
||||
|
||||
if (!Kernel.Instance.FFMpegManager.VideoImageCache.ContainsFilePath(path))
|
||||
{
|
||||
return ExtractImage(video, path, cancellationToken);
|
||||
}
|
||||
|
||||
// Image is already in the cache
|
||||
item.PrimaryImagePath = path;
|
||||
}
|
||||
|
||||
SetLastRefreshed(item, DateTime.UtcNow);
|
||||
return TrueTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mounts the iso if needed.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>IsoMount.</returns>
|
||||
protected Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item.VideoType == VideoType.Iso)
|
||||
{
|
||||
return Kernel.Instance.IsoManager.Mount(item.Path, cancellationToken);
|
||||
}
|
||||
|
||||
return NullMountTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the image.
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
private async Task<bool> ExtractImage(Video video, string path, CancellationToken cancellationToken)
|
||||
{
|
||||
var isoMount = await MountIsoIfNeeded(video, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
// If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
|
||||
// Always use 10 seconds for dvd because our duration could be out of whack
|
||||
var imageOffset = video.VideoType != VideoType.Dvd && video.RunTimeTicks.HasValue && video.RunTimeTicks.Value > 0
|
||||
? TimeSpan.FromTicks(Convert.ToInt64(video.RunTimeTicks.Value * .1))
|
||||
: TimeSpan.FromSeconds(10);
|
||||
|
||||
var inputPath = isoMount == null ?
|
||||
Kernel.Instance.FFMpegManager.GetInputArgument(video) :
|
||||
Kernel.Instance.FFMpegManager.GetInputArgument(video, isoMount);
|
||||
|
||||
var success = await Kernel.Instance.FFMpegManager.ExtractImage(inputPath, imageOffset, path, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (success)
|
||||
{
|
||||
video.PrimaryImagePath = path;
|
||||
SetLastRefreshed(video, DateTime.UtcNow);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLastRefreshed(video, DateTime.UtcNow, ProviderRefreshStatus.Failure);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (isoMount != null)
|
||||
{
|
||||
isoMount.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Logging;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.MediaInfo;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts audio information using ffprobe
|
||||
/// </summary>
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class FFProbeAudioInfoProvider : BaseFFProbeProvider<Audio>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the cache directory.
|
||||
/// </summary>
|
||||
/// <value>The name of the cache directory.</value>
|
||||
protected override string CacheDirectoryName
|
||||
{
|
||||
get
|
||||
{
|
||||
return "ffmpeg-audio-info";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the specified audio.
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="isoMount">The iso mount.</param>
|
||||
/// <returns>Task.</returns>
|
||||
protected override Task Fetch(Audio audio, CancellationToken cancellationToken, FFProbeResult data, IIsoMount isoMount)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
if (data.streams == null)
|
||||
{
|
||||
Logger.Error("Audio item has no streams: " + audio.Path);
|
||||
return;
|
||||
}
|
||||
|
||||
audio.MediaStreams = data.streams.Select(s => GetMediaStream(s, data.format)).ToList();
|
||||
|
||||
// Get the first audio stream
|
||||
var stream = data.streams.First(s => s.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Get duration from stream properties
|
||||
var duration = stream.duration;
|
||||
|
||||
// If it's not there go into format properties
|
||||
if (string.IsNullOrEmpty(duration))
|
||||
{
|
||||
duration = data.format.duration;
|
||||
}
|
||||
|
||||
// If we got something, parse it
|
||||
if (!string.IsNullOrEmpty(duration))
|
||||
{
|
||||
audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration)).Ticks;
|
||||
}
|
||||
|
||||
if (data.format.tags != null)
|
||||
{
|
||||
FetchDataFromTags(audio, data.format.tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches data from the tags dictionary
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
|
||||
{
|
||||
var title = GetDictionaryValue(tags, "title");
|
||||
|
||||
// Only set Name if title was found in the dictionary
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
audio.Name = title;
|
||||
}
|
||||
|
||||
var composer = GetDictionaryValue(tags, "composer");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(composer))
|
||||
{
|
||||
// Only use the comma as a delimeter if there are no slashes or pipes.
|
||||
// We want to be careful not to split names that have commas in them
|
||||
var delimeter = composer.IndexOf('/') == -1 && composer.IndexOf('|') == -1 ? new[] { ',' } : new[] { '/', '|' };
|
||||
|
||||
foreach (var person in composer.Split(delimeter, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var name = person.Trim();
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
audio.AddPerson(new PersonInfo { Name = name, Type = PersonType.Composer });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audio.Album = GetDictionaryValue(tags, "album");
|
||||
audio.Artist = GetDictionaryValue(tags, "artist");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(audio.Artist))
|
||||
{
|
||||
// Add to people too
|
||||
audio.AddPerson(new PersonInfo {Name = audio.Artist, Type = PersonType.MusicArtist});
|
||||
}
|
||||
|
||||
// Several different forms of albumartist
|
||||
audio.AlbumArtist = GetDictionaryValue(tags, "albumartist") ?? GetDictionaryValue(tags, "album artist") ?? GetDictionaryValue(tags, "album_artist");
|
||||
|
||||
// Track number
|
||||
audio.IndexNumber = GetDictionaryNumericValue(tags, "track");
|
||||
|
||||
// Disc number
|
||||
audio.ParentIndexNumber = GetDictionaryDiscValue(tags);
|
||||
|
||||
audio.Language = GetDictionaryValue(tags, "language");
|
||||
|
||||
audio.ProductionYear = GetDictionaryNumericValue(tags, "date");
|
||||
|
||||
// Several different forms of retaildate
|
||||
audio.PremiereDate = GetDictionaryDateTime(tags, "retaildate") ?? GetDictionaryDateTime(tags, "retail date") ?? GetDictionaryDateTime(tags, "retail_date");
|
||||
|
||||
// If we don't have a ProductionYear try and get it from PremiereDate
|
||||
if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
|
||||
{
|
||||
audio.ProductionYear = audio.PremiereDate.Value.Year;
|
||||
}
|
||||
|
||||
FetchGenres(audio, tags);
|
||||
|
||||
// There's several values in tags may or may not be present
|
||||
FetchStudios(audio, tags, "organization");
|
||||
FetchStudios(audio, tags, "ensemble");
|
||||
FetchStudios(audio, tags, "publisher");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the studios from the tags collection
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="tagName">Name of the tag.</param>
|
||||
private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
|
||||
{
|
||||
var val = GetDictionaryValue(tags, tagName);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
audio.AddStudios(val.Split(new[] { '/', '|' }, StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the genres from the tags collection
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
private void FetchGenres(Audio audio, Dictionary<string, string> tags)
|
||||
{
|
||||
var val = GetDictionaryValue(tags, "genre");
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
audio.AddGenres(val.Split(new[] { '/', '|' }, StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private int? GetDictionaryDiscValue(Dictionary<string, string> tags)
|
||||
{
|
||||
var disc = GetDictionaryValue(tags, "disc");
|
||||
|
||||
if (!string.IsNullOrEmpty(disc))
|
||||
{
|
||||
disc = disc.Split('/')[0];
|
||||
|
||||
int num;
|
||||
|
||||
if (int.TryParse(disc, out num))
|
||||
{
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.MediaInfo;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts video information using ffprobe
|
||||
/// </summary>
|
||||
[Export(typeof(BaseMetadataProvider))]
|
||||
public class FFProbeVideoInfoProvider : BaseFFProbeProvider<Video>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the bd info cache.
|
||||
/// </summary>
|
||||
/// <value>The bd info cache.</value>
|
||||
private FileSystemRepository BdInfoCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FFProbeVideoInfoProvider" /> class.
|
||||
/// </summary>
|
||||
public FFProbeVideoInfoProvider()
|
||||
: base()
|
||||
{
|
||||
BdInfoCache = new FileSystemRepository(Path.Combine(Kernel.Instance.ApplicationPaths.CachePath, "bdinfo"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
|
||||
protected override bool RefreshOnFileSystemStampChange
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supports video files and dvd structures
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
public override bool Supports(BaseItem item)
|
||||
{
|
||||
var video = item as Video;
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
if (video.VideoType == VideoType.Iso)
|
||||
{
|
||||
return Kernel.Instance.IsoManager.CanMount(item.Path);
|
||||
}
|
||||
|
||||
return video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Dvd || video.VideoType == VideoType.BluRay;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when [pre fetch].
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="mount">The mount.</param>
|
||||
protected override void OnPreFetch(Video item, IIsoMount mount)
|
||||
{
|
||||
if (item.VideoType == VideoType.Iso)
|
||||
{
|
||||
item.IsoType = DetermineIsoType(mount);
|
||||
}
|
||||
|
||||
if (item.VideoType == VideoType.Dvd || (item.IsoType.HasValue && item.IsoType == IsoType.Dvd))
|
||||
{
|
||||
PopulateDvdStreamFiles(item, mount);
|
||||
}
|
||||
|
||||
base.OnPreFetch(item, mount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mounts the iso if needed.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>IsoMount.</returns>
|
||||
protected override Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item.VideoType == VideoType.Iso)
|
||||
{
|
||||
return Kernel.Instance.IsoManager.Mount(item.Path, cancellationToken);
|
||||
}
|
||||
|
||||
return base.MountIsoIfNeeded(item, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the type of the iso.
|
||||
/// </summary>
|
||||
/// <param name="isoMount">The iso mount.</param>
|
||||
/// <returns>System.Nullable{IsoType}.</returns>
|
||||
private IsoType? DetermineIsoType(IIsoMount isoMount)
|
||||
{
|
||||
var folders = Directory.EnumerateDirectories(isoMount.MountedPath).Select(Path.GetFileName).ToList();
|
||||
|
||||
if (folders.Contains("video_ts", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return IsoType.Dvd;
|
||||
}
|
||||
if (folders.Contains("bdmv", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return IsoType.BluRay;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds vob files and populates the dvd stream file properties
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
/// <param name="isoMount">The iso mount.</param>
|
||||
private void PopulateDvdStreamFiles(Video video, IIsoMount isoMount)
|
||||
{
|
||||
// min size 300 mb
|
||||
const long minPlayableSize = 314572800;
|
||||
|
||||
var root = isoMount != null ? isoMount.MountedPath : video.Path;
|
||||
|
||||
// Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size
|
||||
// Once we reach a file that is at least the minimum, return all subsequent ones
|
||||
video.PlayableStreamFileNames = Directory.EnumerateFiles(root, "*.vob", SearchOption.AllDirectories).SkipWhile(f => new FileInfo(f).Length < minPlayableSize).Select(Path.GetFileName).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the specified video.
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="isoMount">The iso mount.</param>
|
||||
/// <returns>Task.</returns>
|
||||
protected override Task Fetch(Video video, CancellationToken cancellationToken, FFProbeResult data, IIsoMount isoMount)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
if (data.format != null)
|
||||
{
|
||||
// For dvd's this may not always be accurate, so don't set the runtime if the item already has one
|
||||
var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks == null || video.RunTimeTicks.Value == 0;
|
||||
|
||||
if (needToSetRuntime && !string.IsNullOrEmpty(data.format.duration))
|
||||
{
|
||||
video.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration)).Ticks;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.streams != null)
|
||||
{
|
||||
video.MediaStreams = data.streams.Select(s => GetMediaStream(s, data.format)).ToList();
|
||||
}
|
||||
|
||||
if (data.Chapters != null)
|
||||
{
|
||||
video.Chapters = data.Chapters;
|
||||
}
|
||||
|
||||
if (video.Chapters == null || video.Chapters.Count == 0)
|
||||
{
|
||||
AddDummyChapters(video);
|
||||
}
|
||||
|
||||
if (video.VideoType == VideoType.BluRay || (video.IsoType.HasValue && video.IsoType.Value == IsoType.BluRay))
|
||||
{
|
||||
var inputPath = isoMount != null ? isoMount.MountedPath : video.Path;
|
||||
BDInfoProvider.FetchBdInfo(video, inputPath, BdInfoCache, cancellationToken);
|
||||
}
|
||||
|
||||
AddExternalSubtitles(video);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the external subtitles.
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
private void AddExternalSubtitles(Video video)
|
||||
{
|
||||
var useParent = (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso) && !(video is Movie);
|
||||
|
||||
if (useParent && video.Parent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fileSystemChildren = useParent
|
||||
? video.Parent.ResolveArgs.FileSystemChildren
|
||||
: video.ResolveArgs.FileSystemChildren;
|
||||
|
||||
var startIndex = video.MediaStreams == null ? 0 : video.MediaStreams.Count;
|
||||
var streams = new List<MediaStream>();
|
||||
|
||||
foreach (var file in fileSystemChildren.Where(f => !f.IsDirectory))
|
||||
{
|
||||
var extension = Path.GetExtension(file.Path);
|
||||
|
||||
if (string.Equals(extension, ".srt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
streams.Add(new MediaStream
|
||||
{
|
||||
Index = startIndex,
|
||||
Type = MediaStreamType.Subtitle,
|
||||
IsExternal = true,
|
||||
Path = file.Path,
|
||||
Codec = "srt"
|
||||
});
|
||||
|
||||
startIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (video.MediaStreams == null)
|
||||
{
|
||||
video.MediaStreams = new List<MediaStream>();
|
||||
}
|
||||
video.MediaStreams.AddRange(streams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The dummy chapter duration
|
||||
/// </summary>
|
||||
private static readonly long DummyChapterDuration = TimeSpan.FromMinutes(10).Ticks;
|
||||
|
||||
/// <summary>
|
||||
/// Adds the dummy chapters.
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
private void AddDummyChapters(Video video)
|
||||
{
|
||||
var runtime = video.RunTimeTicks ?? 0;
|
||||
|
||||
if (runtime < DummyChapterDuration)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long currentChapterTicks = 0;
|
||||
var index = 1;
|
||||
|
||||
var chapters = new List<ChapterInfo> { };
|
||||
|
||||
while (currentChapterTicks < runtime)
|
||||
{
|
||||
chapters.Add(new ChapterInfo
|
||||
{
|
||||
Name = "Chapter " + index,
|
||||
StartPositionTicks = currentChapterTicks
|
||||
});
|
||||
|
||||
index++;
|
||||
currentChapterTicks += DummyChapterDuration;
|
||||
}
|
||||
|
||||
video.Chapters = chapters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected override void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
BdInfoCache.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(dispose);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user