From b50f78e5da6f3fdfc59e577ca61b88771da7d211 Mon Sep 17 00:00:00 2001 From: LukePulverenti Luke Pulverenti luke pulverenti Date: Thu, 12 Jul 2012 02:55:27 -0400 Subject: Initial check-in --- .../Events/ItemResolveEventArgs.cs | 94 ++++ MediaBrowser.Controller/IO/DirectoryWatchers.cs | 152 ++++++ MediaBrowser.Controller/IO/Shortcut.cs | 182 +++++++ MediaBrowser.Controller/Kernel.cs | 258 +++++++++ MediaBrowser.Controller/Library/ItemController.cs | 326 ++++++++++++ MediaBrowser.Controller/Library/ItemDataCache.cs | 32 ++ .../MediaBrowser.Controller.csproj | 92 ++++ .../Net/CollectionExtensions.cs | 14 + MediaBrowser.Controller/Net/HttpServer.cs | 47 ++ MediaBrowser.Controller/Net/Request.cs | 18 + MediaBrowser.Controller/Net/RequestContext.cs | 37 ++ MediaBrowser.Controller/Net/Response.cs | 49 ++ MediaBrowser.Controller/Net/StreamExtensions.cs | 20 + MediaBrowser.Controller/Properties/AssemblyInfo.cs | 36 ++ MediaBrowser.Controller/Resolvers/AudioResolver.cs | 44 ++ .../Resolvers/BaseItemResolver.cs | 146 +++++ .../Resolvers/FolderResolver.cs | 45 ++ MediaBrowser.Controller/Resolvers/VideoResolver.cs | 114 ++++ MediaBrowser.Controller/UserController.cs | 60 +++ MediaBrowser.Controller/Xml/BaseItemXmlParser.cs | 591 +++++++++++++++++++++ MediaBrowser.Controller/Xml/FolderXmlParser.cs | 8 + MediaBrowser.Controller/Xml/XmlExtensions.cs | 74 +++ MediaBrowser.Controller/packages.config | 5 + 23 files changed, 2444 insertions(+) create mode 100644 MediaBrowser.Controller/Events/ItemResolveEventArgs.cs create mode 100644 MediaBrowser.Controller/IO/DirectoryWatchers.cs create mode 100644 MediaBrowser.Controller/IO/Shortcut.cs create mode 100644 MediaBrowser.Controller/Kernel.cs create mode 100644 MediaBrowser.Controller/Library/ItemController.cs create mode 100644 MediaBrowser.Controller/Library/ItemDataCache.cs create mode 100644 MediaBrowser.Controller/MediaBrowser.Controller.csproj create mode 100644 MediaBrowser.Controller/Net/CollectionExtensions.cs create mode 100644 MediaBrowser.Controller/Net/HttpServer.cs create mode 100644 MediaBrowser.Controller/Net/Request.cs create mode 100644 MediaBrowser.Controller/Net/RequestContext.cs create mode 100644 MediaBrowser.Controller/Net/Response.cs create mode 100644 MediaBrowser.Controller/Net/StreamExtensions.cs create mode 100644 MediaBrowser.Controller/Properties/AssemblyInfo.cs create mode 100644 MediaBrowser.Controller/Resolvers/AudioResolver.cs create mode 100644 MediaBrowser.Controller/Resolvers/BaseItemResolver.cs create mode 100644 MediaBrowser.Controller/Resolvers/FolderResolver.cs create mode 100644 MediaBrowser.Controller/Resolvers/VideoResolver.cs create mode 100644 MediaBrowser.Controller/UserController.cs create mode 100644 MediaBrowser.Controller/Xml/BaseItemXmlParser.cs create mode 100644 MediaBrowser.Controller/Xml/FolderXmlParser.cs create mode 100644 MediaBrowser.Controller/Xml/XmlExtensions.cs create mode 100644 MediaBrowser.Controller/packages.config (limited to 'MediaBrowser.Controller') diff --git a/MediaBrowser.Controller/Events/ItemResolveEventArgs.cs b/MediaBrowser.Controller/Events/ItemResolveEventArgs.cs new file mode 100644 index 000000000..831eb29d4 --- /dev/null +++ b/MediaBrowser.Controller/Events/ItemResolveEventArgs.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Controller.Events +{ + public class ItemResolveEventArgs : PreBeginResolveEventArgs + { + public IEnumerable> FileSystemChildren { get; set; } + + public KeyValuePair? GetFolderByName(string name) + { + foreach (KeyValuePair entry in FileSystemChildren) + { + if (!entry.Value.HasFlag(FileAttributes.Directory)) + { + continue; + } + + if (System.IO.Path.GetFileName(entry.Key).Equals(name, StringComparison.OrdinalIgnoreCase)) + { + return entry; + } + } + + return null; + } + + public KeyValuePair? GetFileByName(string name) + { + foreach (KeyValuePair entry in FileSystemChildren) + { + if (entry.Value.HasFlag(FileAttributes.Directory)) + { + continue; + } + + if (System.IO.Path.GetFileName(entry.Key).Equals(name, StringComparison.OrdinalIgnoreCase)) + { + return entry; + } + } + + return null; + } + + public bool ContainsFile(string name) + { + return GetFileByName(name) != null; + } + + public bool ContainsFolder(string name) + { + return GetFolderByName(name) != null; + } + } + + public class PreBeginResolveEventArgs : EventArgs + { + public string Path { get; set; } + public BaseItem Parent { get; set; } + + public bool Cancel { get; set; } + + public FileAttributes FileAttributes { get; set; } + + public bool IsFolder + { + get + { + return FileAttributes.HasFlag(FileAttributes.Directory); + } + } + + public bool IsHidden + { + get + { + return FileAttributes.HasFlag(FileAttributes.Hidden); + } + } + + public bool IsSystemFile + { + get + { + return FileAttributes.HasFlag(FileAttributes.System); + } + } + + } +} diff --git a/MediaBrowser.Controller/IO/DirectoryWatchers.cs b/MediaBrowser.Controller/IO/DirectoryWatchers.cs new file mode 100644 index 000000000..dd2769583 --- /dev/null +++ b/MediaBrowser.Controller/IO/DirectoryWatchers.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.IO +{ + public class DirectoryWatchers + { + private List FileSystemWatchers = new List(); + private Timer updateTimer = null; + private List affectedPaths = new List(); + + private const int TimerDelayInSeconds = 5; + + public void Start() + { + List pathsToWatch = new List(); + + var rootFolder = Kernel.Instance.RootFolder; + + pathsToWatch.Add(rootFolder.Path); + + foreach (Folder folder in rootFolder.FolderChildren) + { + foreach (Folder subFolder in folder.FolderChildren) + { + if (Path.IsPathRooted(subFolder.Path)) + { + string parent = Path.GetDirectoryName(subFolder.Path); + + if (!pathsToWatch.Contains(parent)) + { + pathsToWatch.Add(parent); + } + } + } + } + + foreach (string path in pathsToWatch) + { + FileSystemWatcher 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. + //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) + { + if (!affectedPaths.Contains(e.FullPath)) + { + affectedPaths.Add(e.FullPath); + } + + if (updateTimer == null) + { + updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(TimerDelayInSeconds), TimeSpan.FromMilliseconds(-1)); + } + else + { + updateTimer.Change(TimeSpan.FromSeconds(TimerDelayInSeconds), TimeSpan.FromMilliseconds(-1)); + } + } + + private void TimerStopped(object stateInfo) + { + updateTimer.Dispose(); + updateTimer = null; + + List paths = affectedPaths; + affectedPaths = new List(); + + ProcessPathChanges(paths); + } + + private void ProcessPathChanges(IEnumerable paths) + { + List itemsToRefresh = new List(); + + 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; + })) + { + Kernel.Instance.ReloadRoot(); + } + else + { + Parallel.For(0, itemsToRefresh.Count, i => + { + Kernel.Instance.ReloadItem(itemsToRefresh[i]); + }); + } + } + + private BaseItem GetAffectedBaseItem(string path) + { + BaseItem item = null; + + while (item == null) + { + 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/Shortcut.cs b/MediaBrowser.Controller/IO/Shortcut.cs new file mode 100644 index 000000000..376d16a79 --- /dev/null +++ b/MediaBrowser.Controller/IO/Shortcut.cs @@ -0,0 +1,182 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace MediaBrowser.Controller.IO +{ + public static class Shortcut + { + #region Signitures were imported from http://pinvoke.net + [Flags()] + enum SLGP_FLAGS + { + /// Retrieves the standard short (8.3 format) file name + SLGP_SHORTPATH = 0x1, + /// Retrieves the Universal Naming Convention (UNC) path name of the file + SLGP_UNCPRIORITY = 0x2, + /// Retrieves the raw path name. A raw path is something that might not exist and may include environment variables that need to be expanded + 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 + { + /// + /// 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. + /// + SLR_NO_UI = 0x1, + /// Obsolete and no longer used + SLR_ANY_MATCH = 0x2, + /// 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. + SLR_UPDATE = 0x4, + /// Do not update the link information + SLR_NOUPDATE = 0x8, + /// Do not execute the search heuristics + SLR_NOSEARCH = 0x10, + /// Do not use distributed link tracking + SLR_NOTRACK = 0x20, + /// 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. + SLR_NOLINKINFO = 0x40, + /// Call the Microsoft Windows Installer + SLR_INVOKE_MSI = 0x80 + } + + + /// The IShellLink interface allows Shell links to be created, modified, and resolved + [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")] + interface IShellLinkW + { + /// Retrieves the path and file name of a Shell link object + void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags); + /// Retrieves the list of item identifiers for a Shell link object + void GetIDList(out IntPtr ppidl); + /// Sets the pointer to an item identifier list (PIDL) for a Shell link object. + void SetIDList(IntPtr pidl); + /// Retrieves the description string for a Shell link object + void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName); + /// Sets the description for a Shell link object. The description can be any application-defined string + void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); + /// Retrieves the name of the working directory for a Shell link object + void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath); + /// Sets the name of the working directory for a Shell link object + void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); + /// Retrieves the command-line arguments associated with a Shell link object + void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath); + /// Sets the command-line arguments for a Shell link object + void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); + /// Retrieves the hot key for a Shell link object + void GetHotkey(out short pwHotkey); + /// Sets a hot key for a Shell link object + void SetHotkey(short wHotkey); + /// Retrieves the show command for a Shell link object + void GetShowCmd(out int piShowCmd); + /// Sets the show command for a Shell link object. The show command sets the initial show state of the window. + void SetShowCmd(int iShowCmd); + /// Retrieves the location (path and index) of the icon for a Shell link object + void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, + int cchIconPath, out int piIcon); + /// Sets the location (path and index) of the icon for a Shell link object + void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); + /// Sets the relative path to the Shell link object + void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); + /// Attempts to find the target of a Shell link, even if it has been moved or renamed + void Resolve(IntPtr hwnd, SLR_FLAGS fFlags); + /// Sets the path and file name of a Shell link object + 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) + { + ShellLink 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) + StringBuilder sb = new StringBuilder(MAX_PATH); + WIN32_FIND_DATAW data = new WIN32_FIND_DATAW(); + ((IShellLinkW)link).GetPath(sb, sb.Capacity, out data, 0); + return sb.ToString(); + } + + public static bool IsShortcut(string filename) + { + return Path.GetExtension(filename).EndsWith("lnk", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/Kernel.cs b/MediaBrowser.Controller/Kernel.cs new file mode 100644 index 000000000..2bb78e7e7 --- /dev/null +++ b/MediaBrowser.Controller/Kernel.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Common.Json; +using MediaBrowser.Common.Logging; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Users; + +namespace MediaBrowser.Controller +{ + public class Kernel + { + public static Kernel Instance { get; private set; } + + public string DataPath { get; private set; } + + public HttpServer HttpServer { get; private set; } + public ItemDataCache ItemDataCache { get; private set; } + public ItemController ItemController { get; private set; } + public UserController UserController { get; private set; } + public PluginController PluginController { get; private set; } + + public Configuration Configuration { get; private set; } + public IEnumerable Plugins { get; private set; } + public IEnumerable Users { get; private set; } + public Folder RootFolder { get; private set; } + + private DirectoryWatchers DirectoryWatchers { get; set; } + + private string MediaRootFolderPath + { + get + { + return Path.Combine(DataPath, "Root"); + } + } + + /// + /// Creates a kernal based on a Data path, which is akin to our current programdata path + /// + public Kernel(string dataPath) + { + Instance = this; + + DataPath = dataPath; + + Logger.LoggerInstance = new FileLogger(Path.Combine(DataPath, "Logs")); + + ItemController = new ItemController(); + UserController = new UserController(Path.Combine(DataPath, "Users")); + PluginController = new PluginController(Path.Combine(DataPath, "Plugins")); + DirectoryWatchers = new DirectoryWatchers(); + ItemDataCache = new ItemDataCache(); + + ItemController.PreBeginResolvePath += ItemController_PreBeginResolvePath; + ItemController.BeginResolvePath += ItemController_BeginResolvePath; + + // Add support for core media types - audio, video, etc + AddBaseItemType(); + AddBaseItemType(); + AddBaseItemType(); + } + + /// + /// Tells the kernel to start spinning up + /// + public void Init() + { + ReloadConfiguration(); + + ReloadHttpServer(); + + ReloadPlugins(); + + // Get users from users folder + // Load root media folder + Parallel.Invoke(ReloadUsers, ReloadRoot); + var b = true; + } + + private void ReloadConfiguration() + { + // Deserialize config + Configuration = GetConfiguration(DataPath); + + Logger.LoggerInstance.LogSeverity = Configuration.LogSeverity; + } + + private void ReloadPlugins() + { + if (Plugins != null) + { + Parallel.For(0, Plugins.Count(), i => + { + Plugins.ElementAt(i).Dispose(); + }); + } + + // Find plugins + Plugins = PluginController.GetAllPlugins(); + + Parallel.For(0, Plugins.Count(), i => + { + Plugins.ElementAt(i).Init(); + }); + } + + private void ReloadHttpServer() + { + if (HttpServer != null) + { + HttpServer.Dispose(); + } + + HttpServer = new HttpServer(Configuration.HttpServerPortNumber); + } + + /// + /// Registers a new BaseItem subclass + /// + public void AddBaseItemType() + where TBaseItemType : BaseItem, new() + where TResolverType : BaseItemResolver, new() + { + ItemController.AddResovler(); + } + + /// + /// Unregisters a new BaseItem subclass + /// + public void RemoveBaseItemType() + where TBaseItemType : BaseItem, new() + where TResolverType : BaseItemResolver, new() + { + ItemController.RemoveResovler(); + } + + /// + /// Fires when a path is about to be resolved, but before child folders and files + /// have been collected from the file system. + /// This gives us a chance to cancel it if needed, resulting in the path being ignored + /// + void ItemController_PreBeginResolvePath(object sender, PreBeginResolveEventArgs e) + { + if (e.IsHidden || e.IsSystemFile) + { + // Ignore hidden files and folders + e.Cancel = true; + } + + else if (Path.GetFileName(e.Path).Equals("trailers", StringComparison.OrdinalIgnoreCase)) + { + // Ignore any folders named "trailers" + e.Cancel = true; + } + } + + /// + /// Fires when a path is about to be resolved, but after child folders and files + /// This gives us a chance to cancel it if needed, resulting in the path being ignored + /// + void ItemController_BeginResolvePath(object sender, ItemResolveEventArgs e) + { + if (e.IsFolder) + { + if (e.ContainsFile(".ignore")) + { + // Ignore any folders containing a file called .ignore + e.Cancel = true; + } + } + } + + private void ReloadUsers() + { + Users = UserController.GetAllUsers(); + } + + /// + /// Reloads the root media folder + /// + public void ReloadRoot() + { + if (!Directory.Exists(MediaRootFolderPath)) + { + Directory.CreateDirectory(MediaRootFolderPath); + } + + DirectoryWatchers.Stop(); + + RootFolder = ItemController.GetItem(MediaRootFolderPath) as Folder; + + DirectoryWatchers.Start(); + } + + private static MD5CryptoServiceProvider md5Provider = new MD5CryptoServiceProvider(); + public static Guid GetMD5(string str) + { + lock (md5Provider) + { + return new Guid(md5Provider.ComputeHash(Encoding.Unicode.GetBytes(str))); + } + } + + private static Configuration GetConfiguration(string directory) + { + string file = Path.Combine(directory, "config.js"); + + if (!File.Exists(file)) + { + return new Configuration(); + } + + return JsonSerializer.Deserialize(file); + } + + public void ReloadItem(BaseItem item) + { + Folder folder = item as Folder; + + if (folder != null && folder.IsRoot) + { + ReloadRoot(); + } + else + { + if (!Directory.Exists(item.Path) && !File.Exists(item.Path)) + { + ReloadItem(item.Parent); + return; + } + + BaseItem newItem = ItemController.GetItem(item.Parent, item.Path); + + List children = item.Parent.Children.ToList(); + + int index = children.IndexOf(item); + + children.RemoveAt(index); + + children.Insert(index, newItem); + + item.Parent.Children = children.ToArray(); + } + } + } +} diff --git a/MediaBrowser.Controller/Library/ItemController.cs b/MediaBrowser.Controller/Library/ItemController.cs new file mode 100644 index 000000000..422790c69 --- /dev/null +++ b/MediaBrowser.Controller/Library/ItemController.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Events; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Library +{ + public class ItemController + { + private List Resolvers = new List(); + + /// + /// Registers a new BaseItem resolver. + /// + public void AddResovler() + where TBaseItemType : BaseItem, new() + where TResolverType : BaseItemResolver, new() + { + Resolvers.Insert(0, new TResolverType()); + } + + /// + /// Registers a new BaseItem resolver. + /// + public void RemoveResovler() + where TBaseItemType : BaseItem, new() + where TResolverType : BaseItemResolver, new() + { + IBaseItemResolver resolver = Resolvers.First(r => r.GetType() == typeof(TResolverType)); + + Resolvers.Remove(resolver); + } + + #region PreBeginResolvePath Event + /// + /// Fires when a path is about to be resolved, but before child folders and files + /// have been collected from the file system. + /// This gives listeners a chance to cancel the operation and cause the path to be ignored. + /// + public event EventHandler PreBeginResolvePath; + private bool OnPreBeginResolvePath(Folder parent, string path, FileAttributes attributes) + { + PreBeginResolveEventArgs args = new PreBeginResolveEventArgs() + { + Path = path, + Parent = parent, + FileAttributes = attributes, + Cancel = false + }; + + if (PreBeginResolvePath != null) + { + PreBeginResolvePath(this, args); + } + + return !args.Cancel; + } + #endregion + + #region BeginResolvePath Event + /// + /// Fires when a path is about to be resolved, but after child folders and files + /// have been collected from the file system. + /// This gives listeners a chance to cancel the operation and cause the path to be ignored. + /// + public event EventHandler BeginResolvePath; + private bool OnBeginResolvePath(ItemResolveEventArgs args) + { + if (BeginResolvePath != null) + { + BeginResolvePath(this, args); + } + + return !args.Cancel; + } + #endregion + + #region Item Events + /// + /// Called when an item is being created. + /// This should be used to fill item values, such as metadata + /// + public event EventHandler> ItemCreating; + + /// + /// Called when an item has been created. + /// This should be used to process or modify item values. + /// + public event EventHandler> ItemCreated; + #endregion + + /// + /// Called when an item has been created + /// + private void OnItemCreated(BaseItem item, Folder parent) + { + GenericItemEventArgs args = new GenericItemEventArgs { Item = item }; + + if (ItemCreating != null) + { + ItemCreating(this, args); + } + + if (ItemCreated != null) + { + ItemCreated(this, args); + } + } + + private void FireCreateEventsRecursive(Folder folder, Folder parent) + { + OnItemCreated(folder, parent); + + int count = folder.Children.Length; + + Parallel.For(0, count, i => + { + BaseItem item = folder.Children[i]; + + Folder childFolder = item as Folder; + + if (childFolder != null) + { + FireCreateEventsRecursive(childFolder, folder); + } + else + { + OnItemCreated(item, folder); + } + }); + } + + private BaseItem ResolveItem(ItemResolveEventArgs args) + { + // If that didn't pan out, try the slow ones + foreach (IBaseItemResolver resolver in Resolvers) + { + var item = resolver.ResolvePath(args); + + if (item != null) + { + return item; + } + } + + return null; + } + + /// + /// Resolves a path into a BaseItem + /// + public BaseItem GetItem(string path) + { + return GetItem(null, path); + } + + /// + /// Resolves a path into a BaseItem + /// + public BaseItem GetItem(Folder parent, string path) + { + BaseItem item = GetItemInternal(parent, path, File.GetAttributes(path)); + + if (item != null) + { + var folder = item as Folder; + + if (folder != null) + { + FireCreateEventsRecursive(folder, parent); + } + else + { + OnItemCreated(item, parent); + } + } + + return item; + } + + /// + /// Resolves a path into a BaseItem + /// + private BaseItem GetItemInternal(Folder parent, string path, FileAttributes attributes) + { + if (!OnPreBeginResolvePath(parent, path, attributes)) + { + return null; + } + + IEnumerable> fileSystemChildren; + + // Gather child folder and files + if (attributes.HasFlag(FileAttributes.Directory)) + { + fileSystemChildren = Directory.GetFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly).Select(f => new KeyValuePair(f, File.GetAttributes(f))); + + bool isVirtualFolder = parent != null && parent.IsRoot; + fileSystemChildren = FilterChildFileSystemEntries(fileSystemChildren, isVirtualFolder); + } + else + { + fileSystemChildren = new KeyValuePair[] { }; + } + + ItemResolveEventArgs args = new ItemResolveEventArgs() + { + Path = path, + FileAttributes = attributes, + FileSystemChildren = fileSystemChildren, + Parent = parent, + Cancel = false + }; + + // Fire BeginResolvePath to see if anyone wants to cancel this operation + if (!OnBeginResolvePath(args)) + { + return null; + } + + BaseItem item = ResolveItem(args); + + var folder = item as Folder; + + if (folder != null) + { + // If it's a folder look for child entities + AttachChildren(folder, fileSystemChildren); + } + + return item; + } + + /// + /// Finds child BaseItems for a given Folder + /// + private void AttachChildren(Folder folder, IEnumerable> fileSystemChildren) + { + List baseItemChildren = new List(); + + int count = fileSystemChildren.Count(); + + // Resolve the child folder paths into entities + Parallel.For(0, count, i => + { + KeyValuePair child = fileSystemChildren.ElementAt(i); + + BaseItem item = GetItemInternal(folder, child.Key, child.Value); + + if (item != null) + { + lock (baseItemChildren) + { + baseItemChildren.Add(item); + } + } + }); + + // Sort them + folder.Children = baseItemChildren.OrderBy(f => + { + return string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName; + + }).ToArray(); + } + + /// + /// Transforms shortcuts into their actual paths + /// + private List> FilterChildFileSystemEntries(IEnumerable> fileSystemChildren, bool flattenShortcuts) + { + List> returnFiles = new List>(); + + // Loop through each file + foreach (KeyValuePair file in fileSystemChildren) + { + // Folders + if (file.Value.HasFlag(FileAttributes.Directory)) + { + returnFiles.Add(file); + } + + // If it's a shortcut, resolve it + else if (Shortcut.IsShortcut(file.Key)) + { + string newPath = Shortcut.ResolveShortcut(file.Key); + FileAttributes newPathAttributes = File.GetAttributes(newPath); + + // Find out if the shortcut is pointing to a directory or file + + if (newPathAttributes.HasFlag(FileAttributes.Directory)) + { + // If we're flattening then get the shortcut's children + + if (flattenShortcuts) + { + IEnumerable> newChildren = Directory.GetFileSystemEntries(newPath, "*", SearchOption.TopDirectoryOnly).Select(f => new KeyValuePair(f, File.GetAttributes(f))); + + returnFiles.AddRange(FilterChildFileSystemEntries(newChildren, false)); + } + else + { + returnFiles.Add(new KeyValuePair(newPath, newPathAttributes)); + } + } + else + { + returnFiles.Add(new KeyValuePair(newPath, newPathAttributes)); + } + } + else + { + returnFiles.Add(file); + } + } + + return returnFiles; + } + } +} diff --git a/MediaBrowser.Controller/Library/ItemDataCache.cs b/MediaBrowser.Controller/Library/ItemDataCache.cs new file mode 100644 index 000000000..35b3551a9 --- /dev/null +++ b/MediaBrowser.Controller/Library/ItemDataCache.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Library +{ + public class ItemDataCache + { + private Dictionary Data = new Dictionary(); + + public void SetValue(BaseItem item, string propertyName, T value) + { + Data[GetKey(item, propertyName)] = value; + } + + public T GetValue(BaseItem item, string propertyName) + { + string key = GetKey(item, propertyName); + + if (Data.ContainsKey(key)) + { + return (T)Data[key]; + } + + return default(T); + } + + private string GetKey(BaseItem item, string propertyName) + { + return item.Id.ToString() + "-" + propertyName; + } + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj new file mode 100644 index 000000000..a84fc8091 --- /dev/null +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -0,0 +1,92 @@ + + + + + Debug + AnyCPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2} + Library + Properties + MediaBrowser.Controller + MediaBrowser.Controller + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\Newtonsoft.Json.4.5.7\lib\net40\Newtonsoft.Json.dll + + + + + ..\packages\Rx-Main.1.0.11226\lib\Net4\System.Reactive.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {9b1ddd79-5134-4df3-ace3-d1957a7350d8} + MediaBrowser.Model + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Controller/Net/CollectionExtensions.cs b/MediaBrowser.Controller/Net/CollectionExtensions.cs new file mode 100644 index 000000000..137fbe50b --- /dev/null +++ b/MediaBrowser.Controller/Net/CollectionExtensions.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace MediaBrowser.Controller.Net +{ + public static class CollectionExtensions + { + public static IDictionary> ToDictionary(this NameValueCollection source) + { + return source.AllKeys.ToDictionary>(key => key, source.GetValues); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/Net/HttpServer.cs b/MediaBrowser.Controller/Net/HttpServer.cs new file mode 100644 index 000000000..bb014ca5a --- /dev/null +++ b/MediaBrowser.Controller/Net/HttpServer.cs @@ -0,0 +1,47 @@ +using System; +using System.Net; +using System.Reactive.Linq; + +namespace MediaBrowser.Controller.Net +{ + public class HttpServer : IObservable, IDisposable + { + private readonly HttpListener listener; + private readonly IObservable stream; + + public HttpServer(int port) + : this("http://+:" + port + "/") + { + } + + public HttpServer(string url) + { + listener = new HttpListener(); + listener.Prefixes.Add(url); + listener.Start(); + stream = ObservableHttpContext(); + } + + private IObservable ObservableHttpContext() + { + return Observable.Create(obs => + Observable.FromAsyncPattern(listener.BeginGetContext, + listener.EndGetContext)() + .Select(c => new RequestContext(c)) + .Subscribe(obs)) + .Repeat() + .Retry() + .Publish() + .RefCount(); + } + public void Dispose() + { + listener.Stop(); + } + + public IDisposable Subscribe(IObserver observer) + { + return stream.Subscribe(observer); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/Net/Request.cs b/MediaBrowser.Controller/Net/Request.cs new file mode 100644 index 000000000..751c1e384 --- /dev/null +++ b/MediaBrowser.Controller/Net/Request.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Controller.Net +{ + public class Request + { + public string HttpMethod { get; set; } + public IDictionary> Headers { get; set; } + public Stream InputStream { get; set; } + public string RawUrl { get; set; } + public int ContentLength + { + get { return int.Parse(Headers["Content-Length"].First()); } + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/Net/RequestContext.cs b/MediaBrowser.Controller/Net/RequestContext.cs new file mode 100644 index 000000000..531faab84 --- /dev/null +++ b/MediaBrowser.Controller/Net/RequestContext.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Net; +using System.IO.Compression; + +namespace MediaBrowser.Controller.Net +{ + public class RequestContext + { + public HttpListenerRequest Request { get; private set; } + public HttpListenerResponse Response { get; private set; } + + public RequestContext(HttpListenerContext context) + { + Response = context.Response; + Request = context.Request; + } + + public void Respond(Response response) + { + Response.AddHeader("Access-Control-Allow-Origin", "*"); + + foreach (var header in response.Headers) + { + Response.AddHeader(header.Key, header.Value); + } + + Response.ContentType = response.ContentType; + Response.StatusCode = response.StatusCode; + + Response.SendChunked = true; + + GZipStream gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress, false); + + response.WriteStream(Response.OutputStream); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/Net/Response.cs b/MediaBrowser.Controller/Net/Response.cs new file mode 100644 index 000000000..a119198cb --- /dev/null +++ b/MediaBrowser.Controller/Net/Response.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace MediaBrowser.Controller.Net +{ + public class Response + { + protected RequestContext RequestContext { get; private set; } + + public Response(RequestContext ctx) + { + RequestContext = ctx; + + WriteStream = s => { }; + StatusCode = 200; + Headers = new Dictionary(); + CacheDuration = TimeSpan.FromTicks(0); + ContentType = "text/html"; + } + + public int StatusCode { get; set; } + public string ContentType { get; set; } + public IDictionary Headers { get; set; } + public TimeSpan CacheDuration { get; set; } + public Action WriteStream { get; set; } + } + + /*public class ByteResponse : Response + { + public ByteResponse(byte[] bytes) + { + WriteStream = async s => + { + await s.WriteAsync(bytes, 0, bytes.Length); + s.Close(); + }; + } + } + + public class StringResponse : ByteResponse + { + public StringResponse(string message) + : base(Encoding.UTF8.GetBytes(message)) + { + } + }*/ +} \ No newline at end of file diff --git a/MediaBrowser.Controller/Net/StreamExtensions.cs b/MediaBrowser.Controller/Net/StreamExtensions.cs new file mode 100644 index 000000000..451a43acb --- /dev/null +++ b/MediaBrowser.Controller/Net/StreamExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reactive.Linq; + +namespace MediaBrowser.Controller.Net +{ + public static class StreamExtensions + { + public static IObservable ReadBytes(this Stream stream, int count) + { + var buffer = new byte[count]; + return Observable.FromAsyncPattern((cb, state) => stream.BeginRead(buffer, 0, count, cb, state), ar => + { + stream.EndRead(ar); + return buffer; + })(); + } + } +} \ 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..350165b1c --- /dev/null +++ b/MediaBrowser.Controller/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +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/Resolvers/AudioResolver.cs b/MediaBrowser.Controller/Resolvers/AudioResolver.cs new file mode 100644 index 000000000..f9ce5ecd7 --- /dev/null +++ b/MediaBrowser.Controller/Resolvers/AudioResolver.cs @@ -0,0 +1,44 @@ +using System.IO; +using MediaBrowser.Controller.Events; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Resolvers +{ + public class AudioResolver : BaseItemResolver