Pushing missing changes

This commit is contained in:
LukePulverenti
2013-02-20 20:33:05 -05:00
parent 845554722e
commit 767cdc1f6f
924 changed files with 103121 additions and 18677 deletions

View File

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

View 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);
}
}
}

View File

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

View 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);
}
}
}

View File

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

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

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

View File

@@ -0,0 +1,10 @@

namespace MediaBrowser.Controller.Entities.Audio
{
/// <summary>
/// Class MusicArtist
/// </summary>
public class MusicArtist : Folder
{
}
}

View File

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

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

View 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

View File

@@ -1,7 +1,10 @@

namespace MediaBrowser.Controller.Entities
{
public class Genre : BaseEntity
{
}
}

namespace MediaBrowser.Controller.Entities
{
/// <summary>
/// Class Genre
/// </summary>
public class Genre : BaseItem
{
}
}

View 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
{
}
}

View 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;
});
}
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,10 @@

namespace MediaBrowser.Controller.Entities
{
public class Studio : BaseEntity
{
}
}

namespace MediaBrowser.Controller.Entities
{
/// <summary>
/// Class Studio
/// </summary>
public class Studio : BaseItem
{
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View 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);
}
}
}

View File

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

View File

@@ -1,7 +1,10 @@

namespace MediaBrowser.Controller.Entities
{
public class Year : BaseEntity
{
}
}

namespace MediaBrowser.Controller.Entities
{
/// <summary>
/// Class Year
/// </summary>
public class Year : BaseItem
{
}
}

View File

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

View File

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

View File

@@ -1 +0,0 @@
84ac1c51e84cfbfb20e7b96c9f1a4442a8cfadf2

View File

@@ -1 +0,0 @@
331e241e29f1b015e303b301c17c37883e39f39d

View File

@@ -1,3 +0,0 @@
This is the 32-bit static build of ffmpeg, located at:
http://ffmpeg.zeranoe.com/builds/

View File

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

View File

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

View File

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

View 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);
}
}
}

View File

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

View File

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

View File

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

View 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();
}
}
}

View File

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

View 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
}
}

View File

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

View 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()
});
}
}
}

View 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
}
}

View 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();
}
}
}
}

View 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);
}
}
}

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

View 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);
}
}
}

View 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";
}
}

View 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);
}
}
}

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

View 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);
}
}
}

View 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);
//}
}
}

View 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);
}
}

View 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);
}
}
}

View File

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

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

File diff suppressed because it is too large Load Diff

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

View File

@@ -0,0 +1 @@
985770c0d2633a13719be2e5cf19554262415f62

View 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.

View File

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

View 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);
}
}

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

View 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);
}
}

View 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();
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View 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);
}
}
}

View 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);
}
}

View 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
}
}

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

View File

@@ -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.*")]

View File

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

View 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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View 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
});
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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