diff options
Diffstat (limited to 'MediaBrowser.Controller')
64 files changed, 6471 insertions, 0 deletions
diff --git a/MediaBrowser.Controller/Drawing/DrawingUtils.cs b/MediaBrowser.Controller/Drawing/DrawingUtils.cs new file mode 100644 index 000000000..8e2f829b9 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/DrawingUtils.cs @@ -0,0 +1,81 @@ +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;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessor.cs b/MediaBrowser.Controller/Drawing/ImageProcessor.cs new file mode 100644 index 000000000..29e40d17d --- /dev/null +++ b/MediaBrowser.Controller/Drawing/ImageProcessor.cs @@ -0,0 +1,148 @@ +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];
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio.cs b/MediaBrowser.Controller/Entities/Audio.cs new file mode 100644 index 000000000..61e901dd2 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Audio.cs @@ -0,0 +1,14 @@ +
+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; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/BaseEntity.cs b/MediaBrowser.Controller/Entities/BaseEntity.cs new file mode 100644 index 000000000..5b4a360c1 --- /dev/null +++ b/MediaBrowser.Controller/Entities/BaseEntity.cs @@ -0,0 +1,94 @@ +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;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs new file mode 100644 index 000000000..4c9008b22 --- /dev/null +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -0,0 +1,202 @@ +using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.IO;
+using System;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public abstract class BaseItem : BaseEntity, IHasProviderIds
+ {
+
+ public IEnumerable<string> PhysicalLocations
+ {
+ get
+ {
+ return _resolveArgs.PhysicalLocations;
+ }
+ }
+
+ public string SortName { get; set; }
+
+ /// <summary>
+ /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
+ /// </summary>
+ public DateTime? PremiereDate { get; set; }
+
+ public string LogoImagePath { get; set; }
+
+ public string ArtImagePath { get; set; }
+
+ public string ThumbnailImagePath { get; set; }
+
+ public string BannerImagePath { get; set; }
+
+ public IEnumerable<string> BackdropImagePaths { get; set; }
+
+ public string OfficialRating { get; set; }
+
+ public string CustomRating { get; set; }
+ public string CustomPin { get; set; }
+
+ public string Language { get; set; }
+ public string Overview { get; set; }
+ public List<string> Taglines { get; set; }
+
+ /// <summary>
+ /// Using a Dictionary to prevent duplicates
+ /// </summary>
+ public Dictionary<string,PersonInfo> People { get; set; }
+
+ public List<string> Studios { get; set; }
+
+ public List<string> Genres { get; set; }
+
+ public string DisplayMediaType { get; set; }
+
+ public float? CommunityRating { get; set; }
+ public long? RunTimeTicks { get; set; }
+
+ public string AspectRatio { get; set; }
+ public int? ProductionYear { get; set; }
+
+ /// <summary>
+ /// If the item is part of a series, this is it's number in the series.
+ /// This could be episode number, album track number, etc.
+ /// </summary>
+ public int? IndexNumber { get; set; }
+
+ /// <summary>
+ /// For an episode this could be the season number, or for a song this could be the disc number.
+ /// </summary>
+ public int? ParentIndexNumber { get; set; }
+
+ public IEnumerable<Video> LocalTrailers { get; set; }
+
+ public string TrailerUrl { get; set; }
+
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ public Dictionary<Guid, UserItemData> UserData { get; set; }
+
+ public UserItemData GetUserData(User user, bool createIfNull)
+ {
+ if (UserData == null || !UserData.ContainsKey(user.Id))
+ {
+ if (createIfNull)
+ {
+ AddUserData(user, new UserItemData());
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ return UserData[user.Id];
+ }
+
+ private void AddUserData(User user, UserItemData data)
+ {
+ if (UserData == null)
+ {
+ UserData = new Dictionary<Guid, UserItemData>();
+ }
+
+ UserData[user.Id] = data;
+ }
+
+ /// <summary>
+ /// Determines if a given user has access to this item
+ /// </summary>
+ internal bool IsParentalAllowed(User user)
+ {
+ return true;
+ }
+
+ /// <summary>
+ /// Finds an item by ID, recursively
+ /// </summary>
+ public virtual BaseItem FindItemById(Guid id)
+ {
+ if (Id == id)
+ {
+ return this;
+ }
+
+ if (LocalTrailers != null)
+ {
+ return LocalTrailers.FirstOrDefault(i => i.Id == id);
+ }
+
+ return null;
+ }
+
+ public virtual bool IsFolder
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Determine if we have changed vs the passed in copy
+ /// </summary>
+ /// <param name="copy"></param>
+ /// <returns></returns>
+ public virtual bool IsChanged(BaseItem copy)
+ {
+ bool changed = copy.DateModified != this.DateModified;
+ if (changed) MediaBrowser.Common.Logging.Logger.LogDebugInfo(this.Name + " changed - original creation: " + this.DateCreated + " new creation: " + copy.DateCreated + " original modified: " + this.DateModified + " new modified: " + copy.DateModified);
+ return changed;
+ }
+
+ /// <summary>
+ /// Determines if the item is considered new based on user settings
+ /// </summary>
+ public bool IsRecentlyAdded(User user)
+ {
+ return (DateTime.UtcNow - DateCreated).TotalDays < user.RecentItemDays;
+ }
+
+ public void AddPerson(PersonInfo person)
+ {
+ if (People == null)
+ {
+ People = new Dictionary<string, PersonInfo>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ People[person.Name] = person;
+ }
+
+ /// <summary>
+ /// Marks the item as either played or unplayed
+ /// </summary>
+ public virtual void SetPlayedStatus(User user, bool wasPlayed)
+ {
+ UserItemData data = GetUserData(user, true);
+
+ if (wasPlayed)
+ {
+ data.PlayCount = Math.Max(data.PlayCount, 1);
+ }
+ else
+ {
+ data.PlayCount = 0;
+ data.PlaybackPositionTicks = 0;
+ }
+ }
+
+ /// <summary>
+ /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
+ /// </summary>
+ /// <returns></returns>
+ public virtual Task ChangedExternally()
+ {
+ return Task.Run(() => RefreshMetadata());
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs new file mode 100644 index 000000000..07529c80f --- /dev/null +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -0,0 +1,619 @@ +using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Resolvers;
+using System;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class Folder : BaseItem
+ {
+ #region Events
+ /// <summary>
+ /// Fires whenever a validation routine updates our children. The added and removed children are properties of the args.
+ /// *** Will fire asynchronously. ***
+ /// </summary>
+ public event EventHandler<ChildrenChangedEventArgs> ChildrenChanged;
+ protected void OnChildrenChanged(ChildrenChangedEventArgs args)
+ {
+ if (ChildrenChanged != null)
+ {
+ Task.Run( () =>
+ {
+ ChildrenChanged(this, args);
+ Kernel.Instance.OnLibraryChanged(args);
+ });
+ }
+ }
+
+ #endregion
+
+ public override bool IsFolder
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public bool IsRoot { get; set; }
+
+ public bool IsVirtualFolder
+ {
+ get
+ {
+ return Parent != null && Parent.IsRoot;
+ }
+ }
+ protected object childLock = new object();
+ protected List<BaseItem> children;
+ protected virtual List<BaseItem> ActualChildren
+ {
+ get
+ {
+ if (children == null)
+ {
+ LoadChildren();
+ }
+ return children;
+ }
+
+ set
+ {
+ children = value;
+ }
+ }
+
+ /// <summary>
+ /// thread-safe access to the actual children of this folder - without regard to user
+ /// </summary>
+ public IEnumerable<BaseItem> Children
+ {
+ get
+ {
+ lock (childLock)
+ return ActualChildren.ToList();
+ }
+ }
+
+ /// <summary>
+ /// thread-safe access to all recursive children of this folder - without regard to user
+ /// </summary>
+ public IEnumerable<BaseItem> RecursiveChildren
+ {
+ get
+ {
+ foreach (var item in Children)
+ {
+ yield return item;
+
+ var subFolder = item as Folder;
+
+ if (subFolder != null)
+ {
+ foreach (var subitem in subFolder.RecursiveChildren)
+ {
+ yield return subitem;
+ }
+ }
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// Loads and validates our children
+ /// </summary>
+ protected virtual void LoadChildren()
+ {
+ //first - load our children from the repo
+ lock (childLock)
+ children = GetCachedChildren();
+
+ //then kick off a validation against the actual file system
+ Task.Run(() => ValidateChildren());
+ }
+
+ protected bool ChildrenValidating = false;
+
+ /// <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>
+ /// <returns></returns>
+ protected async virtual void ValidateChildren()
+ {
+ if (ChildrenValidating) return; //only ever want one of these going at once and don't want them to fire off in sequence so don't use lock
+ ChildrenValidating = true;
+ bool changed = false; //this will save us a little time at the end if nothing changes
+ var changedArgs = new ChildrenChangedEventArgs(this);
+ //get the current valid children from filesystem (or wherever)
+ var nonCachedChildren = await GetNonCachedChildren();
+ if (nonCachedChildren == null) return; //nothing to validate
+ //build a dictionary of the current children we have now by Id so we can compare quickly and easily
+ Dictionary<Guid, BaseItem> currentChildren;
+ lock (childLock)
+ currentChildren = ActualChildren.ToDictionary(i => i.Id);
+
+ //create a list for our validated children
+ var validChildren = new List<BaseItem>();
+ //now traverse the valid children and find any changed or new items
+ foreach (var child in nonCachedChildren)
+ {
+ BaseItem currentChild;
+ currentChildren.TryGetValue(child.Id, out currentChild);
+ if (currentChild == null)
+ {
+ //brand new item - needs to be added
+ changed = true;
+ changedArgs.ItemsAdded.Add(child);
+ //refresh it
+ child.RefreshMetadata();
+ Logger.LogInfo("New Item Added to Library: ("+child.GetType().Name+") "+ child.Name + " (" + child.Path + ")");
+ //save it in repo...
+
+ //and add it to our valid children
+ validChildren.Add(child);
+ //fire an added event...?
+ //if it is a folder we need to validate its children as well
+ Folder folder = child as Folder;
+ if (folder != null)
+ {
+ folder.ValidateChildren();
+ //probably need to refresh too...
+ }
+ }
+ else
+ {
+ //existing item - check if it has changed
+ if (currentChild.IsChanged(child))
+ {
+ changed = true;
+ //update resolve args and refresh meta
+ // Note - we are refreshing the existing child instead of the newly found one so the "Except" operation below
+ // will identify this item as the same one
+ currentChild.ResolveArgs = child.ResolveArgs;
+ currentChild.RefreshMetadata();
+ Logger.LogInfo("Item Changed: ("+currentChild.GetType().Name+") "+ currentChild.Name + " (" + currentChild.Path + ")");
+ //save it in repo...
+ validChildren.Add(currentChild);
+ }
+ else
+ {
+ //current child that didn't change - just put it in the valid children
+ validChildren.Add(currentChild);
+ }
+ }
+ }
+
+ //that's all the new and changed ones - now see if there are any that are missing
+ changedArgs.ItemsRemoved = currentChildren.Values.Except(validChildren);
+ changed |= changedArgs.ItemsRemoved != null;
+
+ //now, if anything changed - replace our children
+ if (changed)
+ {
+ if (changedArgs.ItemsRemoved != null) foreach (var item in changedArgs.ItemsRemoved) Logger.LogDebugInfo("** " + item.Name + " Removed from library.");
+
+ lock (childLock)
+ ActualChildren = validChildren;
+ //and save children in repo...
+
+ //and fire event
+ this.OnChildrenChanged(changedArgs);
+ }
+ ChildrenValidating = false;
+
+ }
+
+ /// <summary>
+ /// Get the children of this folder from the actual file system
+ /// </summary>
+ /// <returns></returns>
+ protected async virtual Task<IEnumerable<BaseItem>> GetNonCachedChildren()
+ {
+ ItemResolveEventArgs args = new ItemResolveEventArgs()
+ {
+ FileInfo = FileData.GetFileData(this.Path),
+ Parent = this.Parent,
+ Cancel = false,
+ Path = this.Path
+ };
+
+ // Gather child folder and files
+ if (args.IsDirectory)
+ {
+ args.FileSystemChildren = FileData.GetFileSystemEntries(this.Path, "*").ToArray();
+
+ bool isVirtualFolder = Parent != null && Parent.IsRoot;
+ args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
+ }
+ else
+ {
+ Logger.LogError("Folder has a path that is not a directory: " + this.Path);
+ return null;
+ }
+
+ if (!EntityResolutionHelper.ShouldResolvePathContents(args))
+ {
+ return null;
+ }
+ return (await Task.WhenAll<BaseItem>(GetChildren(args.FileSystemChildren)).ConfigureAwait(false))
+ .Where(i => i != null).OrderBy(f =>
+ {
+ return string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName;
+
+ });
+
+ }
+
+ /// <summary>
+ /// Resolves a path into a BaseItem
+ /// </summary>
+ protected async Task<BaseItem> GetChild(string path, WIN32_FIND_DATA? fileInfo = null)
+ {
+ ItemResolveEventArgs args = new ItemResolveEventArgs()
+ {
+ FileInfo = fileInfo ?? FileData.GetFileData(path),
+ Parent = this,
+ Cancel = false,
+ Path = path
+ };
+
+ args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
+ args = FileSystemHelper.FilterChildFileSystemEntries(args, false);
+
+ return Kernel.Instance.ResolveItem(args);
+
+ }
+
+ /// <summary>
+ /// Finds child BaseItems for us
+ /// </summary>
+ protected Task<BaseItem>[] GetChildren(WIN32_FIND_DATA[] fileSystemChildren)
+ {
+ Task<BaseItem>[] tasks = new Task<BaseItem>[fileSystemChildren.Length];
+
+ for (int i = 0; i < fileSystemChildren.Length; i++)
+ {
+ var child = fileSystemChildren[i];
+
+ tasks[i] = GetChild(child.Path, child);
+ }
+
+ return tasks;
+ }
+
+
+ /// <summary>
+ /// Get our children from the repo - stubbed for now
+ /// </summary>
+ /// <returns></returns>
+ protected virtual List<BaseItem> GetCachedChildren()
+ {
+ return new List<BaseItem>();
+ }
+
+ /// <summary>
+ /// Gets allowed children of an item
+ /// </summary>
+ public IEnumerable<BaseItem> GetChildren(User user)
+ {
+ lock(childLock)
+ return ActualChildren.Where(c => c.IsParentalAllowed(user));
+ }
+
+ /// <summary>
+ /// Gets allowed recursive children of an item
+ /// </summary>
+ public IEnumerable<BaseItem> GetRecursiveChildren(User user)
+ {
+ foreach (var item in GetChildren(user))
+ {
+ yield return item;
+
+ var subFolder = item as Folder;
+
+ if (subFolder != null)
+ {
+ foreach (var subitem in subFolder.GetRecursiveChildren(user))
+ {
+ yield return subitem;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Folders need to validate and refresh
+ /// </summary>
+ /// <returns></returns>
+ public override Task ChangedExternally()
+ {
+ return Task.Run(() =>
+ {
+ if (this.IsRoot)
+ {
+ Kernel.Instance.ReloadRoot().ConfigureAwait(false);
+ }
+ else
+ {
+ RefreshMetadata();
+ ValidateChildren();
+ }
+ });
+ }
+
+ /// <summary>
+ /// Since it can be slow to make all of these calculations at once, this method will provide a way to get them all back together
+ /// </summary>
+ public ItemSpecialCounts GetSpecialCounts(User user)
+ {
+ var counts = new ItemSpecialCounts();
+
+ IEnumerable<BaseItem> recursiveChildren = GetRecursiveChildren(user);
+
+ var recentlyAddedItems = GetRecentlyAddedItems(recursiveChildren, user);
+
+ counts.RecentlyAddedItemCount = recentlyAddedItems.Count;
+ counts.RecentlyAddedUnPlayedItemCount = GetRecentlyAddedUnplayedItems(recentlyAddedItems, user).Count;
+ counts.InProgressItemCount = GetInProgressItems(recursiveChildren, user).Count;
+ counts.PlayedPercentage = GetPlayedPercentage(recursiveChildren, user);
+
+ return counts;
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that contain the given genre and are allowed for the current user
+ /// </summary>
+ public IEnumerable<BaseItem> GetItemsWithGenre(string genre, User user)
+ {
+ return GetRecursiveChildren(user).Where(f => f.Genres != null && f.Genres.Any(s => s.Equals(genre, StringComparison.OrdinalIgnoreCase)));
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that contain the given year and are allowed for the current user
+ /// </summary>
+ public IEnumerable<BaseItem> GetItemsWithYear(int year, User user)
+ {
+ return GetRecursiveChildren(user).Where(f => f.ProductionYear.HasValue && f.ProductionYear == year);
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that contain the given studio and are allowed for the current user
+ /// </summary>
+ public IEnumerable<BaseItem> GetItemsWithStudio(string studio, User user)
+ {
+ return GetRecursiveChildren(user).Where(f => f.Studios != null && f.Studios.Any(s => s.Equals(studio, StringComparison.OrdinalIgnoreCase)));
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that the user has marked as a favorite
+ /// </summary>
+ public IEnumerable<BaseItem> GetFavoriteItems(User user)
+ {
+ return GetRecursiveChildren(user).Where(c =>
+ {
+ UserItemData data = c.GetUserData(user, false);
+
+ if (data != null)
+ {
+ return data.IsFavorite;
+ }
+
+ return false;
+ });
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
+ /// </summary>
+ public IEnumerable<BaseItem> GetItemsWithPerson(string person, User user)
+ {
+ return GetRecursiveChildren(user).Where(c =>
+ {
+ if (c.People != null)
+ {
+ return c.People.ContainsKey(person);
+ }
+
+ return false;
+ });
+ }
+
+ /// <summary>
+ /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
+ /// </summary>
+ /// <param name="personType">Specify this to limit results to a specific PersonType</param>
+ public IEnumerable<BaseItem> GetItemsWithPerson(string person, string personType, User user)
+ {
+ return GetRecursiveChildren(user).Where(c =>
+ {
+ if (c.People != null)
+ {
+ return c.People.ContainsKey(person) && c.People[person].Type.Equals(personType, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return false;
+ });
+ }
+
+ /// <summary>
+ /// Gets all recently added items (recursive) within a folder, based on configuration and parental settings
+ /// </summary>
+ public List<BaseItem> GetRecentlyAddedItems(User user)
+ {
+ return GetRecentlyAddedItems(GetRecursiveChildren(user), user);
+ }
+
+ /// <summary>
+ /// Gets all recently added unplayed items (recursive) within a folder, based on configuration and parental settings
+ /// </summary>
+ public List<BaseItem> GetRecentlyAddedUnplayedItems(User user)
+ {
+ return GetRecentlyAddedUnplayedItems(GetRecursiveChildren(user), user);
+ }
+
+ /// <summary>
+ /// Gets all in-progress items (recursive) within a folder
+ /// </summary>
+ public List<BaseItem> GetInProgressItems(User user)
+ {
+ return GetInProgressItems(GetRecursiveChildren(user), user);
+ }
+
+ /// <summary>
+ /// Takes a list of items and returns the ones that are recently added
+ /// </summary>
+ private static List<BaseItem> GetRecentlyAddedItems(IEnumerable<BaseItem> itemSet, User user)
+ {
+ var list = new List<BaseItem>();
+
+ foreach (var item in itemSet)
+ {
+ if (!item.IsFolder && item.IsRecentlyAdded(user))
+ {
+ list.Add(item);
+ }
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// Takes a list of items and returns the ones that are recently added and unplayed
+ /// </summary>
+ private static List<BaseItem> GetRecentlyAddedUnplayedItems(IEnumerable<BaseItem> itemSet, User user)
+ {
+ var list = new List<BaseItem>();
+
+ foreach (var item in itemSet)
+ {
+ if (!item.IsFolder && item.IsRecentlyAdded(user))
+ {
+ var userdata = item.GetUserData(user, false);
+
+ if (userdata == null || userdata.PlayCount == 0)
+ {
+ list.Add(item);
+ }
+ }
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// Takes a list of items and returns the ones that are in progress
+ /// </summary>
+ private static List<BaseItem> GetInProgressItems(IEnumerable<BaseItem> itemSet, User user)
+ {
+ var list = new List<BaseItem>();
+
+ foreach (var item in itemSet)
+ {
+ if (!item.IsFolder)
+ {
+ var userdata = item.GetUserData(user, false);
+
+ if (userdata != null && userdata.PlaybackPositionTicks > 0)
+ {
+ list.Add(item);
+ }
+ }
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// Gets the total played percentage for a set of items
+ /// </summary>
+ private static decimal GetPlayedPercentage(IEnumerable<BaseItem> itemSet, User user)
+ {
+ itemSet = itemSet.Where(i => !(i.IsFolder));
+
+ decimal totalPercent = 0;
+
+ int count = 0;
+
+ foreach (BaseItem item in itemSet)
+ {
+ count++;
+
+ UserItemData data = item.GetUserData(user, false);
+
+ if (data == null)
+ {
+ continue;
+ }
+
+ if (data.PlayCount > 0)
+ {
+ totalPercent += 100;
+ }
+ else if (data.PlaybackPositionTicks > 0 && item.RunTimeTicks.HasValue)
+ {
+ decimal itemPercent = data.PlaybackPositionTicks;
+ itemPercent /= item.RunTimeTicks.Value;
+ totalPercent += itemPercent;
+ }
+ }
+
+ if (count == 0)
+ {
+ return 0;
+ }
+
+ return totalPercent / count;
+ }
+
+ /// <summary>
+ /// Marks the item as either played or unplayed
+ /// </summary>
+ public override void SetPlayedStatus(User user, bool wasPlayed)
+ {
+ base.SetPlayedStatus(user, wasPlayed);
+
+ // Now sweep through recursively and update status
+ foreach (BaseItem item in GetChildren(user))
+ {
+ item.SetPlayedStatus(user, wasPlayed);
+ }
+ }
+
+ /// <summary>
+ /// Finds an item by ID, recursively
+ /// </summary>
+ public override BaseItem FindItemById(Guid id)
+ {
+ var result = base.FindItemById(id);
+
+ if (result != null)
+ {
+ return result;
+ }
+
+ //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
+ return RecursiveChildren.FirstOrDefault(i => i.Id == id);
+ }
+
+ /// <summary>
+ /// Finds an item by path, recursively
+ /// </summary>
+ public BaseItem FindByPath(string path)
+ {
+ if (PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase))
+ {
+ return this;
+ }
+
+ //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
+ return RecursiveChildren.FirstOrDefault(i => i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase));
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs new file mode 100644 index 000000000..ba343a2bc --- /dev/null +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -0,0 +1,7 @@ +
+namespace MediaBrowser.Controller.Entities
+{
+ public class Genre : BaseEntity
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs new file mode 100644 index 000000000..cb841530e --- /dev/null +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -0,0 +1,7 @@ +
+namespace MediaBrowser.Controller.Entities.Movies
+{
+ public class BoxSet : Folder
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs new file mode 100644 index 000000000..2d98fa06e --- /dev/null +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -0,0 +1,31 @@ +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;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs new file mode 100644 index 000000000..a12b9e38e --- /dev/null +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -0,0 +1,25 @@ +
+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;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs new file mode 100644 index 000000000..b7c6e6aa4 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -0,0 +1,7 @@ +
+namespace MediaBrowser.Controller.Entities
+{
+ public class Studio : BaseEntity
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs new file mode 100644 index 000000000..5d599fca7 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -0,0 +1,7 @@ +
+namespace MediaBrowser.Controller.Entities.TV
+{
+ public class Episode : Video
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs new file mode 100644 index 000000000..f9c7fecb3 --- /dev/null +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -0,0 +1,34 @@ +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;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs new file mode 100644 index 000000000..7c228a53d --- /dev/null +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -0,0 +1,12 @@ +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; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs new file mode 100644 index 000000000..01eadfafb --- /dev/null +++ b/MediaBrowser.Controller/Entities/User.cs @@ -0,0 +1,21 @@ +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; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs new file mode 100644 index 000000000..bb4950046 --- /dev/null +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -0,0 +1,67 @@ +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;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs new file mode 100644 index 000000000..8dd82fab9 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -0,0 +1,20 @@ +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; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs new file mode 100644 index 000000000..d0b29de56 --- /dev/null +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -0,0 +1,7 @@ +
+namespace MediaBrowser.Controller.Entities
+{
+ public class Year : BaseEntity
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/FFMpeg/FFProbe.cs b/MediaBrowser.Controller/FFMpeg/FFProbe.cs new file mode 100644 index 000000000..f16f0142d --- /dev/null +++ b/MediaBrowser.Controller/FFMpeg/FFProbe.cs @@ -0,0 +1,137 @@ +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");
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/FFMpeg/FFProbeResult.cs b/MediaBrowser.Controller/FFMpeg/FFProbeResult.cs new file mode 100644 index 000000000..db7c9dd3c --- /dev/null +++ b/MediaBrowser.Controller/FFMpeg/FFProbeResult.cs @@ -0,0 +1,119 @@ +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; }
+ }
+}
diff --git a/MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id b/MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id new file mode 100644 index 000000000..73a37bd55 --- /dev/null +++ b/MediaBrowser.Controller/FFMpeg/ffmpeg.exe.REMOVED.git-id @@ -0,0 +1 @@ +84ac1c51e84cfbfb20e7b96c9f1a4442a8cfadf2
\ No newline at end of file diff --git a/MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id b/MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id new file mode 100644 index 000000000..682ead74d --- /dev/null +++ b/MediaBrowser.Controller/FFMpeg/ffprobe.exe.REMOVED.git-id @@ -0,0 +1 @@ +331e241e29f1b015e303b301c17c37883e39f39d
\ No newline at end of file diff --git a/MediaBrowser.Controller/FFMpeg/readme.txt b/MediaBrowser.Controller/FFMpeg/readme.txt new file mode 100644 index 000000000..cdb039bdc --- /dev/null +++ b/MediaBrowser.Controller/FFMpeg/readme.txt @@ -0,0 +1,3 @@ +This is the 32-bit static build of ffmpeg, located at:
+
+http://ffmpeg.zeranoe.com/builds/
\ No newline at end of file diff --git a/MediaBrowser.Controller/IO/DirectoryWatchers.cs b/MediaBrowser.Controller/IO/DirectoryWatchers.cs new file mode 100644 index 000000000..eb1358e16 --- /dev/null +++ b/MediaBrowser.Controller/IO/DirectoryWatchers.cs @@ -0,0 +1,172 @@ +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();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs new file mode 100644 index 000000000..4ae2ee72f --- /dev/null +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -0,0 +1,251 @@ +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; }
+ }
+
+}
diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs new file mode 100644 index 000000000..732cf0803 --- /dev/null +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -0,0 +1,132 @@ +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;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/IO/Shortcut.cs b/MediaBrowser.Controller/IO/Shortcut.cs new file mode 100644 index 000000000..e9ea21f17 --- /dev/null +++ b/MediaBrowser.Controller/IO/Shortcut.cs @@ -0,0 +1,185 @@ +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;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Kernel.cs b/MediaBrowser.Controller/Kernel.cs new file mode 100644 index 000000000..2430260dd --- /dev/null +++ b/MediaBrowser.Controller/Kernel.cs @@ -0,0 +1,386 @@ +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);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs b/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs new file mode 100644 index 000000000..462fcc6d6 --- /dev/null +++ b/MediaBrowser.Controller/Library/ChildrenChangedEventArgs.cs @@ -0,0 +1,34 @@ +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>();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ItemController.cs b/MediaBrowser.Controller/Library/ItemController.cs new file mode 100644 index 000000000..54673e538 --- /dev/null +++ b/MediaBrowser.Controller/Library/ItemController.cs @@ -0,0 +1,136 @@ +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;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs b/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs new file mode 100644 index 000000000..32b8783df --- /dev/null +++ b/MediaBrowser.Controller/Library/ItemResolveEventArgs.cs @@ -0,0 +1,104 @@ +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;
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj new file mode 100644 index 000000000..fc1e578e9 --- /dev/null +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -0,0 +1,150 @@ +<?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>
+ -->
+</Project>
\ No newline at end of file diff --git a/MediaBrowser.Controller/Properties/AssemblyInfo.cs b/MediaBrowser.Controller/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..63cc65d7a --- /dev/null +++ b/MediaBrowser.Controller/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +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")]
diff --git a/MediaBrowser.Controller/Providers/AudioInfoProvider.cs b/MediaBrowser.Controller/Providers/AudioInfoProvider.cs new file mode 100644 index 000000000..302902646 --- /dev/null +++ b/MediaBrowser.Controller/Providers/AudioInfoProvider.cs @@ -0,0 +1,262 @@ +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;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs new file mode 100644 index 000000000..38afb2b52 --- /dev/null +++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs @@ -0,0 +1,724 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Xml;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Common.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides a base class for parsing metadata xml
+ /// </summary>
+ public class BaseItemXmlParser<T>
+ where T : BaseItem, new()
+ {
+ /// <summary>
+ /// Fetches metadata for an item from one xml file
+ /// </summary>
+ public void Fetch(T item, string metadataFile)
+ {
+ // Use XmlReader for best performance
+ using (XmlReader reader = XmlReader.Create(metadataFile))
+ {
+ reader.MoveToContent();
+
+ // Loop through each element
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(reader, item);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches metadata from one Xml Element
+ /// </summary>
+ protected virtual void FetchDataFromXmlNode(XmlReader reader, T item)
+ {
+ switch (reader.Name)
+ {
+ // DateCreated
+ case "Added":
+ DateTime added;
+ if (DateTime.TryParse(reader.ReadElementContentAsString() ?? string.Empty, out added))
+ {
+ item.DateCreated = added.ToUniversalTime();
+ }
+ break;
+
+ // DisplayMediaType
+ case "Type":
+ {
+ item.DisplayMediaType = reader.ReadElementContentAsString();
+
+ switch (item.DisplayMediaType.ToLower())
+ {
+ case "blu-ray":
+ item.DisplayMediaType = VideoType.BluRay.ToString();
+ break;
+ case "dvd":
+ item.DisplayMediaType = VideoType.Dvd.ToString();
+ break;
+ case "":
+ item.DisplayMediaType = null;
+ break;
+ }
+
+ break;
+ }
+
+ // TODO: Do we still need this?
+ case "banner":
+ item.BannerImagePath = reader.ReadElementContentAsString();
+ break;
+
+ case "LocalTitle":
+ item.Name = reader.ReadElementContentAsString();
+ break;
+
+ case "SortTitle":
+ item.SortName = reader.ReadElementContentAsString();
+ break;
+
+ case "Overview":
+ case "Description":
+ item.Overview = reader.ReadElementContentAsString();
+ break;
+
+ case "TagLine":
+ {
+ var list = item.Taglines ?? new List<string>();
+ var tagline = reader.ReadElementContentAsString();
+
+ if (!list.Contains(tagline))
+ {
+ list.Add(tagline);
+ }
+
+ item.Taglines = list;
+ break;
+ }
+
+ case "TagLines":
+ {
+ FetchFromTaglinesNode(reader.ReadSubtree(), item);
+ break;
+ }
+
+ case "ContentRating":
+ case "MPAARating":
+ item.OfficialRating = reader.ReadElementContentAsString();
+ break;
+
+ case "CustomRating":
+ item.CustomRating = reader.ReadElementContentAsString();
+ break;
+
+ case "CustomPin":
+ item.CustomPin = reader.ReadElementContentAsString();
+ break;
+
+ case "Runtime":
+ case "RunningTime":
+ {
+ string text = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(text))
+ {
+ int runtime;
+ if (int.TryParse(text.Split(' ')[0], out runtime))
+ {
+ item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
+ }
+ }
+ break;
+ }
+
+ case "Genre":
+ {
+ var list = item.Genres ?? new List<string>();
+ list.AddRange(GetSplitValues(reader.ReadElementContentAsString(), '|'));
+
+ item.Genres = list;
+ break;
+ }
+
+ case "AspectRatio":
+ item.AspectRatio = reader.ReadElementContentAsString();
+ break;
+
+ case "Network":
+ {
+ var list = item.Studios ?? new List<string>();
+ list.AddRange(GetSplitValues(reader.ReadElementContentAsString(), '|'));
+
+ item.Studios = list;
+ break;
+ }
+
+ case "Director":
+ {
+ foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Director" }))
+ {
+ item.AddPerson(p);
+ }
+ break;
+ }
+ case "Writer":
+ {
+ foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Writer" }))
+ {
+ item.AddPerson(p);
+ }
+ break;
+ }
+
+ case "Actors":
+ case "GuestStars":
+ {
+ foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Actor" }))
+ {
+ item.AddPerson(p);
+ }
+ break;
+ }
+
+ case "Trailer":
+ item.TrailerUrl = reader.ReadElementContentAsString();
+ break;
+
+ case "ProductionYear":
+ {
+ string val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ int ProductionYear;
+ if (int.TryParse(val, out ProductionYear) && ProductionYear > 1850)
+ {
+ item.ProductionYear = ProductionYear;
+ }
+ }
+
+ break;
+ }
+
+ case "Rating":
+ case "IMDBrating":
+
+ string rating = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(rating))
+ {
+ float val;
+
+ if (float.TryParse(rating, out val))
+ {
+ item.CommunityRating = val;
+ }
+ }
+ break;
+
+ case "FirstAired":
+ {
+ string firstAired = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(firstAired))
+ {
+ DateTime airDate;
+
+ if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850)
+ {
+ item.PremiereDate = airDate.ToUniversalTime();
+ item.ProductionYear = airDate.Year;
+ }
+ }
+
+ break;
+ }
+
+ case "TMDbId":
+ string tmdb = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(tmdb))
+ {
+ item.SetProviderId(MetadataProviders.Tmdb, tmdb);
+ }
+ break;
+
+ case "TVcomId":
+ string TVcomId = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(TVcomId))
+ {
+ item.SetProviderId(MetadataProviders.Tvcom, TVcomId);
+ }
+ break;
+
+ case "IMDB_ID":
+ case "IMDB":
+ case "IMDbId":
+ string IMDbId = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(IMDbId))
+ {
+ item.SetProviderId(MetadataProviders.Imdb, IMDbId);
+ }
+ break;
+
+ case "Genres":
+ FetchFromGenresNode(reader.ReadSubtree(), item);
+ break;
+
+ case "Persons":
+ FetchDataFromPersonsNode(reader.ReadSubtree(), item);
+ break;
+
+ case "ParentalRating":
+ FetchFromParentalRatingNode(reader.ReadSubtree(), item);
+ break;
+
+ case "Studios":
+ FetchFromStudiosNode(reader.ReadSubtree(), item);
+ break;
+
+ case "MediaInfo":
+ {
+ var video = item as Video;
+
+ if (video != null)
+ {
+ FetchMediaInfo(reader.ReadSubtree(), video);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+
+ private void FetchMediaInfo(XmlReader reader, Video item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Audio":
+ {
+ AudioStream stream = FetchMediaInfoAudio(reader.ReadSubtree());
+
+ List<AudioStream> streams = item.AudioStreams ?? new List<AudioStream>();
+ streams.Add(stream);
+ item.AudioStreams = streams;
+
+ break;
+ }
+
+ case "Video":
+ FetchMediaInfoVideo(reader.ReadSubtree(), item);
+ break;
+
+ case "Subtitle":
+ {
+ SubtitleStream stream = FetchMediaInfoSubtitles(reader.ReadSubtree());
+
+ List<SubtitleStream> streams = item.Subtitles ?? new List<SubtitleStream>();
+ streams.Add(stream);
+ item.Subtitles = streams;
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ private AudioStream FetchMediaInfoAudio(XmlReader reader)
+ {
+ var stream = new AudioStream();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Default":
+ stream.IsDefault = reader.ReadElementContentAsString() == "True";
+ break;
+
+ case "SamplingRate":
+ stream.SampleRate = reader.ReadIntSafe();
+ break;
+
+ case "BitRate":
+ stream.BitRate = reader.ReadIntSafe();
+ break;
+
+ case "Channels":
+ stream.Channels = reader.ReadIntSafe();
+ break;
+
+ case "Language":
+ stream.Language = reader.ReadElementContentAsString();
+ break;
+
+ case "Codec":
+ stream.Codec = reader.ReadElementContentAsString();
+ break;
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ return stream;
+ }
+
+ private void FetchMediaInfoVideo(XmlReader reader, Video item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Width":
+ item.Width = reader.ReadIntSafe();
+ break;
+
+ case "Height":
+ item.Height = reader.ReadIntSafe();
+ break;
+
+ case "BitRate":
+ item.BitRate = reader.ReadIntSafe();
+ break;
+
+ case "FrameRate":
+ item.FrameRate = reader.ReadFloatSafe();
+ break;
+
+ case "ScanType":
+ item.ScanType = reader.ReadElementContentAsString();
+ break;
+
+ case "Duration":
+ item.RunTimeTicks = TimeSpan.FromMinutes(reader.ReadIntSafe()).Ticks;
+ break;
+
+ case "DurationSeconds":
+ int seconds = reader.ReadIntSafe();
+ if (seconds > 0)
+ {
+ item.RunTimeTicks = TimeSpan.FromSeconds(seconds).Ticks;
+ }
+ break;
+
+ case "Codec":
+ {
+ string videoCodec = reader.ReadElementContentAsString();
+
+ switch (videoCodec.ToLower())
+ {
+ case "sorenson h.263":
+ item.Codec = "Sorenson H263";
+ break;
+ case "h.262":
+ item.Codec = "MPEG-2 Video";
+ break;
+ case "h.264":
+ item.Codec = "AVC";
+ break;
+ default:
+ item.Codec = videoCodec;
+ break;
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ private SubtitleStream FetchMediaInfoSubtitles(XmlReader reader)
+ {
+ var stream = new SubtitleStream();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Language":
+ stream.Language = reader.ReadElementContentAsString();
+ break;
+
+ case "Default":
+ stream.IsDefault = reader.ReadElementContentAsString() == "True";
+ break;
+
+ case "Forced":
+ stream.IsForced = reader.ReadElementContentAsString() == "True";
+ break;
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ return stream;
+ }
+
+ private void FetchFromTaglinesNode(XmlReader reader, T item)
+ {
+ var list = item.Taglines ?? new List<string>();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Tagline":
+ {
+ string val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val) && !list.Contains(val))
+ {
+ list.Add(val);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ item.Taglines = list;
+ }
+
+ private void FetchFromGenresNode(XmlReader reader, T item)
+ {
+ var list = item.Genres ?? new List<string>();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Genre":
+ {
+ string genre = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(genre))
+ {
+ list.Add(genre);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ item.Genres = list;
+ }
+
+ private void FetchDataFromPersonsNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Person":
+ {
+ item.AddPerson(GetPersonFromXmlNode(reader.ReadSubtree()));
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ private void FetchFromStudiosNode(XmlReader reader, T item)
+ {
+ var list = item.Studios ?? new List<string>();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Studio":
+ {
+ string studio = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(studio))
+ {
+ list.Add(studio);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ item.Studios = list;
+ }
+
+ private void FetchFromParentalRatingNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Value":
+ {
+ string ratingString = reader.ReadElementContentAsString();
+
+ int rating = 7;
+
+ if (!string.IsNullOrWhiteSpace(ratingString))
+ {
+ int.TryParse(ratingString, out rating);
+ }
+
+ switch (rating)
+ {
+ case -1:
+ item.OfficialRating = "NR";
+ break;
+ case 0:
+ item.OfficialRating = "UR";
+ break;
+ case 1:
+ item.OfficialRating = "G";
+ break;
+ case 3:
+ item.OfficialRating = "PG";
+ break;
+ case 4:
+ item.OfficialRating = "PG-13";
+ break;
+ case 5:
+ item.OfficialRating = "NC-17";
+ break;
+ case 6:
+ item.OfficialRating = "R";
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ private PersonInfo GetPersonFromXmlNode(XmlReader reader)
+ {
+ var person = new PersonInfo();
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Name":
+ person.Name = reader.ReadElementContentAsString();
+ break;
+
+ case "Type":
+ person.Type = reader.ReadElementContentAsString();
+ break;
+
+ case "Role":
+ person.Overview = reader.ReadElementContentAsString();
+ break;
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ return person;
+ }
+
+ protected IEnumerable<string> GetSplitValues(string value, char deliminator)
+ {
+ value = (value ?? string.Empty).Trim(deliminator);
+
+ return string.IsNullOrWhiteSpace(value) ? new string[] { } : value.Split(deliminator);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs new file mode 100644 index 000000000..50004be44 --- /dev/null +++ b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs @@ -0,0 +1,104 @@ +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
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseProviderInfo.cs b/MediaBrowser.Controller/Providers/BaseProviderInfo.cs new file mode 100644 index 000000000..1538b2262 --- /dev/null +++ b/MediaBrowser.Controller/Providers/BaseProviderInfo.cs @@ -0,0 +1,15 @@ +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; }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs b/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs new file mode 100644 index 000000000..b7d9b7189 --- /dev/null +++ b/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs @@ -0,0 +1,38 @@ +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"));
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs b/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs new file mode 100644 index 000000000..d6fd26d1c --- /dev/null +++ b/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs @@ -0,0 +1,128 @@ +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;
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs b/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs new file mode 100644 index 000000000..8823da691 --- /dev/null +++ b/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs @@ -0,0 +1,47 @@ +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;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs new file mode 100644 index 000000000..7ef53d546 --- /dev/null +++ b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs @@ -0,0 +1,43 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+using System;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class MovieProviderFromXml : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Movie;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ protected override DateTime CompareDate(BaseEntity item)
+ {
+ var entry = item.ResolveArgs.GetFileSystemEntry(Path.Combine(item.Path, "movie.xml"));
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
+ }
+
+ private void Fetch(BaseEntity item, ItemResolveEventArgs args)
+ {
+ if (args.ContainsFile("movie.xml"))
+ {
+ new BaseItemXmlParser<Movie>().Fetch(item as Movie, Path.Combine(args.Path, "movie.xml"));
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs b/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs new file mode 100644 index 000000000..b6b856d29 --- /dev/null +++ b/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs @@ -0,0 +1,45 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+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.Movies
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class MovieSpecialFeaturesProvider : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Movie;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ if (args.ContainsFolder("specials"))
+ {
+ var items = new List<Video>();
+
+ foreach (WIN32_FIND_DATA file in FileData.GetFileSystemEntries(Path.Combine(args.Path, "specials"), "*"))
+ {
+ var video = await Kernel.Instance.ItemController.GetItem(file.Path, fileInfo: file).ConfigureAwait(false) as Video;
+
+ if (video != null)
+ {
+ items.Add(video);
+ }
+ }
+
+ (item as Movie).SpecialFeatures = items;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs b/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs new file mode 100644 index 000000000..0b9cf85eb --- /dev/null +++ b/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs @@ -0,0 +1,67 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class EpisodeImageFromMediaLocationProvider : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Episode;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ return Task.Run(() =>
+ {
+ var episode = item as Episode;
+
+ string metadataFolder = Path.Combine(args.Parent.Path, "metadata");
+
+ string episodeFileName = Path.GetFileName(episode.Path);
+
+ var season = args.Parent as Season;
+
+ SetPrimaryImagePath(episode, season, metadataFolder, episodeFileName);
+ });
+ }
+
+ private void SetPrimaryImagePath(Episode item, Season season, string metadataFolder, string episodeFileName)
+ {
+ // Look for the image file in the metadata folder, and if found, set PrimaryImagePath
+ var imageFiles = new string[] {
+ Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".jpg")),
+ Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".png"))
+ };
+
+ string image;
+
+ if (season == null)
+ {
+ // Epsiode directly in Series folder. Gotta do this the slow way
+ image = imageFiles.FirstOrDefault(f => File.Exists(f));
+ }
+ else
+ {
+ image = imageFiles.FirstOrDefault(f => season.ContainsMetadataFile(f));
+ }
+
+ // If we found something, set PrimaryImagePath
+ if (!string.IsNullOrEmpty(image))
+ {
+ item.PrimaryImagePath = image;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs b/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs new file mode 100644 index 000000000..f3c19a704 --- /dev/null +++ b/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs @@ -0,0 +1,59 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class EpisodeProviderFromXml : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Episode;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
+ }
+
+ private void Fetch(BaseEntity item, ItemResolveEventArgs args)
+ {
+ string metadataFolder = Path.Combine(args.Parent.Path, "metadata");
+
+ string metadataFile = Path.Combine(metadataFolder, Path.ChangeExtension(Path.GetFileName(args.Path), ".xml"));
+
+ FetchMetadata(item as Episode, args.Parent as Season, metadataFile);
+ }
+
+ private void FetchMetadata(Episode item, Season season, string metadataFile)
+ {
+ if (season == null)
+ {
+ // Episode directly in Series folder
+ // Need to validate it the slow way
+ if (!File.Exists(metadataFile))
+ {
+ return;
+ }
+ }
+ else
+ {
+ if (!season.ContainsMetadataFile(metadataFile))
+ {
+ return;
+ }
+ }
+
+ new EpisodeXmlParser().Fetch(item, metadataFile);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs b/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs new file mode 100644 index 000000000..1cb604a51 --- /dev/null +++ b/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs @@ -0,0 +1,60 @@ +using MediaBrowser.Controller.Entities.TV;
+using System.IO;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ public class EpisodeXmlParser : BaseItemXmlParser<Episode>
+ {
+ protected override void FetchDataFromXmlNode(XmlReader reader, Episode item)
+ {
+ switch (reader.Name)
+ {
+ case "filename":
+ {
+ string filename = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(filename))
+ {
+ // Strip off everything but the filename. Some metadata tools like MetaBrowser v1.0 will have an 'episodes' prefix
+ // even though it's actually using the metadata folder.
+ filename = Path.GetFileName(filename);
+
+ string seasonFolder = Path.GetDirectoryName(item.Path);
+ item.PrimaryImagePath = Path.Combine(seasonFolder, "metadata", filename);
+ }
+ break;
+ }
+ case "SeasonNumber":
+ {
+ string number = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(number))
+ {
+ item.ParentIndexNumber = int.Parse(number);
+ }
+ break;
+ }
+
+ case "EpisodeNumber":
+ {
+ string number = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(number))
+ {
+ item.IndexNumber = int.Parse(number);
+ }
+ break;
+ }
+
+ case "EpisodeName":
+ item.Name = reader.ReadElementContentAsString();
+ break;
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs b/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs new file mode 100644 index 000000000..76d7e7ac1 --- /dev/null +++ b/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs @@ -0,0 +1,36 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class SeriesProviderFromXml : BaseMetadataProvider
+ {
+ public override bool Supports(BaseEntity item)
+ {
+ return item is Series;
+ }
+
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
+ {
+ await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
+ }
+
+ private void Fetch(BaseEntity item, ItemResolveEventArgs args)
+ {
+ if (args.ContainsFile("series.xml"))
+ {
+ new SeriesXmlParser().Fetch(item as Series, Path.Combine(args.Path, "series.xml"));
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs b/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs new file mode 100644 index 000000000..36c0a99ef --- /dev/null +++ b/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs @@ -0,0 +1,69 @@ +using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ public class SeriesXmlParser : BaseItemXmlParser<Series>
+ {
+ protected override void FetchDataFromXmlNode(XmlReader reader, Series item)
+ {
+ switch (reader.Name)
+ {
+ case "id":
+ string id = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(id))
+ {
+ item.SetProviderId(MetadataProviders.Tvdb, id);
+ }
+ break;
+
+ case "Airs_DayOfWeek":
+ {
+ string day = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(day))
+ {
+ if (day.Equals("Daily", StringComparison.OrdinalIgnoreCase))
+ {
+ item.AirDays = new DayOfWeek[] {
+ DayOfWeek.Sunday,
+ DayOfWeek.Monday,
+ DayOfWeek.Tuesday,
+ DayOfWeek.Wednesday,
+ DayOfWeek.Thursday,
+ DayOfWeek.Friday,
+ DayOfWeek.Saturday
+ };
+ }
+ else
+ {
+ item.AirDays = new DayOfWeek[] {
+ (DayOfWeek)Enum.Parse(typeof(DayOfWeek), day, true)
+ };
+ }
+ }
+
+ break;
+ }
+
+ case "Airs_Time":
+ item.AirTime = reader.ReadElementContentAsString();
+ break;
+
+ case "SeriesName":
+ item.Name = reader.ReadElementContentAsString();
+ break;
+
+ case "Status":
+ item.Status = reader.ReadElementContentAsString();
+ break;
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/VideoInfoProvider.cs b/MediaBrowser.Controller/Providers/VideoInfoProvider.cs new file mode 100644 index 000000000..264825fe0 --- /dev/null +++ b/MediaBrowser.Controller/Providers/VideoInfoProvider.cs @@ -0,0 +1,168 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.FFMpeg;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Providers
+{
+ [Export(typeof(BaseMetadataProvider))]
+ public class VideoInfoProvider : BaseMediaInfoProvider<Video>
+ {
+ 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; }
+ }
+
+ protected override string CacheDirectory
+ {
+ get { return Kernel.Instance.ApplicationPaths.FFProbeVideoCacheDirectory; }
+ }
+
+ protected override void Fetch(Video video, FFProbeResult data)
+ {
+ if (data.format != null)
+ {
+ if (!string.IsNullOrEmpty(data.format.duration))
+ {
+ video.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration)).Ticks;
+ }
+
+ if (!string.IsNullOrEmpty(data.format.bit_rate))
+ {
+ video.BitRate = int.Parse(data.format.bit_rate);
+ }
+ }
+
+ if (data.streams != null)
+ {
+ // For now, only read info about first video stream
+ // Files with multiple video streams are possible, but extremely rare
+ bool foundVideo = false;
+
+ foreach (MediaStream stream in data.streams)
+ {
+ if (stream.codec_type.Equals("video", StringComparison.OrdinalIgnoreCase))
+ {
+ if (!foundVideo)
+ {
+ FetchFromVideoStream(video, stream);
+ }
+
+ foundVideo = true;
+ }
+ else if (stream.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase))
+ {
+ FetchFromAudioStream(video, stream);
+ }
+ }
+ }
+ }
+
+ private void FetchFromVideoStream(Video video, MediaStream stream)
+ {
+ video.Codec = stream.codec_name;
+ video.Width = stream.width;
+ video.Height = stream.height;
+ video.AspectRatio = stream.display_aspect_ratio;
+
+ if (!string.IsNullOrEmpty(stream.avg_frame_rate))
+ {
+ string[] parts = stream.avg_frame_rate.Split('/');
+
+ if (parts.Length == 2)
+ {
+ video.FrameRate = float.Parse(parts[0]) / float.Parse(parts[1]);
+ }
+ else
+ {
+ video.FrameRate = float.Parse(parts[0]);
+ }
+ }
+ }
+
+ private void FetchFromAudioStream(Video video, MediaStream stream)
+ {
+ var audio = new AudioStream{};
+
+ audio.Codec = stream.codec_name;
+
+ if (!string.IsNullOrEmpty(stream.bit_rate))
+ {
+ audio.BitRate = int.Parse(stream.bit_rate);
+ }
+
+ audio.Channels = stream.channels;
+
+ if (!string.IsNullOrEmpty(stream.sample_rate))
+ {
+ audio.SampleRate = int.Parse(stream.sample_rate);
+ }
+
+ audio.Language = GetDictionaryValue(stream.tags, "language");
+
+ List<AudioStream> streams = video.AudioStreams ?? new List<AudioStream>();
+ streams.Add(audio);
+ video.AudioStreams = streams;
+ }
+
+ private void FetchFromSubtitleStream(Video video, MediaStream stream)
+ {
+ var subtitle = new SubtitleStream{};
+
+ subtitle.Language = GetDictionaryValue(stream.tags, "language");
+
+ List<SubtitleStream> streams = video.Subtitles ?? new List<SubtitleStream>();
+ streams.Add(subtitle);
+ video.Subtitles = streams;
+ }
+
+ /// <summary>
+ /// Determines if there's already enough info in the Video object to allow us to skip running ffprobe
+ /// </summary>
+ protected override bool CanSkipFFProbe(Video video)
+ {
+ if (video.VideoType != VideoType.VideoFile)
+ {
+ // Not supported yet
+ return true;
+ }
+
+ if (video.AudioStreams == null || !video.AudioStreams.Any())
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(video.AspectRatio))
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(video.Codec))
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(video.ScanType))
+ {
+ return false;
+ }
+
+ if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value == 0)
+ {
+ return false;
+ }
+
+ if (Convert.ToInt32(video.FrameRate) == 0 || video.Height == 0 || video.Width == 0 || video.BitRate == 0)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/AudioResolver.cs b/MediaBrowser.Controller/Resolvers/AudioResolver.cs new file mode 100644 index 000000000..8f10e45e5 --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/AudioResolver.cs @@ -0,0 +1,54 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class AudioResolver : BaseItemResolver<Audio>
+ {
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+
+ protected override Audio Resolve(ItemResolveEventArgs args)
+ {
+ // Return audio if the path is a file and has a matching extension
+
+ if (!args.IsDirectory)
+ {
+ if (IsAudioFile(args.Path))
+ {
+ return new Audio();
+ }
+ }
+
+ return null;
+ }
+
+ private static bool IsAudioFile(string path)
+ {
+ string extension = Path.GetExtension(path).ToLower();
+
+ switch (extension)
+ {
+ case ".mp3":
+ case ".wma":
+ case ".aac":
+ case ".acc":
+ case ".flac":
+ case ".m4a":
+ case ".m4b":
+ case ".wav":
+ case ".ape":
+ return true;
+
+ default:
+ return false;
+ }
+
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs new file mode 100644 index 000000000..7c9677e4e --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs @@ -0,0 +1,126 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Common.Extensions;
+using System;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ public abstract class BaseItemResolver<T> : IBaseItemResolver
+ where T : BaseItem, new()
+ {
+ protected virtual T Resolve(ItemResolveEventArgs args)
+ {
+ return null;
+ }
+
+ public virtual ResolverPriority Priority
+ {
+ get
+ {
+ return ResolverPriority.First;
+ }
+ }
+
+ /// <summary>
+ /// Sets initial values on the newly resolved item
+ /// </summary>
+ protected virtual void SetInitialItemValues(T item, ItemResolveEventArgs args)
+ {
+ // If the subclass didn't specify this
+ if (string.IsNullOrEmpty(item.Path))
+ {
+ item.Path = args.Path;
+ }
+
+ // If the subclass didn't specify this
+ if (args.Parent != null)
+ {
+ item.Parent = args.Parent;
+ }
+
+ item.Id = (item.GetType().FullName + item.Path).GetMD5();
+ }
+
+ public BaseItem ResolvePath(ItemResolveEventArgs args)
+ {
+ T item = Resolve(args);
+
+ if (item != null)
+ {
+ // Set initial values on the newly resolved item
+ SetInitialItemValues(item, args);
+
+ // Make sure the item has a name
+ EnsureName(item);
+
+ // Make sure DateCreated and DateModified have values
+ EnsureDates(item, args);
+ }
+
+ return item;
+ }
+
+ private void EnsureName(T item)
+ {
+ // If the subclass didn't supply a name, add it here
+ if (string.IsNullOrEmpty(item.Name))
+ {
+ item.Name = Path.GetFileNameWithoutExtension(item.Path);
+ }
+
+ }
+
+ /// <summary>
+ /// Ensures DateCreated and DateModified have values
+ /// </summary>
+ private void EnsureDates(T item, ItemResolveEventArgs args)
+ {
+ if (!Path.IsPathRooted(item.Path))
+ {
+ return;
+ }
+
+ // See if a different path came out of the resolver than what went in
+ if (!args.Path.Equals(item.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ WIN32_FIND_DATA? childData = args.GetFileSystemEntry(item.Path);
+
+ if (childData != null)
+ {
+ item.DateCreated = childData.Value.CreationTimeUtc;
+ item.DateModified = childData.Value.LastWriteTimeUtc;
+ }
+ else
+ {
+ WIN32_FIND_DATA fileData = FileData.GetFileData(item.Path);
+ item.DateCreated = fileData.CreationTimeUtc;
+ item.DateModified = fileData.LastWriteTimeUtc;
+ }
+ }
+ else
+ {
+ item.DateCreated = args.FileInfo.CreationTimeUtc;
+ item.DateModified = args.FileInfo.LastWriteTimeUtc;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Weed this to keep a list of resolvers, since Resolvers are built with generics
+ /// </summary>
+ public interface IBaseItemResolver
+ {
+ BaseItem ResolvePath(ItemResolveEventArgs args);
+ ResolverPriority Priority { get; }
+ }
+
+ public enum ResolverPriority
+ {
+ First = 1,
+ Second = 2,
+ Third = 3,
+ Last = 4
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs new file mode 100644 index 000000000..b821f8801 --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs @@ -0,0 +1,70 @@ +using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Entities.TV;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ public static class EntityResolutionHelper
+ {
+ /// <summary>
+ /// Any folder named in this list will be ignored - can be added to at runtime for extensibility
+ /// </summary>
+ public static List<string> IgnoreFolders = new List<string>()
+ {
+ "trailers",
+ "metadata",
+ "bdmv",
+ "certificate",
+ "backup",
+ "video_ts",
+ "audio_ts",
+ "ps3_update",
+ "ps3_vprm",
+ "adv_obj",
+ "hvdvd_ts"
+ };
+ /// <summary>
+ /// Determines whether a path should be resolved or ignored entirely - called before we even look at the contents
+ /// </summary>
+ /// <param name="path"></param>
+ /// <returns>false if the path should be ignored</returns>
+ public static bool ShouldResolvePath(WIN32_FIND_DATA path)
+ {
+ bool resolve = true;
+ // Ignore hidden files and folders
+ if (path.IsHidden || path.IsSystemFile)
+ {
+ resolve = false;
+ }
+
+ // Ignore any folders in our list
+ else if (path.IsDirectory && IgnoreFolders.Contains(Path.GetFileName(path.Path), StringComparer.OrdinalIgnoreCase))
+ {
+ resolve = false;
+ }
+
+ return resolve;
+ }
+
+ /// <summary>
+ /// Determines whether a path should be ignored based on its contents - called after the contents have been read
+ /// </summary>
+ public static bool ShouldResolvePathContents(ItemResolveEventArgs args)
+ {
+ bool resolve = true;
+ if (args.ContainsFile(".ignore"))
+ {
+ // Ignore any folders containing a file called .ignore
+ resolve = false;
+ }
+
+ return resolve;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/FolderResolver.cs b/MediaBrowser.Controller/Resolvers/FolderResolver.cs new file mode 100644 index 000000000..028c85f86 --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/FolderResolver.cs @@ -0,0 +1,36 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class FolderResolver : BaseFolderResolver<Folder>
+ {
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+
+ protected override Folder Resolve(ItemResolveEventArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ return new Folder();
+ }
+
+ return null;
+ }
+ }
+
+ public abstract class BaseFolderResolver<TItemType> : BaseItemResolver<TItemType>
+ where TItemType : Folder, new()
+ {
+ protected override void SetInitialItemValues(TItemType item, ItemResolveEventArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ item.IsRoot = args.Parent == null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs b/MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs new file mode 100644 index 000000000..069068067 --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/Movies/BoxSetResolver.cs @@ -0,0 +1,28 @@ +using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers.Movies
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class BoxSetResolver : BaseFolderResolver<BoxSet>
+ {
+ protected override BoxSet Resolve(ItemResolveEventArgs args)
+ {
+ // It's a boxset if all of the following conditions are met:
+ // Is a Directory
+ // Contains [boxset] in the path
+ if (args.IsDirectory)
+ {
+ if (Path.GetFileName(args.Path).IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return new BoxSet();
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs new file mode 100644 index 000000000..825850b20 --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/Movies/MovieResolver.cs @@ -0,0 +1,116 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System.ComponentModel.Composition;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Resolvers.Movies
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class MovieResolver : BaseVideoResolver<Movie>
+ {
+ protected override Movie Resolve(ItemResolveEventArgs args)
+ {
+ // Must be a directory and under a 'Movies' VF
+ if (args.IsDirectory)
+ {
+ // If the parent is not a boxset, the only other allowed parent type is Folder
+ if (!(args.Parent is BoxSet))
+ {
+ if (args.Parent != null && args.Parent.GetType() != typeof(Folder))
+ {
+ return null;
+ }
+ }
+
+ // Optimization to avoid running all these tests against VF's
+ if (args.Parent != null && args.Parent.IsRoot)
+ {
+ return null;
+ }
+
+ // Return a movie if the video resolver finds something in the folder
+ return GetMovie(args);
+ }
+
+ return null;
+ }
+
+ protected override void SetInitialItemValues(Movie item, ItemResolveEventArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ SetProviderIdFromPath(item);
+ }
+
+ private void SetProviderIdFromPath(Movie item)
+ {
+ const string srch = "[tmdbid=";
+ int index = item.Path.IndexOf(srch, System.StringComparison.OrdinalIgnoreCase);
+
+ if (index != -1)
+ {
+ string id = item.Path.Substring(index + srch.Length);
+
+ id = id.Substring(0, id.IndexOf(']'));
+
+ item.SetProviderId(MetadataProviders.Tmdb, id);
+ }
+ }
+
+ private Movie GetMovie(ItemResolveEventArgs args)
+ {
+ //first see if the discovery process has already determined we are a DVD or BD
+ if (args.IsDVDFolder)
+ {
+ return new Movie()
+ {
+ Path = args.Path,
+ VideoType = VideoType.Dvd
+ };
+ }
+ else if (args.IsBDFolder)
+ {
+ return new Movie()
+ {
+ Path = args.Path,
+ VideoType = VideoType.BluRay
+ };
+ }
+ else if (args.IsHDDVDFolder)
+ {
+ return new Movie()
+ {
+ Path = args.Path,
+ VideoType = VideoType.HdDvd
+ };
+ }
+
+ // Loop through each child file/folder and see if we find a video
+ foreach (var child in args.FileSystemChildren)
+ {
+ var childArgs = new ItemResolveEventArgs
+ {
+ FileInfo = child,
+ FileSystemChildren = new WIN32_FIND_DATA[] { },
+ Path = child.Path
+ };
+
+ var item = base.Resolve(childArgs);
+
+ if (item != null)
+ {
+ return new Movie
+ {
+ Path = item.Path,
+ VideoType = item.VideoType
+ };
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs b/MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs new file mode 100644 index 000000000..0961edd1a --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/TV/EpisodeResolver.cs @@ -0,0 +1,21 @@ +using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class EpisodeResolver : BaseVideoResolver<Episode>
+ {
+ protected override Episode Resolve(ItemResolveEventArgs args)
+ {
+ // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
+ if (args.Parent is Season || args.Parent is Series)
+ {
+ return base.Resolve(args);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs b/MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs new file mode 100644 index 000000000..0ad0782e0 --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/TV/SeasonResolver.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class SeasonResolver : BaseFolderResolver<Season>
+ {
+ protected override Season Resolve(ItemResolveEventArgs args)
+ {
+ if (args.Parent is Series && args.IsDirectory)
+ {
+ var season = new Season { };
+
+ season.IndexNumber = TVUtils.GetSeasonNumberFromPath(args.Path);
+
+ return season;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs b/MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs new file mode 100644 index 000000000..b8ff2c37b --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/TV/SeriesResolver.cs @@ -0,0 +1,64 @@ +using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ [Export(typeof(IBaseItemResolver))]
+ public class SeriesResolver : BaseFolderResolver<Series>
+ {
+ protected override Series Resolve(ItemResolveEventArgs args)
+ {
+ if (args.IsDirectory)
+ {
+ // Optimization to avoid running all these tests against VF's
+ if (args.Parent != null && args.Parent.IsRoot)
+ {
+ return null;
+ }
+
+ // Optimization to avoid running these tests against Seasons
+ if (args.Parent is Series)
+ {
+ return null;
+ }
+
+ // It's a Series if any of the following conditions are met:
+ // series.xml exists
+ // [tvdbid= is present in the path
+ // TVUtils.IsSeriesFolder returns true
+ if (args.ContainsFile("series.xml") || Path.GetFileName(args.Path).IndexOf("[tvdbid=", StringComparison.OrdinalIgnoreCase) != -1 || TVUtils.IsSeriesFolder(args.Path, args.FileSystemChildren))
+ {
+ return new Series();
+ }
+ }
+
+ return null;
+ }
+
+ protected override void SetInitialItemValues(Series item, ItemResolveEventArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ SetProviderIdFromPath(item);
+ }
+
+ private void SetProviderIdFromPath(Series item)
+ {
+ const string srch = "[tvdbid=";
+ int index = item.Path.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+
+ if (index != -1)
+ {
+ string id = item.Path.Substring(index + srch.Length);
+
+ id = id.Substring(0, id.IndexOf(']'));
+
+ item.SetProviderId(MetadataProviders.Tvdb, id);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/TV/TVUtils.cs b/MediaBrowser.Controller/Resolvers/TV/TVUtils.cs new file mode 100644 index 000000000..ec3305e16 --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/TV/TVUtils.cs @@ -0,0 +1,164 @@ +using MediaBrowser.Controller.IO;
+using System;
+using System.Text.RegularExpressions;
+
+namespace MediaBrowser.Controller.Resolvers.TV
+{
+ public static class TVUtils
+ {
+ /// <summary>
+ /// A season folder must contain one of these somewhere in the name
+ /// </summary>
+ private static readonly string[] SeasonFolderNames = new string[] {
+ "season",
+ "sæson",
+ "temporada",
+ "saison",
+ "staffel"
+ };
+
+ /// <summary>
+ /// Used to detect paths that represent episodes, need to make sure they don't also
+ /// match movie titles like "2001 A Space..."
+ /// Currently we limit the numbers here to 2 digits to try and avoid this
+ /// </summary>
+ /// <remarks>
+ /// The order here is important, if the order is changed some of the later
+ /// ones might incorrectly match things that higher ones would have caught.
+ /// The most restrictive expressions should appear first
+ /// </remarks>
+ private static readonly Regex[] episodeExpressions = new Regex[] {
+ new Regex(@".*\\[s|S]?(?<seasonnumber>\d{1,2})[x|X](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled), // 01x02 blah.avi S01x01 balh.avi
+ new Regex(@".*\\[s|S](?<seasonnumber>\d{1,2})x?[e|E](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled), // S01E02 blah.avi, S01xE01 blah.avi
+ new Regex(@".*\\(?<seriesname>[^\\]*)[s|S]?(?<seasonnumber>\d{1,2})[x|X](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled), // 01x02 blah.avi S01x01 balh.avi
+ new Regex(@".*\\(?<seriesname>[^\\]*)[s|S](?<seasonnumber>\d{1,2})[x|X|\.]?[e|E](?<epnumber>\d{1,3})[^\\]*$", RegexOptions.Compiled) // S01E02 blah.avi, S01xE01 blah.avi
+ };
+ /// <summary>
+ /// To avoid the following matching movies they are only valid when contained in a folder which has been matched as a being season
+ /// </summary>
+ private static readonly Regex[] episodeExpressionsInASeasonFolder = new Regex[] {
+ new Regex(@".*\\(?<epnumber>\d{1,2})\s?-\s?[^\\]*$", RegexOptions.Compiled), // 01 - blah.avi, 01-blah.avi
+ new Regex(@".*\\(?<epnumber>\d{1,2})[^\d\\]*[^\\]*$", RegexOptions.Compiled), // 01.avi, 01.blah.avi "01 - 22 blah.avi"
+ new Regex(@".*\\(?<seasonnumber>\d)(?<epnumber>\d{1,2})[^\d\\]+[^\\]*$", RegexOptions.Compiled), // 01.avi, 01.blah.avi
+ new Regex(@".*\\\D*\d+(?<epnumber>\d{2})", RegexOptions.Compiled) // hell0 - 101 - hello.avi
+
+ };
+
+ public static int? GetSeasonNumberFromPath(string path)
+ {
+ // Look for one of the season folder names
+ foreach (string name in SeasonFolderNames)
+ {
+ int index = path.IndexOf(name, StringComparison.OrdinalIgnoreCase);
+
+ if (index != -1)
+ {
+ return GetSeasonNumberFromPathSubstring(path.Substring(index + name.Length));
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Extracts the season number from the second half of the Season folder name (everything after "Season", or "Staffel")
+ /// </summary>
+ private static int? GetSeasonNumberFromPathSubstring(string path)
+ {
+ int numericStart = -1;
+ int length = 0;
+
+ // Find out where the numbers start, and then keep going until they end
+ for (int i = 0; i < path.Length; i++)
+ {
+ if (char.IsNumber(path, i))
+ {
+ if (numericStart == -1)
+ {
+ numericStart = i;
+ }
+ length++;
+ }
+ else if (numericStart != -1)
+ {
+ break;
+ }
+ }
+
+ if (numericStart == -1)
+ {
+ return null;
+ }
+
+ return int.Parse(path.Substring(numericStart, length));
+ }
+
+ public static bool IsSeasonFolder(string path)
+ {
+ return GetSeasonNumberFromPath(path) != null;
+ }
+
+ public static bool IsSeriesFolder(string path, WIN32_FIND_DATA[] fileSystemChildren)
+ {
+ // A folder with more than 3 non-season folders in will not becounted as a series
+ int nonSeriesFolders = 0;
+
+ for (int i = 0; i < fileSystemChildren.Length; i++)
+ {
+ var child = fileSystemChildren[i];
+
+ if (child.IsHidden || child.IsSystemFile)
+ {
+ continue;
+ }
+
+ if (child.IsDirectory)
+ {
+ if (IsSeasonFolder(child.Path))
+ {
+ return true;
+ }
+
+ nonSeriesFolders++;
+
+ if (nonSeriesFolders >= 3)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (FileSystemHelper.IsVideoFile(child.Path) && !string.IsNullOrEmpty(EpisodeNumberFromFile(child.Path, false)))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static string EpisodeNumberFromFile(string fullPath, bool isInSeason)
+ {
+ string fl = fullPath.ToLower();
+ foreach (Regex r in episodeExpressions)
+ {
+ Match m = r.Match(fl);
+ if (m.Success)
+ return m.Groups["epnumber"].Value;
+ }
+ if (isInSeason)
+ {
+ foreach (Regex r in episodeExpressionsInASeasonFolder)
+ {
+ Match m = r.Match(fl);
+ if (m.Success)
+ return m.Groups["epnumber"].Value;
+ }
+
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/VideoResolver.cs b/MediaBrowser.Controller/Resolvers/VideoResolver.cs new file mode 100644 index 000000000..bc3be5e43 --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/VideoResolver.cs @@ -0,0 +1,100 @@ +using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.IO;
+using System.ComponentModel.Composition;
+using System.IO;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Resolves a Path into a Video
+ /// </summary>
+ [Export(typeof(IBaseItemResolver))]
+ public class VideoResolver : BaseVideoResolver<Video>
+ {
+ public override ResolverPriority Priority
+ {
+ get { return ResolverPriority.Last; }
+ }
+ }
+
+ /// <summary>
+ /// Resolves a Path into a Video or Video subclass
+ /// </summary>
+ public abstract class BaseVideoResolver<T> : BaseItemResolver<T>
+ where T : Video, new()
+ {
+ protected override T Resolve(ItemResolveEventArgs args)
+ {
+ // If the path is a file check for a matching extensions
+ if (!args.IsDirectory)
+ {
+ if (FileSystemHelper.IsVideoFile(args.Path))
+ {
+ VideoType type = Path.GetExtension(args.Path).EndsWith("iso", System.StringComparison.OrdinalIgnoreCase) ? VideoType.Iso : VideoType.VideoFile;
+
+ return new T
+ {
+ VideoType = type,
+ Path = args.Path
+ };
+ }
+ }
+
+ else
+ {
+ // If the path is a folder, check if it's bluray or dvd
+ T item = ResolveFromFolderName(args.Path);
+
+ if (item != null)
+ {
+ return item;
+ }
+
+ // Also check the subfolders for bluray or dvd
+ for (int i = 0; i < args.FileSystemChildren.Length; i++)
+ {
+ var folder = args.FileSystemChildren[i];
+
+ if (!folder.IsDirectory)
+ {
+ continue;
+ }
+
+ item = ResolveFromFolderName(folder.Path);
+
+ if (item != null)
+ {
+ return item;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private T ResolveFromFolderName(string folder)
+ {
+ if (folder.IndexOf("video_ts", System.StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return new T
+ {
+ VideoType = VideoType.Dvd,
+ Path = Path.GetDirectoryName(folder)
+ };
+ }
+ if (folder.IndexOf("bdmv", System.StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return new T
+ {
+ VideoType = VideoType.BluRay,
+ Path = Path.GetDirectoryName(folder)
+ };
+ }
+
+ return null;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/ServerApplicationPaths.cs b/MediaBrowser.Controller/ServerApplicationPaths.cs new file mode 100644 index 000000000..f657ee259 --- /dev/null +++ b/MediaBrowser.Controller/ServerApplicationPaths.cs @@ -0,0 +1,278 @@ +using System.IO;
+using MediaBrowser.Common.Kernel;
+
+namespace MediaBrowser.Controller
+{
+ /// <summary>
+ /// Extends BaseApplicationPaths to add paths that are only applicable on the server
+ /// </summary>
+ public class ServerApplicationPaths : BaseApplicationPaths
+ {
+ private string _rootFolderPath;
+ /// <summary>
+ /// Gets the path to the root media directory
+ /// </summary>
+ public string RootFolderPath
+ {
+ get
+ {
+ if (_rootFolderPath == null)
+ {
+ _rootFolderPath = Path.Combine(ProgramDataPath, "root");
+ if (!Directory.Exists(_rootFolderPath))
+ {
+ Directory.CreateDirectory(_rootFolderPath);
+ }
+ }
+ return _rootFolderPath;
+ }
+ }
+
+ private string _ibnPath;
+ /// <summary>
+ /// Gets the path to the Images By Name directory
+ /// </summary>
+ public string ImagesByNamePath
+ {
+ get
+ {
+ if (_ibnPath == null)
+ {
+ _ibnPath = Path.Combine(ProgramDataPath, "ImagesByName");
+ if (!Directory.Exists(_ibnPath))
+ {
+ Directory.CreateDirectory(_ibnPath);
+ }
+ }
+
+ return _ibnPath;
+ }
+ }
+
+ private string _PeoplePath;
+ /// <summary>
+ /// Gets the path to the People directory
+ /// </summary>
+ public string PeoplePath
+ {
+ get
+ {
+ if (_PeoplePath == null)
+ {
+ _PeoplePath = Path.Combine(ImagesByNamePath, "People");
+ if (!Directory.Exists(_PeoplePath))
+ {
+ Directory.CreateDirectory(_PeoplePath);
+ }
+ }
+
+ return _PeoplePath;
+ }
+ }
+
+ private string _GenrePath;
+ /// <summary>
+ /// Gets the path to the Genre directory
+ /// </summary>
+ public string GenrePath
+ {
+ get
+ {
+ if (_GenrePath == null)
+ {
+ _GenrePath = Path.Combine(ImagesByNamePath, "Genre");
+ if (!Directory.Exists(_GenrePath))
+ {
+ Directory.CreateDirectory(_GenrePath);
+ }
+ }
+
+ return _GenrePath;
+ }
+ }
+
+ private string _StudioPath;
+ /// <summary>
+ /// Gets the path to the Studio directory
+ /// </summary>
+ public string StudioPath
+ {
+ get
+ {
+ if (_StudioPath == null)
+ {
+ _StudioPath = Path.Combine(ImagesByNamePath, "Studio");
+ if (!Directory.Exists(_StudioPath))
+ {
+ Directory.CreateDirectory(_StudioPath);
+ }
+ }
+
+ return _StudioPath;
+ }
+ }
+
+ private string _yearPath;
+ /// <summary>
+ /// Gets the path to the Year directory
+ /// </summary>
+ public string YearPath
+ {
+ get
+ {
+ if (_yearPath == null)
+ {
+ _yearPath = Path.Combine(ImagesByNamePath, "Year");
+ if (!Directory.Exists(_yearPath))
+ {
+ Directory.CreateDirectory(_yearPath);
+ }
+ }
+
+ return _yearPath;
+ }
+ }
+
+ private string _userConfigurationDirectoryPath;
+ /// <summary>
+ /// Gets the path to the user configuration directory
+ /// </summary>
+ public string UserConfigurationDirectoryPath
+ {
+ get
+ {
+ if (_userConfigurationDirectoryPath == null)
+ {
+ _userConfigurationDirectoryPath = Path.Combine(ConfigurationDirectoryPath, "user");
+ if (!Directory.Exists(_userConfigurationDirectoryPath))
+ {
+ Directory.CreateDirectory(_userConfigurationDirectoryPath);
+ }
+ }
+ return _userConfigurationDirectoryPath;
+ }
+ }
+
+ private string _CacheDirectory;
+ /// <summary>
+ /// Gets the folder path to the cache directory
+ /// </summary>
+ public string CacheDirectory
+ {
+ get
+ {
+ if (_CacheDirectory == null)
+ {
+ _CacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.ProgramDataPath, "cache");
+
+ if (!Directory.Exists(_CacheDirectory))
+ {
+ Directory.CreateDirectory(_CacheDirectory);
+ }
+ }
+
+ return _CacheDirectory;
+ }
+ }
+
+ private string _FFProbeAudioCacheDirectory;
+ /// <summary>
+ /// Gets the folder path to the ffprobe audio cache directory
+ /// </summary>
+ public string FFProbeAudioCacheDirectory
+ {
+ get
+ {
+ if (_FFProbeAudioCacheDirectory == null)
+ {
+ _FFProbeAudioCacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.CacheDirectory, "ffprobe-audio");
+
+ if (!Directory.Exists(_FFProbeAudioCacheDirectory))
+ {
+ Directory.CreateDirectory(_FFProbeAudioCacheDirectory);
+ }
+ }
+
+ return _FFProbeAudioCacheDirectory;
+ }
+ }
+
+ private string _FFProbeVideoCacheDirectory;
+ /// <summary>
+ /// Gets the folder path to the ffprobe video cache directory
+ /// </summary>
+ public string FFProbeVideoCacheDirectory
+ {
+ get
+ {
+ if (_FFProbeVideoCacheDirectory == null)
+ {
+ _FFProbeVideoCacheDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.CacheDirectory, "ffprobe-video");
+
+ if (!Directory.Exists(_FFProbeVideoCacheDirectory))
+ {
+ Directory.CreateDirectory(_FFProbeVideoCacheDirectory);
+ }
+ }
+
+ return _FFProbeVideoCacheDirectory;
+ }
+ }
+
+ private string _FFMpegDirectory;
+ /// <summary>
+ /// Gets the folder path to ffmpeg
+ /// </summary>
+ public string FFMpegDirectory
+ {
+ get
+ {
+ if (_FFMpegDirectory == null)
+ {
+ _FFMpegDirectory = Path.Combine(Kernel.Instance.ApplicationPaths.ProgramDataPath, "FFMpeg");
+
+ if (!Directory.Exists(_FFMpegDirectory))
+ {
+ Directory.CreateDirectory(_FFMpegDirectory);
+ }
+ }
+
+ return _FFMpegDirectory;
+ }
+ }
+
+ private string _FFMpegPath;
+ /// <summary>
+ /// Gets the path to ffmpeg.exe
+ /// </summary>
+ public string FFMpegPath
+ {
+ get
+ {
+ if (_FFMpegPath == null)
+ {
+ _FFMpegPath = Path.Combine(FFMpegDirectory, "ffmpeg.exe");
+ }
+
+ return _FFMpegPath;
+ }
+ }
+
+ private string _FFProbePath;
+ /// <summary>
+ /// Gets the path to ffprobe.exe
+ /// </summary>
+ public string FFProbePath
+ {
+ get
+ {
+ if (_FFProbePath == null)
+ {
+ _FFProbePath = Path.Combine(FFMpegDirectory, "ffprobe.exe");
+ }
+
+ return _FFProbePath;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Weather/BaseWeatherProvider.cs b/MediaBrowser.Controller/Weather/BaseWeatherProvider.cs new file mode 100644 index 000000000..c3d436e66 --- /dev/null +++ b/MediaBrowser.Controller/Weather/BaseWeatherProvider.cs @@ -0,0 +1,34 @@ +using MediaBrowser.Common.Logging;
+using MediaBrowser.Model.Weather;
+using System;
+using System.Net;
+using System.Net.Cache;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Weather
+{
+ public abstract class BaseWeatherProvider : IDisposable
+ {
+ protected HttpClient HttpClient { get; private set; }
+
+ protected BaseWeatherProvider()
+ {
+ var handler = new WebRequestHandler { };
+
+ handler.AutomaticDecompression = DecompressionMethods.Deflate;
+ handler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Revalidate);
+
+ HttpClient = new HttpClient(handler);
+ }
+
+ public virtual void Dispose()
+ {
+ Logger.LogInfo("Disposing " + GetType().Name);
+
+ HttpClient.Dispose();
+ }
+
+ public abstract Task<WeatherInfo> GetWeatherInfoAsync(string zipCode);
+ }
+}
diff --git a/MediaBrowser.Controller/Weather/WeatherProvider.cs b/MediaBrowser.Controller/Weather/WeatherProvider.cs new file mode 100644 index 000000000..0fc728879 --- /dev/null +++ b/MediaBrowser.Controller/Weather/WeatherProvider.cs @@ -0,0 +1,189 @@ +using MediaBrowser.Common.Logging;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Model.Weather;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Weather
+{
+ /// <summary>
+ /// Based on http://www.worldweatheronline.com/free-weather-feed.aspx
+ /// The classes in this file are a reproduction of the json output, which will then be converted to our weather model classes
+ /// </summary>
+ [Export(typeof(BaseWeatherProvider))]
+ public class WeatherProvider : BaseWeatherProvider
+ {
+ public override async Task<WeatherInfo> GetWeatherInfoAsync(string zipCode)
+ {
+ if (string.IsNullOrWhiteSpace(zipCode))
+ {
+ return null;
+ }
+
+ const int numDays = 5;
+ const string apiKey = "24902f60f1231941120109";
+
+ string url = "http://free.worldweatheronline.com/feed/weather.ashx?q=" + zipCode + "&format=json&num_of_days=" + numDays + "&key=" + apiKey;
+
+ Logger.LogInfo("Accessing weather from " + url);
+
+ using (Stream stream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false))
+ {
+ WeatherData data = JsonSerializer.DeserializeFromStream<WeatherResult>(stream).data;
+
+ return GetWeatherInfo(data);
+ }
+ }
+
+ /// <summary>
+ /// Converst the json output to our WeatherInfo model class
+ /// </summary>
+ private WeatherInfo GetWeatherInfo(WeatherData data)
+ {
+ var info = new WeatherInfo();
+
+ if (data.current_condition != null)
+ {
+ if (data.current_condition.Any())
+ {
+ info.CurrentWeather = data.current_condition.First().ToWeatherStatus();
+ }
+ }
+
+ if (data.weather != null)
+ {
+ info.Forecasts = data.weather.Select(w => w.ToWeatherForecast()).ToArray();
+ }
+
+ return info;
+ }
+ }
+
+ class WeatherResult
+ {
+ public WeatherData data { get; set; }
+ }
+
+ public class WeatherData
+ {
+ public WeatherCondition[] current_condition { get; set; }
+ public DailyWeatherInfo[] weather { get; set; }
+ }
+
+ public class WeatherCondition
+ {
+ public string temp_C { get; set; }
+ public string temp_F { get; set; }
+ public string humidity { get; set; }
+ public string weatherCode { get; set; }
+
+ public WeatherStatus ToWeatherStatus()
+ {
+ return new WeatherStatus
+ {
+ TemperatureCelsius = int.Parse(temp_C),
+ TemperatureFahrenheit = int.Parse(temp_F),
+ Humidity = int.Parse(humidity),
+ Condition = DailyWeatherInfo.GetCondition(weatherCode)
+ };
+ }
+ }
+
+ public class DailyWeatherInfo
+ {
+ public string date { get; set; }
+ public string precipMM { get; set; }
+ public string tempMaxC { get; set; }
+ public string tempMaxF { get; set; }
+ public string tempMinC { get; set; }
+ public string tempMinF { get; set; }
+ public string weatherCode { get; set; }
+ public string winddir16Point { get; set; }
+ public string winddirDegree { get; set; }
+ public string winddirection { get; set; }
+ public string windspeedKmph { get; set; }
+ public string windspeedMiles { get; set; }
+
+ public WeatherForecast ToWeatherForecast()
+ {
+ return new WeatherForecast
+ {
+ Date = DateTime.Parse(date),
+ HighTemperatureCelsius = int.Parse(tempMaxC),
+ HighTemperatureFahrenheit = int.Parse(tempMaxF),
+ LowTemperatureCelsius = int.Parse(tempMinC),
+ LowTemperatureFahrenheit = int.Parse(tempMinF),
+ Condition = GetCondition(weatherCode)
+ };
+ }
+
+ public static WeatherConditions GetCondition(string weatherCode)
+ {
+ switch (weatherCode)
+ {
+ case "362":
+ case "365":
+ case "320":
+ case "317":
+ case "182":
+ return WeatherConditions.Sleet;
+ case "338":
+ case "335":
+ case "332":
+ case "329":
+ case "326":
+ case "323":
+ case "377":
+ case "374":
+ case "371":
+ case "368":
+ case "395":
+ case "392":
+ case "350":
+ case "227":
+ case "179":
+ return WeatherConditions.Snow;
+ case "314":
+ case "311":
+ case "308":
+ case "305":
+ case "302":
+ case "299":
+ case "296":
+ case "293":
+ case "284":
+ case "281":
+ case "266":
+ case "263":
+ case "359":
+ case "356":
+ case "353":
+ case "185":
+ case "176":
+ return WeatherConditions.Rain;
+ case "260":
+ case "248":
+ return WeatherConditions.Fog;
+ case "389":
+ case "386":
+ case "200":
+ return WeatherConditions.Thunderstorm;
+ case "230":
+ return WeatherConditions.Blizzard;
+ case "143":
+ return WeatherConditions.Mist;
+ case "122":
+ return WeatherConditions.Overcast;
+ case "119":
+ return WeatherConditions.Cloudy;
+ case "115":
+ return WeatherConditions.PartlyCloudy;
+ default:
+ return WeatherConditions.Sunny;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Xml/XmlExtensions.cs b/MediaBrowser.Controller/Xml/XmlExtensions.cs new file mode 100644 index 000000000..d2e8e1983 --- /dev/null +++ b/MediaBrowser.Controller/Xml/XmlExtensions.cs @@ -0,0 +1,46 @@ +using System.Globalization;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Xml
+{
+ public static class XmlExtensions
+ {
+ private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ /// <summary>
+ /// Reads a float from the current element of an XmlReader
+ /// </summary>
+ public static float ReadFloatSafe(this XmlReader reader)
+ {
+ string valueString = reader.ReadElementContentAsString();
+
+ float value = 0;
+
+ if (!string.IsNullOrWhiteSpace(valueString))
+ {
+ // float.TryParse is local aware, so it can be probamatic, force us culture
+ float.TryParse(valueString, NumberStyles.AllowDecimalPoint, _usCulture, out value);
+ }
+
+ return value;
+ }
+
+ /// <summary>
+ /// Reads an int from the current element of an XmlReader
+ /// </summary>
+ public static int ReadIntSafe(this XmlReader reader)
+ {
+ string valueString = reader.ReadElementContentAsString();
+
+ int value = 0;
+
+ if (!string.IsNullOrWhiteSpace(valueString))
+ {
+
+ int.TryParse(valueString, out value);
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/packages.config b/MediaBrowser.Controller/packages.config new file mode 100644 index 000000000..42f16a267 --- /dev/null +++ b/MediaBrowser.Controller/packages.config @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Rx-Core" version="2.0.20823" targetFramework="net45" />
+ <package id="Rx-Interfaces" version="2.0.20823" targetFramework="net45" />
+ <package id="Rx-Linq" version="2.0.20823" targetFramework="net45" />
+</packages>
\ No newline at end of file |
