commit b50f78e5da6f3fdfc59e577ca61b88771da7d211 Author: LukePulverenti Luke Pulverenti luke pulverenti Date: Thu Jul 12 02:55:27 2012 -0400 Initial check-in diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000000..06171f67de --- /dev/null +++ b/.hgignore @@ -0,0 +1,37 @@ +# use glob syntax +syntax: glob + +*.obj +*.pdb +*.user +*.aps +*.pch +*.vspscc +*.vssscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +*.lib +*.sbr +*.scc +[Bb]in +[Dd]ebug*/ +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]humbs.db +[Tt]est[Rr]esult* +[Bb]uild[Ll]og.* +*.[Pp]ublish.xml +*.resharper + +# ncrunch files +*.ncrunchsolution +*.ncrunchproject \ No newline at end of file diff --git a/MediaBrowser.Api/HttpHandlers/ImageHandler.cs b/MediaBrowser.Api/HttpHandlers/ImageHandler.cs new file mode 100644 index 0000000000..ee4b04c114 --- /dev/null +++ b/MediaBrowser.Api/HttpHandlers/ImageHandler.cs @@ -0,0 +1,14 @@ +using MediaBrowser.Controller.Net; +using System; +using System.IO; +using System.IO.Compression; +using MediaBrowser.Common.Json; +using MediaBrowser.Model.Entities; +using MediaBrowser.Controller; + +namespace MediaBrowser.Api.HttpHandlers +{ + class ImageHandler + { + } +} diff --git a/MediaBrowser.Api/HttpHandlers/ItemHandler.cs b/MediaBrowser.Api/HttpHandlers/ItemHandler.cs new file mode 100644 index 0000000000..40824bd2c4 --- /dev/null +++ b/MediaBrowser.Api/HttpHandlers/ItemHandler.cs @@ -0,0 +1,93 @@ +using MediaBrowser.Controller.Net; +using System; +using System.IO; +using System.IO.Compression; +using MediaBrowser.Common.Json; +using MediaBrowser.Model.Entities; +using MediaBrowser.Controller; + +namespace MediaBrowser.Api.HttpHandlers +{ + public class ItemHandler : Response + { + public ItemHandler(RequestContext ctx) + : base(ctx) + { + ContentType = "application/json"; + + Headers["Content-Encoding"] = "gzip"; + + WriteStream = s => + { + WriteReponse(s); + s.Close(); + }; + } + + private Guid ItemId + { + get + { + string id = RequestContext.Request.QueryString["id"]; + + if (string.IsNullOrEmpty(id)) + { + return Guid.Empty; + } + + return Guid.Parse(id); + } + } + + BaseItem Item + { + get + { + Guid id = ItemId; + + if (id == Guid.Empty) + { + return Kernel.Instance.RootFolder; + } + + return Kernel.Instance.RootFolder.FindById(id); + } + } + + private void WriteReponse(Stream stream) + { + BaseItem item = Item; + + object returnObject; + + Folder folder = item as Folder; + + if (folder != null) + { + returnObject = new + { + Item = item, + Children = folder.Children + }; + } + else + { + returnObject = new + { + Item = item + }; + } + + WriteJsonResponse(returnObject, stream); + } + + private void WriteJsonResponse(object obj, Stream stream) + { + using (GZipStream gzipStream = new GZipStream(stream, CompressionMode.Compress, false)) + { + JsonSerializer.Serialize(obj, gzipStream); + //gzipStream.Flush(); + } + } + } +} diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj new file mode 100644 index 0000000000..933cb38273 --- /dev/null +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -0,0 +1,79 @@ + + + + + Debug + AnyCPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582} + Library + Properties + MediaBrowser.Api + MediaBrowser.Api + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + False + ..\packages\Rx-Main.1.0.11226\lib\Net4\System.Reactive.dll + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {9b1ddd79-5134-4df3-ace3-d1957a7350d8} + MediaBrowser.Model + + + + + + + + xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData\Plugins\$(ProjectName)\" /y + + + \ No newline at end of file diff --git a/MediaBrowser.Api/Plugin.cs b/MediaBrowser.Api/Plugin.cs new file mode 100644 index 0000000000..3137978feb --- /dev/null +++ b/MediaBrowser.Api/Plugin.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Net; +using MediaBrowser.Api.HttpHandlers; + +namespace MediaBrowser.Api +{ + public class Plugin : BasePlugin + { + List HttpHandlers = new List(); + + protected override void InitInternal() + { + HttpHandlers.Add(Kernel.Instance.HttpServer.Where(ctx => ctx.Request.Url.LocalPath.EndsWith("mediabrowser/api/item")).Subscribe(ctx => ctx.Respond(new ItemHandler(ctx)))); + HttpHandlers.Add(Kernel.Instance.HttpServer.Where(ctx => ctx.Request.Url.LocalPath.EndsWith("mediabrowser/api/image")).Subscribe(ctx => ctx.Respond(new ItemHandler(ctx)))); + } + + public override void Dispose() + { + base.Dispose(); + + foreach (var handler in HttpHandlers) + { + handler.Dispose(); + } + + HttpHandlers.Clear(); + } + } +} diff --git a/MediaBrowser.Api/Properties/AssemblyInfo.cs b/MediaBrowser.Api/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..0a7a85dc66 --- /dev/null +++ b/MediaBrowser.Api/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.Api")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MediaBrowser.Api")] +[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("13464b02-f033-48b8-9e1c-d071f8860935")] + +// 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.Api/packages.config b/MediaBrowser.Api/packages.config new file mode 100644 index 0000000000..47102a263f --- /dev/null +++ b/MediaBrowser.Api/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/MediaBrowser.Common/Events/GenericItemEventArgs.cs b/MediaBrowser.Common/Events/GenericItemEventArgs.cs new file mode 100644 index 0000000000..ae7b2f574a --- /dev/null +++ b/MediaBrowser.Common/Events/GenericItemEventArgs.cs @@ -0,0 +1,9 @@ +using System; + +namespace MediaBrowser.Common.Events +{ + public class GenericItemEventArgs : EventArgs + { + public TItemType Item { get; set; } + } +} diff --git a/MediaBrowser.Common/Json/JsonSerializer.cs b/MediaBrowser.Common/Json/JsonSerializer.cs new file mode 100644 index 0000000000..c9b8d8e85e --- /dev/null +++ b/MediaBrowser.Common/Json/JsonSerializer.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; + +namespace MediaBrowser.Common.Json +{ + public class JsonSerializer + { + public static void Serialize(T o, Stream stream) + { + using (StreamWriter streamWriter = new StreamWriter(stream)) + { + using (Newtonsoft.Json.JsonTextWriter writer = new Newtonsoft.Json.JsonTextWriter(streamWriter)) + { + var settings = new Newtonsoft.Json.JsonSerializerSettings() + { + NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore + }; + + Newtonsoft.Json.JsonSerializer.Create(settings).Serialize(writer, o); + } + } + } + + public static void Serialize(T o, string file) + { + using (StreamWriter streamWriter = new StreamWriter(file)) + { + using (Newtonsoft.Json.JsonTextWriter writer = new Newtonsoft.Json.JsonTextWriter(streamWriter)) + { + var settings = new Newtonsoft.Json.JsonSerializerSettings() + { + NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore + }; + + Newtonsoft.Json.JsonSerializer.Create(settings).Serialize(writer, o); + } + } + } + + public static T Deserialize(string file) + { + using (StreamReader streamReader = new StreamReader(file)) + { + using (Newtonsoft.Json.JsonTextReader reader = new Newtonsoft.Json.JsonTextReader(streamReader)) + { + return Newtonsoft.Json.JsonSerializer.Create(new Newtonsoft.Json.JsonSerializerSettings() { }).Deserialize(reader); + } + } + } + } +} diff --git a/MediaBrowser.Common/Logging/BaseLogger.cs b/MediaBrowser.Common/Logging/BaseLogger.cs new file mode 100644 index 0000000000..51c6632d56 --- /dev/null +++ b/MediaBrowser.Common/Logging/BaseLogger.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics; +using System.Text; +using System.Threading; + +namespace MediaBrowser.Common.Logging +{ + public abstract class BaseLogger + { + public LogSeverity LogSeverity { get; set; } + + public void LogInfo(string message, params object[] paramList) + { + LogEntry(message, LogSeverity.Info, paramList); + } + + public void LogDebugInfo(string message, params object[] paramList) + { + LogEntry(message, LogSeverity.Debug, paramList); + } + + public void LogError(string message, params object[] paramList) + { + LogEntry(message, LogSeverity.Error, paramList); + } + + public void LogException(string message, Exception exception, params object[] paramList) + { + StringBuilder builder = new StringBuilder(); + + if (exception != null) + { + var trace = new StackTrace(exception, true); + builder.AppendFormat("Exception. Type={0} Msg={1} Src={2} Method={5} Line={6} Col={7}{4}StackTrace={4}{3}", + exception.GetType().FullName, + exception.Message, + exception.Source, + exception.StackTrace, + Environment.NewLine, + trace.GetFrame(0).GetMethod().Name, + trace.GetFrame(0).GetFileLineNumber(), + trace.GetFrame(0).GetFileColumnNumber()); + } + + StackFrame frame = new StackFrame(1); + + message = string.Format(message, paramList); + + LogError(string.Format("{0} ( {1} )", message, builder)); + } + + public void LogWarning(string message, params object[] paramList) + { + LogEntry(message, LogSeverity.Warning, paramList); + } + + private void LogEntry(string message, LogSeverity severity, params object[] paramList) + { + if (severity < LogSeverity) return; + + message = string.Format(message, paramList); + + Thread currentThread = Thread.CurrentThread; + + LogRow row = new LogRow() + { + Severity = severity, + Message = message, + Category = string.Empty, + ThreadId = currentThread.ManagedThreadId, + ThreadName = currentThread.Name, + Time = DateTime.Now + }; + + LogEntry(row); + } + + protected abstract void LogEntry(LogRow row); + } +} diff --git a/MediaBrowser.Common/Logging/FileLogger.cs b/MediaBrowser.Common/Logging/FileLogger.cs new file mode 100644 index 0000000000..33c64b1391 --- /dev/null +++ b/MediaBrowser.Common/Logging/FileLogger.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Text; + +namespace MediaBrowser.Common.Logging +{ + public class FileLogger : BaseLogger, IDisposable + { + private string LogDirectory { get; set; } + private string CurrentLogFile { get; set; } + + private FileStream FileStream { get; set; } + + public FileLogger(string logDirectory) + { + LogDirectory = logDirectory; + } + + private void EnsureStream() + { + if (FileStream == null) + { + if (!Directory.Exists(LogDirectory)) + { + Directory.CreateDirectory(LogDirectory); + } + + DateTime now = DateTime.Now; + + CurrentLogFile = Path.Combine(LogDirectory, now.ToString("dMyyyy") + "-" + now.Ticks + ".log"); + + FileStream = new FileStream(CurrentLogFile, FileMode.Append, FileAccess.Write, FileShare.Read); + } + } + + protected override void LogEntry(LogRow row) + { + EnsureStream(); + + byte[] bytes = new UTF8Encoding().GetBytes(row.ToString() + Environment.NewLine); + + FileStream.Write(bytes, 0, bytes.Length); + + FileStream.Flush(); + } + + public void Dispose() + { + if (FileStream != null) + { + FileStream.Dispose(); + } + } + } +} diff --git a/MediaBrowser.Common/Logging/LogRow.cs b/MediaBrowser.Common/Logging/LogRow.cs new file mode 100644 index 0000000000..39c69eb454 --- /dev/null +++ b/MediaBrowser.Common/Logging/LogRow.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MediaBrowser.Common.Logging +{ + public struct LogRow + { + + const string TimePattern = "h:mm:ss.fff tt d/M/yyyy"; + + + public LogSeverity Severity { get; set; } + public string Message { get; set; } + public string Category { get; set; } + public int ThreadId { get; set; } + public string ThreadName { get; set; } + public DateTime Time { get; set; } + + public string ShortMessage + { + get + { + var message = Message; + if (message.Length > 120) + { + message = Message.Substring(0, 120).Replace(Environment.NewLine, " ") + " ... "; + } + return message; + } + } + + public string FullDescription + { + get + { + StringBuilder sb = new StringBuilder(); + sb.AppendFormat("Time: {0}", Time); + sb.AppendLine(); + sb.AppendFormat("Thread Id: {0} {1}", ThreadId, ThreadName); + sb.AppendLine(); + sb.AppendLine(Message); + return sb.ToString(); + } + } + + public override string ToString() + { + StringBuilder builder = new StringBuilder(); + builder.Append(Time.ToString(TimePattern)) + .Append(" , ") + .Append(Enum.GetName(typeof(LogSeverity), Severity)) + .Append(" , ") + .Append(Encode(Message)) + .Append(" , ") + .Append(Encode(Category)) + .Append(" , ") + .Append(ThreadId) + .Append(" , ") + .Append(Encode(ThreadName)); + return builder.ToString(); + } + + private string Encode(string str) + { + return (str ?? "").Replace(",", ",,").Replace(Environment.NewLine, " [n] "); + } + + public static LogRow FromString(string message) + { + var split = splitString(message); + return new LogRow() + { + Time = DateTime.ParseExact(split[0], TimePattern, null), + Severity = (LogSeverity)Enum.Parse(typeof(LogSeverity), split[1]), + Message = split[2], + Category = split[3], + ThreadId = int.Parse(split[4]), + ThreadName = split[5] + }; + } + + static string[] splitString(string message) + { + List items = new List(); + bool gotComma = false; + + StringBuilder currentItem = new StringBuilder(); + + foreach (var chr in message) + { + + if (chr == ',' && gotComma) + { + gotComma = false; + currentItem.Append(','); + } + else if (chr == ',') + { + gotComma = true; + } + else if (gotComma) + { + items.Add(currentItem.ToString().Replace(" [n] ", Environment.NewLine).Trim()); + currentItem = new StringBuilder(); + gotComma = false; + } + else + { + currentItem.Append(chr); + } + + } + items.Add(currentItem.ToString().Replace("[n]", Environment.NewLine).Trim()); + return items.ToArray(); + } + } +} diff --git a/MediaBrowser.Common/Logging/LogSeverity.cs b/MediaBrowser.Common/Logging/LogSeverity.cs new file mode 100644 index 0000000000..b9578522e5 --- /dev/null +++ b/MediaBrowser.Common/Logging/LogSeverity.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Logging +{ + [Flags] + public enum LogSeverity + { + None = 0, + Debug = 1, + Info = 2, + Warning = 4, + Error = 8 + } +} diff --git a/MediaBrowser.Common/Logging/Logger.cs b/MediaBrowser.Common/Logging/Logger.cs new file mode 100644 index 0000000000..5f4c2ff795 --- /dev/null +++ b/MediaBrowser.Common/Logging/Logger.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Logging +{ + public static class Logger + { + public static BaseLogger LoggerInstance { get; set; } + + public static void LogInfo(string message, params object[] paramList) + { + LoggerInstance.LogInfo(message, paramList); + } + + public static void LogDebugInfo(string message, params object[] paramList) + { + LoggerInstance.LogDebugInfo(message, paramList); + } + + public static void LogError(string message, params object[] paramList) + { + LoggerInstance.LogError(message, paramList); + } + + public static void LogException(string message, Exception ex, params object[] paramList) + { + LoggerInstance.LogException(message, ex, paramList); + } + + public static void LogWarning(string message, params object[] paramList) + { + LoggerInstance.LogWarning(message, paramList); + } + } +} diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj new file mode 100644 index 0000000000..312f556b6c --- /dev/null +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -0,0 +1,68 @@ + + + + + Debug + AnyCPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F} + Library + Properties + MediaBrowser.Common + MediaBrowser.Common + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Newtonsoft.Json.4.5.7\lib\net40\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs new file mode 100644 index 0000000000..fb37afa089 --- /dev/null +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using MediaBrowser.Common.Json; + +namespace MediaBrowser.Common.Plugins +{ + public abstract class BasePlugin : IDisposable, IPlugin + where TConfigurationType : BasePluginConfiguration, new() + { + public string Path { get; set; } + public TConfigurationType Configuration { get; private set; } + + private string ConfigurationPath + { + get + { + return System.IO.Path.Combine(Path, "config.js"); + } + } + + public void Init() + { + Configuration = GetConfiguration(); + + if (Configuration.Enabled) + { + InitInternal(); + } + } + + protected abstract void InitInternal(); + + public virtual void Dispose() + { + } + + private TConfigurationType GetConfiguration() + { + if (!File.Exists(ConfigurationPath)) + { + return new TConfigurationType(); + } + + return JsonSerializer.Deserialize(ConfigurationPath); + } + } + + public interface IPlugin + { + string Path { get; set; } + + void Init(); + void Dispose(); + } +} diff --git a/MediaBrowser.Common/Plugins/BasePluginConfiguration.cs b/MediaBrowser.Common/Plugins/BasePluginConfiguration.cs new file mode 100644 index 0000000000..ad7972d949 --- /dev/null +++ b/MediaBrowser.Common/Plugins/BasePluginConfiguration.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Plugins +{ + public class BasePluginConfiguration + { + public bool Enabled { get; set; } + + public BasePluginConfiguration() + { + Enabled = true; + } + } +} diff --git a/MediaBrowser.Common/Plugins/PluginController.cs b/MediaBrowser.Common/Plugins/PluginController.cs new file mode 100644 index 0000000000..9ce741ba16 --- /dev/null +++ b/MediaBrowser.Common/Plugins/PluginController.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace MediaBrowser.Common.Plugins +{ + public class PluginController + { + public string PluginsPath { get; set; } + + public PluginController(string pluginFolderPath) + { + PluginsPath = pluginFolderPath; + } + + public IEnumerable GetAllPlugins() + { + AppDomain.CurrentDomain.AssemblyResolve -= new ResolveEventHandler(CurrentDomain_AssemblyResolve); + AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); + + if (!Directory.Exists(PluginsPath)) + { + Directory.CreateDirectory(PluginsPath); + } + + List plugins = new List(); + + foreach (string folder in Directory.GetDirectories(PluginsPath, "*", SearchOption.TopDirectoryOnly)) + { + IPlugin plugin = GetPluginFromDirectory(folder); + + plugin.Path = folder; + + if (plugin != null) + { + plugins.Add(plugin); + } + } + + return plugins; + } + + private IPlugin GetPluginFromDirectory(string path) + { + string dll = Directory.GetFiles(path, "*.dll", SearchOption.TopDirectoryOnly).FirstOrDefault(); + + if (!string.IsNullOrEmpty(dll)) + { + return GetPluginFromDll(dll); + } + + return null; + } + + private IPlugin GetPluginFromDll(string path) + { + return FindPlugin(Assembly.Load(File.ReadAllBytes(path))); + } + + private IPlugin FindPlugin(Assembly assembly) + { + var plugin = assembly.GetTypes().Where(type => typeof(IPlugin).IsAssignableFrom(type)).FirstOrDefault(); + + if (plugin != null) + { + return plugin.GetConstructor(Type.EmptyTypes).Invoke(null) as IPlugin; + } + + return null; + } + + Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + AssemblyName assemblyName = new AssemblyName(args.Name); + + IEnumerable dllPaths = Directory.GetFiles(PluginsPath, "*.dll", SearchOption.AllDirectories); + + string dll = dllPaths.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f) == assemblyName.Name); + + if (!string.IsNullOrEmpty(dll)) + { + return Assembly.Load(File.ReadAllBytes(dll)); + } + + return null; + } + } +} diff --git a/MediaBrowser.Common/Properties/AssemblyInfo.cs b/MediaBrowser.Common/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..53e9df0f6e --- /dev/null +++ b/MediaBrowser.Common/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.Common")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MediaBrowser.Common")] +[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("cdec1bb7-6ffd-409f-b41f-0524a73df9be")] + +// 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.Common/packages.config b/MediaBrowser.Common/packages.config new file mode 100644 index 0000000000..9bfda38024 --- /dev/null +++ b/MediaBrowser.Common/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/MediaBrowser.Configuration/MediaBrowser.Configuration.csproj b/MediaBrowser.Configuration/MediaBrowser.Configuration.csproj new file mode 100644 index 0000000000..356aa502f2 --- /dev/null +++ b/MediaBrowser.Configuration/MediaBrowser.Configuration.csproj @@ -0,0 +1,77 @@ + + + + + Debug + AnyCPU + {933CC468-E22B-48D8-8BCA-2E026F411CA2} + Library + Properties + MediaBrowser.Configuration + MediaBrowser.Configuration + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + False + ..\packages\Rx-Main.1.0.11226\lib\Net4\System.Reactive.dll + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {9b1ddd79-5134-4df3-ace3-d1957a7350d8} + MediaBrowser.Model + + + + + + + + xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData\Plugins\$(ProjectName)\" /y + + + \ No newline at end of file diff --git a/MediaBrowser.Configuration/Plugin.cs b/MediaBrowser.Configuration/Plugin.cs new file mode 100644 index 0000000000..a16ad064b4 --- /dev/null +++ b/MediaBrowser.Configuration/Plugin.cs @@ -0,0 +1,11 @@ +using MediaBrowser.Common.Plugins; + +namespace MediaBrowser.Configuration +{ + public class Plugin : BasePlugin + { + protected override void InitInternal() + { + } + } +} diff --git a/MediaBrowser.Configuration/Properties/AssemblyInfo.cs b/MediaBrowser.Configuration/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..5212f26db7 --- /dev/null +++ b/MediaBrowser.Configuration/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.Configuration")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MediaBrowser.Configuration")] +[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("c109d2b1-2368-43a2-bed1-ec2cfb33e741")] + +// 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.Configuration/packages.config b/MediaBrowser.Configuration/packages.config new file mode 100644 index 0000000000..47102a263f --- /dev/null +++ b/MediaBrowser.Configuration/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/MediaBrowser.Controller/Events/ItemResolveEventArgs.cs b/MediaBrowser.Controller/Events/ItemResolveEventArgs.cs new file mode 100644 index 0000000000..831eb29d46 --- /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 0000000000..dd27695834 --- /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 0000000000..376d16a795 --- /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 0000000000..2bb78e7e72 --- /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 0000000000..422790c692 --- /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 0000000000..35b3551a97 --- /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 0000000000..a84fc8091b --- /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 0000000000..137fbe50b7 --- /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 0000000000..bb014ca5ae --- /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 0000000000..751c1e384c --- /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 0000000000..531faab840 --- /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 0000000000..a119198cb3 --- /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 0000000000..451a43acba --- /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 0000000000..350165b1c6 --- /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 0000000000..f9ce5ecd70 --- /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