Browse Source

Pushing missing changes

LukePulverenti 12 years ago
parent
commit
767cdc1f6f
100 changed files with 16737 additions and 3056 deletions
  1. 230 0
      .gitignore
  2. 49 43
      .hgignore
  3. 5 0
      .hgtags
  4. 74 0
      BDInfo/BDInfo.csproj
  5. 110 0
      BDInfo/BDInfoSettings.cs
  6. 482 0
      BDInfo/BDROM.cs
  7. 496 0
      BDInfo/LanguageCodes.cs
  8. 36 0
      BDInfo/Properties/AssemblyInfo.cs
  9. 5 0
      BDInfo/ReadMe.txt
  10. 312 0
      BDInfo/TSCodecAC3.cs
  11. 151 0
      BDInfo/TSCodecAVC.cs
  12. 162 0
      BDInfo/TSCodecDTS.cs
  13. 249 0
      BDInfo/TSCodecDTSHD.cs
  14. 126 0
      BDInfo/TSCodecLPCM.cs
  15. 211 0
      BDInfo/TSCodecMPEG2.cs
  16. 39 0
      BDInfo/TSCodecMVC.cs
  17. 189 0
      BDInfo/TSCodecTrueHD.cs
  18. 134 0
      BDInfo/TSCodecVC1.cs
  19. 40 0
      BDInfo/TSInterleavedFile.cs
  20. 1287 0
      BDInfo/TSPlaylistFile.cs
  21. 802 0
      BDInfo/TSStream.cs
  22. 145 0
      BDInfo/TSStreamBuffer.cs
  23. 114 0
      BDInfo/TSStreamClip.cs
  24. 248 0
      BDInfo/TSStreamClipFile.cs
  25. 1551 0
      BDInfo/TSStreamFile.cs
  26. 3 0
      Bootstrapper/readme.txt
  27. 20 0
      Bootstrapper/vcredist10_x86/en/package.xml
  28. 45 0
      Bootstrapper/vcredist10_x86/product.xml
  29. 78 438
      MediaBrowser.Api/ApiService.cs
  30. 0 81
      MediaBrowser.Api/Drawing/DrawingUtils.cs
  31. 0 148
      MediaBrowser.Api/Drawing/ImageProcessor.cs
  32. 200 0
      MediaBrowser.Api/EnvironmentService.cs
  33. 0 119
      MediaBrowser.Api/HttpHandlers/AudioHandler.cs
  34. 0 255
      MediaBrowser.Api/HttpHandlers/BaseMediaHandler.cs
  35. 0 38
      MediaBrowser.Api/HttpHandlers/FavoriteStatusHandler.cs
  36. 0 57
      MediaBrowser.Api/HttpHandlers/GenreHandler.cs
  37. 0 78
      MediaBrowser.Api/HttpHandlers/GenresHandler.cs
  38. 0 224
      MediaBrowser.Api/HttpHandlers/ImageHandler.cs
  39. 0 35
      MediaBrowser.Api/HttpHandlers/ItemHandler.cs
  40. 0 84
      MediaBrowser.Api/HttpHandlers/ItemListHandler.cs
  41. 0 46
      MediaBrowser.Api/HttpHandlers/MovieSpecialFeaturesHandler.cs
  42. 0 55
      MediaBrowser.Api/HttpHandlers/PersonHandler.cs
  43. 112 0
      MediaBrowser.Api/HttpHandlers/PlaybackCheckInHandler.cs
  44. 0 38
      MediaBrowser.Api/HttpHandlers/PlayedStatusHandler.cs
  45. 0 38
      MediaBrowser.Api/HttpHandlers/PluginAssemblyHandler.cs
  46. 0 53
      MediaBrowser.Api/HttpHandlers/PluginConfigurationHandler.cs
  47. 0 38
      MediaBrowser.Api/HttpHandlers/PluginsHandler.cs
  48. 0 37
      MediaBrowser.Api/HttpHandlers/ServerConfigurationHandler.cs
  49. 0 57
      MediaBrowser.Api/HttpHandlers/StudioHandler.cs
  50. 0 78
      MediaBrowser.Api/HttpHandlers/StudiosHandler.cs
  51. 263 0
      MediaBrowser.Api/HttpHandlers/UpdateMediaLibraryHandler.cs
  52. 0 29
      MediaBrowser.Api/HttpHandlers/UserAuthenticationHandler.cs
  53. 0 29
      MediaBrowser.Api/HttpHandlers/UserHandler.cs
  54. 0 46
      MediaBrowser.Api/HttpHandlers/UserItemRatingHandler.cs
  55. 0 25
      MediaBrowser.Api/HttpHandlers/UsersHandler.cs
  56. 0 424
      MediaBrowser.Api/HttpHandlers/VideoHandler.cs
  57. 0 43
      MediaBrowser.Api/HttpHandlers/WeatherHandler.cs
  58. 0 55
      MediaBrowser.Api/HttpHandlers/YearHandler.cs
  59. 0 75
      MediaBrowser.Api/HttpHandlers/YearsHandler.cs
  60. 54 0
      MediaBrowser.Api/Images/ImageRequest.cs
  61. 286 0
      MediaBrowser.Api/Images/ImageService.cs
  62. 80 0
      MediaBrowser.Api/Images/ImageWriter.cs
  63. 143 0
      MediaBrowser.Api/Images/UploadImageHandler.cs
  64. 250 0
      MediaBrowser.Api/LibraryService.cs
  65. 112 0
      MediaBrowser.Api/LocalizationService.cs
  66. 160 116
      MediaBrowser.Api/MediaBrowser.Api.csproj
  67. 201 0
      MediaBrowser.Api/PackageService.cs
  68. 329 14
      MediaBrowser.Api/Plugin.cs
  69. 241 0
      MediaBrowser.Api/PluginService.cs
  70. 34 35
      MediaBrowser.Api/Properties/AssemblyInfo.cs
  71. 112 0
      MediaBrowser.Api/Streaming/AudioHandler.cs
  72. 221 0
      MediaBrowser.Api/Streaming/BaseHlsPlaylistHandler.cs
  73. 152 0
      MediaBrowser.Api/Streaming/BaseProgressiveStreamingHandler.cs
  74. 992 0
      MediaBrowser.Api/Streaming/BaseStreamingHandler.cs
  75. 102 0
      MediaBrowser.Api/Streaming/HlsAudioPlaylistHandler.cs
  76. 87 0
      MediaBrowser.Api/Streaming/HlsSegmentHandler.cs
  77. 134 0
      MediaBrowser.Api/Streaming/HlsVideoPlaylistHandler.cs
  78. 185 0
      MediaBrowser.Api/Streaming/VideoHandler.cs
  79. 107 0
      MediaBrowser.Api/SystemService.cs
  80. 161 0
      MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs
  81. 69 0
      MediaBrowser.Api/UserLibrary/GenresService.cs
  82. 788 0
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  83. 103 0
      MediaBrowser.Api/UserLibrary/PersonsService.cs
  84. 69 0
      MediaBrowser.Api/UserLibrary/StudiosService.cs
  85. 506 0
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  86. 75 0
      MediaBrowser.Api/UserLibrary/YearsService.cs
  87. 297 0
      MediaBrowser.Api/UserService.cs
  88. 46 0
      MediaBrowser.Api/WeatherService.cs
  89. 876 0
      MediaBrowser.Api/options.xml
  90. 12 5
      MediaBrowser.Api/packages.config
  91. 1360 0
      MediaBrowser.ApiInteraction.Javascript/ApiClient.js
  92. 60 0
      MediaBrowser.ApiInteraction.Javascript/JavascriptApiClientService.cs
  93. 115 0
      MediaBrowser.ApiInteraction.Javascript/MediaBrowser.ApiInteraction.Javascript.csproj
  94. 34 0
      MediaBrowser.ApiInteraction.Javascript/Properties/AssemblyInfo.cs
  95. 8 0
      MediaBrowser.ApiInteraction.Javascript/packages.config
  96. 0 12
      MediaBrowser.ApiInteraction.Metro/ApiClient.cs
  97. 0 78
      MediaBrowser.ApiInteraction.Metro/DataSerializer.cs
  98. 76 0
      MediaBrowser.ApiInteraction.Portable/DataSerializer.cs
  99. 105 0
      MediaBrowser.ApiInteraction.Portable/MediaBrowser.ApiInteraction.Portable.csproj
  100. 27 30
      MediaBrowser.ApiInteraction.Portable/Properties/AssemblyInfo.cs

+ 230 - 0
.gitignore

@@ -0,0 +1,230 @@
+#################
+## Eclipse
+#################
+
+*.pydevproject
+.project
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.classpath
+.settings/
+.loadpath
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# CDT-specific
+.cproject
+
+# PDT-specific
+.buildpath
+
+#################
+## Media Browser
+#################
+ProgramData*/
+ProgramData-Server*/
+ProgramData-UI*/
+
+#################
+## Visual Studio
+#################
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+
+[Dd]ebug/
+[Rr]elease/
+x64/
+build/
+[Bb]in/
+[Oo]bj/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+*_i.c
+*_p.c
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.log
+*.scc
+*.scc
+*.psess
+*.vsp
+*.vspx
+*.orig
+*.rej
+*.sdf
+*.opensdf
+*.ipch
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+*.ncrunch*
+.*crunch*.local.xml
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.Publish.xml
+*.pubxml
+
+# NuGet Packages Directory
+## TODO: If you have NuGet Package Restore enabled, uncomment the next line
+#packages/
+
+# Windows Azure Build Output
+csx
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+sql/
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.[Pp]ublish.xml
+*.pfx
+*.publishsettings
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file to a newer
+# Visual Studio version. Backup files are not needed, because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+App_Data/*.mdf
+App_Data/*.ldf
+
+#############
+## Windows detritus
+#############
+
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Mac crap
+.DS_Store
+
+
+#############
+## Python
+#############
+
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist/
+build/
+eggs/
+parts/
+var/
+sdist/
+develop-eggs/
+.installed.cfg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg

+ 49 - 43
.hgignore

@@ -1,43 +1,49 @@
-# 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
-*.psess
-*.vsp
-*.orig
-[Bb]in
-[Dd]ebug*/
-obj/
-[Rr]elease*/
-ProgramData*/
-ProgramData-Server*/
-ProgramData-UI*/
-_ReSharper*/
-[Tt]humbs.db
-[Tt]est[Rr]esult*
-[Bb]uild[Ll]og.*
-*.[Pp]ublish.xml
-*.resharper
-
-# ncrunch files
-*.ncrunchsolution
-*.ncrunchproject
+# 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
+*.psess
+*.vsp
+*.vspx
+*.orig
+*.rej
+*.sdf
+*.opensdf
+*.ipch
+[Bb]in
+[Dd]ebug*/
+obj/
+[Rr]elease*/
+ProgramData*/
+ProgramData-Server*/
+ProgramData-UI*/
+_ReSharper*/
+[Tt]humbs.db
+[Tt]est[Rr]esult*
+[Bb]uild[Ll]og.*
+*.[Pp]ublish.xml
+*.resharper
+
+# ncrunch files
+*.ncrunchsolution
+*.ncrunchproject
+Setup/*

+ 5 - 0
.hgtags

@@ -0,0 +1,5 @@
+811bcaa9490681194bd72a01c4b0f91d78a0ec97 CO Version 0.12.12.25
+811bcaa9490681194bd72a01c4b0f91d78a0ec97 CO Version 0.12.12.25
+0000000000000000000000000000000000000000 CO Version 0.12.12.25
+0000000000000000000000000000000000000000 CO Version 0.12.12.25
+e98137e38224a0d82cd7d98a71264e0cddc91ca4 CO Version 0.12.12.25

+ 74 - 0
BDInfo/BDInfo.csproj

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{07B509C0-0C28-4F3F-8963-5263281F7E3D}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>BDInfo</RootNamespace>
+    <AssemblyName>BDInfo</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="BDInfoSettings.cs" />
+    <Compile Include="BDROM.cs" />
+    <Compile Include="LanguageCodes.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="TSCodecAC3.cs" />
+    <Compile Include="TSCodecAVC.cs" />
+    <Compile Include="TSCodecDTS.cs" />
+    <Compile Include="TSCodecDTSHD.cs" />
+    <Compile Include="TSCodecLPCM.cs" />
+    <Compile Include="TSCodecMPEG2.cs" />
+    <Compile Include="TSCodecMVC.cs" />
+    <Compile Include="TSCodecTrueHD.cs" />
+    <Compile Include="TSCodecVC1.cs" />
+    <Compile Include="TSInterleavedFile.cs" />
+    <Compile Include="TSPlaylistFile.cs" />
+    <Compile Include="TSStream.cs" />
+    <Compile Include="TSStreamBuffer.cs" />
+    <Compile Include="TSStreamClip.cs" />
+    <Compile Include="TSStreamClipFile.cs" />
+    <Compile Include="TSStreamFile.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <Content Include="ReadMe.txt" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 110 - 0
BDInfo/BDInfoSettings.cs

@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BDInfo
+{
+    class BDInfoSettings
+    {
+        public static bool GenerateStreamDiagnostics
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        public static bool EnableSSIF
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        public static bool AutosaveReport
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public static bool GenerateFrameDataFile
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public static bool FilterLoopingPlaylists
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        public static bool FilterShortPlaylists
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public static int FilterShortPlaylistsValue
+        {
+            get
+            {
+                return 0;
+            }
+        }
+
+        public static bool UseImagePrefix
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public static string UseImagePrefixValue
+        {
+            get
+            {
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Setting this to false throws an IComparer error on some discs.
+        /// </summary>
+        public static bool KeepStreamOrder
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        public static bool GenerateTextSummary
+        {
+            get
+            {
+                return false;
+            }
+        }
+
+        public static string LastPath
+        {
+            get
+            {
+                return string.Empty;
+            }
+        }
+    }
+}

+ 482 - 0
BDInfo/BDROM.cs

@@ -0,0 +1,482 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace BDInfo
+{
+    public class BDROM
+    {
+        public DirectoryInfo DirectoryRoot = null;
+        public DirectoryInfo DirectoryBDMV = null;
+        public DirectoryInfo DirectoryBDJO = null;
+        public DirectoryInfo DirectoryCLIPINF = null;
+        public DirectoryInfo DirectoryPLAYLIST = null;
+        public DirectoryInfo DirectorySNP = null;
+        public DirectoryInfo DirectorySSIF = null;
+        public DirectoryInfo DirectorySTREAM = null;
+
+        public string VolumeLabel = null;
+        public ulong Size = 0;
+        public bool IsBDPlus = false;
+        public bool IsBDJava = false;
+        public bool IsDBOX = false;
+        public bool IsPSP = false;
+        public bool Is3D = false;
+        public bool Is50Hz = false;
+
+        public Dictionary<string, TSPlaylistFile> PlaylistFiles = 
+            new Dictionary<string, TSPlaylistFile>();
+        public Dictionary<string, TSStreamClipFile> StreamClipFiles =
+            new Dictionary<string, TSStreamClipFile>();
+        public Dictionary<string, TSStreamFile> StreamFiles = 
+            new Dictionary<string, TSStreamFile>();
+        public Dictionary<string, TSInterleavedFile> InterleavedFiles =
+            new Dictionary<string, TSInterleavedFile>();
+
+        private static List<string> ExcludeDirs = new List<string> { "ANY!", "AACS", "BDSVM", "ANYVM", "SLYVM" };
+
+        public delegate bool OnStreamClipFileScanError(
+            TSStreamClipFile streamClipFile, Exception ex);
+
+        public event OnStreamClipFileScanError StreamClipFileScanError;
+
+        public delegate bool OnStreamFileScanError(
+            TSStreamFile streamClipFile, Exception ex);
+
+        public event OnStreamFileScanError StreamFileScanError;
+
+        public delegate bool OnPlaylistFileScanError(
+            TSPlaylistFile playlistFile, Exception ex);
+
+        public event OnPlaylistFileScanError PlaylistFileScanError;
+
+        public BDROM(
+            string path)
+        {
+            //
+            // Locate BDMV directories.
+            //
+
+            DirectoryBDMV = 
+                GetDirectoryBDMV(path);
+            
+            if (DirectoryBDMV == null)
+            {
+                throw new Exception("Unable to locate BD structure.");
+            }
+
+            DirectoryRoot = 
+                DirectoryBDMV.Parent;
+            DirectoryBDJO = 
+                GetDirectory("BDJO", DirectoryBDMV, 0);
+            DirectoryCLIPINF = 
+                GetDirectory("CLIPINF", DirectoryBDMV, 0);
+            DirectoryPLAYLIST =
+                GetDirectory("PLAYLIST", DirectoryBDMV, 0);
+            DirectorySNP =
+                GetDirectory("SNP", DirectoryRoot, 0);
+            DirectorySTREAM = 
+                GetDirectory("STREAM", DirectoryBDMV, 0);
+            DirectorySSIF =
+                GetDirectory("SSIF", DirectorySTREAM, 0);
+
+            if (DirectoryCLIPINF == null
+                || DirectoryPLAYLIST == null)
+            {
+                throw new Exception("Unable to locate BD structure.");
+            }
+
+            //
+            // Initialize basic disc properties.
+            //
+
+            VolumeLabel = GetVolumeLabel(DirectoryRoot);
+            Size = (ulong)GetDirectorySize(DirectoryRoot);
+            
+            if (null != GetDirectory("BDSVM", DirectoryRoot, 0))
+            {
+                IsBDPlus = true;
+            }
+            if (null != GetDirectory("SLYVM", DirectoryRoot, 0))
+            {
+                IsBDPlus = true;
+            }
+            if (null != GetDirectory("ANYVM", DirectoryRoot, 0))
+            {
+                IsBDPlus = true;
+            }
+
+            if (DirectoryBDJO != null &&
+                DirectoryBDJO.GetFiles().Length > 0)
+            {
+                IsBDJava = true;
+            }
+
+            if (DirectorySNP != null &&
+                (DirectorySNP.GetFiles("*.mnv").Length > 0 || DirectorySNP.GetFiles("*.MNV").Length > 0))
+            {
+                IsPSP = true;
+            }
+
+            if (DirectorySSIF != null &&
+                DirectorySSIF.GetFiles().Length > 0)
+            {
+                Is3D = true;
+            }
+
+            if (File.Exists(Path.Combine(DirectoryRoot.FullName, "FilmIndex.xml")))
+            {
+                IsDBOX = true;
+            }
+
+            //
+            // Initialize file lists.
+            //
+
+            if (DirectoryPLAYLIST != null)
+            {
+                FileInfo[] files = DirectoryPLAYLIST.GetFiles("*.mpls");
+                if (files.Length == 0)
+                {
+                    files = DirectoryPLAYLIST.GetFiles("*.MPLS");
+                }
+                foreach (FileInfo file in files)
+                {
+                    PlaylistFiles.Add(
+                        file.Name.ToUpper(), new TSPlaylistFile(this, file));
+                }
+            }
+
+            if (DirectorySTREAM != null)
+            {
+                FileInfo[] files = DirectorySTREAM.GetFiles("*.m2ts");
+                if (files.Length == 0)
+                {
+                    files = DirectoryPLAYLIST.GetFiles("*.M2TS");
+                }
+                foreach (FileInfo file in files)
+                {
+                    StreamFiles.Add(
+                        file.Name.ToUpper(), new TSStreamFile(file));
+                }
+            }
+
+            if (DirectoryCLIPINF != null)
+            {
+                FileInfo[] files = DirectoryCLIPINF.GetFiles("*.clpi");
+                if (files.Length == 0)
+                {
+                    files = DirectoryPLAYLIST.GetFiles("*.CLPI");
+                }
+                foreach (FileInfo file in files)
+                {
+                    StreamClipFiles.Add(
+                        file.Name.ToUpper(), new TSStreamClipFile(file));
+                }
+            }
+
+            if (DirectorySSIF != null)
+            {
+                FileInfo[] files = DirectorySSIF.GetFiles("*.ssif");
+                if (files.Length == 0)
+                {
+                    files = DirectorySSIF.GetFiles("*.SSIF");
+                }
+                foreach (FileInfo file in files)
+                {
+                    InterleavedFiles.Add(
+                        file.Name.ToUpper(), new TSInterleavedFile(file));
+                }
+            }
+        }
+
+        public void Scan()
+        {
+            List<TSStreamClipFile> errorStreamClipFiles = new List<TSStreamClipFile>();
+            foreach (TSStreamClipFile streamClipFile in StreamClipFiles.Values)
+            {
+                try
+                {
+                    streamClipFile.Scan();
+                }
+                catch (Exception ex)
+                {
+                    errorStreamClipFiles.Add(streamClipFile);
+                    if (StreamClipFileScanError != null)
+                    {
+                        if (StreamClipFileScanError(streamClipFile, ex))
+                        {
+                            continue;
+                        }
+                        else
+                        {
+                            break;
+                        }
+                    }
+                    else throw ex;
+                }
+            }
+
+            foreach (TSStreamFile streamFile in StreamFiles.Values)
+            {
+                string ssifName = Path.GetFileNameWithoutExtension(streamFile.Name) + ".SSIF";
+                if (InterleavedFiles.ContainsKey(ssifName))
+                {
+                    streamFile.InterleavedFile = InterleavedFiles[ssifName];
+                }
+            }
+
+            TSStreamFile[] streamFiles = new TSStreamFile[StreamFiles.Count];
+            StreamFiles.Values.CopyTo(streamFiles, 0);
+            Array.Sort(streamFiles, CompareStreamFiles);
+
+            List<TSPlaylistFile> errorPlaylistFiles = new List<TSPlaylistFile>();
+            foreach (TSPlaylistFile playlistFile in PlaylistFiles.Values)
+            {
+                try
+                {
+                    playlistFile.Scan(StreamFiles, StreamClipFiles);
+                }
+                catch (Exception ex)
+                {
+                    errorPlaylistFiles.Add(playlistFile);
+                    if (PlaylistFileScanError != null)
+                    {
+                        if (PlaylistFileScanError(playlistFile, ex))
+                        {
+                            continue;
+                        }
+                        else
+                        {
+                            break;
+                        }
+                    }
+                    else throw ex;
+                }
+            }
+
+            List<TSStreamFile> errorStreamFiles = new List<TSStreamFile>();
+            foreach (TSStreamFile streamFile in streamFiles)
+            {
+                try
+                {
+                    List<TSPlaylistFile> playlists = new List<TSPlaylistFile>();
+                    foreach (TSPlaylistFile playlist in PlaylistFiles.Values)
+                    {
+                        foreach (TSStreamClip streamClip in playlist.StreamClips)
+                        {
+                            if (streamClip.Name == streamFile.Name)
+                            {
+                                playlists.Add(playlist);
+                                break;
+                            }
+                        }
+                    }
+                    streamFile.Scan(playlists, false);
+                }
+                catch (Exception ex)
+                {
+                    errorStreamFiles.Add(streamFile);
+                    if (StreamFileScanError != null)
+                    {
+                        if (StreamFileScanError(streamFile, ex))
+                        {
+                            continue;
+                        }
+                        else
+                        {
+                            break;
+                        }
+                    }
+                    else throw ex;
+                }
+            }
+
+            foreach (TSPlaylistFile playlistFile in PlaylistFiles.Values)
+            {
+                playlistFile.Initialize();
+                if (!Is50Hz)
+                {
+                    foreach (TSVideoStream videoStream in playlistFile.VideoStreams)
+                    {
+                        if (videoStream.FrameRate == TSFrameRate.FRAMERATE_25 ||
+                            videoStream.FrameRate == TSFrameRate.FRAMERATE_50)
+                        {
+                            Is50Hz = true;
+                        }
+                    }
+                }
+            }
+        }
+
+        private DirectoryInfo GetDirectoryBDMV(
+            string path)
+        {
+            DirectoryInfo dir = new DirectoryInfo(path);
+
+            while (dir != null)
+            {
+                if (dir.Name == "BDMV")
+                {
+                    return dir;
+                }
+                dir = dir.Parent;
+            }
+
+            return GetDirectory("BDMV", new DirectoryInfo(path), 0);
+        }
+
+        private DirectoryInfo GetDirectory(
+            string name,
+            DirectoryInfo dir,
+            int searchDepth)
+        {
+            if (dir != null)
+            {
+                DirectoryInfo[] children = dir.GetDirectories();
+                foreach (DirectoryInfo child in children)
+                {
+                    if (child.Name == name)
+                    {
+                        return child;
+                    }
+                }
+                if (searchDepth > 0)
+                {
+                    foreach (DirectoryInfo child in children)
+                    {
+                        GetDirectory(
+                            name, child, searchDepth - 1);
+                    }
+                }
+            }
+            return null;
+        }
+
+        private long GetDirectorySize(DirectoryInfo directoryInfo)
+        {
+            long size = 0;
+
+            //if (!ExcludeDirs.Contains(directoryInfo.Name.ToUpper()))  // TODO: Keep?
+            {
+                FileInfo[] pathFiles = directoryInfo.GetFiles();
+                foreach (FileInfo pathFile in pathFiles)
+                {
+                    if (pathFile.Extension.ToUpper() == ".SSIF")
+                    {
+                        continue;
+                    }
+                    size += pathFile.Length;
+                }
+
+                DirectoryInfo[] pathChildren = directoryInfo.GetDirectories();
+                foreach (DirectoryInfo pathChild in pathChildren)
+                {
+                    size += GetDirectorySize(pathChild);
+                }
+            }
+
+            return size;
+        }
+
+        private string GetVolumeLabel(DirectoryInfo dir)
+        {
+            uint serialNumber = 0;
+            uint maxLength = 0;
+            uint volumeFlags = new uint();
+            StringBuilder volumeLabel = new StringBuilder(256);
+            StringBuilder fileSystemName = new StringBuilder(256);
+            string label = "";
+
+            try
+            {
+                long result = GetVolumeInformation(
+                    dir.Name,
+                    volumeLabel,
+                    (uint)volumeLabel.Capacity,
+                    ref serialNumber,
+                    ref maxLength,
+                    ref volumeFlags,
+                    fileSystemName,
+                    (uint)fileSystemName.Capacity);
+
+                label = volumeLabel.ToString();
+            }
+            catch { }
+
+            if (label.Length == 0)
+            {
+                label = dir.Name;
+            }
+
+            return label;
+        }
+
+        public static int CompareStreamFiles(
+            TSStreamFile x,
+            TSStreamFile y)
+        {
+            // TODO: Use interleaved file sizes
+
+            if ((x == null || x.FileInfo == null) && (y == null || y.FileInfo == null))
+            {
+                return 0;
+            }
+            else if ((x == null || x.FileInfo == null) && (y != null && y.FileInfo != null))
+            {
+                return 1;
+            }
+            else if ((x != null || x.FileInfo != null) && (y == null || y.FileInfo == null))
+            {
+                return -1;
+            }
+            else
+            {
+                if (x.FileInfo.Length > y.FileInfo.Length)
+                {
+                    return 1;
+                }
+                else if (y.FileInfo.Length > x.FileInfo.Length)
+                {
+                    return -1;
+                }
+                else
+                {
+                    return 0;
+                }
+            }
+        }
+
+        [DllImport("kernel32.dll")]
+        private static extern long GetVolumeInformation(
+            string PathName, 
+            StringBuilder VolumeNameBuffer, 
+            uint VolumeNameSize,
+            ref uint VolumeSerialNumber,
+            ref uint MaximumComponentLength,
+            ref uint FileSystemFlags, 
+            StringBuilder FileSystemNameBuffer,
+            uint FileSystemNameSize);
+    }
+}

+ 496 - 0
BDInfo/LanguageCodes.cs

@@ -0,0 +1,496 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public abstract class LanguageCodes
+    {
+        public static string GetName(string code)
+        {
+            switch (code)
+            {
+                case "abk": return "Abkhazian";
+                case "ace": return "Achinese";
+                case "ach": return "Acoli";
+                case "ada": return "Adangme";
+                case "aar": return "Afar";
+                case "afh": return "Afrihili";
+                case "afr": return "Afrikaans";
+                case "afa": return "Afro-Asiatic (Other)";
+                case "aka": return "Akan";
+                case "akk": return "Akkadian";
+                case "alb": return "Albanian";
+                case "sqi": return "Albanian";
+                case "ale": return "Aleut";
+                case "alg": return "Algonquian languages";
+                case "tut": return "Altaic (Other)";
+                case "amh": return "Amharic";
+                case "apa": return "Apache languages";
+                case "ara": return "Arabic";
+                case "arc": return "Aramaic";
+                case "arp": return "Arapaho";
+                case "arn": return "Araucanian";
+                case "arw": return "Arawak";
+                case "arm": return "Armenian";
+                case "hye": return "Armenian";
+                case "art": return "Artificial (Other)";
+                case "asm": return "Assamese";
+                case "ath": return "Athapascan languages";
+                case "aus": return "Australian languages";
+                case "map": return "Austronesian (Other)";
+                case "ava": return "Avaric";
+                case "ave": return "Avestan";
+                case "awa": return "Awadhi";
+                case "aym": return "Aymara";
+                case "aze": return "Azerbaijani";
+                case "ban": return "Balinese";
+                case "bat": return "Baltic (Other)";
+                case "bal": return "Baluchi";
+                case "bam": return "Bambara";
+                case "bai": return "Bamileke languages";
+                case "bad": return "Banda";
+                case "bnt": return "Bantu (Other)";
+                case "bas": return "Basa";
+                case "bak": return "Bashkir";
+                case "baq": return "Basque";
+                case "eus": return "Basque";
+                case "btk": return "Batak (Indonesia)";
+                case "bej": return "Beja";
+                case "bel": return "Belarusian";
+                case "bem": return "Bemba";
+                case "ben": return "Bengali";
+                case "ber": return "Berber (Other)";
+                case "bho": return "Bhojpuri";
+                case "bih": return "Bihari";
+                case "bik": return "Bikol";
+                case "bin": return "Bini";
+                case "bis": return "Bislama";
+                case "bos": return "Bosnian";
+                case "bra": return "Braj";
+                case "bre": return "Breton";
+                case "bug": return "Buginese";
+                case "bul": return "Bulgarian";
+                case "bua": return "Buriat";
+                case "bur": return "Burmese";
+                case "mya": return "Burmese";
+                case "cad": return "Caddo";
+                case "car": return "Carib";
+                case "cat": return "Catalan";
+                case "cau": return "Caucasian (Other)";
+                case "ceb": return "Cebuano";
+                case "cel": return "Celtic (Other)";
+                case "cai": return "Central American Indian (Other)";
+                case "chg": return "Chagatai";
+                case "cmc": return "Chamic languages";
+                case "cha": return "Chamorro";
+                case "che": return "Chechen";
+                case "chr": return "Cherokee";
+                case "chy": return "Cheyenne";
+                case "chb": return "Chibcha";
+                case "chi": return "Chinese";
+                case "zho": return "Chinese";
+                case "chn": return "Chinook jargon";
+                case "chp": return "Chipewyan";
+                case "cho": return "Choctaw";
+                case "chu": return "Church Slavic";
+                case "chk": return "Chuukese";
+                case "chv": return "Chuvash";
+                case "cop": return "Coptic";
+                case "cor": return "Cornish";
+                case "cos": return "Corsican";
+                case "cre": return "Cree";
+                case "mus": return "Creek";
+                case "crp": return "Creoles and pidgins (Other)";
+                case "cpe": return "Creoles and pidgins,";
+                case "cpf": return "Creoles and pidgins,";
+                case "cpp": return "Creoles and pidgins,";
+                case "scr": return "Croatian";
+                case "hrv": return "Croatian";
+                case "cus": return "Cushitic (Other)";
+                case "cze": return "Czech";
+                case "ces": return "Czech";
+                case "dak": return "Dakota";
+                case "dan": return "Danish";
+                case "day": return "Dayak";
+                case "del": return "Delaware";
+                case "din": return "Dinka";
+                case "div": return "Divehi";
+                case "doi": return "Dogri";
+                case "dgr": return "Dogrib";
+                case "dra": return "Dravidian (Other)";
+                case "dua": return "Duala";
+                case "dut": return "Dutch";
+                case "nld": return "Dutch";
+                case "dum": return "Dutch, Middle (ca. 1050-1350)";
+                case "dyu": return "Dyula";
+                case "dzo": return "Dzongkha";
+                case "efi": return "Efik";
+                case "egy": return "Egyptian (Ancient)";
+                case "eka": return "Ekajuk";
+                case "elx": return "Elamite";
+                case "eng": return "English";
+                case "enm": return "English, Middle (1100-1500)";
+                case "ang": return "English, Old (ca.450-1100)";
+                case "epo": return "Esperanto";
+                case "est": return "Estonian";
+                case "ewe": return "Ewe";
+                case "ewo": return "Ewondo";
+                case "fan": return "Fang";
+                case "fat": return "Fanti";
+                case "fao": return "Faroese";
+                case "fij": return "Fijian";
+                case "fin": return "Finnish";
+                case "fiu": return "Finno-Ugrian (Other)";
+                case "fon": return "Fon";
+                case "fre": return "French";
+                case "fra": return "French";
+                case "frm": return "French, Middle (ca.1400-1600)";
+                case "fro": return "French, Old (842-ca.1400)";
+                case "fry": return "Frisian";
+                case "fur": return "Friulian";
+                case "ful": return "Fulah";
+                case "gaa": return "Ga";
+                case "glg": return "Gallegan";
+                case "lug": return "Ganda";
+                case "gay": return "Gayo";
+                case "gba": return "Gbaya";
+                case "gez": return "Geez";
+                case "geo": return "Georgian";
+                case "kat": return "Georgian";
+                case "ger": return "German";
+                case "deu": return "German";
+                case "nds": return "Saxon";
+                case "gmh": return "German, Middle High (ca.1050-1500)";
+                case "goh": return "German, Old High (ca.750-1050)";
+                case "gem": return "Germanic (Other)";
+                case "gil": return "Gilbertese";
+                case "gon": return "Gondi";
+                case "gor": return "Gorontalo";
+                case "got": return "Gothic";
+                case "grb": return "Grebo";
+                case "grc": return "Greek, Ancient (to 1453)";
+                case "gre": return "Greek";
+                case "ell": return "Greek";
+                case "grn": return "Guarani";
+                case "guj": return "Gujarati";
+                case "gwi": return "Gwich´in";
+                case "hai": return "Haida";
+                case "hau": return "Hausa";
+                case "haw": return "Hawaiian";
+                case "heb": return "Hebrew";
+                case "her": return "Herero";
+                case "hil": return "Hiligaynon";
+                case "him": return "Himachali";
+                case "hin": return "Hindi";
+                case "hmo": return "Hiri Motu";
+                case "hit": return "Hittite";
+                case "hmn": return "Hmong";
+                case "hun": return "Hungarian";
+                case "hup": return "Hupa";
+                case "iba": return "Iban";
+                case "ice": return "Icelandic";
+                case "isl": return "Icelandic";
+                case "ibo": return "Igbo";
+                case "ijo": return "Ijo";
+                case "ilo": return "Iloko";
+                case "inc": return "Indic (Other)";
+                case "ine": return "Indo-European (Other)";
+                case "ind": return "Indonesian";
+                case "ina": return "Interlingua (International";
+                case "ile": return "Interlingue";
+                case "iku": return "Inuktitut";
+                case "ipk": return "Inupiaq";
+                case "ira": return "Iranian (Other)";
+                case "gle": return "Irish";
+                case "mga": return "Irish, Middle (900-1200)";
+                case "sga": return "Irish, Old (to 900)";
+                case "iro": return "Iroquoian languages";
+                case "ita": return "Italian";
+                case "jpn": return "Japanese";
+                case "jav": return "Javanese";
+                case "jrb": return "Judeo-Arabic";
+                case "jpr": return "Judeo-Persian";
+                case "kab": return "Kabyle";
+                case "kac": return "Kachin";
+                case "kal": return "Kalaallisut";
+                case "kam": return "Kamba";
+                case "kan": return "Kannada";
+                case "kau": return "Kanuri";
+                case "kaa": return "Kara-Kalpak";
+                case "kar": return "Karen";
+                case "kas": return "Kashmiri";
+                case "kaw": return "Kawi";
+                case "kaz": return "Kazakh";
+                case "kha": return "Khasi";
+                case "khm": return "Khmer";
+                case "khi": return "Khoisan (Other)";
+                case "kho": return "Khotanese";
+                case "kik": return "Kikuyu";
+                case "kmb": return "Kimbundu";
+                case "kin": return "Kinyarwanda";
+                case "kir": return "Kirghiz";
+                case "kom": return "Komi";
+                case "kon": return "Kongo";
+                case "kok": return "Konkani";
+                case "kor": return "Korean";
+                case "kos": return "Kosraean";
+                case "kpe": return "Kpelle";
+                case "kro": return "Kru";
+                case "kua": return "Kuanyama";
+                case "kum": return "Kumyk";
+                case "kur": return "Kurdish";
+                case "kru": return "Kurukh";
+                case "kut": return "Kutenai";
+                case "lad": return "Ladino";
+                case "lah": return "Lahnda";
+                case "lam": return "Lamba";
+                case "lao": return "Lao";
+                case "lat": return "Latin";
+                case "lav": return "Latvian";
+                case "ltz": return "Letzeburgesch";
+                case "lez": return "Lezghian";
+                case "lin": return "Lingala";
+                case "lit": return "Lithuanian";
+                case "loz": return "Lozi";
+                case "lub": return "Luba-Katanga";
+                case "lua": return "Luba-Lulua";
+                case "lui": return "Luiseno";
+                case "lun": return "Lunda";
+                case "luo": return "Luo (Kenya and Tanzania)";
+                case "lus": return "Lushai";
+                case "mac": return "Macedonian";
+                case "mkd": return "Macedonian";
+                case "mad": return "Madurese";
+                case "mag": return "Magahi";
+                case "mai": return "Maithili";
+                case "mak": return "Makasar";
+                case "mlg": return "Malagasy";
+                case "may": return "Malay";
+                case "msa": return "Malay";
+                case "mal": return "Malayalam";
+                case "mlt": return "Maltese";
+                case "mnc": return "Manchu";
+                case "mdr": return "Mandar";
+                case "man": return "Mandingo";
+                case "mni": return "Manipuri";
+                case "mno": return "Manobo languages";
+                case "glv": return "Manx";
+                case "mao": return "Maori";
+                case "mri": return "Maori";
+                case "mar": return "Marathi";
+                case "chm": return "Mari";
+                case "mah": return "Marshall";
+                case "mwr": return "Marwari";
+                case "mas": return "Masai";
+                case "myn": return "Mayan languages";
+                case "men": return "Mende";
+                case "mic": return "Micmac";
+                case "min": return "Minangkabau";
+                case "mis": return "Miscellaneous languages";
+                case "moh": return "Mohawk";
+                case "mol": return "Moldavian";
+                case "mkh": return "Mon-Khmer (Other)";
+                case "lol": return "Mongo";
+                case "mon": return "Mongolian";
+                case "mos": return "Mossi";
+                case "mul": return "Multiple languages";
+                case "mun": return "Munda languages";
+                case "nah": return "Nahuatl";
+                case "nau": return "Nauru";
+                case "nav": return "Navajo";
+                case "nde": return "Ndebele, North";
+                case "nbl": return "Ndebele, South";
+                case "ndo": return "Ndonga";
+                case "nep": return "Nepali";
+                case "new": return "Newari";
+                case "nia": return "Nias";
+                case "nic": return "Niger-Kordofanian (Other)";
+                case "ssa": return "Nilo-Saharan (Other)";
+                case "niu": return "Niuean";
+                case "non": return "Norse, Old";
+                case "nai": return "North American Indian (Other)";
+                case "sme": return "Northern Sami";
+                case "nor": return "Norwegian";
+                case "nob": return "Norwegian Bokmål";
+                case "nno": return "Norwegian Nynorsk";
+                case "nub": return "Nubian languages";
+                case "nym": return "Nyamwezi";
+                case "nya": return "Nyanja";
+                case "nyn": return "Nyankole";
+                case "nyo": return "Nyoro";
+                case "nzi": return "Nzima";
+                case "oci": return "Occitan";
+                case "oji": return "Ojibwa";
+                case "ori": return "Oriya";
+                case "orm": return "Oromo";
+                case "osa": return "Osage";
+                case "oss": return "Ossetian";
+                case "oto": return "Otomian languages";
+                case "pal": return "Pahlavi";
+                case "pau": return "Palauan";
+                case "pli": return "Pali";
+                case "pam": return "Pampanga";
+                case "pag": return "Pangasinan";
+                case "pan": return "Panjabi";
+                case "pap": return "Papiamento";
+                case "paa": return "Papuan (Other)";
+                case "per": return "Persian";
+                case "fas": return "Persian";
+                case "peo": return "Persian, Old (ca.600-400 B.C.)";
+                case "phi": return "Philippine (Other)";
+                case "phn": return "Phoenician";
+                case "pon": return "Pohnpeian";
+                case "pol": return "Polish";
+                case "por": return "Portuguese";
+                case "pra": return "Prakrit languages";
+                case "pro": return "Provençal";
+                case "pus": return "Pushto";
+                case "que": return "Quechua";
+                case "roh": return "Raeto-Romance";
+                case "raj": return "Rajasthani";
+                case "rap": return "Rapanui";
+                case "rar": return "Rarotongan";
+                case "roa": return "Romance (Other)";
+                case "rum": return "Romanian";
+                case "ron": return "Romanian";
+                case "rom": return "Romany";
+                case "run": return "Rundi";
+                case "rus": return "Russian";
+                case "sal": return "Salishan languages";
+                case "sam": return "Samaritan Aramaic";
+                case "smi": return "Sami languages (Other)";
+                case "smo": return "Samoan";
+                case "sad": return "Sandawe";
+                case "sag": return "Sango";
+                case "san": return "Sanskrit";
+                case "sat": return "Santali";
+                case "srd": return "Sardinian";
+                case "sas": return "Sasak";
+                case "sco": return "Scots";
+                case "gla": return "Gaelic";
+                case "sel": return "Selkup";
+                case "sem": return "Semitic (Other)";
+                case "scc": return "Serbian";
+                case "srp": return "Serbian";
+                case "srr": return "Serer";
+                case "shn": return "Shan";
+                case "sna": return "Shona";
+                case "sid": return "Sidamo";
+                case "sgn": return "Sign languages";
+                case "bla": return "Siksika";
+                case "snd": return "Sindhi";
+                case "sin": return "Sinhalese";
+                case "sit": return "Sino-Tibetan (Other)";
+                case "sio": return "Siouan languages";
+                case "den": return "Slave (Athapascan)";
+                case "sla": return "Slavic (Other)";
+                case "slo": return "Slovak";
+                case "slk": return "Slovak";
+                case "slv": return "Slovenian";
+                case "sog": return "Sogdian";
+                case "som": return "Somali";
+                case "son": return "Songhai";
+                case "snk": return "Soninke";
+                case "wen": return "Sorbian languages";
+                case "nso": return "Sotho, Northern";
+                case "sot": return "Sotho, Southern";
+                case "sai": return "South American Indian (Other)";
+                case "spa": return "Spanish";
+                case "suk": return "Sukuma";
+                case "sux": return "Sumerian";
+                case "sun": return "Sundanese";
+                case "sus": return "Susu";
+                case "swa": return "Swahili";
+                case "ssw": return "Swati";
+                case "swe": return "Swedish";
+                case "syr": return "Syriac";
+                case "tgl": return "Tagalog";
+                case "tah": return "Tahitian";
+                case "tai": return "Tai (Other)";
+                case "tgk": return "Tajik";
+                case "tmh": return "Tamashek";
+                case "tam": return "Tamil";
+                case "tat": return "Tatar";
+                case "tel": return "Telugu";
+                case "ter": return "Tereno";
+                case "tet": return "Tetum";
+                case "tha": return "Thai";
+                case "tib": return "Tibetan";
+                case "bod": return "Tibetan";
+                case "tig": return "Tigre";
+                case "tir": return "Tigrinya";
+                case "tem": return "Timne";
+                case "tiv": return "Tiv";
+                case "tli": return "Tlingit";
+                case "tpi": return "Tok Pisin";
+                case "tkl": return "Tokelau";
+                case "tog": return "Tonga (Nyasa)";
+                case "ton": return "Tonga (Tonga Islands)";
+                case "tsi": return "Tsimshian";
+                case "tso": return "Tsonga";
+                case "tsn": return "Tswana";
+                case "tum": return "Tumbuka";
+                case "tur": return "Turkish";
+                case "ota": return "Turkish, Ottoman (1500-1928)";
+                case "tuk": return "Turkmen";
+                case "tvl": return "Tuvalu";
+                case "tyv": return "Tuvinian";
+                case "twi": return "Twi";
+                case "uga": return "Ugaritic";
+                case "uig": return "Uighur";
+                case "ukr": return "Ukrainian";
+                case "umb": return "Umbundu";
+                case "und": return "Undetermined";
+                case "urd": return "Urdu";
+                case "uzb": return "Uzbek";
+                case "vai": return "Vai";
+                case "ven": return "Venda";
+                case "vie": return "Vietnamese";
+                case "vol": return "Volapük";
+                case "vot": return "Votic";
+                case "wak": return "Wakashan languages";
+                case "wal": return "Walamo";
+                case "war": return "Waray";
+                case "was": return "Washo";
+                case "wel": return "Welsh";
+                case "cym": return "Welsh";
+                case "wol": return "Wolof";
+                case "xho": return "Xhosa";
+                case "sah": return "Yakut";
+                case "yao": return "Yao";
+                case "yap": return "Yapese";
+                case "yid": return "Yiddish";
+                case "yor": return "Yoruba";
+                case "ypk": return "Yupik languages";
+                case "znd": return "Zande";
+                case "zap": return "Zapotec";
+                case "zen": return "Zenaga";
+                case "zha": return "Zhuang";
+                case "zul": return "Zulu";
+                case "zun": return "Zuni";
+
+                default: return code;
+            }
+        }
+    }
+}

+ 36 - 0
BDInfo/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("BDInfo")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("BDInfo")]
+[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("2ecb9fe5-e2da-4b49-880b-d9887a84c311")]
+
+// 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")]

+ 5 - 0
BDInfo/ReadMe.txt

@@ -0,0 +1,5 @@
+The source is taken from the BDRom folder of this project:
+
+http://www.cinemasquid.com/blu-ray/tools/bdinfo
+
+BDInfoSettings was taken from the FormSettings class, and changed so that the settings all return defaults.

+ 312 - 0
BDInfo/TSCodecAC3.cs

@@ -0,0 +1,312 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+#undef DEBUG
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public abstract class TSCodecAC3
+    {
+        private static byte[] eac3_blocks =  new byte[] { 1, 2, 3, 6 };
+
+        public static void Scan(
+            TSAudioStream stream,
+            TSStreamBuffer buffer,
+            ref string tag)
+        {
+            if (stream.IsInitialized) return;
+
+            byte[] sync = buffer.ReadBytes(2);
+            if (sync == null ||
+                sync[0] != 0x0B ||
+                sync[1] != 0x77)
+            {
+                return;
+            }
+
+            int sr_code = 0;
+            int frame_size = 0;
+            int frame_size_code = 0;
+            int channel_mode = 0;
+            int lfe_on = 0;
+            int dial_norm = 0;
+            int num_blocks = 0;
+
+            byte[] hdr = buffer.ReadBytes(4);
+            int bsid = (hdr[3] & 0xF8) >> 3;
+            buffer.Seek(-4, SeekOrigin.Current);
+            if (bsid <= 10)
+            {
+                byte[] crc = buffer.ReadBytes(2);
+                sr_code = buffer.ReadBits(2);
+                frame_size_code = buffer.ReadBits(6);
+                bsid = buffer.ReadBits(5);
+                int bsmod = buffer.ReadBits(3);
+
+                channel_mode = buffer.ReadBits(3);
+                int cmixlev = 0;
+                if (((channel_mode & 0x1) > 0) && (channel_mode != 0x1))
+                {
+                    cmixlev = buffer.ReadBits(2);
+                }
+                int surmixlev = 0;
+                if ((channel_mode & 0x4) > 0)
+                {
+                    surmixlev = buffer.ReadBits(2);
+                }
+                int dsurmod = 0;
+                if (channel_mode == 0x2)
+                {
+                    dsurmod = buffer.ReadBits(2);
+                    if (dsurmod == 0x2)
+                    {
+                        stream.AudioMode = TSAudioMode.Surround;
+                    }
+                }
+                lfe_on = buffer.ReadBits(1);
+                dial_norm = buffer.ReadBits(5);
+                int compr = 0;
+                if (1 == buffer.ReadBits(1))
+                {
+                    compr = buffer.ReadBits(8);
+                }
+                int langcod = 0;
+                if (1 == buffer.ReadBits(1))
+                {
+                    langcod = buffer.ReadBits(8);
+                }
+                int mixlevel = 0;
+                int roomtyp = 0;
+                if (1 == buffer.ReadBits(1))
+                {
+                    mixlevel = buffer.ReadBits(5);
+                    roomtyp = buffer.ReadBits(2);
+                }
+                if (channel_mode == 0)
+                {
+                    int dialnorm2 = buffer.ReadBits(5);
+                    int compr2 = 0;
+                    if (1 == buffer.ReadBits(1))
+                    {
+                        compr2 = buffer.ReadBits(8);
+                    }
+                    int langcod2 = 0;
+                    if (1 == buffer.ReadBits(1))
+                    {
+                        langcod2 = buffer.ReadBits(8);
+                    }
+                    int mixlevel2 = 0;
+                    int roomtyp2 = 0;
+                    if (1 == buffer.ReadBits(1))
+                    {
+                        mixlevel2 = buffer.ReadBits(5);
+                        roomtyp2 = buffer.ReadBits(2);
+                    }
+                }
+                int copyrightb = buffer.ReadBits(1);
+                int origbs = buffer.ReadBits(1);
+                if (bsid == 6)
+                {
+                    if (1 == buffer.ReadBits(1))
+                    {
+                        int dmixmod = buffer.ReadBits(2);
+                        int ltrtcmixlev = buffer.ReadBits(3);
+                        int ltrtsurmixlev = buffer.ReadBits(3);
+                        int lorocmixlev = buffer.ReadBits(3);
+                        int lorosurmixlev = buffer.ReadBits(3);
+                    }
+                    if (1 == buffer.ReadBits(1))
+                    {
+                        int dsurexmod = buffer.ReadBits(2);
+                        int dheadphonmod = buffer.ReadBits(2);
+                        if (dheadphonmod == 0x2)
+                        {
+                            // TODO
+                        }
+                        int adconvtyp = buffer.ReadBits(1);
+                        int xbsi2 = buffer.ReadBits(8);
+                        int encinfo = buffer.ReadBits(1);
+                        if (dsurexmod == 2)
+                        {
+                            stream.AudioMode = TSAudioMode.Extended;
+                        }
+                    }
+                }
+            }
+            else
+            {
+                int frame_type = buffer.ReadBits(2);
+                int substreamid = buffer.ReadBits(3);
+                frame_size = (buffer.ReadBits(11) + 1) << 1;
+
+                sr_code = buffer.ReadBits(2);
+                if (sr_code == 3)
+                {
+                    sr_code = buffer.ReadBits(2);
+                }
+                else
+                {
+                    num_blocks = buffer.ReadBits(2);
+                }
+                channel_mode = buffer.ReadBits(3);
+                lfe_on = buffer.ReadBits(1);
+            }
+
+            switch (channel_mode)
+            {
+                case 0: // 1+1
+                    stream.ChannelCount = 2;
+                    if (stream.AudioMode == TSAudioMode.Unknown)
+                    {
+                        stream.AudioMode = TSAudioMode.DualMono;
+                    }
+                    break;
+                case 1: // 1/0
+                    stream.ChannelCount = 1;
+                    break;
+                case 2: // 2/0
+                    stream.ChannelCount = 2;
+                    if (stream.AudioMode == TSAudioMode.Unknown)
+                    {
+                        stream.AudioMode = TSAudioMode.Stereo;
+                    }
+                    break;
+                case 3: // 3/0
+                    stream.ChannelCount = 3;
+                    break;
+                case 4: // 2/1
+                    stream.ChannelCount = 3;
+                    break;
+                case 5: // 3/1
+                    stream.ChannelCount = 4;
+                    break;
+                case 6: // 2/2
+                    stream.ChannelCount = 4;
+                    break;
+                case 7: // 3/2
+                    stream.ChannelCount = 5;
+                    break;
+                default:
+                    stream.ChannelCount = 0;
+                    break;
+            }
+
+            switch (sr_code)
+            {
+                case 0:
+                    stream.SampleRate = 48000;
+                    break;
+                case 1:
+                    stream.SampleRate = 44100;
+                    break;
+                case 2:
+                    stream.SampleRate = 32000;
+                    break;
+                default:
+                    stream.SampleRate = 0;
+                    break;
+            }
+
+            if (bsid <= 10)
+            {
+                switch (frame_size_code >> 1)
+                {
+                    case 18:
+                        stream.BitRate = 640000;
+                        break;
+                    case 17:
+                        stream.BitRate = 576000;
+                        break;
+                    case 16:
+                        stream.BitRate = 512000;
+                        break;
+                    case 15:
+                        stream.BitRate = 448000;
+                        break;
+                    case 14:
+                        stream.BitRate = 384000;
+                        break;
+                    case 13:
+                        stream.BitRate = 320000;
+                        break;
+                    case 12:
+                        stream.BitRate = 256000;
+                        break;
+                    case 11:
+                        stream.BitRate = 224000;
+                        break;
+                    case 10:
+                        stream.BitRate = 192000;
+                        break;
+                    case 9:
+                        stream.BitRate = 160000;
+                        break;
+                    case 8:
+                        stream.BitRate = 128000;
+                        break;
+                    case 7:
+                        stream.BitRate = 112000;
+                        break;
+                    case 6:
+                        stream.BitRate = 96000;
+                        break;
+                    case 5:
+                        stream.BitRate = 80000;
+                        break;
+                    case 4:
+                        stream.BitRate = 64000;
+                        break;
+                    case 3:
+                        stream.BitRate = 56000;
+                        break;
+                    case 2:
+                        stream.BitRate = 48000;
+                        break;
+                    case 1:
+                        stream.BitRate = 40000;
+                        break;
+                    case 0:
+                        stream.BitRate = 32000;
+                        break;
+                    default:
+                        stream.BitRate = 0;
+                        break;
+                }
+            }
+            else
+            {
+                stream.BitRate = (long)
+                    (4.0 * frame_size * stream.SampleRate / (num_blocks * 256));
+            }
+
+            stream.LFE = lfe_on;
+            if (stream.StreamType != TSStreamType.AC3_PLUS_AUDIO &&
+                stream.StreamType != TSStreamType.AC3_PLUS_SECONDARY_AUDIO)
+            {
+                stream.DialNorm = dial_norm - 31;
+            }
+            stream.IsVBR = false;
+            stream.IsInitialized = true;
+        }
+    }
+}

+ 151 - 0
BDInfo/TSCodecAVC.cs

@@ -0,0 +1,151 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public abstract class TSCodecAVC
+    {
+        public static void Scan(
+            TSVideoStream stream,
+            TSStreamBuffer buffer,
+            ref string tag)
+        {
+            uint parse = 0;
+            byte accessUnitDelimiterParse = 0;
+            byte sequenceParameterSetParse = 0;
+            string profile = null;
+            string level = null;
+            byte constraintSet0Flag = 0;
+            byte constraintSet1Flag = 0;
+            byte constraintSet2Flag = 0;
+            byte constraintSet3Flag = 0;
+
+            for (int i = 0; i < buffer.Length; i++)
+            {
+                parse = (parse << 8) + buffer.ReadByte();
+
+                if (parse == 0x00000109)
+                {
+                    accessUnitDelimiterParse = 1;
+                }
+                else if (accessUnitDelimiterParse > 0)
+                {
+                    --accessUnitDelimiterParse;
+                    if (accessUnitDelimiterParse == 0)
+                    {
+                        switch ((parse & 0xFF) >> 5)
+                        {
+                            case 0: // I
+                            case 3: // SI
+                            case 5: // I, SI
+                                tag = "I";
+                                break;
+
+                            case 1: // I, P
+                            case 4: // SI, SP
+                            case 6: // I, SI, P, SP
+                                tag = "P";
+                                break;
+
+                            case 2: // I, P, B
+                            case 7: // I, SI, P, SP, B
+                                tag = "B";
+                                break;
+                        }
+                        if (stream.IsInitialized) return;
+                    }
+                }
+                else if (parse == 0x00000127 || parse == 0x00000167)
+                {
+                    sequenceParameterSetParse = 3;
+                }
+                else if (sequenceParameterSetParse > 0)
+                {
+                    --sequenceParameterSetParse;
+                    switch (sequenceParameterSetParse)
+                    {
+                        case 2:
+                            switch (parse & 0xFF)
+                            {
+                                case 66:
+                                    profile = "Baseline Profile";
+                                    break;
+                                case 77:
+                                    profile = "Main Profile";
+                                    break;
+                                case 88:
+                                    profile = "Extended Profile";
+                                    break;
+                                case 100:
+                                    profile = "High Profile";
+                                    break;
+                                case 110:
+                                    profile = "High 10 Profile";
+                                    break;
+                                case 122:
+                                    profile = "High 4:2:2 Profile";
+                                    break;
+                                case 144:
+                                    profile = "High 4:4:4 Profile";
+                                    break;
+                                default:
+                                    profile = "Unknown Profile";
+                                    break;
+                            }
+                            break;
+
+                        case 1:
+                            constraintSet0Flag = (byte)
+                                ((parse & 0x80) >> 7);
+                            constraintSet1Flag = (byte)
+                                ((parse & 0x40) >> 6);
+                            constraintSet2Flag = (byte)
+                                ((parse & 0x20) >> 5);
+                            constraintSet3Flag = (byte)
+                                ((parse & 0x10) >> 4);
+                            break;
+
+                        case 0:
+                            byte b = (byte)(parse & 0xFF);
+                            if (b == 11 && constraintSet3Flag == 1)
+                            {
+                                level = "1b";
+                            }
+                            else
+                            {
+                                level = string.Format(
+                                    "{0:D}.{1:D}",
+                                    b / 10, (b - ((b / 10) * 10)));
+                            }
+                            stream.EncodingProfile = string.Format(
+                                "{0} {1}", profile, level);
+                            stream.IsVBR = true;
+                            stream.IsInitialized = true;
+                            break;
+                    }
+                }
+            }
+            return;
+        }
+    }
+}

+ 162 - 0
BDInfo/TSCodecDTS.cs

@@ -0,0 +1,162 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public abstract class TSCodecDTS
+    {
+        private static int[] dca_sample_rates =
+        {
+            0, 8000, 16000, 32000, 0, 0, 11025, 22050, 44100, 0, 0,
+            12000, 24000, 48000, 96000, 192000
+        };
+
+        private static int[] dca_bit_rates =
+        {
+            32000, 56000, 64000, 96000, 112000, 128000,
+            192000, 224000, 256000, 320000, 384000,
+            448000, 512000, 576000, 640000, 768000,
+            896000, 1024000, 1152000, 1280000, 1344000,
+            1408000, 1411200, 1472000, 1509000, 1920000,
+            2048000, 3072000, 3840000, 1/*open*/, 2/*variable*/, 3/*lossless*/
+        };
+
+        private static int[] dca_channels =
+        {
+            1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6, 7, 8, 8
+        };
+
+        private static int[] dca_bits_per_sample =
+        {
+            16, 16, 20, 20, 0, 24, 24
+        };
+
+        public static void Scan(
+            TSAudioStream stream,
+            TSStreamBuffer buffer,
+            long bitrate,
+            ref string tag)
+        {
+            if (stream.IsInitialized) return;
+
+            bool syncFound = false;
+            uint sync = 0;
+            for (int i = 0; i < buffer.Length; i++)
+            {
+                sync = (sync << 8) + buffer.ReadByte();
+                if (sync == 0x7FFE8001)
+                {
+                    syncFound = true;
+                    break;
+                }
+            }
+            if (!syncFound) return;
+
+            int frame_type = buffer.ReadBits(1);
+            int samples_deficit = buffer.ReadBits(5);
+            int crc_present = buffer.ReadBits(1);
+            int sample_blocks = buffer.ReadBits(7);
+            int frame_size = buffer.ReadBits(14);
+            if (frame_size < 95)
+            {
+                return;
+            }
+            int amode = buffer.ReadBits(6);
+            int sample_rate = buffer.ReadBits(4);
+            if (sample_rate < 0 || sample_rate >= dca_sample_rates.Length)
+            {
+                return;
+            }
+            int bit_rate = buffer.ReadBits(5);
+            if (bit_rate < 0 || bit_rate >= dca_bit_rates.Length)
+            {
+                return;
+            }
+            int downmix = buffer.ReadBits(1);
+            int dynrange = buffer.ReadBits(1);
+            int timestamp = buffer.ReadBits(1);
+            int aux_data = buffer.ReadBits(1);
+            int hdcd = buffer.ReadBits(1);
+            int ext_descr = buffer.ReadBits(3);
+            int ext_coding = buffer.ReadBits(1);
+            int aspf = buffer.ReadBits(1);
+            int lfe = buffer.ReadBits(2);
+            int predictor_history = buffer.ReadBits(1);
+            if (crc_present == 1)
+            {
+                int crc = buffer.ReadBits(16);
+            }
+            int multirate_inter = buffer.ReadBits(1);
+            int version = buffer.ReadBits(4);
+            int copy_history = buffer.ReadBits(2);
+            int source_pcm_res = buffer.ReadBits(3);
+            int front_sum = buffer.ReadBits(1);
+            int surround_sum = buffer.ReadBits(1);
+            int dialog_norm = buffer.ReadBits(4);
+            if (source_pcm_res < 0 || source_pcm_res >= dca_bits_per_sample.Length)
+            {
+                return;
+            }
+            int subframes = buffer.ReadBits(4);
+            int total_channels = buffer.ReadBits(3) + 1 + ext_coding;
+
+            stream.SampleRate = dca_sample_rates[sample_rate];
+            stream.ChannelCount = total_channels;
+            stream.LFE = (lfe > 0 ? 1 : 0);
+            stream.BitDepth = dca_bits_per_sample[source_pcm_res];
+            stream.DialNorm = -dialog_norm;
+            if ((source_pcm_res & 0x1) == 0x1)
+            {
+                stream.AudioMode = TSAudioMode.Extended;
+            }
+
+            stream.BitRate = (uint)dca_bit_rates[bit_rate];
+            switch (stream.BitRate)
+            {
+                case 1:
+                    if (bitrate > 0)
+                    {
+                        stream.BitRate = bitrate;
+                        stream.IsVBR = false;
+                        stream.IsInitialized = true;
+                    }
+                    else
+                    {
+                        stream.BitRate = 0;
+                    }
+                    break;
+
+                case 2:
+                case 3:
+                    stream.IsVBR = true;
+                    stream.IsInitialized = true;
+                    break;
+                
+                default:
+                    stream.IsVBR = false;
+                    stream.IsInitialized = true;
+                    break;
+            }
+        }
+    }
+}

+ 249 - 0
BDInfo/TSCodecDTSHD.cs

@@ -0,0 +1,249 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public abstract class TSCodecDTSHD
+    {
+        private static int[] SampleRates = new int[] 
+        { 0x1F40, 0x3E80, 0x7D00, 0x0FA00, 0x1F400, 0x5622, 0x0AC44, 0x15888, 0x2B110, 0x56220, 0x2EE0, 0x5DC0, 0x0BB80, 0x17700, 0x2EE00, 0x5DC00 };
+        
+        public static void Scan(
+            TSAudioStream stream,
+            TSStreamBuffer buffer,
+            long bitrate,
+            ref string tag)
+        {
+            if (stream.IsInitialized &&
+                (stream.StreamType == TSStreamType.DTS_HD_SECONDARY_AUDIO ||
+                (stream.CoreStream != null &&
+                 stream.CoreStream.IsInitialized))) return;
+
+            bool syncFound = false;
+            uint sync = 0;
+            for (int i = 0; i < buffer.Length; i++)
+            {
+                sync = (sync << 8) + buffer.ReadByte();
+                if (sync == 0x64582025)
+                {
+                    syncFound = true;
+                    break;
+                }
+            }
+
+            if (!syncFound)
+            {
+                tag = "CORE";
+                if (stream.CoreStream == null)
+                {
+                    stream.CoreStream = new TSAudioStream();
+                    stream.CoreStream.StreamType = TSStreamType.DTS_AUDIO;
+                }
+                if (!stream.CoreStream.IsInitialized)
+                {
+                    buffer.BeginRead();
+                    TSCodecDTS.Scan(stream.CoreStream, buffer, bitrate, ref tag);
+                }
+                return;
+            }
+
+            tag = "HD";
+            int temp1 = buffer.ReadBits(8);
+            int nuSubStreamIndex = buffer.ReadBits(2);
+            int nuExtSSHeaderSize = 0;
+            int nuExtSSFSize = 0;
+            int bBlownUpHeader = buffer.ReadBits(1);
+            if (1 == bBlownUpHeader)
+            {
+                nuExtSSHeaderSize = buffer.ReadBits(12) + 1;
+                nuExtSSFSize = buffer.ReadBits(20) + 1;
+            }
+            else
+            {
+                nuExtSSHeaderSize = buffer.ReadBits(8) + 1;
+                nuExtSSFSize = buffer.ReadBits(16) + 1;
+            }
+            int nuNumAudioPresent = 1;
+            int nuNumAssets = 1;
+            int bStaticFieldsPresent = buffer.ReadBits(1);
+            if (1 == bStaticFieldsPresent)
+            {
+                int nuRefClockCode = buffer.ReadBits(2);
+                int nuExSSFrameDurationCode = buffer.ReadBits(3) + 1;
+                long nuTimeStamp = 0;
+                if (1 == buffer.ReadBits(1))
+                {
+                    nuTimeStamp = (buffer.ReadBits(18) << 18) + buffer.ReadBits(18);
+                }
+                nuNumAudioPresent = buffer.ReadBits(3) + 1;
+                nuNumAssets = buffer.ReadBits(3) + 1;
+                int[] nuActiveExSSMask = new int[nuNumAudioPresent];
+                for (int i = 0; i < nuNumAudioPresent; i++)
+                {
+                    nuActiveExSSMask[i] = buffer.ReadBits(nuSubStreamIndex + 1); //?
+                }
+                for (int i = 0; i < nuNumAudioPresent; i++)
+                {
+                    for (int j = 0; j < nuSubStreamIndex + 1; j++)
+                    {
+                        if (((j + 1) % 2) == 1)
+                        {
+                            int mask = buffer.ReadBits(8);
+                        }
+                    }
+                }
+                if (1 == buffer.ReadBits(1))
+                {
+                    int nuMixMetadataAdjLevel = buffer.ReadBits(2);
+                    int nuBits4MixOutMask = buffer.ReadBits(2) * 4 + 4;
+                    int nuNumMixOutConfigs = buffer.ReadBits(2) + 1;
+                    int[] nuMixOutChMask = new int[nuNumMixOutConfigs];
+                    for (int i = 0; i < nuNumMixOutConfigs; i++)
+                    {
+                        nuMixOutChMask[i] = buffer.ReadBits(nuBits4MixOutMask);
+                    }
+                }
+            }
+            int[] AssetSizes = new int[nuNumAssets];
+            for (int i = 0; i < nuNumAssets; i++)
+            {
+                if (1 == bBlownUpHeader)
+                {
+                    AssetSizes[i] = buffer.ReadBits(20) + 1;
+                }
+                else
+                {
+                    AssetSizes[i] = buffer.ReadBits(16) + 1;
+                }                
+            }
+            for (int i = 0; i < nuNumAssets; i++)
+            {
+                long bufferPosition = buffer.Position;
+                int nuAssetDescriptorFSIZE = buffer.ReadBits(9) + 1;
+                int DescriptorDataForAssetIndex = buffer.ReadBits(3);
+                if (1 == bStaticFieldsPresent)
+                {
+                    int AssetTypeDescrPresent = buffer.ReadBits(1);
+                    if (1 == AssetTypeDescrPresent)
+                    {
+                        int AssetTypeDescriptor = buffer.ReadBits(4);
+                    }
+                    int LanguageDescrPresent = buffer.ReadBits(1);
+                    if (1 == LanguageDescrPresent)
+                    {
+                        int LanguageDescriptor = buffer.ReadBits(24);
+                    }
+                    int bInfoTextPresent = buffer.ReadBits(1);
+                    if (1 == bInfoTextPresent)
+                    {
+                        int nuInfoTextByteSize = buffer.ReadBits(10) + 1;
+                        int[] InfoText = new int[nuInfoTextByteSize];
+                        for (int j = 0; j < nuInfoTextByteSize; j++)
+                        {
+                            InfoText[j] = buffer.ReadBits(8);
+                        }
+                    }
+                    int nuBitResolution = buffer.ReadBits(5) + 1;
+                    int nuMaxSampleRate = buffer.ReadBits(4);
+                    int nuTotalNumChs = buffer.ReadBits(8) + 1;
+                    int bOne2OneMapChannels2Speakers = buffer.ReadBits(1);
+                    int nuSpkrActivityMask = 0;
+                    if (1 == bOne2OneMapChannels2Speakers)
+                    {
+                        int bEmbeddedStereoFlag = 0;
+                        if (nuTotalNumChs > 2)
+                        {
+                            bEmbeddedStereoFlag = buffer.ReadBits(1);
+                        }
+                        int bEmbeddedSixChFlag = 0;
+                        if (nuTotalNumChs > 6)
+                        {
+                            bEmbeddedSixChFlag = buffer.ReadBits(1);
+                        }
+                        int bSpkrMaskEnabled = buffer.ReadBits(1);
+                        int nuNumBits4SAMask = 0;
+                        if (1 == bSpkrMaskEnabled)
+                        {
+                            nuNumBits4SAMask = buffer.ReadBits(2);
+                            nuNumBits4SAMask = nuNumBits4SAMask * 4 + 4;
+                            nuSpkrActivityMask = buffer.ReadBits(nuNumBits4SAMask);
+                        }
+                        // TODO...
+                    }
+                    stream.SampleRate = SampleRates[nuMaxSampleRate];
+                    stream.BitDepth = nuBitResolution;
+                    
+                    stream.LFE = 0;
+                    if ((nuSpkrActivityMask & 0x8) == 0x8)
+                    {
+                        ++stream.LFE;
+                    }
+                    if ((nuSpkrActivityMask & 0x1000) == 0x1000)
+                    {
+                        ++stream.LFE;
+                    }
+                    stream.ChannelCount = nuTotalNumChs - stream.LFE;
+                }
+                if (nuNumAssets > 1)
+                {
+                    // TODO...
+                    break;
+                }
+            }
+
+            // TODO
+            if (stream.CoreStream != null)
+            {
+                TSAudioStream coreStream = (TSAudioStream)stream.CoreStream;
+                if (coreStream.AudioMode == TSAudioMode.Extended &&
+                    stream.ChannelCount == 5)
+                {
+                    stream.AudioMode = TSAudioMode.Extended;
+                }
+                /*
+                if (coreStream.DialNorm != 0)
+                {
+                    stream.DialNorm = coreStream.DialNorm;
+                }
+                */
+            }
+
+            if (stream.StreamType == TSStreamType.DTS_HD_MASTER_AUDIO)
+            {
+                stream.IsVBR = true;
+                stream.IsInitialized = true;
+            }
+            else if (bitrate > 0)
+            {
+                stream.IsVBR = false;
+                stream.BitRate = bitrate;
+                if (stream.CoreStream != null)
+                {
+                    stream.BitRate += stream.CoreStream.BitRate;
+                    stream.IsInitialized = true;
+                }
+                stream.IsInitialized = (stream.BitRate > 0 ? true : false);
+            }            
+        }
+    }
+}

+ 126 - 0
BDInfo/TSCodecLPCM.cs

@@ -0,0 +1,126 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public abstract class TSCodecLPCM
+    {
+        public static void Scan(
+            TSAudioStream stream,
+            TSStreamBuffer buffer,
+            ref string tag)
+        {
+            if (stream.IsInitialized) return;
+
+            byte[] header = buffer.ReadBytes(4);
+            int flags = (header[2] << 8) + header[3];
+
+            switch ((flags & 0xF000) >> 12)
+            {
+                case 1: // 1/0/0
+                    stream.ChannelCount = 1;
+                    stream.LFE = 0;
+                    break;
+                case 3: // 2/0/0
+                    stream.ChannelCount = 2;
+                    stream.LFE = 0;
+                    break;
+                case 4: // 3/0/0
+                    stream.ChannelCount = 3;
+                    stream.LFE = 0;
+                    break;
+                case 5: // 2/1/0
+                    stream.ChannelCount = 3;
+                    stream.LFE = 0;
+                    break;
+                case 6: // 3/1/0
+                    stream.ChannelCount = 4;
+                    stream.LFE = 0;
+                    break;
+                case 7: // 2/2/0
+                    stream.ChannelCount = 4;
+                    stream.LFE = 0;
+                    break;
+                case 8: // 3/2/0
+                    stream.ChannelCount = 5;
+                    stream.LFE = 0;
+                    break;
+                case 9: // 3/2/1
+                    stream.ChannelCount = 5;
+                    stream.LFE = 1;
+                    break;
+                case 10: // 3/4/0
+                    stream.ChannelCount = 7;
+                    stream.LFE = 0;
+                    break;
+                case 11: // 3/4/1
+                    stream.ChannelCount = 7;
+                    stream.LFE = 1;
+                    break;
+                default:
+                    stream.ChannelCount = 0;
+                    stream.LFE = 0;
+                    break;
+            }
+
+            switch ((flags & 0xC0) >> 6)
+            {
+                case 1:
+                    stream.BitDepth = 16;
+                    break;
+                case 2:
+                    stream.BitDepth = 20;
+                    break;
+                case 3:
+                    stream.BitDepth = 24;
+                    break;
+                default:
+                    stream.BitDepth = 0;
+                    break;
+            }
+
+            switch ((flags & 0xF00) >> 8)
+            {
+                case 1:
+                    stream.SampleRate = 48000;
+                    break;
+                case 4:
+                    stream.SampleRate = 96000;
+                    break;
+                case 5:
+                    stream.SampleRate = 192000;
+                    break;
+                default:
+                    stream.SampleRate = 0;
+                    break;
+            }
+
+            stream.BitRate = (uint)
+                (stream.SampleRate * stream.BitDepth *
+                 (stream.ChannelCount + stream.LFE));
+
+            stream.IsVBR = false;
+            stream.IsInitialized = true;
+        }
+    }
+}

+ 211 - 0
BDInfo/TSCodecMPEG2.cs

@@ -0,0 +1,211 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+#undef DEBUG
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public abstract class TSCodecMPEG2
+    {
+        public static void Scan(
+            TSVideoStream stream,
+            TSStreamBuffer buffer,
+            ref string tag)
+        {
+            int parse = 0;
+            int pictureParse = 0;
+            int sequenceHeaderParse = 0;
+            int extensionParse = 0;
+            int sequenceExtensionParse = 0;            
+
+            for (int i = 0; i < buffer.Length; i++)
+            {
+                parse = (parse << 8) + buffer.ReadByte();
+
+                if (parse == 0x00000100)
+                {
+                    pictureParse = 2;
+                }
+                else if (parse == 0x000001B3)
+                {
+                    sequenceHeaderParse = 7;
+                }
+                else if (sequenceHeaderParse > 0)
+                {
+                    --sequenceHeaderParse;
+                    switch (sequenceHeaderParse)
+                    {
+#if DEBUG
+                        case 6:
+                            break;
+
+                        case 5:
+                            break;
+
+                        case 4:
+                            stream.Width =
+                                (int)((parse & 0xFFF000) >> 12);
+                            stream.Height =
+                                (int)(parse & 0xFFF);
+                            break;
+
+                        case 3:
+                            stream.AspectRatio =
+                                (TSAspectRatio)((parse & 0xF0) >> 4);
+
+                            switch ((parse & 0xF0) >> 4)
+                            {
+                                case 0: // Forbidden
+                                    break;
+                                case 1: // Square
+                                    break;
+                                case 2: // 4:3
+                                    break;
+                                case 3: // 16:9
+                                    break;
+                                case 4: // 2.21:1
+                                    break;
+                                default: // Reserved
+                                    break;
+                            }
+
+                            switch (parse & 0xF)
+                            {
+                                case 0: // Forbidden
+                                    break;
+                                case 1: // 23.976
+                                    stream.FrameRateEnumerator = 24000;
+                                    stream.FrameRateDenominator = 1001;
+                                    break;
+                                case 2: // 24
+                                    stream.FrameRateEnumerator = 24000;
+                                    stream.FrameRateDenominator = 1000;
+                                    break;
+                                case 3: // 25
+                                    stream.FrameRateEnumerator = 25000;
+                                    stream.FrameRateDenominator = 1000;
+                                    break;
+                                case 4: // 29.97
+                                    stream.FrameRateEnumerator = 30000;
+                                    stream.FrameRateDenominator = 1001;
+                                    break;
+                                case 5: // 30
+                                    stream.FrameRateEnumerator = 30000;
+                                    stream.FrameRateDenominator = 1000;
+                                    break;
+                                case 6: // 50
+                                    stream.FrameRateEnumerator = 50000;
+                                    stream.FrameRateDenominator = 1000;
+                                    break;
+                                case 7: // 59.94
+                                    stream.FrameRateEnumerator = 60000;
+                                    stream.FrameRateDenominator = 1001;
+                                    break;
+                                case 8: // 60
+                                    stream.FrameRateEnumerator = 60000;
+                                    stream.FrameRateDenominator = 1000;
+                                    break;
+                                default: // Reserved
+                                    stream.FrameRateEnumerator = 0;
+                                    stream.FrameRateDenominator = 0;
+                                    break;
+                            }
+                            break;
+
+                        case 2:
+                            break;
+
+                        case 1:
+                            break;
+#endif
+
+                        case 0:
+#if DEBUG
+                            stream.BitRate =
+                                (((parse & 0xFFFFC0) >> 6) * 200);
+#endif
+                            stream.IsVBR = true;
+                            stream.IsInitialized = true;
+                            break;
+                    }
+                }
+                else if (pictureParse > 0)
+                {
+                    --pictureParse;
+                    if (pictureParse == 0)
+                    {
+                        switch ((parse & 0x38) >> 3)
+                        {
+                            case 1:
+                                tag = "I";
+                                break;
+                            case 2:
+                                tag = "P";
+                                break;
+                            case 3:
+                                tag = "B";
+                                break;
+                            default:
+                                break;
+                        }
+                        if (stream.IsInitialized) return;
+                    }
+                }
+                else if (parse == 0x000001B5)
+                {
+                    extensionParse = 1;
+                }
+                else if (extensionParse > 0)
+                {
+                    --extensionParse;
+                    if (extensionParse == 0)
+                    {
+                        if ((parse & 0xF0) == 0x10)
+                        {
+                            sequenceExtensionParse = 1;
+                        }
+                    }
+                }
+                else if (sequenceExtensionParse > 0)
+                {
+                    --sequenceExtensionParse;
+#if DEBUG
+                    if (sequenceExtensionParse == 0)
+                    {
+                        uint sequenceExtension = 
+                            ((parse & 0x8) >> 3);
+                        if (sequenceExtension == 0)
+                        {
+                            stream.IsInterlaced = true;
+                        }
+                        else
+                        {
+                            stream.IsInterlaced = false;
+                        }
+                    }
+#endif
+                }
+            }
+        }
+    }
+}

+ 39 - 0
BDInfo/TSCodecMVC.cs

@@ -0,0 +1,39 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    // TODO: Do something more interesting here...
+
+    public abstract class TSCodecMVC
+    {
+        public static void Scan(
+            TSVideoStream stream,
+            TSStreamBuffer buffer,
+            ref string tag)
+        {
+            stream.IsVBR = true;
+            stream.IsInitialized = true;
+        }
+    }
+}

+ 189 - 0
BDInfo/TSCodecTrueHD.cs

@@ -0,0 +1,189 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public abstract class TSCodecTrueHD
+    {
+        public static void Scan(
+            TSAudioStream stream,
+            TSStreamBuffer buffer,
+            ref string tag)
+        {
+            if (stream.IsInitialized &&
+                stream.CoreStream != null &&
+                stream.CoreStream.IsInitialized) return;
+
+            bool syncFound = false;
+            uint sync = 0;
+            for (int i = 0; i < buffer.Length; i++)
+            {
+                sync = (sync << 8) + buffer.ReadByte();
+                if (sync == 0xF8726FBA) 
+                {
+                    syncFound = true;
+                    break;
+                }
+            }
+
+            if (!syncFound)
+            {
+                tag = "CORE";
+                if (stream.CoreStream == null)
+                {
+                    stream.CoreStream = new TSAudioStream();
+                    stream.CoreStream.StreamType = TSStreamType.AC3_AUDIO;
+                }
+                if (!stream.CoreStream.IsInitialized)
+                {
+                    buffer.BeginRead();
+                    TSCodecAC3.Scan(stream.CoreStream, buffer, ref tag);
+                }
+                return;
+            }
+
+            tag = "HD";
+            int ratebits = buffer.ReadBits(4);
+            if (ratebits != 0xF)
+            {
+                stream.SampleRate = 
+                    (((ratebits & 8) > 0 ? 44100 : 48000) << (ratebits & 7));
+            }
+            int temp1 = buffer.ReadBits(8);
+            int channels_thd_stream1 = buffer.ReadBits(5);
+            int temp2 = buffer.ReadBits(2);
+
+            stream.ChannelCount = 0;
+            stream.LFE = 0;
+            int c_LFE2 = buffer.ReadBits(1);
+            if (c_LFE2 == 1)
+            {
+                stream.LFE += 1;
+            }
+            int c_Cvh = buffer.ReadBits(1);
+            if (c_Cvh == 1)
+            {
+                stream.ChannelCount += 1;
+            }
+            int c_LRw = buffer.ReadBits(1);
+            if (c_LRw == 1)
+            {
+                stream.ChannelCount += 2;
+            }
+            int c_LRsd = buffer.ReadBits(1);
+            if (c_LRsd == 1)
+            {
+                stream.ChannelCount += 2;
+            }
+            int c_Ts = buffer.ReadBits(1);
+            if (c_Ts == 1)
+            {
+                stream.ChannelCount += 1;
+            }
+            int c_Cs = buffer.ReadBits(1);
+            if (c_Cs == 1)
+            {
+                stream.ChannelCount += 1;
+            }
+            int c_LRrs = buffer.ReadBits(1);
+            if (c_LRrs == 1)
+            {
+                stream.ChannelCount += 2;
+            }
+            int c_LRc = buffer.ReadBits(1);
+            if (c_LRc == 1)
+            {
+                stream.ChannelCount += 2;
+            }
+            int c_LRvh = buffer.ReadBits(1);
+            if (c_LRvh == 1)
+            {
+                stream.ChannelCount += 2;
+            }
+            int c_LRs = buffer.ReadBits(1);
+            if (c_LRs == 1)
+            {
+                stream.ChannelCount += 2;
+            }
+            int c_LFE = buffer.ReadBits(1);
+            if (c_LFE == 1)
+            {
+                stream.LFE += 1;
+            }
+            int c_C = buffer.ReadBits(1);
+            if (c_C == 1)
+            {
+                stream.ChannelCount += 1;
+            }
+            int c_LR = buffer.ReadBits(1);
+            if (c_LR == 1)
+            {
+                stream.ChannelCount += 2;
+            }
+
+            int access_unit_size = 40 << (ratebits & 7);
+            int access_unit_size_pow2 = 64 << (ratebits & 7);
+
+            int a1 = buffer.ReadBits(16);
+            int a2 = buffer.ReadBits(16);
+            int a3 = buffer.ReadBits(16);
+
+            int is_vbr = buffer.ReadBits(1);
+            int peak_bitrate = buffer.ReadBits(15);
+            peak_bitrate = (peak_bitrate * stream.SampleRate) >> 4;
+
+            double peak_bitdepth = 
+                (double)peak_bitrate / 
+                (stream.ChannelCount + stream.LFE) / 
+                stream.SampleRate;
+            if (peak_bitdepth > 14)
+            {
+                stream.BitDepth = 24;
+            }
+            else
+            {
+                stream.BitDepth = 16;
+            }
+
+#if DEBUG
+            System.Diagnostics.Debug.WriteLine(string.Format(
+                "{0}\t{1}\t{2:F2}", 
+                stream.PID, peak_bitrate, peak_bitdepth));
+#endif
+            /*
+            // TODO: Get THD dialnorm from metadata
+            if (stream.CoreStream != null)
+            {
+                TSAudioStream coreStream = (TSAudioStream)stream.CoreStream;
+                if (coreStream.DialNorm != 0)
+                {
+                    stream.DialNorm = coreStream.DialNorm;
+                }
+            }
+            */
+
+            stream.IsVBR = true;
+            stream.IsInitialized = true;
+        }
+    }
+}

+ 134 - 0
BDInfo/TSCodecVC1.cs

@@ -0,0 +1,134 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public abstract class TSCodecVC1
+    {
+        public static void Scan(
+            TSVideoStream stream,
+            TSStreamBuffer buffer,
+            ref string tag)
+        {
+            int parse = 0;
+            byte frameHeaderParse = 0;
+            byte sequenceHeaderParse = 0;
+            bool isInterlaced = false;
+
+            for (int i = 0; i < buffer.Length; i++)
+            {
+                parse = (parse << 8) + buffer.ReadByte();
+
+                if (parse == 0x0000010D)
+                {
+                    frameHeaderParse = 4;
+                }
+                else if (frameHeaderParse > 0)
+                {
+                    --frameHeaderParse;
+                    if (frameHeaderParse == 0)
+                    {
+                        uint pictureType = 0;
+                        if (isInterlaced)
+                        {
+                            if ((parse & 0x80000000) == 0)
+                            {
+                                pictureType = 
+                                    (uint)((parse & 0x78000000) >> 13);
+                            }
+                            else
+                            {
+                                pictureType = 
+                                    (uint)((parse & 0x3c000000) >> 12);
+                            }
+                        }
+                        else
+                        {
+                            pictureType = 
+                                (uint)((parse & 0xf0000000) >> 14);
+                        }
+
+                        if ((pictureType & 0x20000) == 0)
+                        {
+                            tag = "P";
+                        }
+                        else if ((pictureType & 0x10000) == 0)
+                        {
+                            tag = "B";
+                        }
+                        else if ((pictureType & 0x8000) == 0)
+                        {
+                            tag = "I";
+                        }
+                        else if ((pictureType & 0x4000) == 0)
+                        {
+                            tag = "BI";
+                        }
+                        else
+                        {
+                            tag = null;
+                        }
+                        if (stream.IsInitialized) return;
+                    }
+                }
+                else if (parse == 0x0000010F)
+                {
+                    sequenceHeaderParse = 6;
+                }
+                else if (sequenceHeaderParse > 0)
+                {
+                    --sequenceHeaderParse;
+                    switch (sequenceHeaderParse)
+                    {
+                        case 5:
+                            int profileLevel = ((parse & 0x38) >> 3);
+                            if (((parse & 0xC0) >> 6) == 3)
+                            {
+                                stream.EncodingProfile = string.Format(
+                                    "Advanced Profile {0}", profileLevel);
+                            }
+                            else
+                            {
+                                stream.EncodingProfile = string.Format(
+                                    "Main Profile {0}", profileLevel);
+                            }
+                            break;
+
+                        case 0:
+                            if (((parse & 0x40) >> 6) > 0)
+                            {
+                                isInterlaced = true;
+                            }
+                            else
+                            {
+                                isInterlaced = false;
+                            }
+                            break;
+                    }
+                    stream.IsVBR = true;
+                    stream.IsInitialized = true;
+                }
+            }
+        }
+    }
+}

+ 40 - 0
BDInfo/TSInterleavedFile.cs

@@ -0,0 +1,40 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+// TODO: Do more interesting things here...
+
+namespace BDInfo
+{
+    public class TSInterleavedFile
+    {
+        public FileInfo FileInfo = null;
+        public string Name = null;
+
+        public TSInterleavedFile(FileInfo fileInfo)
+        {
+            FileInfo = fileInfo;
+            Name = fileInfo.Name.ToUpper();
+        }
+    }
+}

+ 1287 - 0
BDInfo/TSPlaylistFile.cs

@@ -0,0 +1,1287 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+#undef DEBUG
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+
+namespace BDInfo
+{
+    public class TSPlaylistFile
+    {
+        private FileInfo FileInfo = null;
+        public string FileType = null;
+        public bool IsInitialized = false;
+        public string Name = null;
+        public BDROM BDROM = null;
+        public bool HasHiddenTracks = false;
+        public bool HasLoops = false;
+        public bool IsCustom = false;
+
+        public List<double> Chapters = new List<double>();
+
+        public Dictionary<ushort, TSStream> Streams = 
+            new Dictionary<ushort, TSStream>();
+        public Dictionary<ushort, TSStream> PlaylistStreams =
+            new Dictionary<ushort, TSStream>();
+        public List<TSStreamClip> StreamClips =
+            new List<TSStreamClip>();
+        public List<Dictionary<ushort, TSStream>> AngleStreams =
+            new List<Dictionary<ushort, TSStream>>();
+        public List<Dictionary<double, TSStreamClip>> AngleClips = 
+            new List<Dictionary<double, TSStreamClip>>();
+        public int AngleCount = 0;
+
+        public List<TSStream> SortedStreams = 
+            new List<TSStream>();
+        public List<TSVideoStream> VideoStreams = 
+            new List<TSVideoStream>();
+        public List<TSAudioStream> AudioStreams = 
+            new List<TSAudioStream>();
+        public List<TSTextStream> TextStreams = 
+            new List<TSTextStream>();
+        public List<TSGraphicsStream> GraphicsStreams = 
+            new List<TSGraphicsStream>();
+
+        public TSPlaylistFile(
+            BDROM bdrom,
+            FileInfo fileInfo)
+        {
+            BDROM = bdrom;
+            FileInfo = fileInfo;
+            Name = fileInfo.Name.ToUpper();
+        }
+
+        public TSPlaylistFile(
+            BDROM bdrom,
+            string name,
+            List<TSStreamClip> clips)
+        {
+            BDROM = bdrom;
+            Name = name;
+            IsCustom = true;
+            foreach (TSStreamClip clip in clips)
+            {
+                TSStreamClip newClip = new TSStreamClip(
+                    clip.StreamFile, clip.StreamClipFile);
+
+                newClip.Name = clip.Name;
+                newClip.TimeIn = clip.TimeIn;
+                newClip.TimeOut = clip.TimeOut;
+                newClip.Length = newClip.TimeOut - newClip.TimeIn;
+                newClip.RelativeTimeIn = TotalLength;
+                newClip.RelativeTimeOut = newClip.RelativeTimeIn + newClip.Length;
+                newClip.AngleIndex = clip.AngleIndex;
+                newClip.Chapters.Add(clip.TimeIn);
+                StreamClips.Add(newClip);
+
+                if (newClip.AngleIndex > AngleCount)
+                {
+                    AngleCount = newClip.AngleIndex;
+                }
+                if (newClip.AngleIndex == 0)
+                {
+                    Chapters.Add(newClip.RelativeTimeIn);
+                }
+            }
+            LoadStreamClips();
+            IsInitialized = true;
+        }
+
+        public override string ToString()
+        {
+            return Name;
+        }
+
+        public ulong InterleavedFileSize
+        {
+            get
+            {
+                ulong size = 0;
+                foreach (TSStreamClip clip in StreamClips)
+                {
+                    size += clip.InterleavedFileSize;
+                }
+                return size;
+            }
+        }
+        public ulong FileSize
+        {
+            get
+            {
+                ulong size = 0;
+                foreach (TSStreamClip clip in StreamClips)
+                {
+                    size += clip.FileSize;
+                }
+                return size;
+            }
+        }
+        public double TotalLength
+        {
+            get
+            {
+                double length = 0;
+                foreach (TSStreamClip clip in StreamClips)
+                {
+                    if (clip.AngleIndex == 0)
+                    {
+                        length += clip.Length;
+                    }
+                }
+                return length;
+            }
+        }
+
+        public double TotalAngleLength
+        {
+            get
+            {
+                double length = 0;
+                foreach (TSStreamClip clip in StreamClips)
+                {
+                    length += clip.Length;
+                }
+                return length;
+            }
+        }
+
+        public ulong TotalSize
+        {
+            get
+            {
+                ulong size = 0;
+                foreach (TSStreamClip clip in StreamClips)
+                {
+                    if (clip.AngleIndex == 0)
+                    {
+                        size += clip.PacketSize;
+                    }
+                }
+                return size;
+            }
+        }
+
+        public ulong TotalAngleSize
+        {
+            get
+            {
+                ulong size = 0;
+                foreach (TSStreamClip clip in StreamClips)
+                {
+                    size += clip.PacketSize;
+                }
+                return size;
+            }
+        }
+
+        public ulong TotalBitRate
+        {
+            get
+            {
+                if (TotalLength > 0)
+                {
+                    return (ulong)Math.Round(((TotalSize * 8.0) / TotalLength));
+                }
+                return 0;
+            }
+        }
+
+        public ulong TotalAngleBitRate
+        {
+            get
+            {
+                if (TotalAngleLength > 0)
+                {
+                    return (ulong)Math.Round(((TotalAngleSize * 8.0) / TotalAngleLength));
+                }
+                return 0;
+            }
+        }
+
+        public void Scan(
+            Dictionary<string, TSStreamFile> streamFiles,
+            Dictionary<string, TSStreamClipFile> streamClipFiles)
+        {
+            FileStream fileStream = null;
+            BinaryReader fileReader = null;
+
+            try
+            {
+                Streams.Clear();
+                StreamClips.Clear();
+
+                fileStream = File.OpenRead(FileInfo.FullName);
+                fileReader = new BinaryReader(fileStream);
+
+                byte[] data = new byte[fileStream.Length];
+                int dataLength = fileReader.Read(data, 0, data.Length);
+
+                int pos = 0;
+
+                FileType = ReadString(data, 8, ref pos);
+                if (FileType != "MPLS0100" && FileType != "MPLS0200")
+                {
+                    throw new Exception(string.Format(
+                        "Playlist {0} has an unknown file type {1}.",
+                        FileInfo.Name, FileType));
+                }
+
+                int playlistOffset = ReadInt32(data, ref pos);
+                int chaptersOffset = ReadInt32(data, ref pos);
+                int extensionsOffset = ReadInt32(data, ref pos);
+
+                pos = playlistOffset;
+
+                int playlistLength = ReadInt32(data, ref pos);
+                int playlistReserved = ReadInt16(data, ref pos);
+                int itemCount = ReadInt16(data, ref pos);
+                int subitemCount = ReadInt16(data, ref pos);
+
+                List<TSStreamClip> chapterClips = new List<TSStreamClip>();
+                for (int itemIndex = 0; itemIndex < itemCount; itemIndex++)
+                {
+                    int itemStart = pos;
+                    int itemLength = ReadInt16(data, ref pos);
+                    string itemName = ReadString(data, 5, ref pos);
+                    string itemType = ReadString(data, 4, ref pos);
+
+                    TSStreamFile streamFile = null;
+                    string streamFileName = string.Format(
+                        "{0}.M2TS", itemName);
+                    if (streamFiles.ContainsKey(streamFileName))
+                    {
+                        streamFile = streamFiles[streamFileName];
+                    }
+                    if (streamFile == null)
+                    {
+                        Debug.WriteLine(string.Format(
+                            "Playlist {0} referenced missing file {1}.",
+                            FileInfo.Name, streamFileName));
+                    }
+
+                    TSStreamClipFile streamClipFile = null;
+                    string streamClipFileName = string.Format(
+                        "{0}.CLPI", itemName);
+                    if (streamClipFiles.ContainsKey(streamClipFileName))
+                    {
+                        streamClipFile = streamClipFiles[streamClipFileName];
+                    }
+                    if (streamClipFile == null)
+                    {
+                        throw new Exception(string.Format(
+                            "Playlist {0} referenced missing file {1}.",
+                            FileInfo.Name, streamFileName));
+                    }
+
+                    pos += 1;
+                    int multiangle = (data[pos] >> 4) & 0x01;
+                    int condition = data[pos] & 0x0F;
+                    pos += 2;
+
+                    int inTime = ReadInt32(data, ref pos);
+                    if (inTime < 0) inTime &= 0x7FFFFFFF;
+                    double timeIn = (double)inTime / 45000;
+
+                    int outTime = ReadInt32(data, ref pos);
+                    if (outTime < 0) outTime &= 0x7FFFFFFF;
+                    double timeOut = (double)outTime / 45000;
+
+                    TSStreamClip streamClip = new TSStreamClip(
+                        streamFile, streamClipFile);
+
+                    streamClip.Name = streamFileName; //TODO
+                    streamClip.TimeIn = timeIn;
+                    streamClip.TimeOut = timeOut;
+                    streamClip.Length = streamClip.TimeOut - streamClip.TimeIn;
+                    streamClip.RelativeTimeIn = TotalLength;
+                    streamClip.RelativeTimeOut = streamClip.RelativeTimeIn + streamClip.Length;
+                    StreamClips.Add(streamClip);
+                    chapterClips.Add(streamClip);
+
+                    pos += 12;
+                    if (multiangle > 0)
+                    {
+                        int angles = data[pos];
+                        pos += 2;
+                        for (int angle = 0; angle < angles - 1; angle++)
+                        {
+                            string angleName = ReadString(data, 5, ref pos);
+                            string angleType = ReadString(data, 4, ref pos);
+                            pos += 1;
+
+                            TSStreamFile angleFile = null;
+                            string angleFileName = string.Format(
+                                "{0}.M2TS", angleName);
+                            if (streamFiles.ContainsKey(angleFileName))
+                            {
+                                angleFile = streamFiles[angleFileName];
+                            }
+                            if (angleFile == null)
+                            {
+                                throw new Exception(string.Format(
+                                    "Playlist {0} referenced missing angle file {1}.",
+                                    FileInfo.Name, angleFileName));
+                            }
+
+                            TSStreamClipFile angleClipFile = null;
+                            string angleClipFileName = string.Format(
+                                "{0}.CLPI", angleName);
+                            if (streamClipFiles.ContainsKey(angleClipFileName))
+                            {
+                                angleClipFile = streamClipFiles[angleClipFileName];
+                            }
+                            if (angleClipFile == null)
+                            {
+                                throw new Exception(string.Format(
+                                    "Playlist {0} referenced missing angle file {1}.",
+                                    FileInfo.Name, angleClipFileName));
+                            }
+
+                            TSStreamClip angleClip =
+                                new TSStreamClip(angleFile, angleClipFile);
+                            angleClip.AngleIndex = angle + 1;
+                            angleClip.TimeIn = streamClip.TimeIn;
+                            angleClip.TimeOut = streamClip.TimeOut;
+                            angleClip.RelativeTimeIn = streamClip.RelativeTimeIn;
+                            angleClip.RelativeTimeOut = streamClip.RelativeTimeOut;
+                            angleClip.Length = streamClip.Length;
+                            StreamClips.Add(angleClip);
+                        }
+                        if (angles - 1 > AngleCount) AngleCount = angles - 1;
+                    }
+
+                    int streamInfoLength = ReadInt16(data, ref pos);
+                    pos += 2;
+                    int streamCountVideo = data[pos++];
+                    int streamCountAudio = data[pos++];
+                    int streamCountPG = data[pos++];
+                    int streamCountIG = data[pos++];
+                    int streamCountSecondaryAudio = data[pos++];
+                    int streamCountSecondaryVideo = data[pos++];
+                    int streamCountPIP = data[pos++];
+                    pos += 5;
+
+#if DEBUG
+                    Debug.WriteLine(string.Format(
+                        "{0} : {1} -> V:{2} A:{3} PG:{4} IG:{5} 2A:{6} 2V:{7} PIP:{8}", 
+                        Name, streamFileName, streamCountVideo, streamCountAudio, streamCountPG, streamCountIG, 
+                        streamCountSecondaryAudio, streamCountSecondaryVideo, streamCountPIP));
+#endif
+
+                    for (int i = 0; i < streamCountVideo; i++)
+                    {
+                        TSStream stream = CreatePlaylistStream(data, ref pos);
+                        if (stream != null) PlaylistStreams[stream.PID] = stream;
+                    }
+                    for (int i = 0; i < streamCountAudio; i++)
+                    {
+                        TSStream stream = CreatePlaylistStream(data, ref pos);
+                        if (stream != null) PlaylistStreams[stream.PID] = stream;
+                    }
+                    for (int i = 0; i < streamCountPG; i++)
+                    {
+                        TSStream stream = CreatePlaylistStream(data, ref pos);
+                        if (stream != null) PlaylistStreams[stream.PID] = stream;
+                    }
+                    for (int i = 0; i < streamCountIG; i++)
+                    {
+                        TSStream stream = CreatePlaylistStream(data, ref pos);
+                        if (stream != null) PlaylistStreams[stream.PID] = stream;
+                    }
+                    for (int i = 0; i < streamCountSecondaryAudio; i++)
+                    {
+                        TSStream stream = CreatePlaylistStream(data, ref pos);
+                        if (stream != null) PlaylistStreams[stream.PID] = stream;
+                        pos += 2;
+                    }
+                    for (int i = 0; i < streamCountSecondaryVideo; i++)
+                    {
+                        TSStream stream = CreatePlaylistStream(data, ref pos);
+                        if (stream != null) PlaylistStreams[stream.PID] = stream;
+                        pos += 6;
+                    }
+                    /*
+                     * TODO
+                     * 
+                    for (int i = 0; i < streamCountPIP; i++)
+                    {
+                        TSStream stream = CreatePlaylistStream(data, ref pos);
+                        if (stream != null) PlaylistStreams[stream.PID] = stream;
+                    }
+                    */
+
+                    pos += itemLength - (pos - itemStart) + 2;
+                }
+
+                pos = chaptersOffset + 4;
+
+                int chapterCount = ReadInt16(data, ref pos);
+
+                for (int chapterIndex = 0;
+                    chapterIndex < chapterCount;
+                    chapterIndex++)
+                {
+                    int chapterType = data[pos+1];
+
+                    if (chapterType == 1)
+                    {
+                        int streamFileIndex =
+                            ((int)data[pos + 2] << 8) + data[pos + 3];
+
+                        long chapterTime =
+                            ((long)data[pos + 4] << 24) +
+                            ((long)data[pos + 5] << 16) +
+                            ((long)data[pos + 6] << 8) +
+                            ((long)data[pos + 7]);
+
+                        TSStreamClip streamClip = chapterClips[streamFileIndex];
+
+                        double chapterSeconds = (double)chapterTime / 45000;
+
+                        double relativeSeconds =
+                            chapterSeconds -
+                            streamClip.TimeIn +
+                            streamClip.RelativeTimeIn;
+
+                        // TODO: Ignore short last chapter?
+                        if (TotalLength - relativeSeconds > 1.0)
+                        {
+                            streamClip.Chapters.Add(chapterSeconds);
+                            this.Chapters.Add(relativeSeconds);
+                        }
+                    }
+                    else
+                    {
+                        // TODO: Handle other chapter types?
+                    }
+                    pos += 14;
+                }
+            }
+            finally
+            {
+                if (fileReader != null)
+                {
+                    fileReader.Close();
+                }
+                if (fileStream != null)
+                {
+                    fileStream.Close();
+                }
+            }
+        }
+
+        public void Initialize()
+        {
+            LoadStreamClips();
+
+            Dictionary<string, List<double>> clipTimes = new Dictionary<string, List<double>>();
+            foreach (TSStreamClip clip in StreamClips)
+            {
+                if (clip.AngleIndex == 0)
+                {
+                    if (clipTimes.ContainsKey(clip.Name))
+                    {
+                        if (clipTimes[clip.Name].Contains(clip.TimeIn))
+                        {
+                            HasLoops = true;
+                            break;
+                        }
+                        else
+                        {
+                            clipTimes[clip.Name].Add(clip.TimeIn);
+                        }
+                    }
+                    else
+                    {
+                        clipTimes[clip.Name] = new List<double> { clip.TimeIn };
+                    }
+                }
+            }
+            ClearBitrates();
+            IsInitialized = true;
+        }
+
+        protected TSStream CreatePlaylistStream(byte[] data, ref int pos)
+        {
+            TSStream stream = null;
+
+            int start = pos;
+
+            int headerLength = data[pos++];
+            int headerPos = pos;
+            int headerType = data[pos++];
+
+            int pid = 0;
+            int subpathid = 0;
+            int subclipid = 0;
+
+            switch (headerType)
+            {
+                case 1:
+                    pid = ReadInt16(data, ref pos);
+                    break;
+                case 2:
+                    subpathid = data[pos++];
+                    subclipid = data[pos++];
+                    pid = ReadInt16(data, ref pos);
+                    break;
+                case 3:
+                    subpathid = data[pos++];
+                    pid = ReadInt16(data, ref pos);
+                    break;
+                case 4:
+                    subpathid = data[pos++];
+                    subclipid = data[pos++];
+                    pid = ReadInt16(data, ref pos);
+                    break;
+                default:
+                    break;
+            }
+
+            pos = headerPos + headerLength;
+
+            int streamLength = data[pos++];
+            int streamPos = pos;
+
+            TSStreamType streamType = (TSStreamType)data[pos++];
+            switch (streamType)
+            {
+                case TSStreamType.MVC_VIDEO:
+                    // TODO
+                    break;
+
+                case TSStreamType.AVC_VIDEO:
+                case TSStreamType.MPEG1_VIDEO:
+                case TSStreamType.MPEG2_VIDEO:
+                case TSStreamType.VC1_VIDEO:
+
+                    TSVideoFormat videoFormat = (TSVideoFormat)
+                        (data[pos] >> 4);
+                    TSFrameRate frameRate = (TSFrameRate)
+                        (data[pos] & 0xF);
+                    TSAspectRatio aspectRatio = (TSAspectRatio)
+                        (data[pos + 1] >> 4);
+
+                    stream = new TSVideoStream();
+                    ((TSVideoStream)stream).VideoFormat = videoFormat;
+                    ((TSVideoStream)stream).AspectRatio = aspectRatio;
+                    ((TSVideoStream)stream).FrameRate = frameRate;
+
+#if DEBUG
+                            Debug.WriteLine(string.Format(
+                                "\t{0} {1} {2} {3} {4}",
+                                pid,
+                                streamType,
+                                videoFormat,
+                                frameRate,
+                                aspectRatio));
+#endif
+
+                    break;
+
+                case TSStreamType.AC3_AUDIO:
+                case TSStreamType.AC3_PLUS_AUDIO:
+                case TSStreamType.AC3_PLUS_SECONDARY_AUDIO:
+                case TSStreamType.AC3_TRUE_HD_AUDIO:
+                case TSStreamType.DTS_AUDIO:
+                case TSStreamType.DTS_HD_AUDIO:
+                case TSStreamType.DTS_HD_MASTER_AUDIO:
+                case TSStreamType.DTS_HD_SECONDARY_AUDIO:
+                case TSStreamType.LPCM_AUDIO:
+                case TSStreamType.MPEG1_AUDIO:
+                case TSStreamType.MPEG2_AUDIO:
+
+                    int audioFormat = ReadByte(data, ref pos);
+
+                    TSChannelLayout channelLayout = (TSChannelLayout)
+                        (audioFormat >> 4);
+                    TSSampleRate sampleRate = (TSSampleRate)
+                        (audioFormat & 0xF);
+
+                    string audioLanguage = ReadString(data, 3, ref pos);
+
+                    stream = new TSAudioStream();
+                    ((TSAudioStream)stream).ChannelLayout = channelLayout;
+                    ((TSAudioStream)stream).SampleRate = TSAudioStream.ConvertSampleRate(sampleRate);
+                    ((TSAudioStream)stream).LanguageCode = audioLanguage;
+
+#if DEBUG
+                    Debug.WriteLine(string.Format(
+                        "\t{0} {1} {2} {3} {4}",
+                        pid,
+                        streamType,
+                        audioLanguage,
+                        channelLayout,
+                        sampleRate));
+#endif
+
+                    break;
+
+                case TSStreamType.INTERACTIVE_GRAPHICS:
+                case TSStreamType.PRESENTATION_GRAPHICS:
+
+                    string graphicsLanguage = ReadString(data, 3, ref pos);
+
+                    stream = new TSGraphicsStream();
+                    ((TSGraphicsStream)stream).LanguageCode = graphicsLanguage;
+
+                    if (data[pos] != 0)
+                    {
+                    }
+
+#if DEBUG
+                    Debug.WriteLine(string.Format(
+                        "\t{0} {1} {2}",
+                        pid,
+                        streamType,
+                        graphicsLanguage));
+#endif
+
+                    break;
+
+                case TSStreamType.SUBTITLE:
+
+                    int code = ReadByte(data, ref pos); // TODO
+                    string textLanguage = ReadString(data, 3, ref pos);
+
+                    stream = new TSTextStream();
+                    ((TSTextStream)stream).LanguageCode = textLanguage;
+
+#if DEBUG
+                    Debug.WriteLine(string.Format(
+                        "\t{0} {1} {2}",
+                        pid,
+                        streamType,
+                        textLanguage));
+#endif
+
+                    break;
+
+                default:
+                    break;
+            }
+
+            pos = streamPos + streamLength;
+
+            if (stream != null)
+            {
+                stream.PID = (ushort)pid;
+                stream.StreamType = streamType;
+            }
+
+            return stream;
+        }
+
+        private void LoadStreamClips()
+        {
+            AngleClips.Clear();
+            if (AngleCount > 0)
+            {
+                for (int angleIndex = 0; angleIndex < AngleCount; angleIndex++)
+                {
+                    AngleClips.Add(new Dictionary<double, TSStreamClip>());
+                }
+            }
+
+            TSStreamClip referenceClip = null;
+            if (StreamClips.Count > 0)
+            {
+                referenceClip = StreamClips[0];
+            }
+            foreach (TSStreamClip clip in StreamClips)
+            {
+                if (clip.StreamClipFile.Streams.Count > referenceClip.StreamClipFile.Streams.Count)
+                {
+                    referenceClip = clip;
+                }
+                else if (clip.Length > referenceClip.Length)
+                {
+                    referenceClip = clip;
+                }
+                if (AngleCount > 0)
+                {
+                    if (clip.AngleIndex == 0)
+                    {
+                        for (int angleIndex = 0; angleIndex < AngleCount; angleIndex++)
+                        {
+                            AngleClips[angleIndex][clip.RelativeTimeIn] = clip;
+                        }
+                    }
+                    else
+                    {
+                        AngleClips[clip.AngleIndex - 1][clip.RelativeTimeIn] = clip;
+                    }
+                }
+            }
+
+            foreach (TSStream clipStream
+                in referenceClip.StreamClipFile.Streams.Values)
+            {
+                if (!Streams.ContainsKey(clipStream.PID))
+                {
+                    TSStream stream = clipStream.Clone();
+                    Streams[clipStream.PID] = stream;
+
+                    if (!IsCustom && !PlaylistStreams.ContainsKey(stream.PID))
+                    {
+                        stream.IsHidden = true;
+                        HasHiddenTracks = true;
+                    }
+
+                    if (stream.IsVideoStream)
+                    {
+                        VideoStreams.Add((TSVideoStream)stream);
+                    }
+                    else if (stream.IsAudioStream)
+                    {
+                        AudioStreams.Add((TSAudioStream)stream);
+                    }
+                    else if (stream.IsGraphicsStream)
+                    {
+                        GraphicsStreams.Add((TSGraphicsStream)stream);
+                    }
+                    else if (stream.IsTextStream)
+                    {
+                        TextStreams.Add((TSTextStream)stream);
+                    }
+                }
+            }
+
+            if (referenceClip.StreamFile != null)
+            {
+                // TODO: Better way to add this in?
+                if (BDInfoSettings.EnableSSIF &&
+                    referenceClip.StreamFile.InterleavedFile != null &&
+                    referenceClip.StreamFile.Streams.ContainsKey(4114) &&
+                    !Streams.ContainsKey(4114))
+                {
+                    TSStream stream = referenceClip.StreamFile.Streams[4114].Clone();
+                    Streams[4114] = stream;
+                    if (stream.IsVideoStream)
+                    {
+                        VideoStreams.Add((TSVideoStream)stream);
+                    }
+                }
+
+                foreach (TSStream clipStream
+                    in referenceClip.StreamFile.Streams.Values)
+                {
+                    if (Streams.ContainsKey(clipStream.PID))
+                    {
+                        TSStream stream = Streams[clipStream.PID];
+
+                        if (stream.StreamType != clipStream.StreamType) continue;
+
+                        if (clipStream.BitRate > stream.BitRate)
+                        {
+                            stream.BitRate = clipStream.BitRate;
+                        }
+                        stream.IsVBR = clipStream.IsVBR;
+
+                        if (stream.IsVideoStream &&
+                            clipStream.IsVideoStream)
+                        {
+                            ((TSVideoStream)stream).EncodingProfile =
+                                ((TSVideoStream)clipStream).EncodingProfile;
+                        }
+                        else if (stream.IsAudioStream &&
+                            clipStream.IsAudioStream)
+                        {
+                            TSAudioStream audioStream = (TSAudioStream)stream;
+                            TSAudioStream clipAudioStream = (TSAudioStream)clipStream;
+
+                            if (clipAudioStream.ChannelCount > audioStream.ChannelCount)
+                            {
+                                audioStream.ChannelCount = clipAudioStream.ChannelCount;
+                            }
+                            if (clipAudioStream.LFE > audioStream.LFE)
+                            {
+                                audioStream.LFE = clipAudioStream.LFE;
+                            }
+                            if (clipAudioStream.SampleRate > audioStream.SampleRate)
+                            {
+                                audioStream.SampleRate = clipAudioStream.SampleRate;
+                            }
+                            if (clipAudioStream.BitDepth > audioStream.BitDepth)
+                            {
+                                audioStream.BitDepth = clipAudioStream.BitDepth;
+                            }
+                            if (clipAudioStream.DialNorm < audioStream.DialNorm)
+                            {
+                                audioStream.DialNorm = clipAudioStream.DialNorm;
+                            }
+                            if (clipAudioStream.AudioMode != TSAudioMode.Unknown)
+                            {
+                                audioStream.AudioMode = clipAudioStream.AudioMode;
+                            }
+                            if (clipAudioStream.CoreStream != null &&
+                                audioStream.CoreStream == null)
+                            {
+                                audioStream.CoreStream = (TSAudioStream)
+                                    clipAudioStream.CoreStream.Clone();
+                            }
+                        }
+                    }
+                }
+            }
+
+            for (int i = 0; i < AngleCount; i++)
+            {
+                AngleStreams.Add(new Dictionary<ushort, TSStream>());
+            }
+
+            if (!BDInfoSettings.KeepStreamOrder)
+            {
+                VideoStreams.Sort(CompareVideoStreams);
+            }
+            foreach (TSStream stream in VideoStreams)
+            {
+                SortedStreams.Add(stream);
+                for (int i = 0; i < AngleCount; i++)
+                {
+                    TSStream angleStream = stream.Clone();
+                    angleStream.AngleIndex = i + 1;
+                    AngleStreams[i][angleStream.PID] = angleStream;
+                    SortedStreams.Add(angleStream);
+                }
+            }
+
+            if (!BDInfoSettings.KeepStreamOrder)
+            {
+                AudioStreams.Sort(CompareAudioStreams);
+            }
+            foreach (TSStream stream in AudioStreams)
+            {
+                SortedStreams.Add(stream);
+            }
+
+            if (!BDInfoSettings.KeepStreamOrder)
+            {
+                GraphicsStreams.Sort(CompareGraphicsStreams);
+            }
+            foreach (TSStream stream in GraphicsStreams)
+            {
+                SortedStreams.Add(stream);
+            }
+
+            if (!BDInfoSettings.KeepStreamOrder)
+            {
+                TextStreams.Sort(CompareTextStreams);
+            }
+            foreach (TSStream stream in TextStreams)
+            {
+                SortedStreams.Add(stream);
+            }
+        }
+
+        public void ClearBitrates()
+        {
+            foreach (TSStreamClip clip in StreamClips)
+            {
+                clip.PayloadBytes = 0;
+                clip.PacketCount = 0;
+                clip.PacketSeconds = 0;
+
+                if (clip.StreamFile != null)
+                {
+                    foreach (TSStream stream in clip.StreamFile.Streams.Values)
+                    {
+                        stream.PayloadBytes = 0;
+                        stream.PacketCount = 0;
+                        stream.PacketSeconds = 0;
+                    }
+
+                    if (clip.StreamFile != null &&
+                        clip.StreamFile.StreamDiagnostics != null)
+                    {
+                        clip.StreamFile.StreamDiagnostics.Clear();
+                    }
+                }
+            }
+
+            foreach (TSStream stream in SortedStreams)
+            {
+                stream.PayloadBytes = 0;
+                stream.PacketCount = 0;
+                stream.PacketSeconds = 0;
+            }
+        }
+
+        public bool IsValid
+        {
+            get
+            {
+                if (!IsInitialized) return false;
+
+                if (BDInfoSettings.FilterShortPlaylists &&
+                    TotalLength < BDInfoSettings.FilterShortPlaylistsValue)
+                {
+                    return false;
+                }
+
+                if (HasLoops &&
+                    BDInfoSettings.FilterLoopingPlaylists)
+                {
+                    return false;
+                }
+
+                return true;
+            }
+        }
+
+        public static int CompareVideoStreams(
+            TSVideoStream x, 
+            TSVideoStream y)
+        {
+            if (x == null && y == null)
+            {
+                return 0;
+            }
+            else if (x == null && y != null)
+            {
+                return 1;
+            }
+            else if (x != null && y == null)
+            {
+                return -1;
+            }
+            else
+            {
+                if (x.Height > y.Height)
+                {
+                    return -1;
+                }
+                else if (y.Height > x.Height)
+                {
+                    return 1;
+                }
+                else if (x.PID > y.PID)
+                {
+                    return 1;
+                }
+                else if (y.PID > x.PID)
+                {
+                    return -1;
+                }
+                else
+                {
+                    return 0;
+                }
+            }
+        }
+
+        public static int CompareAudioStreams(
+            TSAudioStream x, 
+            TSAudioStream y)
+        {
+            if (x == y)
+            {
+                return 0;
+            }
+            else if (x == null && y == null)
+            {
+                return 0;
+            }
+            else if (x == null && y != null)
+            {
+                return -1;
+            }
+            else if (x != null && y == null)
+            {
+                return 1;
+            }
+            else
+            {
+                if (x.ChannelCount > y.ChannelCount)
+                {
+                    return -1;
+                }
+                else if (y.ChannelCount > x.ChannelCount)
+                {
+                    return 1;
+                }
+                else
+                {
+                    int sortX = GetStreamTypeSortIndex(x.StreamType);
+                    int sortY = GetStreamTypeSortIndex(y.StreamType);
+
+                    if (sortX > sortY)
+                    {
+                        return -1;
+                    }
+                    else if (sortY > sortX)
+                    {
+                        return 1;
+                    }
+                    else
+                    {
+                        if (x.LanguageCode == "eng")
+                        {
+                            return -1;
+                        }
+                        else if (y.LanguageCode == "eng")
+                        {
+                            return 1;
+                        }
+                        else if (x.LanguageCode != y.LanguageCode)
+                        {
+                            return string.Compare(
+                                x.LanguageName, y.LanguageName);
+                        }
+                        else if (x.PID < y.PID)
+                        {
+                            return -1;
+                        }
+                        else if (y.PID < x.PID)
+                        {
+                            return 1;
+                        }
+                        return 0;
+                    }
+                }
+            }
+        }
+
+        public static int CompareTextStreams(
+            TSTextStream x,
+            TSTextStream y)
+        {
+            if (x == y)
+            {
+                return 0;
+            }
+            else if (x == null && y == null)
+            {
+                return 0;
+            }
+            else if (x == null && y != null)
+            {
+                return -1;
+            }
+            else if (x != null && y == null)
+            {
+                return 1;
+            }
+            else
+            {
+                if (x.LanguageCode == "eng")
+                {
+                    return -1;
+                }
+                else if (y.LanguageCode == "eng")
+                {
+                    return 1;
+                }
+                else
+                {
+                    if (x.LanguageCode == y.LanguageCode)
+                    {
+                        if (x.PID > y.PID)
+                        {
+                            return 1;
+                        }
+                        else if (y.PID > x.PID)
+                        {
+                            return -1;
+                        }
+                        else
+                        {
+                            return 0;
+                        }
+                    }
+                    else
+                    {
+                        return string.Compare(
+                            x.LanguageName, y.LanguageName);
+                    }
+                }
+            }
+        }
+
+        private static int CompareGraphicsStreams(
+            TSGraphicsStream x,
+            TSGraphicsStream y)
+        {
+            if (x == y)
+            {
+                return 0;
+            }
+            else if (x == null && y == null)
+            {
+                return 0;
+            }
+            else if (x == null && y != null)
+            {
+                return -1;
+            }
+            else if (x != null && y == null)
+            {
+                return 1;
+            }
+            else
+            {
+                int sortX = GetStreamTypeSortIndex(x.StreamType);
+                int sortY = GetStreamTypeSortIndex(y.StreamType);
+
+                if (sortX > sortY)
+                {
+                    return -1;
+                }
+                else if (sortY > sortX)
+                {
+                    return 1;
+                }
+                else if (x.LanguageCode == "eng")
+                {
+                    return -1;
+                }
+                else if (y.LanguageCode == "eng")
+                {
+                    return 1;
+                }
+                else
+                {
+                    if (x.LanguageCode == y.LanguageCode)
+                    {
+                        if (x.PID > y.PID)
+                        {
+                            return 1;
+                        }
+                        else if (y.PID > x.PID)
+                        {
+                            return -1;
+                        }
+                        else
+                        {
+                            return 0;
+                        }
+                    }
+                    else
+                    {
+                        return string.Compare(x.LanguageName, y.LanguageName);
+                    }
+                }
+            }
+        }
+
+        private static int GetStreamTypeSortIndex(TSStreamType streamType)
+        {
+            switch (streamType)
+            {
+                case TSStreamType.Unknown:
+                    return 0;
+                case TSStreamType.MPEG1_VIDEO:
+                    return 1;
+                case TSStreamType.MPEG2_VIDEO:
+                    return 2;
+                case TSStreamType.AVC_VIDEO:
+                    return 3;
+                case TSStreamType.VC1_VIDEO:
+                    return 4;
+                case TSStreamType.MVC_VIDEO:
+                    return 5;
+
+                case TSStreamType.MPEG1_AUDIO:
+                    return 1;
+                case TSStreamType.MPEG2_AUDIO:
+                    return 2;
+                case TSStreamType.AC3_PLUS_SECONDARY_AUDIO:
+                    return 3;
+                case TSStreamType.DTS_HD_SECONDARY_AUDIO:
+                    return 4;
+                case TSStreamType.AC3_AUDIO:
+                    return 5;
+                case TSStreamType.DTS_AUDIO:
+                    return 6;
+                case TSStreamType.AC3_PLUS_AUDIO:
+                    return 7;
+                case TSStreamType.DTS_HD_AUDIO:
+                    return 8;
+                case TSStreamType.AC3_TRUE_HD_AUDIO:
+                    return 9;
+                case TSStreamType.DTS_HD_MASTER_AUDIO:
+                    return 10;
+                case TSStreamType.LPCM_AUDIO:
+                    return 11;
+
+                case TSStreamType.SUBTITLE:
+                    return 1;
+                case TSStreamType.INTERACTIVE_GRAPHICS:
+                    return 2;
+                case TSStreamType.PRESENTATION_GRAPHICS:
+                    return 3;
+
+                default:
+                    return 0;
+            }
+        }
+
+        protected string ReadString(
+            byte[] data,
+            int count,
+            ref int pos)
+        {
+            string val =
+                ASCIIEncoding.ASCII.GetString(data, pos, count);
+
+            pos += count;
+
+            return val;
+        }
+
+        protected int ReadInt32(
+            byte[] data,
+            ref int pos)
+        {
+            int val =
+                ((int)data[pos] << 24) +
+                ((int)data[pos + 1] << 16) +
+                ((int)data[pos + 2] << 8) +
+                ((int)data[pos + 3]);
+
+            pos += 4;
+
+            return val;
+        }
+
+        protected int ReadInt16(
+            byte[] data,
+            ref int pos)
+        {
+            int val =
+                ((int)data[pos] << 8) +
+                ((int)data[pos + 1]);
+
+            pos += 2;
+
+            return val;
+        }
+
+        protected byte ReadByte(
+            byte[] data,
+            ref int pos)
+        {
+            return data[pos++];
+        }
+    }
+}

+ 802 - 0
BDInfo/TSStream.cs

@@ -0,0 +1,802 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public enum TSStreamType : byte
+    {
+        Unknown = 0,
+        MPEG1_VIDEO = 0x01,
+        MPEG2_VIDEO = 0x02,
+        AVC_VIDEO = 0x1b,
+        MVC_VIDEO = 0x20,
+        VC1_VIDEO = 0xea,
+        MPEG1_AUDIO = 0x03,
+        MPEG2_AUDIO = 0x04,
+        LPCM_AUDIO = 0x80,
+        AC3_AUDIO = 0x81,
+        AC3_PLUS_AUDIO = 0x84,
+        AC3_PLUS_SECONDARY_AUDIO = 0xA1,
+        AC3_TRUE_HD_AUDIO = 0x83,
+        DTS_AUDIO = 0x82,
+        DTS_HD_AUDIO = 0x85,
+        DTS_HD_SECONDARY_AUDIO = 0xA2,
+        DTS_HD_MASTER_AUDIO = 0x86,
+        PRESENTATION_GRAPHICS = 0x90,
+        INTERACTIVE_GRAPHICS = 0x91,
+        SUBTITLE = 0x92
+    }
+
+    public enum TSVideoFormat : byte
+    {
+        Unknown = 0,
+        VIDEOFORMAT_480i = 1,
+        VIDEOFORMAT_576i = 2,
+        VIDEOFORMAT_480p = 3,
+        VIDEOFORMAT_1080i = 4,
+        VIDEOFORMAT_720p = 5,
+        VIDEOFORMAT_1080p = 6,
+        VIDEOFORMAT_576p = 7,
+    }
+
+    public enum TSFrameRate : byte
+    {
+        Unknown = 0,
+        FRAMERATE_23_976 = 1,
+        FRAMERATE_24 = 2,
+        FRAMERATE_25 = 3,
+        FRAMERATE_29_97 = 4,
+        FRAMERATE_50 = 6,
+        FRAMERATE_59_94 = 7
+    }
+
+    public enum TSChannelLayout : byte
+    {
+        Unknown = 0,
+        CHANNELLAYOUT_MONO = 1,
+        CHANNELLAYOUT_STEREO = 3,
+        CHANNELLAYOUT_MULTI = 6,
+        CHANNELLAYOUT_COMBO = 12
+    }
+
+    public enum TSSampleRate : byte
+    {
+        Unknown = 0,
+        SAMPLERATE_48 = 1,
+        SAMPLERATE_96 = 4,
+        SAMPLERATE_192 = 5,
+        SAMPLERATE_48_192 = 12,
+        SAMPLERATE_48_96 = 14
+    }
+
+    public enum TSAspectRatio
+    {
+        Unknown = 0,
+        ASPECT_4_3 = 2,
+        ASPECT_16_9 = 3,
+        ASPECT_2_21 = 4
+    }
+
+    public class TSDescriptor
+    {
+        public byte Name;
+        public byte[] Value;
+
+        public TSDescriptor(byte name, byte length)
+        {
+            Name = name;
+            Value = new byte[length];
+        }
+
+        public TSDescriptor Clone()
+        {
+            TSDescriptor descriptor = 
+                new TSDescriptor(Name, (byte)Value.Length);
+            Value.CopyTo(descriptor.Value, 0);
+            return descriptor;
+        }
+    }
+
+    public abstract class TSStream
+    {
+        public TSStream()
+        {
+        }
+
+        public override string ToString()
+        {
+            return string.Format("{0} ({1})", CodecShortName, PID);
+        }
+
+        public ushort PID;
+        public TSStreamType StreamType;
+        public List<TSDescriptor> Descriptors = null;
+        public long BitRate = 0;
+        public long ActiveBitRate = 0;
+        public bool IsVBR = false;
+        public bool IsInitialized = false;
+        public string LanguageName;
+        public bool IsHidden = false;
+
+        public ulong PayloadBytes = 0;
+        public ulong PacketCount = 0;
+        public double PacketSeconds = 0;
+        public int AngleIndex = 0;
+
+        public ulong PacketSize
+        {
+            get
+            {
+                return PacketCount * 192;
+            }
+        }
+
+        private string _LanguageCode;
+        public string LanguageCode
+        {
+            get 
+            {
+                return _LanguageCode; 
+            }
+            set 
+            {
+                _LanguageCode = value;
+                LanguageName = LanguageCodes.GetName(value);
+            } 
+        }
+
+        public bool IsVideoStream
+        {
+            get
+            {
+                switch (StreamType)
+                {
+                    case TSStreamType.MPEG1_VIDEO:
+                    case TSStreamType.MPEG2_VIDEO:
+                    case TSStreamType.AVC_VIDEO:
+                    case TSStreamType.MVC_VIDEO:
+                    case TSStreamType.VC1_VIDEO:
+                        return true;
+
+                    default:
+                        return false;
+                }
+            }
+        }
+
+        public bool IsAudioStream
+        {
+            get
+            {
+                switch (StreamType)
+                {
+                    case TSStreamType.MPEG1_AUDIO:
+                    case TSStreamType.MPEG2_AUDIO:
+                    case TSStreamType.LPCM_AUDIO:
+                    case TSStreamType.AC3_AUDIO:
+                    case TSStreamType.AC3_PLUS_AUDIO:
+                    case TSStreamType.AC3_PLUS_SECONDARY_AUDIO:
+                    case TSStreamType.AC3_TRUE_HD_AUDIO:
+                    case TSStreamType.DTS_AUDIO:
+                    case TSStreamType.DTS_HD_AUDIO:
+                    case TSStreamType.DTS_HD_SECONDARY_AUDIO:
+                    case TSStreamType.DTS_HD_MASTER_AUDIO:
+                        return true;
+
+                    default:
+                        return false;
+                }
+            }
+        }
+
+        public bool IsGraphicsStream
+        {
+            get
+            {
+                switch (StreamType)
+                {
+                    case TSStreamType.PRESENTATION_GRAPHICS:
+                    case TSStreamType.INTERACTIVE_GRAPHICS:
+                        return true;
+
+                    default:
+                        return false;
+                }
+            }
+        }
+
+        public bool IsTextStream
+        {
+            get
+            {
+                switch (StreamType)
+                {
+                    case TSStreamType.SUBTITLE:
+                        return true;
+
+                    default:
+                        return false;
+                }
+            }
+        }
+
+        public string CodecName
+        {
+            get
+            {
+                switch (StreamType)
+                {
+                    case TSStreamType.MPEG1_VIDEO:
+                        return "MPEG-1 Video";
+                    case TSStreamType.MPEG2_VIDEO:
+                        return "MPEG-2 Video";
+                    case TSStreamType.AVC_VIDEO:
+                        return "MPEG-4 AVC Video";
+                    case TSStreamType.MVC_VIDEO:
+                        return "MPEG-4 MVC Video";
+                    case TSStreamType.VC1_VIDEO:
+                        return "VC-1 Video";
+                    case TSStreamType.MPEG1_AUDIO:
+                        return "MP1 Audio";
+                    case TSStreamType.MPEG2_AUDIO:
+                        return "MP2 Audio";
+                    case TSStreamType.LPCM_AUDIO:
+                        return "LPCM Audio";
+                    case TSStreamType.AC3_AUDIO:
+                        if (((TSAudioStream)this).AudioMode == TSAudioMode.Extended)
+                            return "Dolby Digital EX Audio";
+                        else
+                            return "Dolby Digital Audio";
+                    case TSStreamType.AC3_PLUS_AUDIO:
+                    case TSStreamType.AC3_PLUS_SECONDARY_AUDIO:
+                        return "Dolby Digital Plus Audio";
+                    case TSStreamType.AC3_TRUE_HD_AUDIO:
+                        return "Dolby TrueHD Audio";
+                    case TSStreamType.DTS_AUDIO:
+                        if (((TSAudioStream)this).AudioMode == TSAudioMode.Extended)
+                            return "DTS-ES Audio";
+                        else
+                            return "DTS Audio";
+                    case TSStreamType.DTS_HD_AUDIO:
+                        return "DTS-HD High-Res Audio";
+                    case TSStreamType.DTS_HD_SECONDARY_AUDIO:
+                        return "DTS Express";
+                    case TSStreamType.DTS_HD_MASTER_AUDIO:
+                        return "DTS-HD Master Audio";
+                    case TSStreamType.PRESENTATION_GRAPHICS:
+                        return "Presentation Graphics";
+                    case TSStreamType.INTERACTIVE_GRAPHICS:
+                        return "Interactive Graphics";
+                    case TSStreamType.SUBTITLE:
+                        return "Subtitle";
+                    default:
+                        return "UNKNOWN";
+                }
+            }
+        }
+
+        public string CodecAltName
+        {
+            get
+            {
+                switch (StreamType)
+                {
+                    case TSStreamType.MPEG1_VIDEO:
+                        return "MPEG-1";
+                    case TSStreamType.MPEG2_VIDEO:
+                        return "MPEG-2";
+                    case TSStreamType.AVC_VIDEO:
+                        return "AVC";
+                    case TSStreamType.MVC_VIDEO:
+                        return "MVC";
+                    case TSStreamType.VC1_VIDEO:
+                        return "VC-1";
+                    case TSStreamType.MPEG1_AUDIO:
+                        return "MP1";
+                    case TSStreamType.MPEG2_AUDIO:
+                        return "MP2";
+                    case TSStreamType.LPCM_AUDIO:
+                        return "LPCM";
+                    case TSStreamType.AC3_AUDIO:
+                        return "DD AC3";
+                    case TSStreamType.AC3_PLUS_AUDIO:
+                    case TSStreamType.AC3_PLUS_SECONDARY_AUDIO:
+                        return "DD AC3+";
+                    case TSStreamType.AC3_TRUE_HD_AUDIO:
+                        return "Dolby TrueHD";
+                    case TSStreamType.DTS_AUDIO:
+                        return "DTS";
+                    case TSStreamType.DTS_HD_AUDIO:
+                        return "DTS-HD Hi-Res";
+                    case TSStreamType.DTS_HD_SECONDARY_AUDIO:
+                        return "DTS Express";
+                    case TSStreamType.DTS_HD_MASTER_AUDIO:
+                        return "DTS-HD Master";
+                    case TSStreamType.PRESENTATION_GRAPHICS:
+                        return "PGS";
+                    case TSStreamType.INTERACTIVE_GRAPHICS:
+                        return "IGS";
+                    case TSStreamType.SUBTITLE:
+                        return "SUB";
+                    default:
+                        return "UNKNOWN";
+                }
+            }
+        }
+
+        public string CodecShortName
+        {
+            get
+            {
+                switch (StreamType)
+                {
+                    case TSStreamType.MPEG1_VIDEO:
+                        return "MPEG-1";
+                    case TSStreamType.MPEG2_VIDEO:
+                        return "MPEG-2";
+                    case TSStreamType.AVC_VIDEO:
+                        return "AVC";
+                    case TSStreamType.MVC_VIDEO:
+                        return "MVC";
+                    case TSStreamType.VC1_VIDEO:
+                        return "VC-1";
+                    case TSStreamType.MPEG1_AUDIO:
+                        return "MP1";
+                    case TSStreamType.MPEG2_AUDIO:
+                        return "MP2";
+                    case TSStreamType.LPCM_AUDIO:
+                        return "LPCM";
+                    case TSStreamType.AC3_AUDIO:
+                        if (((TSAudioStream)this).AudioMode == TSAudioMode.Extended)
+                            return "AC3-EX";
+                        else
+                            return "AC3";
+                    case TSStreamType.AC3_PLUS_AUDIO:
+                    case TSStreamType.AC3_PLUS_SECONDARY_AUDIO:
+                        return "AC3+";
+                    case TSStreamType.AC3_TRUE_HD_AUDIO:
+                        return "TrueHD";
+                    case TSStreamType.DTS_AUDIO:
+                        if (((TSAudioStream)this).AudioMode == TSAudioMode.Extended)
+                            return "DTS-ES";
+                        else
+                            return "DTS";
+                    case TSStreamType.DTS_HD_AUDIO:
+                        return "DTS-HD HR";
+                    case TSStreamType.DTS_HD_SECONDARY_AUDIO:
+                        return "DTS Express";
+                    case TSStreamType.DTS_HD_MASTER_AUDIO:
+                        return "DTS-HD MA";
+                    case TSStreamType.PRESENTATION_GRAPHICS:
+                        return "PGS";
+                    case TSStreamType.INTERACTIVE_GRAPHICS:
+                        return "IGS";
+                    case TSStreamType.SUBTITLE:
+                        return "SUB";
+                    default:
+                        return "UNKNOWN";
+                }
+            }
+        }
+
+        public virtual string Description
+        {
+            get
+            {
+                return "";
+            }
+        }
+
+        public abstract TSStream Clone();
+        
+        protected void CopyTo(TSStream stream)
+        {
+            stream.PID = PID;
+            stream.StreamType = StreamType;
+            stream.IsVBR = IsVBR;
+            stream.BitRate = BitRate;
+            stream.IsInitialized = IsInitialized;
+            stream.LanguageCode = _LanguageCode;
+            if (Descriptors != null)
+            {
+                stream.Descriptors = new List<TSDescriptor>();
+                foreach (TSDescriptor descriptor in Descriptors)
+                {
+                    stream.Descriptors.Add(descriptor.Clone());
+                }
+            }
+        }
+    }
+
+    public class TSVideoStream : TSStream
+    {
+        public TSVideoStream()
+        {
+        }
+
+        public int Width;
+        public int Height;
+        public bool IsInterlaced;        
+        public int FrameRateEnumerator;
+        public int FrameRateDenominator;
+        public TSAspectRatio AspectRatio;
+        public string EncodingProfile;
+
+        private TSVideoFormat _VideoFormat;
+        public TSVideoFormat VideoFormat
+        {
+            get
+            {
+                return _VideoFormat;
+            }
+            set
+            {
+                _VideoFormat = value;
+                switch (value)
+                {
+                    case TSVideoFormat.VIDEOFORMAT_480i:
+                        Height = 480;
+                        IsInterlaced = true;
+                        break;
+                    case TSVideoFormat.VIDEOFORMAT_480p:
+                        Height = 480;
+                        IsInterlaced = false;
+                        break;
+                    case TSVideoFormat.VIDEOFORMAT_576i:
+                        Height = 576;
+                        IsInterlaced = true;
+                        break;
+                    case TSVideoFormat.VIDEOFORMAT_576p:
+                        Height = 576;
+                        IsInterlaced = false;
+                        break;
+                    case TSVideoFormat.VIDEOFORMAT_720p:
+                        Height = 720;
+                        IsInterlaced = false;
+                        break;
+                    case TSVideoFormat.VIDEOFORMAT_1080i:
+                        Height = 1080;
+                        IsInterlaced = true;
+                        break;
+                    case TSVideoFormat.VIDEOFORMAT_1080p:
+                        Height = 1080;
+                        IsInterlaced = false;
+                        break;
+                }
+            }
+        }
+
+        private TSFrameRate _FrameRate;
+        public TSFrameRate FrameRate
+        {
+            get
+            {
+                return _FrameRate;
+            }
+            set
+            {
+                _FrameRate = value;
+                switch (value)
+                {
+                    case TSFrameRate.FRAMERATE_23_976:
+                        FrameRateEnumerator = 24000;
+                        FrameRateDenominator = 1001;
+                        break;
+                    case TSFrameRate.FRAMERATE_24:
+                        FrameRateEnumerator = 24000;
+                        FrameRateDenominator = 1000;
+                        break;
+                    case TSFrameRate.FRAMERATE_25:
+                        FrameRateEnumerator = 25000;
+                        FrameRateDenominator = 1000;
+                        break;
+                    case TSFrameRate.FRAMERATE_29_97:
+                        FrameRateEnumerator = 30000;
+                        FrameRateDenominator = 1001;
+                        break;
+                    case TSFrameRate.FRAMERATE_50:
+                        FrameRateEnumerator = 50000;
+                        FrameRateDenominator = 1000;
+                        break;
+                    case TSFrameRate.FRAMERATE_59_94:
+                        FrameRateEnumerator = 60000;
+                        FrameRateDenominator = 1001;
+                        break;
+                }
+            }
+        }
+
+        public override string Description
+        {
+            get
+            {
+                string description = "";
+
+                if (Height > 0)
+                {
+                    description += string.Format("{0:D}{1} / ",
+                        Height,
+                        IsInterlaced ? "i" : "p");
+                }
+                if (FrameRateEnumerator > 0 &&
+                    FrameRateDenominator > 0)
+                {
+                    if (FrameRateEnumerator % FrameRateDenominator == 0)
+                    {
+                        description += string.Format("{0:D} fps / ",
+                            FrameRateEnumerator / FrameRateDenominator);
+                    }
+                    else
+                    {
+                        description += string.Format("{0:F3} fps / ",
+                            (double)FrameRateEnumerator / FrameRateDenominator);
+                    }
+
+                }
+                if (AspectRatio == TSAspectRatio.ASPECT_4_3)
+                {
+                    description += "4:3 / ";
+                }
+                else if (AspectRatio == TSAspectRatio.ASPECT_16_9)
+                {
+                    description += "16:9 / ";
+                }
+                if (EncodingProfile != null)
+                {
+                    description += EncodingProfile + " / ";
+                }
+                if (description.EndsWith(" / "))
+                {
+                    description = description.Substring(0, description.Length - 3);
+                }
+                return description;
+            }
+        }
+
+        public override TSStream Clone()
+        {
+            TSVideoStream stream = new TSVideoStream();
+            CopyTo(stream);
+
+            stream.VideoFormat = _VideoFormat;
+            stream.FrameRate = _FrameRate;
+            stream.Width = Width;
+            stream.Height = Height;
+            stream.IsInterlaced = IsInterlaced;        
+            stream.FrameRateEnumerator = FrameRateEnumerator;
+            stream.FrameRateDenominator = FrameRateDenominator;
+            stream.AspectRatio = AspectRatio;
+            stream.EncodingProfile = EncodingProfile;
+
+            return stream;
+        }
+    }
+
+    public enum TSAudioMode
+    {
+        Unknown,
+        DualMono,
+        Stereo,
+        Surround,
+        Extended
+    }
+
+    public class TSAudioStream : TSStream
+    {
+        public TSAudioStream()
+        {
+        }
+
+        public int SampleRate;
+        public int ChannelCount;
+        public int BitDepth;
+        public int LFE;
+        public int DialNorm;
+        public TSAudioMode AudioMode;
+        public TSAudioStream CoreStream;
+        public TSChannelLayout ChannelLayout;
+
+        public static int ConvertSampleRate(
+            TSSampleRate sampleRate)
+        {
+            switch (sampleRate)
+            {
+                case TSSampleRate.SAMPLERATE_48:
+                    return 48000;
+
+                case TSSampleRate.SAMPLERATE_96:
+                case TSSampleRate.SAMPLERATE_48_96:
+                    return 96000;
+
+                case TSSampleRate.SAMPLERATE_192:
+                case TSSampleRate.SAMPLERATE_48_192:
+                    return 192000;
+            }
+            return 0;
+        }
+
+        public string ChannelDescription
+        {
+            get
+            {
+                if (ChannelLayout == TSChannelLayout.CHANNELLAYOUT_MONO &&
+                    ChannelCount == 2)
+                {
+                }
+
+                string description = "";
+                if (ChannelCount > 0)
+                {
+                    description += string.Format(
+                        "{0:D}.{1:D}",
+                        ChannelCount, LFE);
+                }
+                else
+                {
+                    switch (ChannelLayout)
+                    {
+                        case TSChannelLayout.CHANNELLAYOUT_MONO:
+                            description += "1.0";
+                            break;
+                        case TSChannelLayout.CHANNELLAYOUT_STEREO:
+                            description += "2.0";
+                            break;
+                        case TSChannelLayout.CHANNELLAYOUT_MULTI:
+                            description += "5.1";
+                            break;
+                    }
+                }
+                if (AudioMode == TSAudioMode.Extended)
+                {
+                    if (StreamType == TSStreamType.AC3_AUDIO)
+                    {
+                        description += "-EX";
+                    }
+                    if (StreamType == TSStreamType.DTS_AUDIO ||
+                        StreamType == TSStreamType.DTS_HD_AUDIO ||
+                        StreamType == TSStreamType.DTS_HD_MASTER_AUDIO)
+                    {
+                        description += "-ES";
+                    }
+                }
+                return description;
+            }
+        }
+
+        public override string Description
+        {
+            get
+            {
+                string description = ChannelDescription;
+
+                if (SampleRate > 0)
+                {
+                    description += string.Format(
+                        " / {0:D} kHz", SampleRate / 1000);
+                }
+                if (BitRate > 0)
+                {
+                    description += string.Format(
+                        " / {0:D} kbps", (uint)Math.Round((double)BitRate / 1000));
+                }
+                if (BitDepth > 0)
+                {
+                    description += string.Format(
+                        " / {0:D}-bit", BitDepth);
+                }
+                if (DialNorm != 0)
+                {
+                    description += string.Format(
+                        " / DN {0}dB", DialNorm);
+                }
+                if (ChannelCount == 2)
+                {
+                    switch (AudioMode)
+                    {
+                        case TSAudioMode.DualMono:
+                            description += " / Dual Mono";
+                            break;
+
+                        case TSAudioMode.Surround:
+                            description += " / Dolby Surround";
+                            break;
+                    }
+                }
+                if (description.EndsWith(" / "))
+                {
+                    description = description.Substring(0, description.Length - 3);
+                }
+                if (CoreStream != null)
+                {
+                    string codec = "";
+                    switch (CoreStream.StreamType)
+                    {
+                        case TSStreamType.AC3_AUDIO:
+                            codec = "AC3 Embedded";
+                            break;
+                        case TSStreamType.DTS_AUDIO:
+                            codec = "DTS Core";
+                            break;
+                    }
+                    description += string.Format(
+                        " ({0}: {1})",
+                        codec,
+                        CoreStream.Description);
+                }
+                return description;
+            }
+        }
+
+        public override TSStream Clone()
+        {
+            TSAudioStream stream = new TSAudioStream();
+            CopyTo(stream);
+
+            stream.SampleRate = SampleRate;
+            stream.ChannelLayout = ChannelLayout;
+            stream.ChannelCount = ChannelCount;
+            stream.BitDepth = BitDepth;
+            stream.LFE = LFE;
+            stream.DialNorm = DialNorm;
+            stream.AudioMode = AudioMode;
+            if (CoreStream != null)
+            {
+                stream.CoreStream = (TSAudioStream)CoreStream.Clone();
+            }
+
+            return stream;
+        }
+    }
+
+    public class TSGraphicsStream : TSStream
+    {
+        public TSGraphicsStream()
+        {
+            IsVBR = true;
+            IsInitialized = true;
+        }
+
+        public override TSStream Clone()
+        {
+            TSGraphicsStream stream = new TSGraphicsStream();
+            CopyTo(stream);
+            return stream;
+        }
+    }
+
+    public class TSTextStream : TSStream
+    {
+        public TSTextStream()
+        {
+            IsVBR = true;
+            IsInitialized = true;
+        }
+
+        public override TSStream Clone()
+        {
+            TSTextStream stream = new TSTextStream();
+            CopyTo(stream);
+            return stream;
+        }
+    }
+}

+ 145 - 0
BDInfo/TSStreamBuffer.cs

@@ -0,0 +1,145 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+
+namespace BDInfo
+{
+    public class TSStreamBuffer
+    {
+        private MemoryStream Stream = new MemoryStream();
+        private int SkipBits = 0;
+        private byte[] Buffer;
+        private int BufferLength = 0;
+        public int TransferLength = 0;
+
+        public TSStreamBuffer()
+        {
+            Buffer = new byte[4096];
+            Stream = new MemoryStream(Buffer);
+        }
+
+        public long Length
+        {
+            get
+            {
+                return (long)BufferLength;
+            }
+        }
+
+        public long Position
+        {
+            get
+            {
+                return Stream.Position;
+            }
+        }
+
+        public void Add(
+            byte[] buffer,
+            int offset,
+            int length)
+        {
+            TransferLength += length;
+
+            if (BufferLength + length >= Buffer.Length)
+            {
+                length = Buffer.Length - BufferLength;
+            }
+            if (length > 0)
+            {
+                Array.Copy(buffer, offset, Buffer, BufferLength, length);
+                BufferLength += length;
+            }
+        }
+
+        public void Seek(
+            long offset,
+            SeekOrigin loc)
+        {
+            Stream.Seek(offset, loc);
+        }
+
+        public void Reset()
+        {
+            BufferLength = 0;
+            TransferLength = 0;
+        }
+
+        public void BeginRead()
+        {
+            SkipBits = 0;
+            Stream.Seek(0, SeekOrigin.Begin);
+        }
+
+        public void EndRead()
+        {
+        }
+
+        public byte[] ReadBytes(int bytes)
+        {
+            if (Stream.Position + bytes >= BufferLength)
+            {
+                return null;
+            }
+
+            byte[] value = new byte[bytes];
+            Stream.Read(value, 0, bytes);
+            return value;
+        }
+
+        public byte ReadByte()
+        {
+            return (byte)Stream.ReadByte();
+        }
+
+        public int ReadBits(int bits)
+        {
+            long pos = Stream.Position;
+
+            int shift = 24;
+            int data = 0;
+            for (int i = 0; i < 4; i++)
+            {
+                if (pos + i >= BufferLength) break;
+                data += (Stream.ReadByte() << shift);
+                shift -= 8;
+            }
+            BitVector32 vector = new BitVector32(data);
+
+            int value = 0;
+            for (int i = SkipBits; i < SkipBits + bits; i++)
+            {
+                value <<= 1;
+                value += (vector[1 << (32 - i - 1)] ? 1 : 0);
+            }
+
+            SkipBits += bits;
+            Stream.Seek(pos + (SkipBits >> 3), SeekOrigin.Begin);
+            SkipBits = SkipBits % 8;
+
+            return value;
+        }
+    }
+}

+ 114 - 0
BDInfo/TSStreamClip.cs

@@ -0,0 +1,114 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BDInfo
+{
+    public class TSStreamClip
+    {
+        public int AngleIndex = 0;
+        public string Name;
+        public double TimeIn;
+        public double TimeOut;
+        public double RelativeTimeIn;
+        public double RelativeTimeOut;
+        public double Length;
+
+        public ulong FileSize = 0;
+        public ulong InterleavedFileSize = 0;
+        public ulong PayloadBytes = 0;
+        public ulong PacketCount = 0;
+        public double PacketSeconds = 0;
+
+        public List<double> Chapters = new List<double>();
+
+        public TSStreamFile StreamFile = null;
+        public TSStreamClipFile StreamClipFile = null;
+
+        public TSStreamClip(
+            TSStreamFile streamFile,
+            TSStreamClipFile streamClipFile)
+        {
+            if (streamFile != null)
+            {
+                Name = streamFile.Name;
+                StreamFile = streamFile;
+                FileSize = (ulong)StreamFile.FileInfo.Length;
+                if (StreamFile.InterleavedFile != null)
+                {
+                    InterleavedFileSize = (ulong)StreamFile.InterleavedFile.FileInfo.Length;
+                }
+            }
+            StreamClipFile = streamClipFile;
+        }
+
+        public string DisplayName
+        {
+            get
+            {
+                if (StreamFile != null &&
+                    StreamFile.InterleavedFile != null &&
+                    BDInfoSettings.EnableSSIF)
+                {
+                    return StreamFile.InterleavedFile.Name;
+                }
+                return Name;
+            }
+        }
+
+        public ulong PacketSize
+        {
+            get
+            {
+                return PacketCount * 192;
+            }
+        }
+
+        public ulong PacketBitRate
+        {
+            get
+            {
+                if (PacketSeconds > 0)
+                {
+                    return (ulong)Math.Round(((PacketSize * 8.0) / PacketSeconds));
+                }
+                return 0;
+            }
+        }
+
+        public bool IsCompatible(TSStreamClip clip)
+        {
+            foreach (TSStream stream1 in StreamFile.Streams.Values)
+            {
+                if (clip.StreamFile.Streams.ContainsKey(stream1.PID))
+                {
+                    TSStream stream2 = clip.StreamFile.Streams[stream1.PID];
+                    if (stream1.StreamType != stream2.StreamType)
+                    {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+    }
+}

+ 248 - 0
BDInfo/TSStreamClipFile.cs

@@ -0,0 +1,248 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+#undef DEBUG
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+
+namespace BDInfo
+{
+    public class TSStreamClipFile
+    {
+        public FileInfo FileInfo = null;
+        public string FileType = null;
+        public bool IsValid = false;
+        public string Name = null;
+
+        public Dictionary<ushort, TSStream> Streams =
+            new Dictionary<ushort,TSStream>();
+
+        public TSStreamClipFile(
+            FileInfo fileInfo)
+        {
+            FileInfo = fileInfo;
+            Name = fileInfo.Name.ToUpper();
+        }
+
+        public void Scan()
+        {
+            FileStream fileStream = null;
+            BinaryReader fileReader = null;
+
+            try
+            {
+#if DEBUG
+                Debug.WriteLine(string.Format(
+                    "Scanning {0}...", Name));
+#endif
+                Streams.Clear();
+
+                fileStream = File.OpenRead(FileInfo.FullName);
+                fileReader = new BinaryReader(fileStream);
+
+                byte[] data = new byte[fileStream.Length];
+                fileReader.Read(data, 0, data.Length);
+
+                byte[] fileType = new byte[8];
+                Array.Copy(data, 0, fileType, 0, fileType.Length);
+                
+                FileType = ASCIIEncoding.ASCII.GetString(fileType);
+                if (FileType != "HDMV0100" &&
+                    FileType != "HDMV0200")
+                {
+                    throw new Exception(string.Format(
+                        "Clip info file {0} has an unknown file type {1}.",
+                        FileInfo.Name, FileType));
+                }
+#if DEBUG                
+                Debug.WriteLine(string.Format(
+                    "\tFileType: {0}", FileType));
+#endif
+                int clipIndex =
+                    ((int)data[12] << 24) +
+                    ((int)data[13] << 16) +
+                    ((int)data[14] << 8) +
+                    ((int)data[15]);
+
+                int clipLength =
+                    ((int)data[clipIndex] << 24) +
+                    ((int)data[clipIndex + 1] << 16) +
+                    ((int)data[clipIndex + 2] << 8) +
+                    ((int)data[clipIndex + 3]);
+
+                byte[] clipData = new byte[clipLength];
+                Array.Copy(data, clipIndex + 4, clipData, 0, clipData.Length);
+
+                int streamCount = clipData[8];
+#if DEBUG
+                Debug.WriteLine(string.Format(
+                    "\tStreamCount: {0}", streamCount));
+#endif
+                int streamOffset = 10;
+                for (int streamIndex = 0;
+                    streamIndex < streamCount;
+                    streamIndex++)
+                {
+                    TSStream stream = null;
+
+                    ushort PID = (ushort)
+                        ((clipData[streamOffset] << 8) + 
+                          clipData[streamOffset + 1]);
+                    
+                    streamOffset += 2;
+
+                    TSStreamType streamType = (TSStreamType)
+                        clipData[streamOffset + 1];
+                    switch (streamType)
+                    {
+                        case TSStreamType.MVC_VIDEO:
+                            // TODO
+                            break;
+
+                        case TSStreamType.AVC_VIDEO:
+                        case TSStreamType.MPEG1_VIDEO:
+                        case TSStreamType.MPEG2_VIDEO:
+                        case TSStreamType.VC1_VIDEO:
+                        {
+                            TSVideoFormat videoFormat = (TSVideoFormat)
+                                (clipData[streamOffset + 2] >> 4);
+                            TSFrameRate frameRate = (TSFrameRate)
+                                (clipData[streamOffset + 2] & 0xF);
+                            TSAspectRatio aspectRatio = (TSAspectRatio)
+                                (clipData[streamOffset + 3] >> 4);
+
+                            stream = new TSVideoStream();
+                            ((TSVideoStream)stream).VideoFormat = videoFormat;
+                            ((TSVideoStream)stream).AspectRatio = aspectRatio;
+                            ((TSVideoStream)stream).FrameRate = frameRate;
+#if DEBUG
+                            Debug.WriteLine(string.Format(
+                                "\t{0} {1} {2} {3} {4}",
+                                PID,
+                                streamType,
+                                videoFormat,
+                                frameRate,
+                                aspectRatio));
+#endif
+                        }
+                        break;
+
+                        case TSStreamType.AC3_AUDIO:
+                        case TSStreamType.AC3_PLUS_AUDIO:
+                        case TSStreamType.AC3_PLUS_SECONDARY_AUDIO:
+                        case TSStreamType.AC3_TRUE_HD_AUDIO:
+                        case TSStreamType.DTS_AUDIO:
+                        case TSStreamType.DTS_HD_AUDIO:
+                        case TSStreamType.DTS_HD_MASTER_AUDIO:
+                        case TSStreamType.DTS_HD_SECONDARY_AUDIO:
+                        case TSStreamType.LPCM_AUDIO:
+                        case TSStreamType.MPEG1_AUDIO:
+                        case TSStreamType.MPEG2_AUDIO:
+                        {
+                            byte[] languageBytes = new byte[3];
+                            Array.Copy(clipData, streamOffset + 3,
+                                languageBytes, 0, languageBytes.Length);
+                            string languageCode =
+                                ASCIIEncoding.ASCII.GetString(languageBytes);
+
+                            TSChannelLayout channelLayout = (TSChannelLayout)
+                                (clipData[streamOffset + 2] >> 4);
+                            TSSampleRate sampleRate = (TSSampleRate)
+                                (clipData[streamOffset + 2] & 0xF);
+
+                            stream = new TSAudioStream();
+                            ((TSAudioStream)stream).LanguageCode = languageCode;
+                            ((TSAudioStream)stream).ChannelLayout = channelLayout;
+                            ((TSAudioStream)stream).SampleRate = TSAudioStream.ConvertSampleRate(sampleRate);
+                            ((TSAudioStream)stream).LanguageCode = languageCode;
+#if DEBUG
+                            Debug.WriteLine(string.Format(
+                                "\t{0} {1} {2} {3} {4}",
+                                PID,
+                                streamType,
+                                languageCode,
+                                channelLayout,
+                                sampleRate));
+#endif
+                        }
+                        break;
+
+                        case TSStreamType.INTERACTIVE_GRAPHICS:
+                        case TSStreamType.PRESENTATION_GRAPHICS:
+                        {
+                            byte[] languageBytes = new byte[3];
+                            Array.Copy(clipData, streamOffset + 2,
+                                languageBytes, 0, languageBytes.Length);
+                            string languageCode =
+                                ASCIIEncoding.ASCII.GetString(languageBytes);
+
+                            stream = new TSGraphicsStream();
+                            stream.LanguageCode = languageCode;
+#if DEBUG
+                            Debug.WriteLine(string.Format(
+                                "\t{0} {1} {2}",
+                                PID,
+                                streamType,
+                                languageCode));
+#endif
+                        }
+                        break;
+
+                        case TSStreamType.SUBTITLE:
+                        {
+                            byte[] languageBytes = new byte[3];
+                            Array.Copy(clipData, streamOffset + 3,
+                                languageBytes, 0, languageBytes.Length);
+                            string languageCode =
+                                ASCIIEncoding.ASCII.GetString(languageBytes);
+#if DEBUG
+                            Debug.WriteLine(string.Format(
+                                "\t{0} {1} {2}",
+                                PID,
+                                streamType,
+                                languageCode));
+#endif
+                            stream = new TSTextStream();
+                            stream.LanguageCode = languageCode;
+                        }
+                        break;
+                    }
+
+                    if (stream != null)
+                    {
+                        stream.PID = PID;
+                        stream.StreamType = streamType;
+                        Streams.Add(PID, stream);
+                    }
+
+                    streamOffset += clipData[streamOffset] + 1;
+                }                
+                IsValid = true;
+            }
+            finally
+            {
+                if (fileReader != null) fileReader.Close();
+                if (fileStream != null) fileStream.Close();
+            }
+        }
+    }
+}

+ 1551 - 0
BDInfo/TSStreamFile.cs

@@ -0,0 +1,1551 @@
+//============================================================================
+// BDInfo - Blu-ray Video and Audio Analysis Tool
+// Copyright © 2010 Cinema Squid
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+//=============================================================================
+
+#undef DEBUG
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+
+namespace BDInfo
+{
+    public class TSStreamState
+    {
+        public ulong TransferCount = 0;
+
+        public string StreamTag = null;
+
+        public ulong TotalPackets = 0;
+        public ulong WindowPackets = 0;
+
+        public ulong TotalBytes = 0;
+        public ulong WindowBytes = 0;
+
+        public long PeakTransferLength = 0;
+        public long PeakTransferRate = 0;
+
+        public double TransferMarker = 0;
+        public double TransferInterval = 0;
+
+        public TSStreamBuffer StreamBuffer = new TSStreamBuffer();
+
+        public uint Parse = 0;
+        public bool TransferState = false;
+        public int TransferLength = 0;
+        public int PacketLength = 0;
+        public byte PacketLengthParse = 0;
+        public byte PacketParse = 0;
+
+        public byte PTSParse = 0;
+        public ulong PTS = 0;
+        public ulong PTSTemp = 0;
+        public ulong PTSLast = 0;
+        public ulong PTSPrev = 0;
+        public ulong PTSDiff = 0;
+        public ulong PTSCount = 0;
+        public ulong PTSTransfer = 0;
+
+        public byte DTSParse = 0;
+        public ulong DTSTemp = 0;
+        public ulong DTSPrev = 0;
+
+        public byte PESHeaderLength = 0;
+        public byte PESHeaderFlags = 0;
+#if DEBUG
+        public byte PESHeaderIndex = 0;
+        public byte[] PESHeader = new byte[256 + 9];
+#endif
+    }
+
+    public class TSPacketParser
+    {
+        public bool SyncState = false;
+        public byte TimeCodeParse = 4;
+        public byte PacketLength = 0;
+        public byte HeaderParse = 0;
+
+        public uint TimeCode;
+        public byte TransportErrorIndicator;
+        public byte PayloadUnitStartIndicator;
+        public byte TransportPriority;
+        public ushort PID;
+        public byte TransportScramblingControl;
+        public byte AdaptionFieldControl;
+
+        public bool AdaptionFieldState = false;
+        public byte AdaptionFieldParse = 0;
+        public byte AdaptionFieldLength = 0;
+
+        public ushort PCRPID = 0xFFFF;
+        public byte PCRParse = 0;
+        public ulong PreviousPCR = 0;
+        public ulong PCR = 0;
+        public ulong PCRCount = 0;
+        public ulong PTSFirst = ulong.MaxValue;
+        public ulong PTSLast = ulong.MinValue;
+        public ulong PTSDiff = 0;
+
+        public byte[] PAT = new byte[1024];
+        public bool PATSectionStart = false;
+        public byte PATPointerField = 0;
+        public uint PATOffset = 0;
+        public byte PATSectionLengthParse = 0;
+        public ushort PATSectionLength = 0;
+        public uint PATSectionParse = 0;
+        public bool PATTransferState = false;
+        public byte PATSectionNumber = 0;
+        public byte PATLastSectionNumber = 0;
+
+        public ushort TransportStreamId = 0xFFFF;
+
+        public List<TSDescriptor> PMTProgramDescriptors = new List<TSDescriptor>();
+        public ushort PMTPID = 0xFFFF;
+        public Dictionary<ushort, byte[]> PMT = new Dictionary<ushort, byte[]>();
+        public bool PMTSectionStart = false;
+        public ushort PMTProgramInfoLength = 0;
+        public byte PMTProgramDescriptor = 0;
+        public byte PMTProgramDescriptorLengthParse = 0;
+        public byte PMTProgramDescriptorLength = 0;
+        public ushort PMTStreamInfoLength = 0;
+        public uint PMTStreamDescriptorLengthParse = 0;
+        public uint PMTStreamDescriptorLength = 0;
+        public byte PMTPointerField = 0;
+        public uint PMTOffset = 0;
+        public uint PMTSectionLengthParse = 0;
+        public ushort PMTSectionLength = 0;
+        public uint PMTSectionParse = 0;
+        public bool PMTTransferState = false;
+        public byte PMTSectionNumber = 0;
+        public byte PMTLastSectionNumber = 0;
+
+        public byte PMTTemp = 0;
+
+        public TSStream Stream = null;
+        public TSStreamState StreamState = null;
+
+        public ulong TotalPackets = 0;
+    }
+
+    public class TSStreamDiagnostics
+    {
+        public ulong Bytes = 0;
+        public ulong Packets = 0;
+        public double Marker = 0;
+        public double Interval = 0;
+        public string Tag = null;
+    }
+
+    public class TSStreamFile
+    {
+        public FileInfo FileInfo = null;
+        public string Name = null;
+        public long Size = 0;
+        public double Length = 0;
+
+        public TSInterleavedFile InterleavedFile = null;
+
+        private Dictionary<ushort, TSStreamState> StreamStates =
+            new Dictionary<ushort, TSStreamState>();
+
+        public Dictionary<ushort, TSStream> Streams =
+            new Dictionary<ushort, TSStream>();
+
+        public Dictionary<ushort, List<TSStreamDiagnostics>> StreamDiagnostics =
+            new Dictionary<ushort, List<TSStreamDiagnostics>>();
+
+        private List<TSPlaylistFile> Playlists = null;
+
+        public TSStreamFile(FileInfo fileInfo)
+        {
+            FileInfo = fileInfo;
+            Name = fileInfo.Name.ToUpper();
+        }
+
+        public string DisplayName
+        {
+            get
+            {
+                if (BDInfoSettings.EnableSSIF &&
+                    InterleavedFile != null)
+                {
+                    return InterleavedFile.Name;
+                }
+                return Name;
+            }
+        }
+
+        private bool ScanStream(
+            TSStream stream,
+            TSStreamState streamState,
+            TSStreamBuffer buffer)
+        {
+            streamState.StreamTag = null;
+
+            long bitrate = 0;
+            if (stream.IsAudioStream &&
+                streamState.PTSTransfer > 0)
+            {
+                bitrate = (long)Math.Round(
+                    (buffer.TransferLength * 8.0) /
+                    ((double)streamState.PTSTransfer / 90000));
+
+                if (bitrate > streamState.PeakTransferRate)
+                {
+                    streamState.PeakTransferRate = bitrate;
+                }
+            }
+            if (buffer.TransferLength > streamState.PeakTransferLength)
+            {
+                streamState.PeakTransferLength = buffer.TransferLength;
+            }
+
+            buffer.BeginRead();
+            switch (stream.StreamType)
+            {
+                case TSStreamType.MPEG2_VIDEO:
+                    TSCodecMPEG2.Scan(
+                        (TSVideoStream)stream, buffer, ref streamState.StreamTag);
+                    break;
+
+                case TSStreamType.AVC_VIDEO:
+                    TSCodecAVC.Scan(
+                        (TSVideoStream)stream, buffer, ref streamState.StreamTag);
+                    break;
+
+                case TSStreamType.MVC_VIDEO:
+                    TSCodecMVC.Scan(
+                        (TSVideoStream)stream, buffer, ref streamState.StreamTag);
+                    break;
+
+                case TSStreamType.VC1_VIDEO:
+                    TSCodecVC1.Scan(
+                        (TSVideoStream)stream, buffer, ref streamState.StreamTag);
+                    break;
+
+                case TSStreamType.AC3_AUDIO:
+                    TSCodecAC3.Scan(
+                        (TSAudioStream)stream, buffer, ref streamState.StreamTag);
+                    break;
+
+                case TSStreamType.AC3_PLUS_AUDIO:
+                case TSStreamType.AC3_PLUS_SECONDARY_AUDIO:
+                    TSCodecAC3.Scan(
+                        (TSAudioStream)stream, buffer, ref streamState.StreamTag);
+                    break;
+
+                case TSStreamType.AC3_TRUE_HD_AUDIO:
+                    TSCodecTrueHD.Scan(
+                        (TSAudioStream)stream, buffer, ref streamState.StreamTag);
+                    break;
+
+                case TSStreamType.LPCM_AUDIO:
+                    TSCodecLPCM.Scan(
+                        (TSAudioStream)stream, buffer, ref streamState.StreamTag);
+                    break;
+
+                case TSStreamType.DTS_AUDIO:
+                    TSCodecDTS.Scan(
+                        (TSAudioStream)stream, buffer, bitrate, ref streamState.StreamTag);
+                    break;
+
+                case TSStreamType.DTS_HD_AUDIO:
+                case TSStreamType.DTS_HD_MASTER_AUDIO:
+                case TSStreamType.DTS_HD_SECONDARY_AUDIO:
+                    TSCodecDTSHD.Scan(
+                        (TSAudioStream)stream, buffer, bitrate, ref streamState.StreamTag);
+                    break;
+
+                default:
+                    stream.IsInitialized = true;
+                    break;
+            }
+            buffer.EndRead();
+            streamState.StreamBuffer.Reset();
+
+            bool isAVC = false;
+            bool isMVC = false;
+            foreach (TSStream finishedStream in Streams.Values)
+            {
+                if (!finishedStream.IsInitialized)
+                {
+                    return false;
+                }
+                if (finishedStream.StreamType == TSStreamType.AVC_VIDEO)
+                {
+                    isAVC = true;
+                }
+                if (finishedStream.StreamType == TSStreamType.MVC_VIDEO)
+                {
+                    isMVC = true;
+                }
+            }
+            if (isMVC && !isAVC)
+            {
+                return false;
+            }
+            return true;
+        }
+
+        private void UpdateStreamBitrates(
+            ushort PTSPID,
+            ulong PTS,
+            ulong PTSDiff)
+        {
+            if (Playlists == null) return;
+
+            foreach (ushort PID in StreamStates.Keys)
+            {
+                if (Streams.ContainsKey(PID) &&
+                    Streams[PID].IsVideoStream &&
+                    PID != PTSPID)
+                {
+                    continue;
+                }
+                if (StreamStates[PID].WindowPackets == 0)
+                {
+                    continue;
+                }
+                UpdateStreamBitrate(PID, PTSPID, PTS, PTSDiff);
+            }
+
+            foreach (TSPlaylistFile playlist in Playlists)
+            {
+                double packetSeconds = 0;
+                foreach (TSStreamClip clip in playlist.StreamClips)
+                {
+                    if (clip.AngleIndex == 0)
+                    {
+                        packetSeconds += clip.PacketSeconds;
+                    }
+                }
+                if (packetSeconds > 0)
+                {
+                    foreach (TSStream playlistStream in playlist.SortedStreams)
+                    {
+                        if (playlistStream.IsVBR)
+                        {
+                            playlistStream.BitRate = (long)Math.Round(
+                                ((playlistStream.PayloadBytes * 8.0) / packetSeconds));
+
+                            if (playlistStream.StreamType == TSStreamType.AC3_TRUE_HD_AUDIO &&
+                                ((TSAudioStream)playlistStream).CoreStream != null)
+                            {
+                                playlistStream.BitRate -=
+                                    ((TSAudioStream)playlistStream).CoreStream.BitRate;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        private void UpdateStreamBitrate(
+            ushort PID,
+            ushort PTSPID,
+            ulong PTS,
+            ulong PTSDiff)
+        {
+            if (Playlists == null) return;
+
+            TSStreamState streamState = StreamStates[PID];
+            double streamTime = (double)PTS / 90000;
+            double streamInterval = (double)PTSDiff / 90000;
+            double streamOffset = streamTime + streamInterval;
+
+            foreach (TSPlaylistFile playlist in Playlists)
+            {
+                foreach (TSStreamClip clip in playlist.StreamClips)
+                {
+                    if (clip.Name != this.Name) continue;
+
+                    if (streamTime == 0 ||
+                        (streamTime >= clip.TimeIn &&
+                         streamTime <= clip.TimeOut))
+                    {
+                        clip.PayloadBytes += streamState.WindowBytes;
+                        clip.PacketCount += streamState.WindowPackets;
+
+                        if (streamOffset > clip.TimeIn &&
+                            streamOffset - clip.TimeIn > clip.PacketSeconds)
+                        {
+                            clip.PacketSeconds = streamOffset - clip.TimeIn;
+                        }
+
+                        Dictionary<ushort, TSStream> playlistStreams = playlist.Streams;
+                        if (clip.AngleIndex > 0 && 
+                            clip.AngleIndex < playlist.AngleStreams.Count + 1)
+                        {
+                            playlistStreams = playlist.AngleStreams[clip.AngleIndex - 1];
+                        }
+                        if (playlistStreams.ContainsKey(PID))
+                        {
+                            TSStream stream = playlistStreams[PID];
+
+                            stream.PayloadBytes += streamState.WindowBytes;
+                            stream.PacketCount += streamState.WindowPackets;
+
+                            if (stream.IsVideoStream)
+                            {
+                                stream.PacketSeconds += streamInterval;
+
+                                stream.ActiveBitRate = (long)Math.Round(
+                                    ((stream.PayloadBytes * 8.0) /
+                                    stream.PacketSeconds));
+                            }
+
+                            if (stream.StreamType == TSStreamType.AC3_TRUE_HD_AUDIO &&
+                                ((TSAudioStream)stream).CoreStream != null)
+                            {
+                                stream.ActiveBitRate -=
+                                    ((TSAudioStream)stream).CoreStream.BitRate;
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (Streams.ContainsKey(PID))
+            {
+                TSStream stream = Streams[PID];
+                stream.PayloadBytes += streamState.WindowBytes;
+                stream.PacketCount += streamState.WindowPackets;
+                
+                if (stream.IsVideoStream)
+                {
+                    TSStreamDiagnostics diag = new TSStreamDiagnostics();
+                    diag.Marker = (double)PTS / 90000;
+                    diag.Interval = (double)PTSDiff / 90000;
+                    diag.Bytes = streamState.WindowBytes;
+                    diag.Packets = streamState.WindowPackets;
+                    diag.Tag = streamState.StreamTag;
+                    StreamDiagnostics[PID].Add(diag);
+
+                    stream.PacketSeconds += streamInterval;
+                }
+            }
+            streamState.WindowPackets = 0;
+            streamState.WindowBytes = 0;
+        }
+
+        public void Scan(List<TSPlaylistFile> playlists, bool isFullScan)
+        {
+            if (playlists == null || playlists.Count == 0)
+            {
+                return;
+            }
+
+            Playlists = playlists;
+            int dataSize = 16384;
+            FileStream fileStream = null;
+            try
+            {                
+                string fileName;
+                if (BDInfoSettings.EnableSSIF &&
+                    InterleavedFile != null)
+                {
+                    fileName = InterleavedFile.FileInfo.FullName;
+                }
+                else
+                {
+                    fileName = FileInfo.FullName;
+                }
+                fileStream = new FileStream(
+                    fileName,
+                    FileMode.Open,
+                    FileAccess.Read,
+                    FileShare.Read,
+                    dataSize, false);
+
+                Size = 0;
+                Length = 0;
+
+                Streams.Clear();
+                StreamStates.Clear();
+                StreamDiagnostics.Clear();
+
+                TSPacketParser parser = 
+                    new TSPacketParser();
+                
+                long fileLength = (uint)fileStream.Length;
+                byte[] buffer = new byte[dataSize];
+                int bufferLength = 0;
+                while ((bufferLength = 
+                    fileStream.Read(buffer, 0, buffer.Length)) > 0)
+                {
+                    int offset = 0;
+                    for (int i = 0; i < bufferLength; i++)
+                    {
+                        if (parser.SyncState == false)
+                        {
+                            if (parser.TimeCodeParse > 0)
+                            {
+                                parser.TimeCodeParse--;
+                                switch (parser.TimeCodeParse)
+                                {
+                                    case 3:
+                                        parser.TimeCode = 0;
+                                        parser.TimeCode |=
+                                            ((uint)buffer[i] & 0x3F) << 24;
+                                        break;
+                                    case 2:
+                                        parser.TimeCode |=
+                                            ((uint)buffer[i] & 0xFF) << 16;
+                                        break;
+                                    case 1:
+                                        parser.TimeCode |=
+                                            ((uint)buffer[i] & 0xFF) << 8;
+                                        break;
+                                    case 0:
+                                        parser.TimeCode |=
+                                            ((uint)buffer[i] & 0xFF);
+                                        break;
+                                }
+                            }
+                            else if (buffer[i] == 0x47)
+                            {
+                                parser.SyncState = true;
+                                parser.PacketLength = 187;
+                                parser.TimeCodeParse = 4;
+                                parser.HeaderParse = 3;
+                            }
+                        }
+                        else if (parser.HeaderParse > 0)
+                        {
+                            parser.PacketLength--;
+                            parser.HeaderParse--;
+
+                            switch (parser.HeaderParse)
+                            {
+                                case 2:
+                                {
+                                    parser.TransportErrorIndicator =
+                                        (byte)((buffer[i] >> 7) & 0x1);
+                                    parser.PayloadUnitStartIndicator =
+                                        (byte)((buffer[i] >> 6) & 0x1);
+                                    parser.TransportPriority =
+                                        (byte)((buffer[i] >> 5) & 0x1);
+                                    parser.PID =
+                                        (ushort)((buffer[i] & 0x1f) << 8);
+                                }
+                                break;
+
+                                case 1:
+                                {
+                                    parser.PID |= (ushort)buffer[i];
+                                    if (Streams.ContainsKey(parser.PID))
+                                    {
+                                        parser.Stream = Streams[parser.PID];
+                                    }
+                                    else
+                                    {
+                                        parser.Stream = null;
+                                    }
+                                    if (!StreamStates.ContainsKey(parser.PID))
+                                    {
+                                        StreamStates[parser.PID] = new TSStreamState();
+                                    }
+                                    parser.StreamState = StreamStates[parser.PID];
+                                    parser.StreamState.TotalPackets++;
+                                    parser.StreamState.WindowPackets++;
+                                    parser.TotalPackets++;
+                                }
+                                break;
+
+                                case 0:
+                                {
+                                    parser.TransportScramblingControl =
+                                        (byte)((buffer[i] >> 6) & 0x3);
+                                    parser.AdaptionFieldControl =
+                                        (byte)((buffer[i] >> 4) & 0x3);
+
+                                    if ((parser.AdaptionFieldControl & 0x2) == 0x2)
+                                    {
+                                        parser.AdaptionFieldState = true;
+                                    }
+                                    if (parser.PayloadUnitStartIndicator == 1)
+                                    {
+                                        if (parser.PID == 0)
+                                        {
+                                            parser.PATSectionStart = true;
+                                        }
+                                        else if (parser.PID == parser.PMTPID)
+                                        {
+                                            parser.PMTSectionStart = true;
+                                        }
+                                        else if (parser.StreamState != null &&
+                                            parser.StreamState.TransferState)
+                                        {
+                                            parser.StreamState.TransferState = false;
+                                            parser.StreamState.TransferCount++;
+
+                                            bool isFinished = ScanStream(
+                                                parser.Stream, 
+                                                parser.StreamState, 
+                                                parser.StreamState.StreamBuffer);
+
+                                            if (!isFullScan && isFinished)
+                                            {
+                                                return;
+                                            }
+                                        }
+                                    }
+                                }
+                                break;
+                            }
+                        }
+                        else if (parser.AdaptionFieldState)
+                        {
+                            parser.PacketLength--;
+                            parser.AdaptionFieldParse = buffer[i];
+                            parser.AdaptionFieldLength = buffer[i];
+                            parser.AdaptionFieldState = false;
+                        }
+                        else if (parser.AdaptionFieldParse > 0)
+                        {
+                            parser.PacketLength--;
+                            parser.AdaptionFieldParse--;
+                            if ((parser.AdaptionFieldLength - parser.AdaptionFieldParse) == 1)
+                            {
+                                if ((buffer[i] & 0x10) == 0x10)
+                                {
+                                    parser.PCRParse = 6;
+                                    parser.PCR = 0;
+                                }
+                            }
+                            else if (parser.PCRParse > 0)
+                            {
+                                parser.PCRParse--;
+                                parser.PCR = (parser.PCR << 8) + (ulong)buffer[i];
+                                if (parser.PCRParse == 0)
+                                {
+                                    parser.PreviousPCR = parser.PCR;
+                                    parser.PCR = (parser.PCR & 0x1FF) +
+                                        ((parser.PCR >> 15) * 300);
+                                }
+                                parser.PCRCount++;
+                            }
+                            if (parser.PacketLength == 0)
+                            {
+                                parser.SyncState = false;
+                            }
+                        }
+                        else if (parser.PID == 0)
+                        {
+                            if (parser.PATTransferState)
+                            {
+                                if ((bufferLength - i) > parser.PATSectionLength)
+                                {
+                                    offset = parser.PATSectionLength;
+                                }
+                                else
+                                {
+                                    offset = (bufferLength - i);
+                                }
+                                if (parser.PacketLength <= offset)
+                                {
+                                    offset = parser.PacketLength;
+                                }
+
+                                for (int k = 0; k < offset; k++)
+                                {
+                                    parser.PAT[parser.PATOffset++] = buffer[i++];
+                                    parser.PATSectionLength--;
+                                    parser.PacketLength--;
+                                } --i;
+
+                                if (parser.PATSectionLength == 0)
+                                {
+                                    parser.PATTransferState = false;
+                                    if (parser.PATSectionNumber == parser.PATLastSectionNumber)
+                                    {
+                                        for (int k = 0; k < (parser.PATOffset - 4); k += 4)
+                                        {
+                                            uint programNumber = (uint)
+                                                ((parser.PAT[k] << 8) +  
+                                                  parser.PAT[k + 1]);
+
+                                            ushort programPID = (ushort)                                                 
+                                                (((parser.PAT[k + 2] & 0x1F) << 8) +
+                                                   parser.PAT[k + 3]);
+
+                                            if (programNumber == 1)
+                                            {
+                                                parser.PMTPID = programPID;
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                            else
+                            {
+                                --parser.PacketLength;
+                                if (parser.PATSectionStart)
+                                {
+                                    parser.PATPointerField = buffer[i];
+                                    if (parser.PATPointerField == 0)
+                                    {
+                                        parser.PATSectionLengthParse = 3;
+                                    }
+                                    parser.PATSectionStart = false;
+                                }
+                                else if (parser.PATPointerField > 0)
+                                {
+                                    --parser.PATPointerField;
+                                    if (parser.PATPointerField == 0)
+                                    {
+                                        parser.PATSectionLengthParse = 3;
+                                    }
+                                }
+                                else if (parser.PATSectionLengthParse > 0)
+                                {
+                                    --parser.PATSectionLengthParse;
+                                    switch (parser.PATSectionLengthParse)
+                                    {
+                                        case 2:
+                                            break;
+                                        case 1:
+                                            parser.PATSectionLength = (ushort)
+                                                ((buffer[i] & 0xF) << 8);
+                                            break;
+                                        case 0:
+                                            parser.PATSectionLength |= buffer[i];
+                                            if (parser.PATSectionLength > 1021)
+                                            {
+                                                parser.PATSectionLength = 0;
+                                            }
+                                            else
+                                            {
+                                                parser.PATSectionParse = 5;
+                                            }
+                                            break;
+                                    }
+                                }
+                                else if (parser.PATSectionParse > 0)
+                                {
+                                    --parser.PATSectionLength;
+                                    --parser.PATSectionParse;
+
+                                    switch (parser.PATSectionParse)
+                                    {
+                                        case 4:
+                                            parser.TransportStreamId = (ushort)
+                                                (buffer[i] << 8);
+                                            break;
+                                        case 3:
+                                            parser.TransportStreamId |= buffer[i];
+                                            break;
+                                        case 2:
+                                            break;
+                                        case 1:
+                                            parser.PATSectionNumber = buffer[i];
+                                            if (parser.PATSectionNumber == 0)
+                                            {
+                                                parser.PATOffset = 0;
+                                            }
+                                            break;
+                                        case 0:
+                                            parser.PATLastSectionNumber = buffer[i];
+                                            parser.PATTransferState = true;
+                                            break;
+                                    }
+                                }
+                            }
+                            if (parser.PacketLength == 0)
+                            {
+                                parser.SyncState = false;
+                            }
+                        }
+                        else if (parser.PID == parser.PMTPID)
+                        {
+                            if (parser.PMTTransferState)
+                            {
+                                if ((bufferLength - i) >= parser.PMTSectionLength)
+                                {
+                                    offset = parser.PMTSectionLength;
+                                }
+                                else
+                                {
+                                    offset = (bufferLength - i);
+                                }
+                                if (parser.PacketLength <= offset)
+                                {
+                                    offset = parser.PacketLength;
+                                }
+                                if (!parser.PMT.ContainsKey(parser.PID))
+                                {
+                                    parser.PMT[parser.PID] = new byte[1024];
+                                }
+
+                                byte[] PMT = parser.PMT[parser.PID];
+                                for (int k = 0; k < offset; k++)
+                                {
+                                    PMT[parser.PMTOffset++] = buffer[i++];
+                                    --parser.PMTSectionLength;
+                                    --parser.PacketLength;
+                                } --i;
+
+                                if (parser.PMTSectionLength == 0)
+                                {
+                                    parser.PMTTransferState = false;
+                                    if (parser.PMTSectionNumber == parser.PMTLastSectionNumber)
+                                    {
+                                        //Console.WriteLine("PMT Start: " + parser.PMTTemp);
+                                        try
+                                        {
+                                            for (int k = 0; k < (parser.PMTOffset - 4); k += 5)
+                                            {
+                                                byte streamType = PMT[k];
+
+                                                ushort streamPID = (ushort)
+                                                    (((PMT[k + 1] & 0x1F) << 8) +
+                                                       PMT[k + 2]);
+
+                                                ushort streamInfoLength = (ushort)
+                                                    (((PMT[k + 3] & 0xF) << 8) +
+                                                       PMT[k + 4]);
+
+                                                /*
+                                                if (streamInfoLength == 2)
+                                                {
+                                                    // TODO: Cleanup
+                                                    //streamInfoLength = 0;
+                                                }
+
+                                                Console.WriteLine(string.Format(
+                                                    "Type: {0} PID: {1} Length: {2}",
+                                                    streamType, streamPID, streamInfoLength));
+                                                 */
+
+                                                if (!Streams.ContainsKey(streamPID))
+                                                {
+                                                    List<TSDescriptor> streamDescriptors =
+                                                        new List<TSDescriptor>();
+
+                                                    /*
+                                                     * TODO: Getting bad streamInfoLength
+                                                    if (streamInfoLength > 0)
+                                                    {
+                                                        for (int d = 0; d < streamInfoLength; d++)
+                                                        {
+                                                            byte name = PMT[k + d + 5];
+                                                            byte length = PMT[k + d + 6];
+                                                            TSDescriptor descriptor =
+                                                                new TSDescriptor(name, length);
+                                                            for (int v = 0; v < length; v++)
+                                                            {
+                                                                descriptor.Value[v] =
+                                                                    PMT[k + d + v + 7];
+                                                            }
+                                                            streamDescriptors.Add(descriptor);
+                                                            d += (length + 1);
+                                                        }
+                                                    }
+                                                    */
+                                                    CreateStream(streamPID, streamType, streamDescriptors);
+                                                }
+                                                k += streamInfoLength;
+                                            }
+                                        }
+                                        catch (Exception ex)
+                                        {
+                                            // TODO
+                                            Console.WriteLine(ex.Message);
+                                        }
+                                    }
+                                }
+                            }
+                            else
+                            {
+                                --parser.PacketLength;
+                                if (parser.PMTSectionStart)
+                                {
+                                    parser.PMTPointerField = buffer[i];
+                                    if (parser.PMTPointerField == 0)
+                                    {
+                                        parser.PMTSectionLengthParse = 3;
+                                    }
+                                    parser.PMTSectionStart = false;
+                                }
+                                else if (parser.PMTPointerField > 0)
+                                {
+                                    --parser.PMTPointerField;
+                                    if (parser.PMTPointerField == 0)
+                                    {
+                                        parser.PMTSectionLengthParse = 3;
+                                    }
+                                }
+                                else if (parser.PMTSectionLengthParse > 0)
+                                {
+                                    --parser.PMTSectionLengthParse;
+                                    switch (parser.PMTSectionLengthParse)
+                                    {
+                                        case 2:
+                                            if (buffer[i] != 0x2)
+                                            {
+                                                parser.PMTSectionLengthParse = 0;
+                                            }
+                                            break;
+                                        case 1:
+                                            parser.PMTSectionLength = (ushort)
+                                                ((buffer[i] & 0xF) << 8);
+                                            break;
+                                        case 0:
+                                            parser.PMTSectionLength |= buffer[i];
+                                            if (parser.PMTSectionLength > 1021)
+                                            {
+                                                parser.PMTSectionLength = 0;
+                                            }
+                                            else
+                                            {
+                                                parser.PMTSectionParse = 9;
+                                            }
+                                            break;
+                                    }
+                                }
+                                else if (parser.PMTSectionParse > 0)
+                                {
+                                    --parser.PMTSectionLength;
+                                    --parser.PMTSectionParse;
+
+                                    switch (parser.PMTSectionParse)
+                                    {
+                                        case 8:
+                                        case 7:
+                                            break;
+                                        case 6:
+                                            parser.PMTTemp = buffer[i];
+                                            break;
+                                        case 5:
+                                            parser.PMTSectionNumber = buffer[i];
+                                            if (parser.PMTSectionNumber == 0)
+                                            {
+                                                parser.PMTOffset = 0;
+                                            }
+                                            break;
+                                        case 4:
+                                            parser.PMTLastSectionNumber = buffer[i];
+                                            break;
+                                        case 3:
+                                            parser.PCRPID = (ushort)
+                                                ((buffer[i] & 0x1F) << 8);
+                                            break;
+                                        case 2:
+                                            parser.PCRPID |= buffer[i];
+                                            break;
+                                        case 1:
+                                            parser.PMTProgramInfoLength = (ushort)
+                                                ((buffer[i] & 0xF) << 8);
+                                            break;
+                                        case 0:
+                                            parser.PMTProgramInfoLength |= buffer[i];
+                                            if (parser.PMTProgramInfoLength == 0)
+                                            {
+                                                parser.PMTTransferState = true;
+                                            }
+                                            else
+                                            {
+                                                parser.PMTProgramDescriptorLengthParse = 2;
+                                            }
+                                            break;
+                                    }
+                                }
+                                else if (parser.PMTProgramInfoLength > 0)
+                                {
+                                    --parser.PMTSectionLength;
+                                    --parser.PMTProgramInfoLength;
+
+                                    if (parser.PMTProgramDescriptorLengthParse > 0)
+                                    {
+                                        --parser.PMTProgramDescriptorLengthParse;
+                                        switch (parser.PMTProgramDescriptorLengthParse)
+                                        {
+                                            case 1:
+                                                parser.PMTProgramDescriptor = buffer[i];
+                                                break;
+                                            case 0:
+                                                parser.PMTProgramDescriptorLength = buffer[i];
+                                                parser.PMTProgramDescriptors.Add(
+                                                    new TSDescriptor(
+                                                        parser.PMTProgramDescriptor, 
+                                                        parser.PMTProgramDescriptorLength));
+                                                break;
+                                        }
+                                    }
+                                    else if (parser.PMTProgramDescriptorLength > 0)
+                                    {
+                                        --parser.PMTProgramDescriptorLength;
+
+                                        TSDescriptor descriptor = parser.PMTProgramDescriptors[
+                                            parser.PMTProgramDescriptors.Count - 1];
+
+                                        int valueIndex =
+                                            descriptor.Value.Length - 
+                                            parser.PMTProgramDescriptorLength - 1;
+
+                                        descriptor.Value[valueIndex] = buffer[i];
+
+                                        if (parser.PMTProgramDescriptorLength == 0 &&
+                                            parser.PMTProgramInfoLength > 0)
+                                        {
+                                            parser.PMTProgramDescriptorLengthParse = 2;
+                                        }
+                                    }
+                                    if (parser.PMTProgramInfoLength == 0)
+                                    {
+                                        parser.PMTTransferState = true;
+                                    }
+                                }
+                            }
+                            if (parser.PacketLength == 0)
+                            {
+                                parser.SyncState = false;
+                            }
+                        }
+                        else if (parser.Stream != null && 
+                            parser.StreamState != null && 
+                            parser.TransportScramblingControl == 0)
+                        {
+                            TSStream stream = parser.Stream;
+                            TSStreamState streamState = parser.StreamState;
+
+                            streamState.Parse =
+                                (streamState.Parse << 8) + buffer[i];
+
+                            if (streamState.TransferState)
+                            {
+                                if ((bufferLength - i) >= streamState.PacketLength && 
+                                    streamState.PacketLength > 0)
+                                {
+                                    offset = streamState.PacketLength;
+                                }
+                                else
+                                {
+                                    offset = (bufferLength - i);
+                                }
+                                if (parser.PacketLength <= offset)
+                                {
+                                    offset = parser.PacketLength;
+                                }
+                                streamState.TransferLength = offset;
+
+                                if (!stream.IsInitialized ||
+                                    stream.IsVideoStream)
+                                {
+                                    streamState.StreamBuffer.Add(
+                                        buffer, i, offset);
+                                }
+                                else
+                                {
+                                    streamState.StreamBuffer.TransferLength += offset;
+                                }
+
+                                i += (int)(streamState.TransferLength - 1);
+                                streamState.PacketLength -= streamState.TransferLength;
+                                parser.PacketLength -= (byte)streamState.TransferLength;
+
+                                streamState.TotalBytes += (ulong)streamState.TransferLength;
+                                streamState.WindowBytes += (ulong)streamState.TransferLength;
+
+                                if (streamState.PacketLength == 0)
+                                {
+                                    streamState.TransferState = false;
+                                    streamState.TransferCount++;
+                                    bool isFinished = ScanStream(
+                                        stream,
+                                        streamState,
+                                        streamState.StreamBuffer);
+
+                                    if (!isFullScan && isFinished)
+                                    {
+                                        return;
+                                    }
+                                }
+                            }
+                            else
+                            {
+                                --parser.PacketLength;
+
+                                bool headerFound = false;
+                                if (stream.IsVideoStream && 
+                                    streamState.Parse == 0x000001FD)
+                                {
+                                    headerFound = true;
+                                }
+                                if (stream.IsVideoStream &&
+                                    streamState.Parse >= 0x000001E0 &&
+                                    streamState.Parse <= 0x000001EF)
+                                {
+                                    headerFound = true;
+                                }
+                                if (stream.IsAudioStream &&
+                                    streamState.Parse == 0x000001BD)
+                                {
+                                    headerFound = true;
+                                }
+                                if (stream.IsAudioStream &&
+                                    (streamState.Parse == 0x000001FA ||
+                                     streamState.Parse == 0x000001FD))
+                                {
+                                    headerFound = true;
+                                }
+
+                                if (!stream.IsVideoStream &&
+                                    !stream.IsAudioStream &&
+                                    (streamState.Parse == 0x000001FA ||
+                                     streamState.Parse == 0x000001FD ||
+                                     streamState.Parse == 0x000001BD ||
+                                     (streamState.Parse >= 0x000001E0 &&
+                                      streamState.Parse <= 0x000001EF)))
+                                {
+                                    headerFound = true;
+                                }
+
+                                if (headerFound)
+                                {
+                                    streamState.PacketLengthParse = 2;
+#if DEBUG
+                                    streamState.PESHeaderIndex = 0;
+                                    streamState.PESHeader[streamState.PESHeaderIndex++] =
+                                        (byte)((streamState.Parse >> 24) & 0xFF);
+                                    streamState.PESHeader[streamState.PESHeaderIndex++] =
+                                        (byte)((streamState.Parse >> 16) & 0xFF);
+                                    streamState.PESHeader[streamState.PESHeaderIndex++] =
+                                        (byte)((streamState.Parse >> 8) & 0xFF);
+                                    streamState.PESHeader[streamState.PESHeaderIndex++] =
+                                        (byte)(streamState.Parse & 0xFF);
+#endif
+                                }
+                                else if (streamState.PacketLengthParse > 0)
+                                {
+                                    --streamState.PacketLengthParse;
+                                    switch (streamState.PacketLengthParse)
+                                    {
+                                        case 1:
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] =
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+
+                                        case 0:
+                                            streamState.PacketLength =
+                                                (int)(streamState.Parse & 0xFFFF);
+                                            streamState.PacketParse = 3;
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] =
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+                                    }
+                                }
+                                else if (streamState.PacketParse > 0)
+                                {
+                                    --streamState.PacketLength;
+                                    --streamState.PacketParse;
+
+                                    switch (streamState.PacketParse)
+                                    {
+                                        case 2:
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] =
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+                                        
+                                        case 1:
+                                            streamState.PESHeaderFlags = 
+                                                (byte)(streamState.Parse & 0xFF);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] =
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+                                        
+                                        case 0:
+                                            streamState.PESHeaderLength = 
+                                                (byte)(streamState.Parse & 0xFF);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] =
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            if ((streamState.PESHeaderFlags & 0xC0) == 0x80)
+                                            {
+                                                streamState.PTSParse = 5;
+                                            }
+                                            else if ((streamState.PESHeaderFlags & 0xC0) == 0xC0)
+                                            {
+                                                streamState.DTSParse = 10;
+                                            }
+                                            if (streamState.PESHeaderLength == 0)
+                                            {
+                                                streamState.TransferState = true;
+                                            }
+                                            break;
+                                    }
+                                }
+                                else if (streamState.PTSParse > 0)
+                                {
+                                    --streamState.PacketLength;
+                                    --streamState.PESHeaderLength;
+                                    --streamState.PTSParse;
+
+                                    switch (streamState.PTSParse)
+                                    {
+                                        case 4:
+                                            streamState.PTSTemp = 
+                                                ((streamState.Parse & 0xE) << 29);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xff);
+#endif
+                                            break;
+                                        
+                                        case 3:
+                                            streamState.PTSTemp |= 
+                                                ((streamState.Parse & 0xFF) << 22);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+                                        
+                                        case 2:
+                                            streamState.PTSTemp |= 
+                                                ((streamState.Parse & 0xFE) << 14);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+                                        
+                                        case 1:
+                                            streamState.PTSTemp |= 
+                                                ((streamState.Parse & 0xFF) << 7);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+                                        
+                                        case 0:
+                                            streamState.PTSTemp |= 
+                                                ((streamState.Parse & 0xFE) >> 1);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xff);
+#endif                                        
+                                            streamState.PTS = streamState.PTSTemp;
+
+                                            if (streamState.PTS > streamState.PTSLast)
+                                            {
+                                                if (streamState.PTSLast > 0)
+                                                {
+                                                    streamState.PTSTransfer = (streamState.PTS - streamState.PTSLast);
+                                                }                                                
+                                                streamState.PTSLast = streamState.PTS;
+                                            }
+
+                                            streamState.PTSDiff = streamState.PTS - streamState.DTSPrev;
+
+                                            if (streamState.PTSCount > 0 && 
+                                                stream.IsVideoStream)
+                                            {
+                                                UpdateStreamBitrates(stream.PID, streamState.PTS, streamState.PTSDiff);
+                                                if (streamState.DTSTemp < parser.PTSFirst)
+                                                {
+                                                    parser.PTSFirst = streamState.DTSTemp;
+                                                }
+                                                if (streamState.DTSTemp > parser.PTSLast)
+                                                {
+                                                    parser.PTSLast = streamState.DTSTemp;
+                                                }
+                                                Length = (double)(parser.PTSLast - parser.PTSFirst) / 90000;
+                                            }
+                                            
+                                            streamState.DTSPrev = streamState.PTS;
+                                            streamState.PTSCount++;
+                                            if (streamState.PESHeaderLength == 0)
+                                            {
+                                                streamState.TransferState = true;
+                                            }
+                                            break;
+                                    }
+                                }
+                                else if (streamState.DTSParse > 0)
+                                {
+                                    --streamState.PacketLength;
+                                    --streamState.PESHeaderLength;
+                                    --streamState.DTSParse;
+
+                                    switch (streamState.DTSParse)
+                                    {
+                                        case 9:
+                                            streamState.PTSTemp = 
+                                                ((streamState.Parse & 0xE) << 29);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+                                        
+                                        case 8:
+                                            streamState.PTSTemp |= 
+                                                ((streamState.Parse & 0xFF) << 22);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+                                        
+                                        case 7:
+                                            streamState.PTSTemp |= 
+                                                ((streamState.Parse & 0xFE) << 14);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xff);
+#endif
+                                            break;
+                                        
+                                        case 6:
+                                            streamState.PTSTemp |= 
+                                                ((streamState.Parse & 0xFF) << 7);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+                                        
+                                        case 5:
+                                            streamState.PTSTemp |= 
+                                                ((streamState.Parse & 0xFE) >> 1);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xff);
+#endif
+                                            streamState.PTS = streamState.PTSTemp;
+                                            if (streamState.PTS > streamState.PTSLast)
+                                            {
+                                                streamState.PTSLast = streamState.PTS;
+                                            }
+                                            break;
+                                        
+                                        case 4:
+                                            streamState.DTSTemp = 
+                                                ((streamState.Parse & 0xE) << 29);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xff);
+#endif
+                                            break;
+                                        
+                                        case 3:
+                                            streamState.DTSTemp |= 
+                                                ((streamState.Parse & 0xFF) << 22);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xff);
+#endif
+                                            break;
+                                        
+                                        case 2:
+                                            streamState.DTSTemp |= 
+                                                ((streamState.Parse & 0xFE) << 14);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xff);
+#endif
+                                            break;
+                                        
+                                        case 1:
+                                            streamState.DTSTemp |= 
+                                                ((streamState.Parse & 0xFF) << 7);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xFF);
+#endif
+                                            break;
+                                        
+                                        case 0:
+                                            streamState.DTSTemp |= 
+                                                ((streamState.Parse & 0xFE) >> 1);
+#if DEBUG
+                                            streamState.PESHeader[streamState.PESHeaderIndex++] = 
+                                                (byte)(streamState.Parse & 0xff);
+#endif
+                                            streamState.PTSDiff = streamState.DTSTemp - streamState.DTSPrev;
+
+                                            if (streamState.PTSCount > 0 &&
+                                                stream.IsVideoStream)
+                                            {
+                                                UpdateStreamBitrates(stream.PID, streamState.DTSTemp, streamState.PTSDiff);
+                                                if (streamState.DTSTemp < parser.PTSFirst)
+                                                {
+                                                    parser.PTSFirst = streamState.DTSTemp;
+                                                }
+                                                if (streamState.DTSTemp > parser.PTSLast)
+                                                {
+                                                    parser.PTSLast = streamState.DTSTemp;
+                                                }
+                                                Length = (double)(parser.PTSLast - parser.PTSFirst) / 90000;
+                                            }
+                                            streamState.DTSPrev = streamState.DTSTemp;
+                                            streamState.PTSCount++;
+                                            if (streamState.PESHeaderLength == 0)
+                                            {
+                                                streamState.TransferState = true;
+                                            }
+                                            break;
+                                    }
+                                }
+                                else if (streamState.PESHeaderLength > 0)
+                                {
+                                    --streamState.PacketLength;
+                                    --streamState.PESHeaderLength;
+#if DEBUG
+                                    streamState.PESHeader[streamState.PESHeaderIndex++] =
+                                        (byte)(streamState.Parse & 0xFF);
+#endif
+                                    if (streamState.PESHeaderLength == 0)
+                                    {
+                                        streamState.TransferState = true;
+                                    }
+                                }
+                            }
+                            if (parser.PacketLength == 0)
+                            {
+                                parser.SyncState = false;
+                            }
+                        }
+                        else
+                        {
+                            parser.PacketLength--;
+                            if ((bufferLength - i) >= parser.PacketLength)
+                            {
+                                i = i + parser.PacketLength;
+                                parser.PacketLength = 0;
+                            }
+                            else
+                            {
+                                parser.PacketLength -= (byte)((bufferLength - i) + 1);
+                                i = bufferLength;
+                            }
+                            if (parser.PacketLength == 0)
+                            {
+                                parser.SyncState = false;
+                            }
+                        }
+                    }
+                    Size += bufferLength;
+                }
+
+                ulong PTSLast = 0;
+                ulong PTSDiff = 0;
+                foreach (TSStream stream in Streams.Values)
+                {
+                    if (!stream.IsVideoStream) continue;
+
+                    if (StreamStates.ContainsKey(stream.PID) &&
+                        StreamStates[stream.PID].PTSLast > PTSLast)
+                    {
+                        PTSLast = StreamStates[stream.PID].PTSLast;
+                        PTSDiff = PTSLast - StreamStates[stream.PID].DTSPrev;
+                    }
+                    UpdateStreamBitrates(stream.PID, PTSLast, PTSDiff);
+                }
+            }
+            finally
+            {
+                if (fileStream != null)
+                {
+                    fileStream.Close();
+                }
+            }
+        }
+
+        private TSStream CreateStream(
+            ushort streamPID, 
+            byte streamType, 
+            List<TSDescriptor> streamDescriptors)
+        {
+            TSStream stream = null;
+
+            switch ((TSStreamType)streamType)
+            {
+                case TSStreamType.MVC_VIDEO:
+                case TSStreamType.AVC_VIDEO:
+                case TSStreamType.MPEG1_VIDEO:
+                case TSStreamType.MPEG2_VIDEO:
+                case TSStreamType.VC1_VIDEO:
+                {
+                    stream = new TSVideoStream();
+                }
+                break;
+
+                case TSStreamType.AC3_AUDIO:
+                case TSStreamType.AC3_PLUS_AUDIO:
+                case TSStreamType.AC3_PLUS_SECONDARY_AUDIO:
+                case TSStreamType.AC3_TRUE_HD_AUDIO:
+                case TSStreamType.DTS_AUDIO:
+                case TSStreamType.DTS_HD_AUDIO:
+                case TSStreamType.DTS_HD_MASTER_AUDIO:
+                case TSStreamType.DTS_HD_SECONDARY_AUDIO:
+                case TSStreamType.LPCM_AUDIO:
+                case TSStreamType.MPEG1_AUDIO:
+                case TSStreamType.MPEG2_AUDIO:
+                {
+                    stream = new TSAudioStream();
+                }
+                break;
+
+                case TSStreamType.INTERACTIVE_GRAPHICS:
+                case TSStreamType.PRESENTATION_GRAPHICS:
+                {
+                    stream = new TSGraphicsStream();
+                }
+                break;
+
+                case TSStreamType.SUBTITLE:
+                {
+                    stream = new TSTextStream();
+                }
+                break;
+
+                default:
+                    break;
+            }
+
+            if (stream != null &&
+                !Streams.ContainsKey(streamPID))
+            {
+                stream.PID = streamPID;
+                stream.StreamType = (TSStreamType)streamType;
+                stream.Descriptors = streamDescriptors;
+                Streams[stream.PID] = stream;
+            }
+            if (!StreamDiagnostics.ContainsKey(streamPID))
+            {
+                StreamDiagnostics[streamPID] =
+                    new List<TSStreamDiagnostics>();
+            }
+
+            return stream;
+        } 
+    }
+}

+ 3 - 0
Bootstrapper/readme.txt

@@ -0,0 +1,3 @@
+If Publishing, the folders in here must be copied to:
+
+C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0A\Bootstrapper\Packages

+ 20 - 0
Bootstrapper/vcredist10_x86/en/package.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8" ?>
+
+<Package
+  xmlns="http://schemas.microsoft.com/developer/2004/01/bootstrapper"
+  Name="DisplayName"
+  Culture="Culture"
+>
+
+    <!-- Defines a localizable string table for error messages-->
+    <Strings>
+        <String Name="DisplayName">Visual C++ 2010 Runtime Libraries (x86)</String>
+        <String Name="Culture">en</String>
+        <String Name="AdminRequired">You do not have the permissions required to install Visual C++ 2010 Runtime Libraries (x86). Please contact your administrator.</String>
+        <String Name="InvalidPlatformWin9x">Installation of Visual C++ 2010 Runtime Libraries (x86) is not supported on Windows 95. Contact your application vendor.</String>
+        <String Name="InvalidPlatformWinNT">Installation of Visual C++ 2010 Runtime Libraries (x86) is not supported on Windows NT 4.0. Contact your application vendor.</String>
+        <String Name="GeneralFailure">A failure occurred attempting to install Visual C++ 2010 Runtime Libraries (x86).</String>
+        <String Name="VCRedistExe">http://go.microsoft.com/fwlink/?LinkID=210621</String>
+    </Strings>
+
+</Package>

+ 45 - 0
Bootstrapper/vcredist10_x86/product.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8" ?> 
+
+<Product
+  xmlns="http://schemas.microsoft.com/developer/2004/01/bootstrapper"
+  ProductCode="Microsoft.Visual.C++.10.0.x86"
+>
+
+  <!-- Defines list of files to be copied on build -->
+  <PackageFiles>
+    <PackageFile Name="vcredist_x86.exe" HomeSite="VCRedistExe"/>
+  </PackageFiles>
+<InstallChecks>
+    <MsiProductCheck Property="VCRedistInstalled" Product="{F0C3E5D1-1ADE-321E-8167-68EF0DE699A5}"/>
+</InstallChecks>
+  
+  <!-- Defines how to invoke the setup for the Visual C++ 10.0 redist -->
+  <!-- TODO: Needs EstrimatedTempSpace, LogFile, and an update of EstimatedDiskSpace -->
+  <Commands Reboot="Defer">
+    <Command PackageFile="vcredist_x86.exe" 
+	     Arguments=' /q:a ' 
+	     >
+
+      <!-- These checks determine whether the package is to be installed -->
+      <InstallConditions>
+        <BypassIf Property="VCRedistInstalled" Compare="ValueGreaterThanOrEqualTo" Value="3"/>
+        <!-- Block install if user does not have admin privileges -->
+        <FailIf Property="AdminUser" Compare="ValueEqualTo" Value="false" String="AdminRequired"/>
+
+        <!-- Block install on Win95 -->
+        <FailIf Property="Version9X" Compare="VersionLessThan" Value="4.10" String="InvalidPlatformWin9x"/>
+
+        <!-- Block install on NT 4 or less -->
+        <FailIf Property="VersionNT" Compare="VersionLessThan" Value="5.00" String="InvalidPlatformWinNT"/>
+
+      </InstallConditions>
+      
+      <ExitCodes>
+        <ExitCode Value="0" Result="Success"/>
+        <ExitCode Value="3010" Result="SuccessReboot"/>
+        <DefaultExitCode Result="Fail" FormatMessageFromSystem="true" String="GeneralFailure" />
+      </ExitCodes>
+      
+    </Command>
+  </Commands>
+</Product>

+ 78 - 438
MediaBrowser.Api/ApiService.cs

@@ -1,438 +1,78 @@
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Model.DTO;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api
-{
-    /// <summary>
-    /// Contains some helpers for the api
-    /// </summary>
-    public static class ApiService
-    {
-        /// <summary>
-        /// Gets an Item by Id, or the root item if none is supplied
-        /// </summary>
-        public static BaseItem GetItemById(string id)
-        {
-            Guid guid = string.IsNullOrEmpty(id) ? Guid.Empty : new Guid(id);
-
-            return Kernel.Instance.GetItemById(guid);
-        }
-
-        /// <summary>
-        /// Gets a User by Id
-        /// </summary>
-        /// <param name="logActivity">Whether or not to update the user's LastActivityDate</param>
-        public static User GetUserById(string id, bool logActivity)
-        {
-            var guid = new Guid(id);
-
-            var user = Kernel.Instance.Users.FirstOrDefault(u => u.Id == guid);
-
-            if (logActivity)
-            {
-                LogUserActivity(user);
-            }
-
-            return user;
-        }
-
-        /// <summary>
-        /// Gets the default User
-        /// </summary>
-        /// <param name="logActivity">Whether or not to update the user's LastActivityDate</param>
-        public static User GetDefaultUser(bool logActivity)
-        {
-            User user = Kernel.Instance.GetDefaultUser();
-
-            if (logActivity)
-            {
-                LogUserActivity(user);
-            }
-
-            return user;
-        }
-
-        /// <summary>
-        /// Updates LastActivityDate for a given User
-        /// </summary>
-        public static void LogUserActivity(User user)
-        {
-            user.LastActivityDate = DateTime.UtcNow;
-            Kernel.Instance.SaveUser(user);
-        }
-
-        /// <summary>
-        /// Converts a BaseItem to a DTOBaseItem
-        /// </summary>
-        public async static Task<DtoBaseItem> GetDtoBaseItem(BaseItem item, User user,
-            bool includeChildren = true,
-            bool includePeople = true)
-        {
-            var dto = new DtoBaseItem();
-
-            var tasks = new List<Task>();
-
-            tasks.Add(AttachStudios(dto, item));
-
-            if (includeChildren)
-            {
-                tasks.Add(AttachChildren(dto, item, user));
-                tasks.Add(AttachLocalTrailers(dto, item, user));
-            }
-
-            if (includePeople)
-            {
-                tasks.Add(AttachPeople(dto, item));
-            }
-
-            AttachBasicFields(dto, item, user);
-
-            // Make sure all the tasks we kicked off have completed.
-            if (tasks.Count > 0)
-            {
-                await Task.WhenAll(tasks).ConfigureAwait(false);
-            }
-
-            return dto;
-        }
-
-        /// <summary>
-        /// Sets simple property values on a DTOBaseItem
-        /// </summary>
-        private static void AttachBasicFields(DtoBaseItem dto, BaseItem item, User user)
-        {
-            dto.AspectRatio = item.AspectRatio;
-            dto.BackdropCount = item.BackdropImagePaths == null ? 0 : item.BackdropImagePaths.Count();
-            dto.DateCreated = item.DateCreated;
-            dto.DisplayMediaType = item.DisplayMediaType;
-
-            if (item.Genres != null)
-            {
-                dto.Genres = item.Genres.ToArray();
-            }
-
-            dto.HasArt = !string.IsNullOrEmpty(item.ArtImagePath);
-            dto.HasBanner = !string.IsNullOrEmpty(item.BannerImagePath);
-            dto.HasLogo = !string.IsNullOrEmpty(item.LogoImagePath);
-            dto.HasPrimaryImage = !string.IsNullOrEmpty(item.PrimaryImagePath);
-            dto.HasThumb = !string.IsNullOrEmpty(item.ThumbnailImagePath);
-            dto.Id = item.Id;
-            dto.IsNew = item.IsRecentlyAdded(user);
-            dto.IndexNumber = item.IndexNumber;
-            dto.IsFolder = item.IsFolder;
-            dto.Language = item.Language;
-            dto.LocalTrailerCount = item.LocalTrailers == null ? 0 : item.LocalTrailers.Count();
-            dto.Name = item.Name;
-            dto.OfficialRating = item.OfficialRating;
-            dto.Overview = item.Overview;
-
-            // If there are no backdrops, indicate what parent has them in case the Ui wants to allow inheritance
-            if (dto.BackdropCount == 0)
-            {
-                int backdropCount;
-                dto.ParentBackdropItemId = GetParentBackdropItemId(item, out backdropCount);
-                dto.ParentBackdropCount = backdropCount;
-            }
-
-            if (item.Parent != null)
-            {
-                dto.ParentId = item.Parent.Id;
-            }
-
-            dto.ParentIndexNumber = item.ParentIndexNumber;
-
-            // If there is no logo, indicate what parent has one in case the Ui wants to allow inheritance
-            if (!dto.HasLogo)
-            {
-                dto.ParentLogoItemId = GetParentLogoItemId(item);
-            }
-
-            dto.Path = item.Path;
-
-            dto.PremiereDate = item.PremiereDate;
-            dto.ProductionYear = item.ProductionYear;
-            dto.ProviderIds = item.ProviderIds;
-            dto.RunTimeTicks = item.RunTimeTicks;
-            dto.SortName = item.SortName;
-
-            if (item.Taglines != null)
-            {
-                dto.Taglines = item.Taglines.ToArray();
-            }
-
-            dto.TrailerUrl = item.TrailerUrl;
-            dto.Type = item.GetType().Name;
-            dto.CommunityRating = item.CommunityRating;
-
-            dto.UserData = GetDtoUserItemData(item.GetUserData(user, false));
-
-            var folder = item as Folder;
-
-            if (folder != null)
-            {
-                dto.SpecialCounts = folder.GetSpecialCounts(user);
-
-                dto.IsRoot = folder.IsRoot;
-                dto.IsVirtualFolder = folder.IsVirtualFolder;
-            }
-
-            // Add AudioInfo
-            var audio = item as Audio;
-
-            if (audio != null)
-            {
-                dto.AudioInfo = new AudioInfo
-                {
-                    Album = audio.Album,
-                    AlbumArtist = audio.AlbumArtist,
-                    Artist = audio.Artist,
-                    BitRate = audio.BitRate,
-                    Channels = audio.Channels
-                };
-            }
-
-            // Add VideoInfo
-            var video = item as Video;
-
-            if (video != null)
-            {
-                dto.VideoInfo = new VideoInfo
-                {
-                    Height = video.Height,
-                    Width = video.Width,
-                    Codec = video.Codec,
-                    VideoType = video.VideoType,
-                    ScanType = video.ScanType
-                };
-
-                if (video.AudioStreams != null)
-                {
-                    dto.VideoInfo.AudioStreams = video.AudioStreams.ToArray();
-                }
-
-                if (video.Subtitles != null)
-                {
-                    dto.VideoInfo.Subtitles = video.Subtitles.ToArray();
-                }
-            }
-
-            // Add SeriesInfo
-            var series = item as Series;
-
-            if (series != null)
-            {
-                DayOfWeek[] airDays = series.AirDays == null ? new DayOfWeek[] { } : series.AirDays.ToArray(); 
-
-                dto.SeriesInfo = new SeriesInfo
-                {
-                    AirDays = airDays,
-                    AirTime = series.AirTime,
-                    Status = series.Status
-                };
-            }
-
-            // Add MovieInfo
-            var movie = item as Movie;
-
-            if (movie != null)
-            {
-                int specialFeatureCount = movie.SpecialFeatures == null ? 0 : movie.SpecialFeatures.Count();
-
-                dto.MovieInfo = new MovieInfo
-                {
-                    SpecialFeatureCount = specialFeatureCount
-                };
-            }
-        }
-
-        /// <summary>
-        /// Attaches Studio DTO's to a DTOBaseItem
-        /// </summary>
-        private static async Task AttachStudios(DtoBaseItem dto, BaseItem item)
-        {
-            // Attach Studios by transforming them into BaseItemStudio (DTO)
-            if (item.Studios != null)
-            {
-                Studio[] entities = await Task.WhenAll(item.Studios.Select(c => Kernel.Instance.ItemController.GetStudio(c))).ConfigureAwait(false);
-
-                dto.Studios = new BaseItemStudio[entities.Length];
-
-                for (int i = 0; i < entities.Length; i++)
-                {
-                    Studio entity = entities[i];
-                    var baseItemStudio = new BaseItemStudio{};
-
-                    baseItemStudio.Name = entity.Name;
-
-                    baseItemStudio.HasImage = !string.IsNullOrEmpty(entity.PrimaryImagePath);
-
-                    dto.Studios[i] = baseItemStudio;
-                }
-            }
-        }
-
-        /// <summary>
-        /// Attaches child DTO's to a DTOBaseItem
-        /// </summary>
-        private static async Task AttachChildren(DtoBaseItem dto, BaseItem item, User user)
-        {
-            var folder = item as Folder;
-
-            if (folder != null)
-            {
-                IEnumerable<BaseItem> children = folder.GetChildren(user);
-
-                dto.Children = await Task.WhenAll(children.Select(c => GetDtoBaseItem(c, user, false, false))).ConfigureAwait(false);
-            }
-        }
-
-        /// <summary>
-        /// Attaches trailer DTO's to a DTOBaseItem
-        /// </summary>
-        private static async Task AttachLocalTrailers(DtoBaseItem dto, BaseItem item, User user)
-        {
-            if (item.LocalTrailers != null && item.LocalTrailers.Any())
-            {
-                dto.LocalTrailers = await Task.WhenAll(item.LocalTrailers.Select(c => GetDtoBaseItem(c, user, false, false))).ConfigureAwait(false);
-            }
-        }
-
-        /// <summary>
-        /// Attaches People DTO's to a DTOBaseItem
-        /// </summary>
-        private static async Task AttachPeople(DtoBaseItem dto, BaseItem item)
-        {
-            // Attach People by transforming them into BaseItemPerson (DTO)
-            if (item.People != null)
-            {
-                IEnumerable<Person> entities = await Task.WhenAll(item.People.Select(c => Kernel.Instance.ItemController.GetPerson(c.Key))).ConfigureAwait(false);
-
-                dto.People = item.People.Select(p =>
-                {
-                    var baseItemPerson = new BaseItemPerson{};
-
-                    baseItemPerson.Name = p.Key;
-                    baseItemPerson.Overview = p.Value.Overview;
-                    baseItemPerson.Type = p.Value.Type;
-
-                    Person ibnObject = entities.First(i => i.Name.Equals(p.Key, StringComparison.OrdinalIgnoreCase));
-
-                    if (ibnObject != null)
-                    {
-                        baseItemPerson.HasImage = !string.IsNullOrEmpty(ibnObject.PrimaryImagePath);
-                    }
-
-                    return baseItemPerson;
-                }).ToArray();
-            }
-        }
-
-        /// <summary>
-        /// If an item does not any backdrops, this can be used to find the first parent that does have one
-        /// </summary>
-        private static Guid? GetParentBackdropItemId(BaseItem item, out int backdropCount)
-        {
-            backdropCount = 0;
-
-            var parent = item.Parent;
-
-            while (parent != null)
-            {
-                if (parent.BackdropImagePaths != null && parent.BackdropImagePaths.Any())
-                {
-                    backdropCount = parent.BackdropImagePaths.Count();
-                    return parent.Id;
-                }
-
-                parent = parent.Parent;
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// If an item does not have a logo, this can be used to find the first parent that does have one
-        /// </summary>
-        private static Guid? GetParentLogoItemId(BaseItem item)
-        {
-            var parent = item.Parent;
-
-            while (parent != null)
-            {
-                if (!string.IsNullOrEmpty(parent.LogoImagePath))
-                {
-                    return parent.Id;
-                }
-
-                parent = parent.Parent;
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Gets an ImagesByName entity along with the number of items containing it
-        /// </summary>
-        public static IbnItem GetIbnItem(BaseEntity entity, int itemCount)
-        {
-            return new IbnItem
-            {
-                Id = entity.Id,
-                BaseItemCount = itemCount,
-                HasImage = !string.IsNullOrEmpty(entity.PrimaryImagePath),
-                Name = entity.Name
-            };
-        }
-
-        /// <summary>
-        /// Converts a User to a DTOUser
-        /// </summary>
-        public static DtoUser GetDtoUser(User user)
-        {
-            return new DtoUser
-            {
-                Id = user.Id,
-                Name = user.Name,
-                HasImage = !string.IsNullOrEmpty(user.PrimaryImagePath),
-                HasPassword = !string.IsNullOrEmpty(user.Password),
-                LastActivityDate = user.LastActivityDate,
-                LastLoginDate = user.LastLoginDate
-            };
-        }
-
-        /// <summary>
-        /// Converts a UserItemData to a DTOUserItemData
-        /// </summary>
-        public static DtoUserItemData GetDtoUserItemData(UserItemData data)
-        {
-            if (data == null)
-            {
-                return null;
-            }
-
-            return new DtoUserItemData
-            {
-                IsFavorite = data.IsFavorite,
-                Likes = data.Likes,
-                PlaybackPositionTicks = data.PlaybackPositionTicks,
-                PlayCount = data.PlayCount,
-                Rating = data.Rating
-            };
-        }
-
-        public static bool IsApiUrlMatch(string url, HttpListenerRequest request)
-        {
-            url = "/api/" + url;
-
-            return request.Url.LocalPath.EndsWith(url, StringComparison.OrdinalIgnoreCase);
-        }
-    }
-}
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Connectivity;
+using ServiceStack.Common.Web;
+using System;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Contains some helpers for the api
+    /// </summary>
+    public static class ApiService
+    {
+        /// <summary>
+        /// Gets a User by Id
+        /// </summary>
+        /// <param name="id">The id of the user</param>
+        /// <returns>User.</returns>
+        /// <exception cref="System.ArgumentNullException">id</exception>
+        public static User GetUserById(string id)
+        {
+            if (string.IsNullOrEmpty(id))
+            {
+                throw new ArgumentNullException("id");
+            }
+
+            var guid = new Guid(id);
+
+            return Kernel.Instance.GetUserById(guid);
+        }
+
+        /// <summary>
+        /// Determines whether [is API URL match] [the specified URL].
+        /// </summary>
+        /// <param name="url">The URL.</param>
+        /// <param name="request">The request.</param>
+        /// <returns><c>true</c> if [is API URL match] [the specified URL]; otherwise, <c>false</c>.</returns>
+        public static bool IsApiUrlMatch(string url, HttpListenerRequest request)
+        {
+            url = "/api/" + url;
+
+            return request.Url.LocalPath.EndsWith(url, StringComparison.OrdinalIgnoreCase);
+        }
+
+        ///// <summary>
+        ///// Gets the current user.
+        ///// </summary>
+        ///// <param name="request">The request.</param>
+        ///// <returns>Task{User}.</returns>
+        //public static async Task<User> GetCurrentUser(AuthenticatedRequest request)
+        //{
+        //    var user = GetUserById(request.UserId);
+
+        //    if (user == null)
+        //    {
+        //        throw HttpError.Unauthorized("Invalid user or password entered.");
+        //    }
+
+        //    var clientType = ClientType.Other;
+
+        //    if (!string.IsNullOrEmpty(request.Client))
+        //    {
+        //        ClientType type;
+
+        //        if (Enum.TryParse(request.Client, true, out type))
+        //        {
+        //            clientType = type;
+        //        }
+        //    }
+
+        //    await Kernel.Instance.UserManager.LogUserActivity(user, clientType, request.Device).ConfigureAwait(false);
+
+        //    return user;
+        //}
+    }
+}

+ 0 - 81
MediaBrowser.Api/Drawing/DrawingUtils.cs

@@ -1,81 +0,0 @@
-using System;
-using System.Drawing;
-
-namespace MediaBrowser.Api.Drawing
-{
-    public static class DrawingUtils
-    {
-        /// <summary>
-        /// Resizes a set of dimensions
-        /// </summary>
-        public static Size Resize(int currentWidth, int currentHeight, int? width, int? height, int? maxWidth, int? maxHeight)
-        {
-            return Resize(new Size(currentWidth, currentHeight), width, height, maxWidth, maxHeight);
-        }
-
-        /// <summary>
-        /// Resizes a set of dimensions
-        /// </summary>
-        /// <param name="size">The original size object</param>
-        /// <param name="width">A new fixed width, if desired</param>
-        /// <param name="height">A new fixed neight, if desired</param>
-        /// <param name="maxWidth">A max fixed width, if desired</param>
-        /// <param name="maxHeight">A max fixed height, if desired</param>
-        /// <returns>A new size object</returns>
-        public static Size Resize(Size size, int? width, int? height, int? maxWidth, int? maxHeight)
-        {
-            decimal newWidth = size.Width;
-            decimal newHeight = size.Height;
-
-            if (width.HasValue && height.HasValue)
-            {
-                newWidth = width.Value;
-                newHeight = height.Value;
-            }
-
-            else if (height.HasValue)
-            {
-                newWidth = GetNewWidth(newHeight, newWidth, height.Value);
-                newHeight = height.Value;
-            }
-
-            else if (width.HasValue)
-            {
-                newHeight = GetNewHeight(newHeight, newWidth, width.Value);
-                newWidth = width.Value;
-            }
-
-            if (maxHeight.HasValue && maxHeight < newHeight)
-            {
-                newWidth = GetNewWidth(newHeight, newWidth, maxHeight.Value);
-                newHeight = maxHeight.Value;
-            }
-
-            if (maxWidth.HasValue && maxWidth < newWidth)
-            {
-                newHeight = GetNewHeight(newHeight, newWidth, maxWidth.Value);
-                newWidth = maxWidth.Value;
-            }
-
-            return new Size(Convert.ToInt32(newWidth), Convert.ToInt32(newHeight));
-        }
-
-        private static decimal GetNewWidth(decimal currentHeight, decimal currentWidth, int newHeight)
-        {
-            decimal scaleFactor = newHeight;
-            scaleFactor /= currentHeight;
-            scaleFactor *= currentWidth;
-
-            return scaleFactor;
-        }
-
-        private static decimal GetNewHeight(decimal currentHeight, decimal currentWidth, int newWidth)
-        {
-            decimal scaleFactor = newWidth;
-            scaleFactor /= currentWidth;
-            scaleFactor *= currentHeight;
-
-            return scaleFactor;
-        }
-    }
-}

+ 0 - 148
MediaBrowser.Api/Drawing/ImageProcessor.cs

@@ -1,148 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Drawing;
-using System.Drawing.Drawing2D;
-using System.Drawing.Imaging;
-using System.IO;
-using System.Linq;
-
-namespace MediaBrowser.Api.Drawing
-{
-    public static class ImageProcessor
-    {
-        /// <summary>
-        /// Processes an image by resizing to target dimensions
-        /// </summary>
-        /// <param name="entity">The entity that owns the image</param>
-        /// <param name="imageType">The image type</param>
-        /// <param name="imageIndex">The image index (currently only used with backdrops)</param>
-        /// <param name="toStream">The stream to save the new image to</param>
-        /// <param name="width">Use if a fixed width is required. Aspect ratio will be preserved.</param>
-        /// <param name="height">Use if a fixed height is required. Aspect ratio will be preserved.</param>
-        /// <param name="maxWidth">Use if a max width is required. Aspect ratio will be preserved.</param>
-        /// <param name="maxHeight">Use if a max height is required. Aspect ratio will be preserved.</param>
-        /// <param name="quality">Quality level, from 0-100. Currently only applies to JPG. The default value should suffice.</param>
-        public static void ProcessImage(BaseEntity entity, ImageType imageType, int imageIndex, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality)
-        {
-            Image originalImage = Image.FromFile(GetImagePath(entity, imageType, imageIndex));
-
-            // Determine the output size based on incoming parameters
-            Size newSize = DrawingUtils.Resize(originalImage.Size, width, height, maxWidth, maxHeight);
-
-            Bitmap thumbnail;
-
-            // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
-            if (originalImage.PixelFormat.HasFlag(PixelFormat.Indexed))
-            {
-                thumbnail = new Bitmap(originalImage, newSize.Width, newSize.Height);
-            }
-            else
-            {
-                thumbnail = new Bitmap(newSize.Width, newSize.Height, originalImage.PixelFormat);
-            }
-
-            thumbnail.MakeTransparent();
-
-            // Preserve the original resolution
-            thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
-
-            Graphics thumbnailGraph = Graphics.FromImage(thumbnail);
-
-            thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
-            thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
-            thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
-            thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
-            thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
-
-            thumbnailGraph.DrawImage(originalImage, 0, 0, newSize.Width, newSize.Height);
-
-            ImageFormat outputFormat = originalImage.RawFormat;
-
-            // Write to the output stream
-            SaveImage(outputFormat, thumbnail, toStream, quality);
-
-            thumbnailGraph.Dispose();
-            thumbnail.Dispose();
-            originalImage.Dispose();
-        }
-
-        public static string GetImagePath(BaseEntity entity, ImageType imageType, int imageIndex)
-        {
-            var item = entity as BaseItem;
-
-            if (item != null)
-            {
-                if (imageType == ImageType.Logo)
-                {
-                    return item.LogoImagePath;
-                }
-                if (imageType == ImageType.Backdrop)
-                {
-                    return item.BackdropImagePaths.ElementAt(imageIndex);
-                }
-                if (imageType == ImageType.Banner)
-                {
-                    return item.BannerImagePath;
-                }
-                if (imageType == ImageType.Art)
-                {
-                    return item.ArtImagePath;
-                }
-                if (imageType == ImageType.Thumbnail)
-                {
-                    return item.ThumbnailImagePath;
-                }
-            }
-
-            return entity.PrimaryImagePath;
-        }
-
-        public static void SaveImage(ImageFormat outputFormat, Image newImage, Stream toStream, int? quality)
-        {
-            // Use special save methods for jpeg and png that will result in a much higher quality image
-            // All other formats use the generic Image.Save
-            if (ImageFormat.Jpeg.Equals(outputFormat))
-            {
-                SaveJpeg(newImage, toStream, quality);
-            }
-            else if (ImageFormat.Png.Equals(outputFormat))
-            {
-                newImage.Save(toStream, ImageFormat.Png);
-            }
-            else
-            {
-                newImage.Save(toStream, outputFormat);
-            }
-        }
-
-        public static void SaveJpeg(Image image, Stream target, int? quality)
-        {
-            if (!quality.HasValue)
-            {
-                quality = 90;
-            }
-
-            using (var encoderParameters = new EncoderParameters(1))
-            {
-                encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality.Value);
-                image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
-            }
-        }
-
-        public static ImageCodecInfo GetImageCodecInfo(string mimeType)
-        {
-            ImageCodecInfo[] info = ImageCodecInfo.GetImageEncoders();
-
-            for (int i = 0; i < info.Length; i++)
-            {
-                ImageCodecInfo ici = info[i];
-                if (ici.MimeType.Equals(mimeType, StringComparison.OrdinalIgnoreCase))
-                {
-                    return ici;
-                }
-            }
-            return info[1];
-        }
-    }
-}

+ 200 - 0
MediaBrowser.Api/EnvironmentService.cs

@@ -0,0 +1,200 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.IO;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class GetDirectoryContents
+    /// </summary>
+    [Route("/Environment/DirectoryContents", "GET")]
+    public class GetDirectoryContents : IReturn<List<FileSystemEntryInfo>>
+    {
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        public string Path { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether [include files].
+        /// </summary>
+        /// <value><c>true</c> if [include files]; otherwise, <c>false</c>.</value>
+        public bool IncludeFiles { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether [include directories].
+        /// </summary>
+        /// <value><c>true</c> if [include directories]; otherwise, <c>false</c>.</value>
+        public bool IncludeDirectories { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether [include hidden].
+        /// </summary>
+        /// <value><c>true</c> if [include hidden]; otherwise, <c>false</c>.</value>
+        public bool IncludeHidden { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetDrives
+    /// </summary>
+    [Route("/Environment/Drives", "GET")]
+    public class GetDrives : IReturn<List<FileSystemEntryInfo>>
+    {
+    }
+
+    /// <summary>
+    /// Class GetNetworkComputers
+    /// </summary>
+    [Route("/Environment/NetworkComputers", "GET")]
+    public class GetNetworkComputers : IReturn<List<FileSystemEntryInfo>>
+    {
+    }
+
+    /// <summary>
+    /// Class EnvironmentService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class EnvironmentService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        /// <exception cref="System.ArgumentNullException">Path</exception>
+        /// <exception cref="System.ArgumentException"></exception>
+        public object Get(GetDirectoryContents request)
+        {
+            var path = request.Path;
+
+            if (string.IsNullOrEmpty(path))
+            {
+                throw new ArgumentNullException("Path");
+            }
+
+            // Reject invalid input
+            if (!Path.IsPathRooted(path))
+            {
+                throw new ArgumentException(string.Format("Invalid path: {0}", path));
+            }
+
+            if (path.StartsWith(NetworkPrefix, StringComparison.OrdinalIgnoreCase) && path.LastIndexOf('\\') == 1)
+            {
+                return GetNetworkShares(path).ToList();
+            }
+
+            return GetFileSystemEntries(request).ToList();
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetDrives request)
+        {
+            return GetDrives().ToList();
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetNetworkComputers request)
+        {
+            return GetNetworkComputers().ToList();
+        }
+
+        /// <summary>
+        /// Gets the list that is returned when an empty path is supplied
+        /// </summary>
+        /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
+        private IEnumerable<FileSystemEntryInfo> GetDrives()
+        {
+            // Only include drives in the ready state or this method could end up being very slow, waiting for drives to timeout
+            return DriveInfo.GetDrives().Where(d => d.IsReady).Select(d => new FileSystemEntryInfo
+            {
+                Name = GetName(d),
+                Path = d.RootDirectory.FullName,
+                Type = FileSystemEntryType.Directory
+
+            });
+        }
+
+        /// <summary>
+        /// Gets the network computers.
+        /// </summary>
+        /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
+        private IEnumerable<FileSystemEntryInfo> GetNetworkComputers()
+        {
+            return NetUtils.GetNetworkComputers().Select(c => new FileSystemEntryInfo
+            {
+                Name = c,
+                Path = NetworkPrefix + c,
+                Type = FileSystemEntryType.NetworkComputer
+            });
+        }
+
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <param name="drive">The drive.</param>
+        /// <returns>System.String.</returns>
+        private string GetName(DriveInfo drive)
+        {
+            return drive.Name;
+        }
+
+        /// <summary>
+        /// Gets the network shares.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
+        private IEnumerable<FileSystemEntryInfo> GetNetworkShares(string path)
+        {
+            return new ShareCollection(path).OfType<Share>().Where(s => s.ShareType == ShareType.Disk).Select(c => new FileSystemEntryInfo
+            {
+                Name = c.NetName,
+                Path = Path.Combine(path, c.NetName),
+                Type = FileSystemEntryType.NetworkShare
+            });
+        }
+
+        /// <summary>
+        /// Gets the file system entries.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>IEnumerable{FileSystemEntryInfo}.</returns>
+        private IEnumerable<FileSystemEntryInfo> GetFileSystemEntries(GetDirectoryContents request)
+        {
+            var fileSystemEntries = FileSystem.GetFileSystemEntries(request.Path, "*", request.IncludeFiles, request.IncludeDirectories).Where(f => !f.IsSystemFile);
+
+            if (!request.IncludeHidden)
+            {
+                fileSystemEntries = fileSystemEntries.Where(f => !f.IsHidden);
+            }
+
+            return fileSystemEntries.Select(f => new FileSystemEntryInfo
+            {
+                Name = f.cFileName,
+                Path = f.Path,
+                Type = f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File
+            });
+        }
+
+        /// <summary>
+        /// Gets the network prefix.
+        /// </summary>
+        /// <value>The network prefix.</value>
+        private string NetworkPrefix
+        {
+            get { return Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture) + Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture); }
+        }
+    }
+}

+ 0 - 119
MediaBrowser.Api/HttpHandlers/AudioHandler.cs

@@ -1,119 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Net;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Supported output formats are: mp3,flac,ogg,wav,asf,wma,aac
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class AudioHandler : BaseMediaHandler<Audio, AudioOutputFormats>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("audio", request);
-        }
-
-        /// <summary>
-        /// We can output these formats directly, but we cannot encode to them.
-        /// </summary>
-        protected override IEnumerable<AudioOutputFormats> UnsupportedOutputEncodingFormats
-        {
-            get
-            {
-                return new AudioOutputFormats[] { AudioOutputFormats.Aac, AudioOutputFormats.Flac, AudioOutputFormats.Wma };
-            }
-        }
-
-        private int? GetMaxAcceptedBitRate(AudioOutputFormats audioFormat)
-        {
-            return GetMaxAcceptedBitRate(audioFormat.ToString());
-        }
-
-        private int? GetMaxAcceptedBitRate(string audioFormat)
-        {
-            if (audioFormat.Equals("mp3", System.StringComparison.OrdinalIgnoreCase))
-            {
-                return 320000;
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Determines whether or not the original file requires transcoding
-        /// </summary>
-        protected override bool RequiresConversion()
-        {
-            if (base.RequiresConversion())
-            {
-                return true;
-            }
-
-            string currentFormat = Path.GetExtension(LibraryItem.Path).Replace(".", string.Empty);
-
-            int? bitrate = GetMaxAcceptedBitRate(currentFormat);
-
-            // If the bitrate is greater than our desired bitrate, we need to transcode
-            if (bitrate.HasValue && bitrate.Value < LibraryItem.BitRate)
-            {
-                return true;
-            }
-
-            // If the number of channels is greater than our desired channels, we need to transcode
-            if (AudioChannels.HasValue && AudioChannels.Value < LibraryItem.Channels)
-            {
-                return true;
-            }
-
-            // If the sample rate is greater than our desired sample rate, we need to transcode
-            if (AudioSampleRate.HasValue && AudioSampleRate.Value < LibraryItem.SampleRate)
-            {
-                return true;
-            }
-
-            // Yay
-            return false;
-        }
-
-        /// <summary>
-        /// Creates arguments to pass to ffmpeg
-        /// </summary>
-        protected override string GetCommandLineArguments()
-        {
-            var audioTranscodeParams = new List<string>();
-
-            AudioOutputFormats outputFormat = GetConversionOutputFormat();
-
-            int? bitrate = GetMaxAcceptedBitRate(outputFormat);
-
-            if (bitrate.HasValue)
-            {
-                audioTranscodeParams.Add("-ab " + bitrate.Value);
-            }
-
-            int? channels = GetNumAudioChannelsParam(LibraryItem.Channels);
-
-            if (channels.HasValue)
-            {
-                audioTranscodeParams.Add("-ac " + channels.Value);
-            }
-
-            int? sampleRate = GetSampleRateParam(LibraryItem.SampleRate);
-
-            if (sampleRate.HasValue)
-            {
-                audioTranscodeParams.Add("-ar " + sampleRate.Value);
-            }
-
-            audioTranscodeParams.Add("-f " + outputFormat);
-
-            return "-i \"" + LibraryItem.Path + "\" -vn " + string.Join(" ", audioTranscodeParams.ToArray()) + " -";
-        }
-    }
-}

+ 0 - 255
MediaBrowser.Api/HttpHandlers/BaseMediaHandler.cs

@@ -1,255 +0,0 @@
-using MediaBrowser.Common.Logging;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    public abstract class BaseMediaHandler<TBaseItemType, TOutputType> : BaseHandler
-        where TBaseItemType : BaseItem, new()
-    {
-        /// <summary>
-        /// Supported values: mp3,flac,ogg,wav,asf,wma,aac
-        /// </summary>
-        protected virtual IEnumerable<TOutputType> OutputFormats
-        {
-            get
-            {
-                return QueryString["outputformats"].Split(',').Select(o => (TOutputType)Enum.Parse(typeof(TOutputType), o, true));
-            }
-        }
-
-        /// <summary>
-        /// These formats can be outputted directly but cannot be encoded to
-        /// </summary>
-        protected virtual IEnumerable<TOutputType> UnsupportedOutputEncodingFormats
-        {
-            get
-            {
-                return new TOutputType[] { };
-            }
-        }
-
-        private TBaseItemType _libraryItem;
-        /// <summary>
-        /// Gets the library item that will be played, if any
-        /// </summary>
-        protected TBaseItemType LibraryItem
-        {
-            get
-            {
-                if (_libraryItem == null)
-                {
-                    string id = QueryString["id"];
-
-                    if (!string.IsNullOrEmpty(id))
-                    {
-                        _libraryItem = Kernel.Instance.GetItemById(Guid.Parse(id)) as TBaseItemType;
-                    }
-                }
-
-                return _libraryItem;
-            }
-        }
-
-        public int? AudioChannels
-        {
-            get
-            {
-                string val = QueryString["audiochannels"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return null;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        public int? AudioSampleRate
-        {
-            get
-            {
-                string val = QueryString["audiosamplerate"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return 44100;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        protected override Task<ResponseInfo> GetResponseInfo()
-        {
-            ResponseInfo info = new ResponseInfo
-            {
-                ContentType = MimeTypes.GetMimeType("." + GetConversionOutputFormat()),
-                CompressResponse = false
-            };
-
-            return Task.FromResult<ResponseInfo>(info);
-        }
-
-        public override Task ProcessRequest(HttpListenerContext ctx)
-        {
-            HttpListenerContext = ctx;
-
-            if (!RequiresConversion())
-            {
-                return new StaticFileHandler { Path = LibraryItem.Path }.ProcessRequest(ctx);
-            }
-
-            return base.ProcessRequest(ctx);
-        }
-
-        protected abstract string GetCommandLineArguments();
-
-        /// <summary>
-        /// Gets the format we'll be converting to
-        /// </summary>
-        protected virtual TOutputType GetConversionOutputFormat()
-        {
-            return OutputFormats.First(f => !UnsupportedOutputEncodingFormats.Any(s => s.ToString().Equals(f.ToString(), StringComparison.OrdinalIgnoreCase)));
-        }
-
-        protected virtual bool RequiresConversion()
-        {
-            string currentFormat = Path.GetExtension(LibraryItem.Path).Replace(".", string.Empty);
-
-            if (OutputFormats.Any(f => currentFormat.EndsWith(f.ToString(), StringComparison.OrdinalIgnoreCase)))
-            {
-                // We can output these files directly, but we can't encode them
-                if (UnsupportedOutputEncodingFormats.Any(f => currentFormat.EndsWith(f.ToString(), StringComparison.OrdinalIgnoreCase)))
-                {
-                    return false;
-                }
-            }
-            else
-            {
-                // If it's not in a format the consumer accepts, return true
-                return true;
-            }
-
-            return false;
-        }
-
-        private FileStream LogFileStream { get; set; }
-
-        protected async override Task WriteResponseToOutputStream(Stream stream)
-        {
-            var startInfo = new ProcessStartInfo{};
-
-            startInfo.CreateNoWindow = true;
-
-            startInfo.UseShellExecute = false;
-
-            // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
-            startInfo.RedirectStandardOutput = true;
-            startInfo.RedirectStandardError = true;
-
-            startInfo.FileName = Kernel.Instance.ApplicationPaths.FFMpegPath;
-            startInfo.WorkingDirectory = Kernel.Instance.ApplicationPaths.FFMpegDirectory;
-            startInfo.Arguments = GetCommandLineArguments();
-
-            Logger.LogInfo(startInfo.FileName + " " + startInfo.Arguments);
-
-            var process = new Process{};
-            process.StartInfo = startInfo;
-
-            // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
-            LogFileStream = new FileStream(Path.Combine(Kernel.Instance.ApplicationPaths.LogDirectoryPath, "ffmpeg-" + Guid.NewGuid().ToString() + ".txt"), FileMode.Create);
-
-            process.EnableRaisingEvents = true;
-
-            process.Exited += ProcessExited;
-
-            try
-            {
-                process.Start();
-
-                // MUST read both stdout and stderr asynchronously or a deadlock may occurr
-
-                // Kick off two tasks
-                Task mediaTask = process.StandardOutput.BaseStream.CopyToAsync(stream);
-                Task debugLogTask = process.StandardError.BaseStream.CopyToAsync(LogFileStream);
-
-                await mediaTask.ConfigureAwait(false);
-                //await debugLogTask.ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                Logger.LogException(ex);
-
-                // Hate having to do this
-                try
-                {
-                    process.Kill();
-                }
-                catch
-                {
-                }
-            }
-        }
-
-        void ProcessExited(object sender, EventArgs e)
-        {
-            if (LogFileStream != null)
-            {
-                LogFileStream.Dispose();
-            }
-
-            var process = sender as Process;
-
-            Logger.LogInfo("FFMpeg exited with code " + process.ExitCode);
-
-            process.Dispose();
-        }
-
-        /// <summary>
-        /// Gets the number of audio channels to specify on the command line
-        /// </summary>
-        protected int? GetNumAudioChannelsParam(int libraryItemChannels)
-        {
-            // If the user requested a max number of channels
-            if (AudioChannels.HasValue)
-            {
-                // Only specify the param if we're going to downmix
-                if (AudioChannels.Value < libraryItemChannels)
-                {
-                    return AudioChannels.Value;
-                }
-            }
-
-            return null;
-        }
-
-        /// <summary>
-        /// Gets the number of audio channels to specify on the command line
-        /// </summary>
-        protected int? GetSampleRateParam(int libraryItemSampleRate)
-        {
-            // If the user requested a max value
-            if (AudioSampleRate.HasValue)
-            {
-                // Only specify the param if we're going to downmix
-                if (AudioSampleRate.Value < libraryItemSampleRate)
-                {
-                    return AudioSampleRate.Value;
-                }
-            }
-
-            return null;
-        }
-    }
-}

+ 0 - 38
MediaBrowser.Api/HttpHandlers/FavoriteStatusHandler.cs

@@ -1,38 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.ComponentModel.Composition;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Provides a handler to set user favorite status for an item
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class FavoriteStatusHandler : BaseSerializationHandler<DtoUserItemData>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("FavoriteStatus", request);
-        }
-
-        protected override Task<DtoUserItemData> GetObjectToSerialize()
-        {
-            // Get the item
-            BaseItem item = ApiService.GetItemById(QueryString["id"]);
-
-            // Get the user
-            User user = ApiService.GetUserById(QueryString["userid"], true);
-
-            // Get the user data for this item
-            UserItemData data = item.GetUserData(user, true);
-
-            // Set favorite status
-            data.IsFavorite = QueryString["isfavorite"] == "1";
-
-            return Task.FromResult(ApiService.GetDtoUserItemData(data));
-        }
-    }
-}

+ 0 - 57
MediaBrowser.Api/HttpHandlers/GenreHandler.cs

@@ -1,57 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Gets a single genre
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class GenreHandler : BaseSerializationHandler<IbnItem>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("genre", request);
-        }
-
-        protected override Task<IbnItem> GetObjectToSerialize()
-        {
-            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
-            var user = ApiService.GetUserById(QueryString["userid"], true);
-
-            string name = QueryString["name"];
-
-            return GetGenre(parent, user, name);
-        }
-
-        /// <summary>
-        /// Gets a Genre
-        /// </summary>
-        private async Task<IbnItem> GetGenre(Folder parent, User user, string name)
-        {
-            int count = 0;
-
-            // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
-
-            foreach (var item in allItems)
-            {
-                if (item.Genres != null && item.Genres.Any(s => s.Equals(name, StringComparison.OrdinalIgnoreCase)))
-                {
-                    count++;
-                }
-            }
-
-            // Get the original entity so that we can also supply the PrimaryImagePath
-            return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetGenre(name).ConfigureAwait(false), count);
-        }
-    }
-}

+ 0 - 78
MediaBrowser.Api/HttpHandlers/GenresHandler.cs

@@ -1,78 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    public class GenresHandler : BaseSerializationHandler<IbnItem[]>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("genres", request);
-        }
-
-        protected override Task<IbnItem[]> GetObjectToSerialize()
-        {
-            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
-            User user = ApiService.GetUserById(QueryString["userid"], true);
-
-            return GetAllGenres(parent, user);
-        }
-
-        /// <summary>
-        /// Gets all genres from all recursive children of a folder
-        /// The CategoryInfo class is used to keep track of the number of times each genres appears
-        /// </summary>
-        private async Task<IbnItem[]> GetAllGenres(Folder parent, User user)
-        {
-            var data = new Dictionary<string, int>();
-
-            // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
-
-            foreach (var item in allItems)
-            {
-                // Add each genre from the item to the data dictionary
-                // If the genre already exists, increment the count
-                if (item.Genres == null)
-                {
-                    continue;
-                }
-
-                foreach (string val in item.Genres)
-                {
-                    if (!data.ContainsKey(val))
-                    {
-                        data.Add(val, 1);
-                    }
-                    else
-                    {
-                        data[val]++;
-                    }
-                }
-            }
-
-            // Get the Genre objects
-            Genre[] entities = await Task.WhenAll(data.Keys.Select(key => Kernel.Instance.ItemController.GetGenre(key))).ConfigureAwait(false);
-
-            // Convert to an array of IBNItem
-            var items = new IbnItem[entities.Length];
-
-            for (int i = 0; i < entities.Length; i++)
-            {
-                Genre e = entities[i];
-
-                items[i] = ApiService.GetIbnItem(e, data[e.Name]);
-            }
-
-            return items;
-        }
-    }
-}

+ 0 - 224
MediaBrowser.Api/HttpHandlers/ImageHandler.cs

@@ -1,224 +0,0 @@
-using MediaBrowser.Common.Net;
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Entities;
-using System;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    public class ImageHandler : BaseHandler
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("image", request);
-        }
-
-        private string _imagePath;
-
-        private async Task<string> GetImagePath()
-        {
-            _imagePath = _imagePath ?? await DiscoverImagePath();
-
-            return _imagePath;
-        }
-
-        private BaseEntity _sourceEntity;
-
-        private async Task<BaseEntity> GetSourceEntity()
-        {
-            if (_sourceEntity == null)
-            {
-                if (!string.IsNullOrEmpty(QueryString["personname"]))
-                {
-                    _sourceEntity =
-                        await Kernel.Instance.ItemController.GetPerson(QueryString["personname"]).ConfigureAwait(false);
-                }
-
-                else if (!string.IsNullOrEmpty(QueryString["genre"]))
-                {
-                    _sourceEntity =
-                        await Kernel.Instance.ItemController.GetGenre(QueryString["genre"]).ConfigureAwait(false);
-                }
-
-                else if (!string.IsNullOrEmpty(QueryString["year"]))
-                {
-                    _sourceEntity =
-                        await
-                        Kernel.Instance.ItemController.GetYear(int.Parse(QueryString["year"])).ConfigureAwait(false);
-                }
-
-                else if (!string.IsNullOrEmpty(QueryString["studio"]))
-                {
-                    _sourceEntity =
-                        await Kernel.Instance.ItemController.GetStudio(QueryString["studio"]).ConfigureAwait(false);
-                }
-
-                else if (!string.IsNullOrEmpty(QueryString["userid"]))
-                {
-                    _sourceEntity = ApiService.GetUserById(QueryString["userid"], false);
-                }
-
-                else
-                {
-                    _sourceEntity = ApiService.GetItemById(QueryString["id"]);
-                }
-            }
-
-            return _sourceEntity;
-        }
-
-        private async Task<string> DiscoverImagePath()
-        {
-            var entity = await GetSourceEntity().ConfigureAwait(false);
-
-            return ImageProcessor.GetImagePath(entity, ImageType, ImageIndex);
-        }
-
-        protected async override Task<ResponseInfo> GetResponseInfo()
-        {
-            string path = await GetImagePath().ConfigureAwait(false);
-
-            ResponseInfo info = new ResponseInfo
-            {
-                CacheDuration = TimeSpan.FromDays(365),
-                ContentType = MimeTypes.GetMimeType(path)
-            };
-
-            DateTime? date = File.GetLastWriteTimeUtc(path);
-
-            // If the file does not exist it will return jan 1, 1601
-            // http://msdn.microsoft.com/en-us/library/system.io.file.getlastwritetimeutc.aspx
-            if (date.Value.Year == 1601)
-            {
-                if (!File.Exists(path))
-                {
-                    info.StatusCode = 404;
-                    date = null;
-                }
-            }
-
-            info.DateLastModified = date;
-
-            return info;
-        }
-
-        private int ImageIndex
-        {
-            get
-            {
-                string val = QueryString["index"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return 0;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        private int? Height
-        {
-            get
-            {
-                string val = QueryString["height"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return null;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        private int? Width
-        {
-            get
-            {
-                string val = QueryString["width"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return null;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        private int? MaxHeight
-        {
-            get
-            {
-                string val = QueryString["maxheight"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return null;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        private int? MaxWidth
-        {
-            get
-            {
-                string val = QueryString["maxwidth"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return null;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        private int? Quality
-        {
-            get
-            {
-                string val = QueryString["quality"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return null;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        private ImageType ImageType
-        {
-            get
-            {
-                string imageType = QueryString["type"];
-
-                if (string.IsNullOrEmpty(imageType))
-                {
-                    return ImageType.Primary;
-                }
-
-                return (ImageType)Enum.Parse(typeof(ImageType), imageType, true);
-            }
-        }
-
-        protected override async Task WriteResponseToOutputStream(Stream stream)
-        {
-            var entity = await GetSourceEntity().ConfigureAwait(false);
-
-            ImageProcessor.ProcessImage(entity, ImageType, ImageIndex, stream, Width, Height, MaxWidth, MaxHeight, Quality);
-        }
-    }
-}

+ 0 - 35
MediaBrowser.Api/HttpHandlers/ItemHandler.cs

@@ -1,35 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.ComponentModel.Composition;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Provides a handler to retrieve a single item
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class ItemHandler : BaseSerializationHandler<DtoBaseItem>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("item", request);
-        }
-        
-        protected override Task<DtoBaseItem> GetObjectToSerialize()
-        {
-            User user = ApiService.GetUserById(QueryString["userid"], true);
-
-            BaseItem item = ApiService.GetItemById(QueryString["id"]);
-
-            if (item == null)
-            {
-                return null;
-            }
-
-            return ApiService.GetDtoBaseItem(item, user);
-        }
-    }
-}

+ 0 - 84
MediaBrowser.Api/HttpHandlers/ItemListHandler.cs

@@ -1,84 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    public class ItemListHandler : BaseSerializationHandler<DtoBaseItem[]>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("itemlist", request);
-        }
-
-        protected override Task<DtoBaseItem[]> GetObjectToSerialize()
-        {
-            User user = ApiService.GetUserById(QueryString["userid"], true);
-
-            return Task.WhenAll(GetItemsToSerialize(user).Select(i => ApiService.GetDtoBaseItem(i, user, includeChildren: false, includePeople: false)));
-        }
-
-        private IEnumerable<BaseItem> GetItemsToSerialize(User user)
-        {
-            var parent = ApiService.GetItemById(ItemId) as Folder;
-
-            if (ListType.Equals("inprogressitems", StringComparison.OrdinalIgnoreCase))
-            {
-                return parent.GetInProgressItems(user);
-            }
-            if (ListType.Equals("recentlyaddeditems", StringComparison.OrdinalIgnoreCase))
-            {
-                return parent.GetRecentlyAddedItems(user);
-            }
-            if (ListType.Equals("recentlyaddedunplayeditems", StringComparison.OrdinalIgnoreCase))
-            {
-                return parent.GetRecentlyAddedUnplayedItems(user);
-            }
-            if (ListType.Equals("itemswithgenre", StringComparison.OrdinalIgnoreCase))
-            {
-                return parent.GetItemsWithGenre(QueryString["name"], user);
-            }
-            if (ListType.Equals("itemswithyear", StringComparison.OrdinalIgnoreCase))
-            {
-                return parent.GetItemsWithYear(int.Parse(QueryString["year"]), user);
-            }
-            if (ListType.Equals("itemswithstudio", StringComparison.OrdinalIgnoreCase))
-            {
-                return parent.GetItemsWithStudio(QueryString["name"], user);
-            }
-            if (ListType.Equals("itemswithperson", StringComparison.OrdinalIgnoreCase))
-            {
-                return parent.GetItemsWithPerson(QueryString["name"], null, user);
-            }
-            if (ListType.Equals("favorites", StringComparison.OrdinalIgnoreCase))
-            {
-                return parent.GetFavoriteItems(user);
-            }
-
-            throw new InvalidOperationException();
-        }
-
-        protected string ItemId
-        {
-            get
-            {
-                return QueryString["id"];
-            }
-        }
-
-        private string ListType
-        {
-            get
-            {
-                return QueryString["listtype"] ?? string.Empty;
-            }
-        }
-    }
-}

+ 0 - 46
MediaBrowser.Api/HttpHandlers/MovieSpecialFeaturesHandler.cs

@@ -1,46 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Model.DTO;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// This handler retrieves special features for movies
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class MovieSpecialFeaturesHandler : BaseSerializationHandler<DtoBaseItem[]>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("MovieSpecialFeatures", request);
-        }
-
-        protected override Task<DtoBaseItem[]> GetObjectToSerialize()
-        {
-            User user = ApiService.GetUserById(QueryString["userid"], true);
-
-            var movie = ApiService.GetItemById(ItemId) as Movie;
-
-            // If none
-            if (movie.SpecialFeatures == null)
-            {
-                return Task.FromResult(new DtoBaseItem[] { });
-            }
-
-            return Task.WhenAll(movie.SpecialFeatures.Select(i => ApiService.GetDtoBaseItem(i, user, includeChildren: false)));
-        }
-
-        protected string ItemId
-        {
-            get
-            {
-                return QueryString["id"];
-            }
-        }
-    }
-}

+ 0 - 55
MediaBrowser.Api/HttpHandlers/PersonHandler.cs

@@ -1,55 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Gets a single Person
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class PersonHandler : BaseSerializationHandler<IbnItem>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("person", request);
-        }
-
-        protected override Task<IbnItem> GetObjectToSerialize()
-        {
-            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
-            var user = ApiService.GetUserById(QueryString["userid"], true);
-
-            string name = QueryString["name"];
-
-            return GetPerson(parent, user, name);
-        }
-
-        /// <summary>
-        /// Gets a Person
-        /// </summary>
-        private async Task<IbnItem> GetPerson(Folder parent, User user, string name)
-        {
-            int count = 0;
-
-            // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
-
-            foreach (var item in allItems)
-            {
-                if (item.People != null && item.People.ContainsKey(name))
-                {
-                    count++;
-                }
-            }
-
-            // Get the original entity so that we can also supply the PrimaryImagePath
-            return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetPerson(name).ConfigureAwait(false), count);
-        }
-    }
-}

+ 112 - 0
MediaBrowser.Api/HttpHandlers/PlaybackCheckInHandler.cs

@@ -0,0 +1,112 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Connectivity;
+using MediaBrowser.Model.DTO;
+using System;
+using System.ComponentModel.Composition;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Provides a handler to set played status for an item
+    /// </summary>
+    [Export(typeof(IHttpServerHandler))]
+    public class PlaybackCheckInHandler : BaseSerializationHandler<Kernel, DtoUserItemData>
+    {
+        /// <summary>
+        /// Gets the object to serialize.
+        /// </summary>
+        /// <returns>Task{DtoUserItemData}.</returns>
+        protected override async Task<DtoUserItemData> GetObjectToSerialize()
+        {
+            // Get the user
+            var user = await this.GetCurrentUser().ConfigureAwait(false);
+
+            var clientType = ClientType.Other;
+
+            if (!string.IsNullOrEmpty(QueryString["client"]))
+            {
+                ClientType type;
+
+                if (Enum.TryParse(QueryString["client"], true, out type))
+                {
+                    clientType = type;
+                }
+            }
+
+            var device = QueryString["device"];
+            
+            // Get the item
+            var item = DtoBuilder.GetItemByClientId(QueryString["id"], user.Id);
+
+            // Playback start check-in
+            if (QueryString["type"].Equals("start", StringComparison.OrdinalIgnoreCase))
+            {
+                Kernel.UserDataManager.OnPlaybackStart(user, item, clientType, device);
+            }
+            else
+            {
+                long? positionTicks = null;
+
+                if (!string.IsNullOrEmpty(QueryString["positionTicks"]))
+                {
+                    positionTicks = long.Parse(QueryString["positionTicks"]);
+                }
+
+                // Progress check-ins require position ticks
+                if (QueryString["type"].Equals("progress", StringComparison.OrdinalIgnoreCase))
+                {
+                    await Kernel.UserDataManager.OnPlaybackProgress(user, item, positionTicks, clientType, device).ConfigureAwait(false);
+                }
+                else if (QueryString["type"].Equals("stopped", StringComparison.OrdinalIgnoreCase))
+                {
+                    await Kernel.UserDataManager.OnPlaybackStopped(user, item, positionTicks, clientType, device).ConfigureAwait(false);
+                }
+            }
+
+            var data = item.GetUserData(user, true);
+
+            return DtoBuilder.GetDtoUserItemData(data);
+        }
+
+        /// <summary>
+        /// Gets the current user.
+        /// </summary>
+        /// <returns>User.</returns>
+        /// <exception cref="System.UnauthorizedAccessException"></exception>
+        public async Task<User> GetCurrentUser()
+        {
+            var handler = this;
+            var id = handler.QueryString["userid"];
+
+            var user = ApiService.GetUserById(id);
+
+            if (user == null)
+            {
+                throw new UnauthorizedAccessException(string.Format("User with Id {0} does not exist", id));
+            }
+
+            var clientType = ClientType.Other;
+
+            if (!string.IsNullOrEmpty(handler.QueryString["client"]))
+            {
+                ClientType type;
+
+                if (Enum.TryParse(handler.QueryString["client"], true, out type))
+                {
+                    clientType = type;
+                }
+            }
+
+            var device = handler.QueryString["device"];
+
+            await Controller.Kernel.Instance.UserManager.LogUserActivity(user, clientType, device).ConfigureAwait(false);
+
+            return user;
+        }
+
+    }
+}

+ 0 - 38
MediaBrowser.Api/HttpHandlers/PlayedStatusHandler.cs

@@ -1,38 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.ComponentModel.Composition;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Provides a handler to set played status for an item
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class PlayedStatusHandler : BaseSerializationHandler<DtoUserItemData>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("PlayedStatus", request);
-        }
-
-        protected override Task<DtoUserItemData> GetObjectToSerialize()
-        {
-            // Get the item
-            BaseItem item = ApiService.GetItemById(QueryString["id"]);
-
-            // Get the user
-            User user = ApiService.GetUserById(QueryString["userid"], true);
-
-            bool wasPlayed = QueryString["played"] == "1";
-
-            item.SetPlayedStatus(user, wasPlayed);
-
-            UserItemData data = item.GetUserData(user, true);
-
-            return Task.FromResult(ApiService.GetDtoUserItemData(data));
-        }
-    }
-}

+ 0 - 38
MediaBrowser.Api/HttpHandlers/PluginAssemblyHandler.cs

@@ -1,38 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using System;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    class PluginAssemblyHandler : BaseHandler
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("pluginassembly", request);
-        }
-
-        protected override Task<ResponseInfo> GetResponseInfo()
-        {
-            throw new NotImplementedException();
-        }
-
-        protected override Task WriteResponseToOutputStream(Stream stream)
-        {
-            throw new NotImplementedException();
-        }
-
-        public override Task ProcessRequest(HttpListenerContext ctx)
-        {
-            string filename = ctx.Request.QueryString["assemblyfilename"];
-
-            string path = Path.Combine(Kernel.Instance.ApplicationPaths.PluginsPath, filename);
-
-            return new StaticFileHandler { Path = path }.ProcessRequest(ctx);
-        }
-    }
-}

+ 0 - 53
MediaBrowser.Api/HttpHandlers/PluginConfigurationHandler.cs

@@ -1,53 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.Plugins;
-using System;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    public class PluginConfigurationHandler : BaseSerializationHandler<BasePluginConfiguration>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("pluginconfiguration", request);
-        }
-
-        private BasePlugin _plugin;
-        private BasePlugin Plugin
-        {
-            get
-            {
-                if (_plugin == null)
-                {
-                    string name = QueryString["assemblyfilename"];
-
-                    _plugin = Kernel.Instance.Plugins.First(p => p.AssemblyFileName.Equals(name, StringComparison.OrdinalIgnoreCase));
-                }
-
-                return _plugin;
-            }
-        }
-
-        protected override Task<BasePluginConfiguration> GetObjectToSerialize()
-        {
-            return Task.FromResult(Plugin.Configuration);
-        }
-
-        protected override async Task<ResponseInfo> GetResponseInfo()
-        {
-            var info = await base.GetResponseInfo().ConfigureAwait(false);
-
-            info.DateLastModified = Plugin.ConfigurationDateLastModified;
-
-            info.CacheDuration = TimeSpan.FromDays(7);
-
-            return info;
-        }
-    }
-}

+ 0 - 38
MediaBrowser.Api/HttpHandlers/PluginsHandler.cs

@@ -1,38 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.DTO;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Provides information about installed plugins
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class PluginsHandler : BaseSerializationHandler<IEnumerable<PluginInfo>>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("plugins", request);
-        }
-
-        protected override Task<IEnumerable<PluginInfo>> GetObjectToSerialize()
-        {
-            var plugins = Kernel.Instance.Plugins.Select(p => new PluginInfo
-            {
-                Name = p.Name,
-                Enabled = p.Enabled,
-                DownloadToUI = p.DownloadToUi,
-                Version = p.Version.ToString(),
-                AssemblyFileName = p.AssemblyFileName,
-                ConfigurationDateLastModified = p.ConfigurationDateLastModified
-            });
-
-            return Task.FromResult(plugins);
-        }
-    }
-}

+ 0 - 37
MediaBrowser.Api/HttpHandlers/ServerConfigurationHandler.cs

@@ -1,37 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.Configuration;
-using System;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    class ServerConfigurationHandler : BaseSerializationHandler<ServerConfiguration>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("serverconfiguration", request);
-        }
-
-        protected override Task<ServerConfiguration> GetObjectToSerialize()
-        {
-            return Task.FromResult(Kernel.Instance.Configuration);
-        }
-
-        protected override async Task<ResponseInfo> GetResponseInfo()
-        {
-            var info = await base.GetResponseInfo().ConfigureAwait(false);
-
-            info.DateLastModified =
-                File.GetLastWriteTimeUtc(Kernel.Instance.ApplicationPaths.SystemConfigurationFilePath);
-
-            info.CacheDuration = TimeSpan.FromDays(7);
-
-            return info;
-        }
-    }
-}

+ 0 - 57
MediaBrowser.Api/HttpHandlers/StudioHandler.cs

@@ -1,57 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Gets a single studio
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class StudioHandler : BaseSerializationHandler<IbnItem>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("studio", request);
-        }
-
-        protected override Task<IbnItem> GetObjectToSerialize()
-        {
-            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
-            var user = ApiService.GetUserById(QueryString["userid"], true);
-
-            string name = QueryString["name"];
-
-            return GetStudio(parent, user, name);
-        }
-
-        /// <summary>
-        /// Gets a Studio
-        /// </summary>
-        private async Task<IbnItem> GetStudio(Folder parent, User user, string name)
-        {
-            int count = 0;
-
-            // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
-
-            foreach (var item in allItems)
-            {
-                if (item.Studios != null && item.Studios.Any(s => s.Equals(name, StringComparison.OrdinalIgnoreCase)))
-                {
-                    count++;
-                }
-            }
-
-            // Get the original entity so that we can also supply the PrimaryImagePath
-            return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetStudio(name).ConfigureAwait(false), count);
-        }
-    }
-}

+ 0 - 78
MediaBrowser.Api/HttpHandlers/StudiosHandler.cs

@@ -1,78 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    public class StudiosHandler : BaseSerializationHandler<IbnItem[]>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("studios", request);
-        }
-
-        protected override Task<IbnItem[]> GetObjectToSerialize()
-        {
-            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
-            var user = ApiService.GetUserById(QueryString["userid"], true);
-
-            return GetAllStudios(parent, user);
-        }
-
-        /// <summary>
-        /// Gets all studios from all recursive children of a folder
-        /// The CategoryInfo class is used to keep track of the number of times each studio appears
-        /// </summary>
-        private async Task<IbnItem[]> GetAllStudios(Folder parent, User user)
-        {
-            var data = new Dictionary<string, int>();
-
-            // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
-
-            foreach (var item in allItems)
-            {
-                // Add each studio from the item to the data dictionary
-                // If the studio already exists, increment the count
-                if (item.Studios == null)
-                {
-                    continue;
-                }
-
-                foreach (string val in item.Studios)
-                {
-                    if (!data.ContainsKey(val))
-                    {
-                        data.Add(val, 1);
-                    }
-                    else
-                    {
-                        data[val]++;
-                    }
-                }
-            }
-
-            // Get the Studio objects
-            Studio[] entities = await Task.WhenAll(data.Keys.Select(key => Kernel.Instance.ItemController.GetStudio(key))).ConfigureAwait(false);
-
-            // Convert to an array of IBNItem
-            var items = new IbnItem[entities.Length];
-
-            for (int i = 0; i < entities.Length; i++)
-            {
-                Studio e = entities[i];
-
-                items[i] = ApiService.GetIbnItem(e, data[e.Name]);
-            }
-
-            return items;
-        }
-    }
-}

+ 263 - 0
MediaBrowser.Api/HttpHandlers/UpdateMediaLibraryHandler.cs

@@ -0,0 +1,263 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.HttpHandlers
+{
+    /// <summary>
+    /// Makes changes to the user's media library
+    /// </summary>
+    [Export(typeof(IHttpServerHandler))]
+    public class UpdateMediaLibraryHandler : BaseActionHandler<Kernel>
+    {
+        /// <summary>
+        /// Executes the action.
+        /// </summary>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.NotImplementedException"></exception>
+        protected override Task ExecuteAction()
+        {
+            return Task.Run(() =>
+            {
+                var action = QueryString["action"];
+
+                if (string.IsNullOrEmpty(action))
+                {
+                    throw new ArgumentNullException();
+                }
+
+                User user = null;
+
+                if (!string.IsNullOrEmpty(QueryString["userId"]))
+                {
+                    user = ApiService.GetUserById(QueryString["userId"]);
+                }
+
+                if (action.Equals("AddVirtualFolder", StringComparison.OrdinalIgnoreCase))
+                {
+                    AddVirtualFolder(Uri.UnescapeDataString(QueryString["name"]), user);
+                }
+
+                if (action.Equals("RemoveVirtualFolder", StringComparison.OrdinalIgnoreCase))
+                {
+                    RemoveVirtualFolder(QueryString["name"], user);
+                }
+
+                if (action.Equals("RenameVirtualFolder", StringComparison.OrdinalIgnoreCase))
+                {
+                    RenameVirtualFolder(QueryString["name"], QueryString["newName"], user);
+                }
+
+                if (action.Equals("RemoveMediaPath", StringComparison.OrdinalIgnoreCase))
+                {
+                    RemoveMediaPath(QueryString["virtualFolderName"], QueryString["mediaPath"], user);
+                }
+
+                if (action.Equals("AddMediaPath", StringComparison.OrdinalIgnoreCase))
+                {
+                    AddMediaPath(QueryString["virtualFolderName"], QueryString["mediaPath"], user);
+                }
+
+                throw new ArgumentOutOfRangeException();
+            });
+        }
+
+        /// <summary>
+        /// Adds a virtual folder to either the default view or a user view
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="user">The user.</param>
+        private void AddVirtualFolder(string name, User user)
+        {
+            name = FileSystem.GetValidFilename(name);
+
+            var rootFolderPath = user != null ? user.RootFolderPath : Kernel.ApplicationPaths.DefaultUserViewsPath;
+            var virtualFolderPath = Path.Combine(rootFolderPath, name);
+
+            if (Directory.Exists(virtualFolderPath))
+            {
+                throw new ArgumentException("There is already a media collection with the name " + name + ".");
+            }
+
+            Directory.CreateDirectory(virtualFolderPath);
+        }
+
+        /// <summary>
+        /// Adds an additional mediaPath to an existing virtual folder, within either the default view or a user view
+        /// </summary>
+        /// <param name="virtualFolderName">Name of the virtual folder.</param>
+        /// <param name="path">The path.</param>
+        /// <param name="user">The user.</param>
+        private void AddMediaPath(string virtualFolderName, string path, User user)
+        {
+            if (!Path.IsPathRooted(path))
+            {
+                throw new ArgumentException("The path is not valid.");
+            }
+
+            if (!Directory.Exists(path))
+            {
+                throw new DirectoryNotFoundException("The path does not exist.");
+            }
+
+            // Strip off trailing slash, but not on drives
+            path = path.TrimEnd(Path.DirectorySeparatorChar);
+            if (path.EndsWith(":", StringComparison.OrdinalIgnoreCase))
+            {
+                path += Path.DirectorySeparatorChar;
+            }
+
+            var rootFolderPath = user != null ? user.RootFolderPath : Kernel.ApplicationPaths.DefaultUserViewsPath;
+            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
+
+            ValidateNewMediaPath(rootFolderPath, path);
+
+            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
+
+            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ".lnk");
+
+            while (File.Exists(lnk))
+            {
+                shortcutFilename += "1";
+                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ".lnk");
+            }
+
+            FileSystem.CreateShortcut(lnk, path);
+        }
+
+        /// <summary>
+        /// Validates that a new media path can be added
+        /// </summary>
+        /// <param name="currentViewRootFolderPath">The current view root folder path.</param>
+        /// <param name="mediaPath">The media path.</param>
+        private void ValidateNewMediaPath(string currentViewRootFolderPath, string mediaPath)
+        {
+            var duplicate = Directory.EnumerateFiles(Kernel.ApplicationPaths.RootFolderPath, "*.lnk", SearchOption.AllDirectories)
+                .Select(FileSystem.ResolveShortcut)
+                .FirstOrDefault(p => !IsNewPathValid(mediaPath, p));
+
+            if (!string.IsNullOrEmpty(duplicate))
+            {
+                throw new ArgumentException(string.Format("The path cannot be added to the library because {0} already exists.", duplicate));
+            }
+
+            // Make sure the current root folder doesn't already have a shortcut to the same path
+            duplicate = Directory.EnumerateFiles(currentViewRootFolderPath, "*.lnk", SearchOption.AllDirectories)
+                .Select(FileSystem.ResolveShortcut)
+                .FirstOrDefault(p => mediaPath.Equals(p, StringComparison.OrdinalIgnoreCase));
+
+            if (!string.IsNullOrEmpty(duplicate))
+            {
+                throw new ArgumentException(string.Format("The path {0} already exists in the library", mediaPath));
+            }
+        }
+
+        /// <summary>
+        /// Validates that a new path can be added based on an existing path
+        /// </summary>
+        /// <param name="newPath">The new path.</param>
+        /// <param name="existingPath">The existing path.</param>
+        /// <returns><c>true</c> if [is new path valid] [the specified new path]; otherwise, <c>false</c>.</returns>
+        private bool IsNewPathValid(string newPath, string existingPath)
+        {
+            // Example: D:\Movies is the existing path
+            // D:\ cannot be added
+            // Neither can D:\Movies\Kids
+            // A D:\Movies duplicate is ok here since that will be caught later
+
+            if (newPath.Equals(existingPath, StringComparison.OrdinalIgnoreCase))
+            {
+                return true;
+            }
+
+            // Validate the D:\Movies\Kids scenario
+            if (newPath.StartsWith(existingPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            // Validate the D:\ scenario
+            if (existingPath.StartsWith(newPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Renames a virtual folder within either the default view or a user view
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="newName">The new name.</param>
+        /// <param name="user">The user.</param>
+        private void RenameVirtualFolder(string name, string newName, User user)
+        {
+            var rootFolderPath = user != null ? user.RootFolderPath : Kernel.ApplicationPaths.DefaultUserViewsPath;
+
+            var currentPath = Path.Combine(rootFolderPath, name);
+            var newPath = Path.Combine(rootFolderPath, newName);
+
+            if (!Directory.Exists(currentPath))
+            {
+                throw new DirectoryNotFoundException("The media collection does not exist");
+            }
+
+            if (Directory.Exists(newPath))
+            {
+                throw new ArgumentException("There is already a media collection with the name " + newPath + ".");
+            }
+
+            Directory.Move(currentPath, newPath);
+        }
+
+        /// <summary>
+        /// Deletes a virtual folder from either the default view or a user view
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="user">The user.</param>
+        private void RemoveVirtualFolder(string name, User user)
+        {
+            var rootFolderPath = user != null ? user.RootFolderPath : Kernel.ApplicationPaths.DefaultUserViewsPath;
+            var path = Path.Combine(rootFolderPath, name);
+
+            if (!Directory.Exists(path))
+            {
+                throw new DirectoryNotFoundException("The media folder does not exist");
+            }
+
+            Directory.Delete(path, true);
+        }
+
+        /// <summary>
+        /// Deletes a shortcut from within a virtual folder, within either the default view or a user view
+        /// </summary>
+        /// <param name="virtualFolderName">Name of the virtual folder.</param>
+        /// <param name="mediaPath">The media path.</param>
+        /// <param name="user">The user.</param>
+        private void RemoveMediaPath(string virtualFolderName, string mediaPath, User user)
+        {
+            var rootFolderPath = user != null ? user.RootFolderPath : Kernel.ApplicationPaths.DefaultUserViewsPath;
+            var path = Path.Combine(rootFolderPath, virtualFolderName);
+
+            if (!Directory.Exists(path))
+            {
+                throw new DirectoryNotFoundException("The media folder does not exist");
+            }
+
+            var shortcut = Directory.EnumerateFiles(path, "*.lnk", SearchOption.AllDirectories).FirstOrDefault(f => FileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
+
+            if (string.IsNullOrEmpty(shortcut))
+            {
+                throw new DirectoryNotFoundException("The media folder does not exist");
+            }
+            File.Delete(shortcut);
+        }
+    }
+}

+ 0 - 29
MediaBrowser.Api/HttpHandlers/UserAuthenticationHandler.cs

@@ -1,29 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Authentication;
-using System.ComponentModel.Composition;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    class UserAuthenticationHandler : BaseSerializationHandler<AuthenticationResult>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("UserAuthentication", request);
-        }
-        
-        protected override async Task<AuthenticationResult> GetObjectToSerialize()
-        {
-            string userId = await GetFormValue("userid").ConfigureAwait(false);
-            User user = ApiService.GetUserById(userId, false);
-
-            string password = await GetFormValue("password").ConfigureAwait(false);
-
-            return Kernel.Instance.AuthenticateUser(user, password);
-        }
-    }
-}

+ 0 - 29
MediaBrowser.Api/HttpHandlers/UserHandler.cs

@@ -1,29 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.ComponentModel.Composition;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    class UserHandler : BaseSerializationHandler<DtoUser>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("user", request);
-        }
-
-        protected override Task<DtoUser> GetObjectToSerialize()
-        {
-            string id = QueryString["id"];
-
-            User user = string.IsNullOrEmpty(id) ? ApiService.GetDefaultUser(false) : ApiService.GetUserById(id, false);
-
-            DtoUser dto = ApiService.GetDtoUser(user);
-
-            return Task.FromResult(dto);
-        }
-    }
-}

+ 0 - 46
MediaBrowser.Api/HttpHandlers/UserItemRatingHandler.cs

@@ -1,46 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.ComponentModel.Composition;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Provides a handler to set a user's rating for an item
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class UserItemRatingHandler : BaseSerializationHandler<DtoUserItemData>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("UserItemRating", request);
-        }
-
-        protected override Task<DtoUserItemData> GetObjectToSerialize()
-        {
-            // Get the item
-            BaseItem item = ApiService.GetItemById(QueryString["id"]);
-
-            // Get the user
-            User user = ApiService.GetUserById(QueryString["userid"], true);
-
-            // Get the user data for this item
-            UserItemData data = item.GetUserData(user, true);
-
-            // If clearing the rating, set it to null
-            if (QueryString["clear"] == "1")
-            {
-                data.Rating = null;
-            }
-
-            else
-            {
-                data.Likes = QueryString["likes"] == "1";
-            }
-
-            return Task.FromResult(ApiService.GetDtoUserItemData(data));
-        }
-    }
-}

+ 0 - 25
MediaBrowser.Api/HttpHandlers/UsersHandler.cs

@@ -1,25 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.DTO;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    class UsersHandler : BaseSerializationHandler<IEnumerable<DtoUser>>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("users", request);
-        }
-        
-        protected override Task<IEnumerable<DtoUser>> GetObjectToSerialize()
-        {
-            return Task.FromResult(Kernel.Instance.Users.Select(u => ApiService.GetDtoUser(u)));
-        }
-    }
-}

+ 0 - 424
MediaBrowser.Api/HttpHandlers/VideoHandler.cs

@@ -1,424 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Drawing;
-using System.Linq;
-using System.Net;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Supported output formats: mkv,m4v,mp4,asf,wmv,mov,webm,ogv,3gp,avi,ts,flv
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    class VideoHandler : BaseMediaHandler<Video, VideoOutputFormats>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("video", request);
-        }
-
-        /// <summary>
-        /// We can output these files directly, but we can't encode them
-        /// </summary>
-        protected override IEnumerable<VideoOutputFormats> UnsupportedOutputEncodingFormats
-        {
-            get
-            {
-                // mp4, 3gp, mov - muxer does not support non-seekable output
-                // avi, mov, mkv, m4v - can't stream these when encoding. the player will try to download them completely before starting playback.
-                // wmv - can't seem to figure out the output format name
-                return new VideoOutputFormats[] { VideoOutputFormats.Mp4, VideoOutputFormats.ThreeGp, VideoOutputFormats.M4V, VideoOutputFormats.Mkv, VideoOutputFormats.Avi, VideoOutputFormats.Mov, VideoOutputFormats.Wmv };
-            }
-        }
-
-        /// <summary>
-        /// Determines whether or not we can just output the original file directly
-        /// </summary>
-        protected override bool RequiresConversion()
-        {
-            if (base.RequiresConversion())
-            {
-                return true;
-            }
-
-            // See if the video requires conversion
-            if (RequiresVideoConversion())
-            {
-                return true;
-            }
-
-            // See if the audio requires conversion
-            AudioStream audioStream = (LibraryItem.AudioStreams ?? new List<AudioStream>()).FirstOrDefault();
-
-            if (audioStream != null)
-            {
-                if (RequiresAudioConversion(audioStream))
-                {
-                    return true;
-                }
-            }
-
-            // Yay
-            return false;
-        }
-
-        /// <summary>
-        /// Translates the output file extension to the format param that follows "-f" on the ffmpeg command line
-        /// </summary>
-        private string GetFfMpegOutputFormat(VideoOutputFormats outputFormat)
-        {
-            if (outputFormat == VideoOutputFormats.Mkv)
-            {
-                return "matroska";
-            }
-            if (outputFormat == VideoOutputFormats.Ts)
-            {
-                return "mpegts";
-            }
-            if (outputFormat == VideoOutputFormats.Ogv)
-            {
-                return "ogg";
-            }
-
-            return outputFormat.ToString().ToLower();
-        }
-
-        /// <summary>
-        /// Creates arguments to pass to ffmpeg
-        /// </summary>
-        protected override string GetCommandLineArguments()
-        {
-            VideoOutputFormats outputFormat = GetConversionOutputFormat();
-
-            return string.Format("-i \"{0}\" -threads 0 {1} {2} -f {3} -",
-                LibraryItem.Path,
-                GetVideoArguments(outputFormat),
-                GetAudioArguments(outputFormat),
-                GetFfMpegOutputFormat(outputFormat)
-                );
-        }
-
-        /// <summary>
-        /// Gets video arguments to pass to ffmpeg
-        /// </summary>
-        private string GetVideoArguments(VideoOutputFormats outputFormat)
-        {
-            // Get the output codec name
-            string codec = GetVideoCodec(outputFormat);
-
-            string args = "-vcodec " + codec;
-
-            // If we're encoding video, add additional params
-            if (!codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
-            {
-                // Add resolution params, if specified
-                if (Width.HasValue || Height.HasValue || MaxHeight.HasValue || MaxWidth.HasValue)
-                {
-                    Size size = DrawingUtils.Resize(LibraryItem.Width, LibraryItem.Height, Width, Height, MaxWidth, MaxHeight);
-
-                    args += string.Format(" -s {0}x{1}", size.Width, size.Height);
-                }
-            }
-
-            return args;
-        }
-
-        /// <summary>
-        /// Gets audio arguments to pass to ffmpeg
-        /// </summary>
-        private string GetAudioArguments(VideoOutputFormats outputFormat)
-        {
-            AudioStream audioStream = (LibraryItem.AudioStreams ?? new List<AudioStream>()).FirstOrDefault();
-
-            // If the video doesn't have an audio stream, return empty
-            if (audioStream == null)
-            {
-                return string.Empty;
-            }
-
-            // Get the output codec name
-            string codec = GetAudioCodec(audioStream, outputFormat);
-
-            string args = "-acodec " + codec;
-
-            // If we're encoding audio, add additional params
-            if (!codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
-            {
-                // Add the number of audio channels
-                int? channels = GetNumAudioChannelsParam(codec, audioStream.Channels);
-
-                if (channels.HasValue)
-                {
-                    args += " -ac " + channels.Value;
-                }
-
-                // Add the audio sample rate
-                int? sampleRate = GetSampleRateParam(audioStream.SampleRate);
-
-                if (sampleRate.HasValue)
-                {
-                    args += " -ar " + sampleRate.Value;
-                }
-
-            }
-
-            return args;
-        }
-
-        /// <summary>
-        /// Gets the name of the output video codec
-        /// </summary>
-        private string GetVideoCodec(VideoOutputFormats outputFormat)
-        {
-            // Some output containers require specific codecs
-
-            if (outputFormat == VideoOutputFormats.Webm)
-            {
-                // Per webm specification, it must be vpx
-                return "libvpx";
-            }
-            if (outputFormat == VideoOutputFormats.Asf)
-            {
-                return "wmv2";
-            }
-            if (outputFormat == VideoOutputFormats.Wmv)
-            {
-                return "wmv2";
-            }
-            if (outputFormat == VideoOutputFormats.Ogv)
-            {
-                return "libtheora";
-            }
-
-            // Skip encoding when possible
-            if (!RequiresVideoConversion())
-            {
-                return "copy";
-            }
-
-            return "libx264";
-        }
-
-        /// <summary>
-        /// Gets the name of the output audio codec
-        /// </summary>
-        private string GetAudioCodec(AudioStream audioStream, VideoOutputFormats outputFormat)
-        {
-            // Some output containers require specific codecs
-
-            if (outputFormat == VideoOutputFormats.Webm)
-            {
-                // Per webm specification, it must be vorbis
-                return "libvorbis";
-            }
-            if (outputFormat == VideoOutputFormats.Asf)
-            {
-                return "wmav2";
-            }
-            if (outputFormat == VideoOutputFormats.Wmv)
-            {
-                return "wmav2";
-            }
-            if (outputFormat == VideoOutputFormats.Ogv)
-            {
-                return "libvorbis";
-            }
-
-            // Skip encoding when possible
-            if (!RequiresAudioConversion(audioStream))
-            {
-                return "copy";
-            }
-
-            return "libvo_aacenc";
-        }
-
-        /// <summary>
-        /// Gets the number of audio channels to specify on the command line
-        /// </summary>
-        private int? GetNumAudioChannelsParam(string audioCodec, int libraryItemChannels)
-        {
-            if (libraryItemChannels > 2)
-            {
-                if (audioCodec.Equals("libvo_aacenc"))
-                {
-                    // libvo_aacenc currently only supports two channel output
-                    return 2;
-                }
-                if (audioCodec.Equals("wmav2"))
-                {
-                    // wmav2 currently only supports two channel output
-                    return 2;
-                }
-            }
-
-            return GetNumAudioChannelsParam(libraryItemChannels);
-        }
-
-        /// <summary>
-        /// Determines if the video stream requires encoding
-        /// </summary>
-        private bool RequiresVideoConversion()
-        {
-            // Check dimensions
-
-            // If a specific width is required, validate that
-            if (Width.HasValue)
-            {
-                if (Width.Value != LibraryItem.Width)
-                {
-                    return true;
-                }
-            }
-
-            // If a specific height is required, validate that
-            if (Height.HasValue)
-            {
-                if (Height.Value != LibraryItem.Height)
-                {
-                    return true;
-                }
-            }
-
-            // If a max width is required, validate that
-            if (MaxWidth.HasValue)
-            {
-                if (MaxWidth.Value < LibraryItem.Width)
-                {
-                    return true;
-                }
-            }
-
-            // If a max height is required, validate that
-            if (MaxHeight.HasValue)
-            {
-                if (MaxHeight.Value < LibraryItem.Height)
-                {
-                    return true;
-                }
-            }
-
-            // If the codec is already h264, don't encode
-            if (LibraryItem.Codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 || LibraryItem.Codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                return false;
-            }
-
-            return false;
-        }
-
-        /// <summary>
-        /// Determines if the audio stream requires encoding
-        /// </summary>
-        private bool RequiresAudioConversion(AudioStream audio)
-        {
-
-            // If the input stream has more audio channels than the client can handle, we need to encode
-            if (AudioChannels.HasValue)
-            {
-                if (audio.Channels > AudioChannels.Value)
-                {
-                    return true;
-                }
-            }
-
-            // Aac, ac-3 and mp3 are all pretty much universally supported. No need to encode them
-
-            if (audio.Codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                return false;
-            }
-
-            if (audio.Codec.IndexOf("ac-3", StringComparison.OrdinalIgnoreCase) != -1 || audio.Codec.IndexOf("ac3", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                return false;
-            }
-
-            if (audio.Codec.IndexOf("mpeg", StringComparison.OrdinalIgnoreCase) != -1 || audio.Codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                return false;
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        /// Gets the fixed output video height, in pixels
-        /// </summary>
-        private int? Height
-        {
-            get
-            {
-                string val = QueryString["height"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return null;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        /// <summary>
-        /// Gets the fixed output video width, in pixels
-        /// </summary>
-        private int? Width
-        {
-            get
-            {
-                string val = QueryString["width"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return null;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        /// <summary>
-        /// Gets the maximum output video height, in pixels
-        /// </summary>
-        private int? MaxHeight
-        {
-            get
-            {
-                string val = QueryString["maxheight"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return null;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-        /// <summary>
-        /// Gets the maximum output video width, in pixels
-        /// </summary>
-        private int? MaxWidth
-        {
-            get
-            {
-                string val = QueryString["maxwidth"];
-
-                if (string.IsNullOrEmpty(val))
-                {
-                    return null;
-                }
-
-                return int.Parse(val);
-            }
-        }
-
-    }
-}

+ 0 - 43
MediaBrowser.Api/HttpHandlers/WeatherHandler.cs

@@ -1,43 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.Weather;
-using System;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    class WeatherHandler : BaseSerializationHandler<WeatherInfo>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("weather", request);
-        }
-
-        protected override Task<WeatherInfo> GetObjectToSerialize()
-        {
-            // If a specific zip code was requested on the query string, use that. Otherwise use the value from configuration
-
-            string zipCode = QueryString["zipcode"];
-
-            if (string.IsNullOrWhiteSpace(zipCode))
-            {
-                zipCode = Kernel.Instance.Configuration.WeatherZipCode;
-            }
-
-            return Kernel.Instance.WeatherProviders.First().GetWeatherInfoAsync(zipCode);
-        }
-
-        protected override async Task<ResponseInfo> GetResponseInfo()
-        {
-            var info = await base.GetResponseInfo().ConfigureAwait(false);
-
-            info.CacheDuration = TimeSpan.FromMinutes(15);
-
-            return info;
-        }
-    }
-}

+ 0 - 55
MediaBrowser.Api/HttpHandlers/YearHandler.cs

@@ -1,55 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    /// <summary>
-    /// Gets a single year
-    /// </summary>
-    [Export(typeof(BaseHandler))]
-    public class YearHandler : BaseSerializationHandler<IbnItem>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("year", request);
-        }
-
-        protected override Task<IbnItem> GetObjectToSerialize()
-        {
-            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
-            var user = ApiService.GetUserById(QueryString["userid"], true);
-
-            string year = QueryString["year"];
-
-            return GetYear(parent, user, int.Parse(year));
-        }
-
-        /// <summary>
-        /// Gets a Year
-        /// </summary>
-        private async Task<IbnItem> GetYear(Folder parent, User user, int year)
-        {
-            int count = 0;
-
-            // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
-
-            foreach (var item in allItems)
-            {
-                if (item.ProductionYear.HasValue && item.ProductionYear.Value == year)
-                {
-                    count++;
-                }
-            }
-
-            // Get the original entity so that we can also supply the PrimaryImagePath
-            return ApiService.GetIbnItem(await Kernel.Instance.ItemController.GetYear(year).ConfigureAwait(false), count);
-        }
-    }
-}

+ 0 - 75
MediaBrowser.Api/HttpHandlers/YearsHandler.cs

@@ -1,75 +0,0 @@
-using MediaBrowser.Common.Net.Handlers;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.DTO;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Api.HttpHandlers
-{
-    [Export(typeof(BaseHandler))]
-    public class YearsHandler : BaseSerializationHandler<IbnItem[]>
-    {
-        public override bool HandlesRequest(HttpListenerRequest request)
-        {
-            return ApiService.IsApiUrlMatch("years", request);
-        }
-
-        protected override Task<IbnItem[]> GetObjectToSerialize()
-        {
-            var parent = ApiService.GetItemById(QueryString["id"]) as Folder;
-            User user = ApiService.GetUserById(QueryString["userid"], true);
-
-            return GetAllYears(parent, user);
-        }
-
-        /// <summary>
-        /// Gets all years from all recursive children of a folder
-        /// The CategoryInfo class is used to keep track of the number of times each year appears
-        /// </summary>
-        private async Task<IbnItem[]> GetAllYears(Folder parent, User user)
-        {
-            var data = new Dictionary<int, int>();
-
-            // Get all the allowed recursive children
-            IEnumerable<BaseItem> allItems = parent.GetRecursiveChildren(user);
-
-            foreach (var item in allItems)
-            {
-                // Add the year from the item to the data dictionary
-                // If the year already exists, increment the count
-                if (item.ProductionYear == null)
-                {
-                    continue;
-                }
-
-                if (!data.ContainsKey(item.ProductionYear.Value))
-                {
-                    data.Add(item.ProductionYear.Value, 1);
-                }
-                else
-                {
-                    data[item.ProductionYear.Value]++;
-                }
-            }
-
-            // Get the Year objects
-            Year[] entities = await Task.WhenAll(data.Keys.Select(key => Kernel.Instance.ItemController.GetYear(key))).ConfigureAwait(false);
-
-            // Convert to an array of IBNItem
-            var items = new IbnItem[entities.Length];
-
-            for (int i = 0; i < entities.Length; i++)
-            {
-                Year e = entities[i];
-
-                items[i] = ApiService.GetIbnItem(e, data[int.Parse(e.Name)]);
-            }
-
-            return items;
-        }
-    }
-}

+ 54 - 0
MediaBrowser.Api/Images/ImageRequest.cs

@@ -0,0 +1,54 @@
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Api.Images
+{
+    /// <summary>
+    /// Class ImageRequest
+    /// </summary>
+    public class ImageRequest : DeleteImageRequest
+    {
+        /// <summary>
+        /// The max width
+        /// </summary>
+        public int? MaxWidth;
+        /// <summary>
+        /// The max height
+        /// </summary>
+        public int? MaxHeight;
+        /// <summary>
+        /// The width
+        /// </summary>
+        public int? Width;
+        /// <summary>
+        /// The height
+        /// </summary>
+        public int? Height;
+        /// <summary>
+        /// Gets or sets the quality.
+        /// </summary>
+        /// <value>The quality.</value>
+        public int? Quality { get; set; }
+        /// <summary>
+        /// Gets or sets the tag.
+        /// </summary>
+        /// <value>The tag.</value>
+        public string Tag { get; set; }
+    }
+
+    /// <summary>
+    /// Class DeleteImageRequest
+    /// </summary>
+    public class DeleteImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the type of the image.
+        /// </summary>
+        /// <value>The type of the image.</value>
+        public ImageType Type { get; set; }
+        /// <summary>
+        /// Gets or sets the index.
+        /// </summary>
+        /// <value>The index.</value>
+        public int? Index { get; set; }
+    }
+}

+ 286 - 0
MediaBrowser.Api/Images/ImageService.cs

@@ -0,0 +1,286 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using ServiceStack.ServiceHost;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Images
+{
+    /// <summary>
+    /// Class GetItemImage
+    /// </summary>
+    [Route("/Items/{Id}/Images/{Type}", "GET")]
+    [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")]
+    public class GetItemImage : ImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetPersonImage
+    /// </summary>
+    [Route("/Persons/{Name}/Images/{Type}", "GET")]
+    [Route("/Persons/{Name}/Images/{Type}/{Index}", "GET")]
+    public class GetPersonImage : ImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetStudioImage
+    /// </summary>
+    [Route("/Studios/{Name}/Images/{Type}", "GET")]
+    [Route("/Studios/{Name}/Images/{Type}/{Index}", "GET")]
+    public class GetStudioImage : ImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetGenreImage
+    /// </summary>
+    [Route("/Genres/{Name}/Images/{Type}", "GET")]
+    [Route("/Genres/{Name}/Images/{Type}/{Index}", "GET")]
+    public class GetGenreImage : ImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetYearImage
+    /// </summary>
+    [Route("/Years/{Year}/Images/{Type}", "GET")]
+    [Route("/Years/{Year}/Images/{Type}/{Index}", "GET")]
+    public class GetYearImage : ImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the year.
+        /// </summary>
+        /// <value>The year.</value>
+        public int Year { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetUserImage
+    /// </summary>
+    [Route("/Users/{Id}/Images/{Type}", "GET")]
+    [Route("/Users/{Id}/Images/{Type}/{Index}", "GET")]
+    public class GetUserImage : ImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class DeleteUserImage
+    /// </summary>
+    [Route("/Users/{Id}/Images/{Type}", "DELETE")]
+    [Route("/Users/{Id}/Images/{Type}/{Index}", "DELETE")]
+    public class DeleteUserImage : DeleteImageRequest
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+    }
+    
+    /// <summary>
+    /// Class ImageService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class ImageService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetItemImage request)
+        {
+            var item = DtoBuilder.GetItemByClientId(request.Id);
+
+            return GetImage(request, item);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetUserImage request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var item = kernel.Users.First(i => i.Id == request.Id);
+
+            return GetImage(request, item);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetYearImage request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var item = kernel.LibraryManager.GetYear(request.Year).Result;
+
+            return GetImage(request, item);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetStudioImage request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var item = kernel.LibraryManager.GetStudio(request.Name).Result;
+
+            return GetImage(request, item);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPersonImage request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var item = kernel.LibraryManager.GetPerson(request.Name).Result;
+
+            return GetImage(request, item);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetGenreImage request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var item = kernel.LibraryManager.GetGenre(request.Name).Result;
+
+            return GetImage(request, item);
+        }
+
+        /// <summary>
+        /// Deletes the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Delete(DeleteUserImage request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var item = kernel.Users.First(i => i.Id == request.Id);
+
+            var task = item.DeleteImage(request.Type);
+
+            Task.WaitAll(task);
+        }
+        
+        /// <summary>
+        /// Gets the image.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="item">The item.</param>
+        /// <returns>System.Object.</returns>
+        /// <exception cref="ResourceNotFoundException"></exception>
+        private object GetImage(ImageRequest request, BaseItem item)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var index = request.Index ?? 0;
+
+            var imagePath = GetImagePath(kernel, request, item);
+
+            if (string.IsNullOrEmpty(imagePath))
+            {
+                throw new ResourceNotFoundException();
+            }
+
+            // See if we can avoid a file system lookup by looking for the file in ResolveArgs
+            var originalFileImageDateModified = kernel.ImageManager.GetImageDateModified(item, request.Type, index);
+
+            var supportedImageEnhancers = kernel.ImageEnhancers.Where(i => i.Supports(item, request.Type)).ToList();
+
+            // If the file does not exist GetLastWriteTimeUtc will return jan 1, 1601 as opposed to throwing an exception
+            // http://msdn.microsoft.com/en-us/library/system.io.file.getlastwritetimeutc.aspx
+            if (originalFileImageDateModified.Year == 1601 && !File.Exists(imagePath))
+            {
+                throw new ResourceNotFoundException(string.Format("File not found: {0}", imagePath));
+            }
+
+            var contentType = MimeTypes.GetMimeType(imagePath);
+            var dateLastModified = (supportedImageEnhancers.Select(e => e.LastConfigurationChange(item, request.Type)).Concat(new[] { originalFileImageDateModified })).Max();
+
+            var cacheGuid = kernel.ImageManager.GetImageCacheTag(imagePath, originalFileImageDateModified, supportedImageEnhancers, item, request.Type);
+
+            TimeSpan? cacheDuration = null;
+
+            if (!string.IsNullOrEmpty(request.Tag) && cacheGuid == new Guid(request.Tag))
+            {
+                cacheDuration = TimeSpan.FromDays(365);
+            }
+
+            return ToCachedResult(cacheGuid, dateLastModified, cacheDuration, () => new ImageWriter
+            {
+                Item = item,
+                Request = request,
+                CropWhiteSpace = request.Type == ImageType.Logo || request.Type == ImageType.Art,
+                OriginalImageDateModified = originalFileImageDateModified,
+                ContentType = contentType
+
+            }, contentType);
+        }
+
+        /// <summary>
+        /// Gets the image path.
+        /// </summary>
+        /// <param name="kernel">The kernel.</param>
+        /// <param name="request">The request.</param>
+        /// <param name="item">The item.</param>
+        /// <returns>System.String.</returns>
+        private string GetImagePath(Kernel kernel, ImageRequest request, BaseItem item)
+        {
+            var index = request.Index ?? 0;
+
+            return kernel.ImageManager.GetImagePath(item, request.Type, index);
+        }
+    }
+}

+ 80 - 0
MediaBrowser.Api/Images/ImageWriter.cs

@@ -0,0 +1,80 @@
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using ServiceStack.Service;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Images
+{
+    /// <summary>
+    /// Class ImageWriter
+    /// </summary>
+    public class ImageWriter : IStreamWriter, IHasOptions
+    {
+        /// <summary>
+        /// Gets or sets the request.
+        /// </summary>
+        /// <value>The request.</value>
+        public ImageRequest Request { get; set; }
+        /// <summary>
+        /// Gets or sets the item.
+        /// </summary>
+        /// <value>The item.</value>
+        public BaseItem Item { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether [crop white space].
+        /// </summary>
+        /// <value><c>true</c> if [crop white space]; otherwise, <c>false</c>.</value>
+        public bool CropWhiteSpace { get; set; }
+        /// <summary>
+        /// The original image date modified
+        /// </summary>
+        public DateTime OriginalImageDateModified;
+        /// <summary>
+        /// Gets or sets the type of the content.
+        /// </summary>
+        /// <value>The type of the content.</value>
+        public string ContentType { get; set; }
+
+        /// <summary>
+        /// Writes to.
+        /// </summary>
+        /// <param name="responseStream">The response stream.</param>
+        public void WriteTo(Stream responseStream)
+        {
+            Options["Content-Type"] = ContentType;
+
+            var task = WriteToAsync(responseStream);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Writes to async.
+        /// </summary>
+        /// <param name="responseStream">The response stream.</param>
+        /// <returns>Task.</returns>
+        private Task WriteToAsync(Stream responseStream)
+        {
+            return Kernel.Instance.ImageManager.ProcessImage(Item, Request.Type, Request.Index ?? 0, CropWhiteSpace,
+                                                    OriginalImageDateModified, responseStream, Request.Width, Request.Height, Request.MaxWidth,
+                                                    Request.MaxHeight, Request.Quality);
+        }
+
+        /// <summary>
+        /// The _options
+        /// </summary>
+        private readonly IDictionary<string, string> _options = new Dictionary<string, string> { };
+        /// <summary>
+        /// Gets the options.
+        /// </summary>
+        /// <value>The options.</value>
+        public IDictionary<string, string> Options
+        {
+            get { return _options; }
+        }
+    }
+}

+ 143 - 0
MediaBrowser.Api/Images/UploadImageHandler.cs

@@ -0,0 +1,143 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Images
+{
+    /// <summary>
+    /// Class UploadImageHandler
+    /// </summary>
+    [Export(typeof(IHttpServerHandler))]
+    class UploadImageHandler : BaseActionHandler<Kernel>
+    {
+        /// <summary>
+        /// The _source entity
+        /// </summary>
+        private BaseItem _sourceEntity;
+
+        /// <summary>
+        /// Gets the source entity.
+        /// </summary>
+        /// <returns>Task{BaseItem}.</returns>
+        private async Task<BaseItem> GetSourceEntity()
+        {
+            if (_sourceEntity == null)
+            {
+                if (!string.IsNullOrEmpty(QueryString["personname"]))
+                {
+                    _sourceEntity =
+                        await Kernel.LibraryManager.GetPerson(QueryString["personname"]).ConfigureAwait(false);
+                }
+
+                else if (!string.IsNullOrEmpty(QueryString["genre"]))
+                {
+                    _sourceEntity =
+                        await Kernel.LibraryManager.GetGenre(QueryString["genre"]).ConfigureAwait(false);
+                }
+
+                else if (!string.IsNullOrEmpty(QueryString["year"]))
+                {
+                    _sourceEntity =
+                        await
+                        Kernel.LibraryManager.GetYear(int.Parse(QueryString["year"])).ConfigureAwait(false);
+                }
+
+                else if (!string.IsNullOrEmpty(QueryString["studio"]))
+                {
+                    _sourceEntity =
+                        await Kernel.LibraryManager.GetStudio(QueryString["studio"]).ConfigureAwait(false);
+                }
+
+                else if (!string.IsNullOrEmpty(QueryString["userid"]))
+                {
+                    _sourceEntity = ApiService.GetUserById(QueryString["userid"]);
+                }
+
+                else
+                {
+                    _sourceEntity = DtoBuilder.GetItemByClientId(QueryString["id"]);
+                }
+            }
+
+            return _sourceEntity;
+        }
+
+        /// <summary>
+        /// Gets the type of the image.
+        /// </summary>
+        /// <value>The type of the image.</value>
+        private ImageType ImageType
+        {
+            get
+            {
+                var imageType = QueryString["type"];
+
+                return (ImageType)Enum.Parse(typeof(ImageType), imageType, true);
+            }
+        }
+
+        /// <summary>
+        /// Performs the action.
+        /// </summary>
+        /// <returns>Task.</returns>
+        protected override async Task ExecuteAction()
+        {
+            var entity = await GetSourceEntity().ConfigureAwait(false);
+
+            using (var reader = new StreamReader(HttpListenerContext.Request.InputStream))
+            {
+                var text = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+                var bytes = Convert.FromBase64String(text);
+
+                string filename;
+
+                switch (ImageType)
+                {
+                    case ImageType.Art:
+                        filename = "clearart";
+                        break;
+                    case ImageType.Primary:
+                        filename = "folder";
+                        break;
+                    default:
+                        filename = ImageType.ToString().ToLower();
+                        break;
+                }
+
+                // Use the client filename to determine the original extension
+                var clientFileName = QueryString["filename"];
+
+                var oldImagePath = entity.GetImage(ImageType);
+
+                var imagePath = Path.Combine(entity.MetaLocation, filename + Path.GetExtension(clientFileName));
+
+                // Save to file system
+                using (var fs = new FileStream(imagePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true))
+                {
+                    await fs.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+                }
+
+                // Set the image
+                entity.SetImage(ImageType, imagePath);
+
+                // If the new and old paths are different, delete the old one
+                if (!string.IsNullOrEmpty(oldImagePath) && !oldImagePath.Equals(imagePath, StringComparison.OrdinalIgnoreCase))
+                {
+                    File.Delete(oldImagePath);
+                }
+
+                // Directory watchers should repeat this, but do a quick refresh first
+                await entity.RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false).ConfigureAwait(false);
+            }
+        }
+    }
+}

+ 250 - 0
MediaBrowser.Api/LibraryService.cs

@@ -0,0 +1,250 @@
+using MediaBrowser.Common.Mef;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Entities;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class GetPhyscialPaths
+    /// </summary>
+    [Route("/Library/PhysicalPaths", "GET")]
+    public class GetPhyscialPaths : IReturn<List<string>>
+    {
+    }
+
+    /// <summary>
+    /// Class GetItemTypes
+    /// </summary>
+    [Route("/Library/ItemTypes", "GET")]
+    public class GetItemTypes : IReturn<List<string>>
+    {
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance has internet provider.
+        /// </summary>
+        /// <value><c>true</c> if this instance has internet provider; otherwise, <c>false</c>.</value>
+        public bool HasInternetProvider { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetPerson
+    /// </summary>
+    [Route("/Library/Persons/{Name}", "GET")]
+    public class GetPerson : IReturn<DtoBaseItem>
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetStudio
+    /// </summary>
+    [Route("/Library/Studios/{Name}", "GET")]
+    public class GetStudio : IReturn<DtoBaseItem>
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetGenre
+    /// </summary>
+    [Route("/Library/Genres/{Name}", "GET")]
+    public class GetGenre : IReturn<DtoBaseItem>
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetYear
+    /// </summary>
+    [Route("/Library/Years/{Year}", "GET")]
+    public class GetYear : IReturn<DtoBaseItem>
+    {
+        /// <summary>
+        /// Gets or sets the year.
+        /// </summary>
+        /// <value>The year.</value>
+        public int Year { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetDefaultVirtualFolders
+    /// </summary>
+    [Route("/Library/DefaultVirtualFolders", "GET")]
+    public class GetDefaultVirtualFolders : IReturn<List<VirtualFolderInfo>>
+    {
+    }
+
+    /// <summary>
+    /// Class LibraryService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class LibraryService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetDefaultVirtualFolders request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var result = kernel.LibraryManager.GetDefaultVirtualFolders().ToList();
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPerson request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var item = kernel.LibraryManager.GetPerson(request.Name).Result;
+
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
+            
+            var result = DtoBuilder.GetDtoBaseItem(item, fields.ToList()).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetGenre request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var item = kernel.LibraryManager.GetGenre(request.Name).Result;
+
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
+
+            var result = DtoBuilder.GetDtoBaseItem(item, fields.ToList()).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetStudio request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var item = kernel.LibraryManager.GetStudio(request.Name).Result;
+
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
+
+            var result = DtoBuilder.GetDtoBaseItem(item, fields.ToList()).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetYear request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var item = kernel.LibraryManager.GetYear(request.Year).Result;
+
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
+
+            var result = DtoBuilder.GetDtoBaseItem(item, fields.ToList()).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPhyscialPaths request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var result = kernel.RootFolder.Children.SelectMany(c => c.ResolveArgs.PhysicalLocations).ToList();
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetItemTypes request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var allTypes = kernel.Assemblies.SelectMany(MefUtils.GetTypes).Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(BaseItem)));
+
+            if (request.HasInternetProvider)
+            {
+                allTypes = allTypes.Where(t =>
+                {
+                    if (t == typeof(UserRootFolder) || t == typeof(AggregateFolder) || t == typeof(Folder) || t == typeof(IndexFolder) || t == typeof(CollectionFolder) || t == typeof(Year))
+                    {
+                        return false;
+                    }
+
+                    if (t == typeof(User))
+                    {
+                        return false;
+                    }
+
+                    // For now it seems internet providers generally only deal with video subclasses
+                    if (t == typeof(Video))
+                    {
+                        return false;
+                    }
+
+                    if (t.IsSubclassOf(typeof(BasePluginFolder)))
+                    {
+                        return false;
+                    }
+
+                    return true;
+                });
+            }
+
+            return allTypes.Select(t => t.Name).OrderBy(s => s).ToList();
+        }
+    }
+}

+ 112 - 0
MediaBrowser.Api/LocalizationService.cs

@@ -0,0 +1,112 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Localization;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MoreLinq;
+using ServiceStack.ServiceHost;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Globalization;
+using System.Linq;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class GetCultures
+    /// </summary>
+    [Route("/Localization/Cultures", "GET")]
+    public class GetCultures : IReturn<List<CultureDto>>
+    {
+    }
+
+    /// <summary>
+    /// Class GetCountries
+    /// </summary>
+    [Route("/Localization/Countries", "GET")]
+    public class GetCountries : IReturn<List<CountryInfo>>
+    {
+    }
+
+    /// <summary>
+    /// Class ParentalRatings
+    /// </summary>
+    [Route("/Localization/ParentalRatings", "GET")]
+    public class GetParentalRatings : IReturn<List<ParentalRating>>
+    {
+    }
+
+    /// <summary>
+    /// Class CulturesService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class LocalizationService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetParentalRatings request)
+        {
+            var ratings =
+                Ratings.RatingsDict.Select(k => new ParentalRating { Name = k.Key, Value = k.Value });
+
+            var result = ratings.OrderBy(p => p.Value).Where(p => p.Value > 0).ToList();
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetCountries request)
+        {
+            var result = CultureInfo.GetCultures(CultureTypes.SpecificCultures)
+
+                .Select(c => new RegionInfo(c.LCID))
+                .OrderBy(c => c.DisplayName)
+
+                // Try to eliminate dupes
+                .DistinctBy(c => c.TwoLetterISORegionName)
+
+                .Select(c => new CountryInfo
+                {
+                    Name = c.Name,
+                    DisplayName = c.DisplayName,
+                    TwoLetterISORegionName = c.TwoLetterISORegionName,
+                    ThreeLetterISORegionName = c.ThreeLetterISORegionName
+                })
+                .ToList();
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetCultures request)
+        {
+            var result = CultureInfo.GetCultures(CultureTypes.AllCultures)
+                .OrderBy(c => c.DisplayName)
+
+                // Try to eliminate dupes
+                .DistinctBy(c => c.TwoLetterISOLanguageName + c.ThreeLetterISOLanguageName)
+
+                .Select(c => new CultureDto
+                {
+                    Name = c.Name,
+                    DisplayName = c.DisplayName,
+                    ThreeLetterISOLanguageName = c.ThreeLetterISOLanguageName,
+                    TwoLetterISOLanguageName = c.TwoLetterISOLanguageName
+                })
+                .ToList();
+
+            return ToOptimizedResult(result);
+        }
+    }
+
+}

+ 160 - 116
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -1,117 +1,161 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
-  <PropertyGroup>
-    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
-    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
-    <ProjectGuid>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</ProjectGuid>
-    <OutputType>Library</OutputType>
-    <AppDesignerFolder>Properties</AppDesignerFolder>
-    <RootNamespace>MediaBrowser.Api</RootNamespace>
-    <AssemblyName>MediaBrowser.Api</AssemblyName>
-    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
-    <FileAlignment>512</FileAlignment>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
-    <DebugSymbols>true</DebugSymbols>
-    <DebugType>full</DebugType>
-    <Optimize>false</Optimize>
-    <OutputPath>bin\Debug\</OutputPath>
-    <DefineConstants>DEBUG;TRACE</DefineConstants>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
-    <DebugType>pdbonly</DebugType>
-    <Optimize>true</Optimize>
-    <OutputPath>bin\Release\</OutputPath>
-    <DefineConstants>TRACE</DefineConstants>
-    <ErrorReport>prompt</ErrorReport>
-    <WarningLevel>4</WarningLevel>
-  </PropertyGroup>
-  <PropertyGroup>
-    <RunPostBuildEvent>Always</RunPostBuildEvent>
-  </PropertyGroup>
-  <ItemGroup>
-    <Reference Include="System" />
-    <Reference Include="System.ComponentModel.Composition" />
-    <Reference Include="System.Core" />
-    <Reference Include="System.Drawing" />
-    <Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\Rx-Core.2.0.20823\lib\Net45\System.Reactive.Core.dll</HintPath>
-    </Reference>
-    <Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\Rx-Interfaces.2.0.20823\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
-    </Reference>
-    <Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
-      <SpecificVersion>False</SpecificVersion>
-      <HintPath>..\packages\Rx-Linq.2.0.20823\lib\Net45\System.Reactive.Linq.dll</HintPath>
-    </Reference>
-    <Reference Include="System.Runtime.Serialization" />
-    <Reference Include="System.Xml.Linq" />
-    <Reference Include="System.Data.DataSetExtensions" />
-    <Reference Include="Microsoft.CSharp" />
-    <Reference Include="System.Data" />
-    <Reference Include="System.Xml" />
-  </ItemGroup>
-  <ItemGroup>
-    <Compile Include="ApiService.cs" />
-    <Compile Include="HttpHandlers\AudioHandler.cs" />
-    <Compile Include="HttpHandlers\BaseMediaHandler.cs" />
-    <Compile Include="HttpHandlers\FavoriteStatusHandler.cs" />
-    <Compile Include="HttpHandlers\MovieSpecialFeaturesHandler.cs" />
-    <Compile Include="HttpHandlers\PlayedStatusHandler.cs" />
-    <Compile Include="HttpHandlers\UserHandler.cs" />
-    <Compile Include="HttpHandlers\GenreHandler.cs" />
-    <Compile Include="HttpHandlers\GenresHandler.cs" />
-    <Compile Include="HttpHandlers\ImageHandler.cs" />
-    <Compile Include="HttpHandlers\ItemHandler.cs" />
-    <Compile Include="HttpHandlers\ItemListHandler.cs" />
-    <Compile Include="HttpHandlers\PersonHandler.cs" />
-    <Compile Include="HttpHandlers\PluginAssemblyHandler.cs" />
-    <Compile Include="HttpHandlers\PluginConfigurationHandler.cs" />
-    <Compile Include="HttpHandlers\PluginsHandler.cs" />
-    <Compile Include="HttpHandlers\ServerConfigurationHandler.cs" />
-    <Compile Include="HttpHandlers\StudioHandler.cs" />
-    <Compile Include="HttpHandlers\StudiosHandler.cs" />
-    <Compile Include="HttpHandlers\UserAuthenticationHandler.cs" />
-    <Compile Include="HttpHandlers\UserItemRatingHandler.cs" />
-    <Compile Include="HttpHandlers\UsersHandler.cs" />
-    <Compile Include="HttpHandlers\VideoHandler.cs" />
-    <Compile Include="HttpHandlers\WeatherHandler.cs" />
-    <Compile Include="HttpHandlers\YearHandler.cs" />
-    <Compile Include="HttpHandlers\YearsHandler.cs" />
-    <Compile Include="Plugin.cs" />
-    <Compile Include="Properties\AssemblyInfo.cs" />
-  </ItemGroup>
-  <ItemGroup>
-    <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
-      <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
-      <Name>MediaBrowser.Common</Name>
-    </ProjectReference>
-    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
-      <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
-      <Name>MediaBrowser.Controller</Name>
-    </ProjectReference>
-    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
-      <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
-      <Name>MediaBrowser.Model</Name>
-    </ProjectReference>
-  </ItemGroup>
-  <ItemGroup>
-    <None Include="packages.config" />
-  </ItemGroup>
-  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
-  <PropertyGroup>
-    <PostBuildEvent>xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData-Server\Plugins\" /y</PostBuildEvent>
-  </PropertyGroup>
-  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
-       Other similar extension points exist, see Microsoft.Common.targets.
-  <Target Name="BeforeBuild">
-  </Target>
-  <Target Name="AfterBuild">
-  </Target>
-  -->
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>MediaBrowser.Api</RootNamespace>
+    <AssemblyName>MediaBrowser.Api</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
+    <RestorePackages>true</RestorePackages>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <PlatformTarget>AnyCPU</PlatformTarget>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup>
+    <RunPostBuildEvent>Always</RunPostBuildEvent>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="MoreLinq">
+      <HintPath>..\packages\morelinq.1.0.15631-beta\lib\net35\MoreLinq.dll</HintPath>
+    </Reference>
+    <Reference Include="protobuf-net, Version=2.0.0.621, Culture=neutral, PublicKeyToken=257b51d87d2e4d67, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\protobuf-net.2.0.0.621\lib\net40\protobuf-net.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack">
+      <HintPath>..\packages\ServiceStack.3.9.37\lib\net35\ServiceStack.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Common">
+      <HintPath>..\packages\ServiceStack.Common.3.9.37\lib\net35\ServiceStack.Common.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Interfaces">
+      <HintPath>..\packages\ServiceStack.Common.3.9.37\lib\net35\ServiceStack.Interfaces.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.OrmLite">
+      <HintPath>..\packages\ServiceStack.OrmLite.SqlServer.3.9.37\lib\ServiceStack.OrmLite.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.OrmLite.SqlServer">
+      <HintPath>..\packages\ServiceStack.OrmLite.SqlServer.3.9.37\lib\ServiceStack.OrmLite.SqlServer.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Redis">
+      <HintPath>..\packages\ServiceStack.Redis.3.9.37\lib\net35\ServiceStack.Redis.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.ServiceInterface">
+      <HintPath>..\packages\ServiceStack.3.9.37\lib\net35\ServiceStack.ServiceInterface.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Text">
+      <HintPath>..\packages\ServiceStack.Text.3.9.37\lib\net35\ServiceStack.Text.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.ComponentModel.Composition" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Deployment" />
+    <Reference Include="System.Drawing" />
+    <Reference Include="System.Reactive.Core, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\Rx-Core.2.0.21114\lib\Net45\System.Reactive.Core.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Reactive.Interfaces, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\Rx-Interfaces.2.0.21114\lib\Net45\System.Reactive.Interfaces.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Reactive.Linq, Version=2.0.20823.0, Culture=neutral, PublicKeyToken=f300afd708cefcd3, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\Rx-Linq.2.0.21114\lib\Net45\System.Reactive.Linq.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Runtime.Serialization" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="ApiService.cs" />
+    <Compile Include="EnvironmentService.cs" />
+    <Compile Include="HttpHandlers\PlaybackCheckInHandler.cs" />
+    <Compile Include="Images\ImageRequest.cs" />
+    <Compile Include="Images\ImageService.cs" />
+    <Compile Include="Images\ImageWriter.cs" />
+    <Compile Include="Images\UploadImageHandler.cs" />
+    <Compile Include="LibraryService.cs" />
+    <Compile Include="LocalizationService.cs" />
+    <Compile Include="PackageService.cs" />
+    <Compile Include="PluginService.cs" />
+    <Compile Include="SystemService.cs" />
+    <Compile Include="Streaming\AudioHandler.cs" />
+    <Compile Include="Streaming\BaseHlsPlaylistHandler.cs" />
+    <Compile Include="Streaming\BaseProgressiveStreamingHandler.cs" />
+    <Compile Include="Streaming\BaseStreamingHandler.cs" />
+    <Compile Include="UserLibrary\BaseItemsByNameService.cs" />
+    <Compile Include="UserLibrary\GenresService.cs" />
+    <Compile Include="UserLibrary\ItemsService.cs" />
+    <Compile Include="UserLibrary\PersonsService.cs" />
+    <Compile Include="UserLibrary\StudiosService.cs" />
+    <Compile Include="UserLibrary\UserLibraryService.cs" />
+    <Compile Include="UserLibrary\YearsService.cs" />
+    <Compile Include="UserService.cs" />
+    <Compile Include="Streaming\HlsAudioPlaylistHandler.cs" />
+    <Compile Include="Streaming\HlsSegmentHandler.cs" />
+    <Compile Include="Streaming\HlsVideoPlaylistHandler.cs" />
+    <Compile Include="HttpHandlers\UpdateMediaLibraryHandler.cs" />
+    <Compile Include="Streaming\VideoHandler.cs" />
+    <Compile Include="Plugin.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="WeatherService.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+      <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+      <Name>MediaBrowser.Common</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
+      <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
+      <Name>MediaBrowser.Controller</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+      <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+      <Name>MediaBrowser.Model</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <Content Include="options.xml" />
+  </ItemGroup>
+  <ItemGroup />
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <PropertyGroup>
+    <PostBuildEvent>xcopy "$(TargetPath)" "$(SolutionDir)\MediaBrowser.ServerApplication\" /y</PostBuildEvent>
+  </PropertyGroup>
+  <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
 </Project>

+ 201 - 0
MediaBrowser.Api/PackageService.cs

@@ -0,0 +1,201 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Tasks;
+using MediaBrowser.Model.Updates;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class GetPackage
+    /// </summary>
+    [Route("/Packages/{Name}", "GET")]
+    public class GetPackage : IReturn<PackageInfo>
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetPackages
+    /// </summary>
+    [Route("/Packages", "GET")]
+    public class GetPackages : IReturn<List<PackageInfo>>
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public PackageType? PackageType { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetPackageVersionUpdates
+    /// </summary>
+    [Route("/Packages/Updates", "GET")]
+    public class GetPackageVersionUpdates : IReturn<List<PackageVersionInfo>>
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public PackageType PackageType { get; set; }
+    }
+
+    /// <summary>
+    /// Class InstallPackage
+    /// </summary>
+    [Route("/Packages/Installed/{Name}", "POST")]
+    public class InstallPackage : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the version.
+        /// </summary>
+        /// <value>The version.</value>
+        public string Version { get; set; }
+
+        /// <summary>
+        /// Gets or sets the update class.
+        /// </summary>
+        /// <value>The update class.</value>
+        public PackageVersionClass UpdateClass { get; set; }
+    }
+
+    /// <summary>
+    /// Class CancelPackageInstallation
+    /// </summary>
+    [Route("/Packages/Installing/{Id}", "DELETE")]
+    public class CancelPackageInstallation : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class PackageService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class PackageService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        /// <exception cref="System.ArgumentException">Unsupported PackageType</exception>
+        public object Get(GetPackageVersionUpdates request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var result = new List<PackageVersionInfo>();
+
+            if (request.PackageType == PackageType.UserInstalled || request.PackageType == PackageType.All)
+            {
+                result.AddRange(kernel.InstallationManager.GetAvailablePluginUpdates(false, CancellationToken.None).Result.ToList());
+            }
+
+            else if (request.PackageType == PackageType.System || request.PackageType == PackageType.All)
+            {
+                var updateCheckResult = new ApplicationUpdateCheck().CheckForApplicationUpdate(CancellationToken.None, new Progress<TaskProgress> { }).Result;
+
+                if (updateCheckResult.UpdateAvailable)
+                {
+                    result.Add(new PackageVersionInfo
+                    {
+                        versionStr = updateCheckResult.AvailableVersion.ToString()
+                    });
+                }
+            }
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPackage request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var packages = kernel.InstallationManager.GetAvailablePackages(CancellationToken.None, applicationVersion: kernel.ApplicationVersion).Result;
+
+            var result = packages.FirstOrDefault(p => p.name.Equals(request.Name, StringComparison.OrdinalIgnoreCase));
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPackages request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var packages = kernel.InstallationManager.GetAvailablePackages(CancellationToken.None, request.PackageType, kernel.ApplicationVersion).Result;
+
+            return ToOptimizedResult(packages.ToList());
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <exception cref="ResourceNotFoundException"></exception>
+        public void Post(InstallPackage request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var package = string.IsNullOrEmpty(request.Version) ?
+                kernel.InstallationManager.GetLatestCompatibleVersion(request.Name, request.UpdateClass).Result :
+                kernel.InstallationManager.GetPackage(request.Name, request.UpdateClass, Version.Parse(request.Version)).Result;
+
+            if (package == null)
+            {
+                throw new ResourceNotFoundException(string.Format("Package not found: {0}", request.Name));
+            }
+
+            Task.Run(() => kernel.InstallationManager.InstallPackage(package, new Progress<double> { }, CancellationToken.None));
+        }
+
+        /// <summary>
+        /// Deletes the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Delete(CancelPackageInstallation request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var info = kernel.InstallationManager.CurrentInstallations.FirstOrDefault(i => i.Item1.Id == request.Id);
+
+            if (info != null)
+            {
+                info.Item2.Cancel();
+            }
+        }
+    }
+
+}

+ 329 - 14
MediaBrowser.Api/Plugin.cs

@@ -1,14 +1,329 @@
-using MediaBrowser.Common.Plugins;
-using System.ComponentModel.Composition;
-
-namespace MediaBrowser.Api
-{
-    [Export(typeof(BasePlugin))]
-    public class Plugin : BasePlugin
-    {
-        public override string Name
-        {
-            get { return "Media Browser API"; }
-        }
-    }
-}
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.Composition;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class Plugin
+    /// </summary>
+    [Export(typeof(IPlugin))]
+    public class Plugin : BasePlugin<BasePluginConfiguration>
+    {
+        /// <summary>
+        /// Gets the name of the plugin
+        /// </summary>
+        /// <value>The name.</value>
+        public override string Name
+        {
+            get { return "Web Api"; }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether this instance is a core plugin.
+        /// </summary>
+        /// <value><c>true</c> if this instance is a core plugin; otherwise, <c>false</c>.</value>
+        public override bool IsCorePlugin
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Gets the instance.
+        /// </summary>
+        /// <value>The instance.</value>
+        public static Plugin Instance { get; private set; }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Plugin" /> class.
+        /// </summary>
+        public Plugin()
+        {
+            Instance = this;
+        }
+
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources.
+        /// </summary>
+        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected override void DisposeOnServer(bool dispose)
+        {
+            if (dispose)
+            {
+                var jobCount = ActiveTranscodingJobs.Count;
+
+                Parallel.ForEach(ActiveTranscodingJobs, OnTranscodeKillTimerStopped);
+             
+                // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files
+                if (jobCount > 0)
+                {
+                    Thread.Sleep(1000);
+                }
+            }
+
+            base.DisposeOnServer(dispose);
+        }
+
+        /// <summary>
+        /// The active transcoding jobs
+        /// </summary>
+        private readonly List<TranscodingJob> ActiveTranscodingJobs = new List<TranscodingJob>();
+
+        /// <summary>
+        /// Called when [transcode beginning].
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="process">The process.</param>
+        public void OnTranscodeBeginning(string path, TranscodingJobType type, Process process)
+        {
+            lock (ActiveTranscodingJobs)
+            {
+                ActiveTranscodingJobs.Add(new TranscodingJob
+                {
+                    Type = type,
+                    Path = path,
+                    Process = process,
+                    ActiveRequestCount = 1
+                });
+            }
+        }
+
+        /// <summary>
+        /// Called when [transcode failed to start].
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="type">The type.</param>
+        public void OnTranscodeFailedToStart(string path, TranscodingJobType type)
+        {
+            lock (ActiveTranscodingJobs)
+            {
+                var job = ActiveTranscodingJobs.First(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+
+                ActiveTranscodingJobs.Remove(job);
+            }
+        }
+
+        /// <summary>
+        /// Determines whether [has active transcoding job] [the specified path].
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="type">The type.</param>
+        /// <returns><c>true</c> if [has active transcoding job] [the specified path]; otherwise, <c>false</c>.</returns>
+        public bool HasActiveTranscodingJob(string path, TranscodingJobType type)
+        {
+            lock (ActiveTranscodingJobs)
+            {
+                return ActiveTranscodingJobs.Any(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+            }
+        }
+
+        /// <summary>
+        /// Called when [transcode begin request].
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="type">The type.</param>
+        public void OnTranscodeBeginRequest(string path, TranscodingJobType type)
+        {
+            lock (ActiveTranscodingJobs)
+            {
+                var job = ActiveTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+
+                if (job == null)
+                {
+                    return;
+                }
+
+                job.ActiveRequestCount++;
+
+                if (job.KillTimer != null)
+                {
+                    job.KillTimer.Dispose();
+                    job.KillTimer = null;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Called when [transcode end request].
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="type">The type.</param>
+        public void OnTranscodeEndRequest(string path, TranscodingJobType type)
+        {
+            lock (ActiveTranscodingJobs)
+            {
+                var job = ActiveTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+
+                if (job == null)
+                {
+                    return;   
+                }
+
+                job.ActiveRequestCount--;
+
+                if (job.ActiveRequestCount == 0)
+                {
+                    var timerDuration = type == TranscodingJobType.Progressive ? 1000 : 30000;
+
+                    if (job.KillTimer == null)
+                    {
+                        job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite);
+                    }
+                    else
+                    {
+                        job.KillTimer.Change(timerDuration, Timeout.Infinite);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Called when [transcoding finished].
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="type">The type.</param>
+        public void OnTranscodingFinished(string path, TranscodingJobType type)
+        {
+            lock (ActiveTranscodingJobs)
+            {
+                var job = ActiveTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+
+                if (job == null)
+                {
+                    return;
+                }
+
+                ActiveTranscodingJobs.Remove(job);
+
+                if (job.KillTimer != null)
+                {
+                    job.KillTimer.Dispose();
+                    job.KillTimer = null;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Called when [transcode kill timer stopped].
+        /// </summary>
+        /// <param name="state">The state.</param>
+        private void OnTranscodeKillTimerStopped(object state)
+        {
+            var job = (TranscodingJob)state;
+
+            lock (ActiveTranscodingJobs)
+            {
+                ActiveTranscodingJobs.Remove(job);
+
+                if (job.KillTimer != null)
+                {
+                    job.KillTimer.Dispose();
+                    job.KillTimer = null;
+                }
+            }
+            
+            var process = job.Process;
+
+            var hasExited = true;
+
+            try
+            {
+                hasExited = process.HasExited;
+            }
+            catch (Win32Exception ex)
+            {
+                Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
+            }
+            catch (InvalidOperationException ex)
+            {
+                Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
+            }
+            catch (NotSupportedException ex)
+            {
+                Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
+            }
+
+            if (hasExited)
+            {
+                return;
+            }
+
+            try
+            {
+                Logger.Info("Killing ffmpeg process for {0}", job.Path);
+
+                process.Kill();
+            }
+            catch (Win32Exception ex)
+            {
+                Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+            }
+            catch (InvalidOperationException ex)
+            {
+                Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+            }
+            catch (NotSupportedException ex)
+            {
+                Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+            }
+        }
+    }
+
+    /// <summary>
+    /// Class TranscodingJob
+    /// </summary>
+    public class TranscodingJob
+    {
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        public string Path { get; set; }
+        /// <summary>
+        /// Gets or sets the type.
+        /// </summary>
+        /// <value>The type.</value>
+        public TranscodingJobType Type { get; set; }
+        /// <summary>
+        /// Gets or sets the process.
+        /// </summary>
+        /// <value>The process.</value>
+        public Process Process { get; set; }
+        /// <summary>
+        /// Gets or sets the active request count.
+        /// </summary>
+        /// <value>The active request count.</value>
+        public int ActiveRequestCount { get; set; }
+        /// <summary>
+        /// Gets or sets the kill timer.
+        /// </summary>
+        /// <value>The kill timer.</value>
+        public Timer KillTimer { get; set; }
+    }
+
+    /// <summary>
+    /// Enum TranscodingJobType
+    /// </summary>
+    public enum TranscodingJobType
+    {
+        /// <summary>
+        /// The progressive
+        /// </summary>
+        Progressive,
+        /// <summary>
+        /// The HLS
+        /// </summary>
+        Hls
+    }
+}

+ 241 - 0
MediaBrowser.Api/PluginService.cs

@@ -0,0 +1,241 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Plugins;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using ServiceStack.Text.Controller;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class Plugins
+    /// </summary>
+    [Route("/Plugins", "GET")]
+    public class GetPlugins : IReturn<List<PluginInfo>>
+    {
+    }
+
+    /// <summary>
+    /// Class GetPluginAssembly
+    /// </summary>
+    [Route("/Plugins/{Id}/Assembly", "GET")]
+    public class GetPluginAssembly
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class UninstallPlugin
+    /// </summary>
+    [Route("/Plugins/{Id}", "DELETE")]
+    public class UninstallPlugin : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetPluginConfiguration
+    /// </summary>
+    [Route("/Plugins/{Id}/Configuration", "GET")]
+    public class GetPluginConfiguration
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class UpdatePluginConfiguration
+    /// </summary>
+    [Route("/Plugins/{Id}/Configuration", "POST")]
+    public class UpdatePluginConfiguration : IRequiresRequestStream, IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+
+        /// <summary>
+        /// The raw Http Request Input Stream
+        /// </summary>
+        /// <value>The request stream.</value>
+        public Stream RequestStream { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetPluginConfigurationFile
+    /// </summary>
+    [Route("/Plugins/{Id}/ConfigurationFile", "GET")]
+    public class GetPluginConfigurationFile
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetPluginSecurityInfo
+    /// </summary>
+    [Route("/Plugins/SecurityInfo", "GET")]
+    [Restrict(VisibleLocalhostOnly = true)]
+    public class GetPluginSecurityInfo : IReturn<PluginSecurityInfo>
+    {
+    }
+
+    /// <summary>
+    /// Class UpdatePluginSecurityInfo
+    /// </summary>
+    [Route("/Plugins/SecurityInfo", "GET")]
+    public class UpdatePluginSecurityInfo : IReturnVoid, IRequiresRequestStream
+    {
+        /// <summary>
+        /// The raw Http Request Input Stream
+        /// </summary>
+        /// <value>The request stream.</value>
+        public Stream RequestStream { get; set; }
+    }
+
+    /// <summary>
+    /// Class PluginsService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class PluginService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPlugins request)
+        {
+            var result = Kernel.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()).ToList();
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPluginAssembly request)
+        {
+            var plugin = Kernel.Plugins.First(p => p.UniqueId == request.Id);
+
+            return ToStaticFileResult(plugin.AssemblyFilePath);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPluginConfiguration request)
+        {
+            var plugin = Kernel.Plugins.First(p => p.UniqueId == request.Id);
+
+            var dateModified = plugin.ConfigurationDateLastModified;
+
+            var cacheKey = (plugin.Version.ToString() + dateModified.Ticks).GetMD5();
+
+            return ToOptimizedResultUsingCache(cacheKey, dateModified, null, () => plugin.Configuration);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPluginConfigurationFile request)
+        {
+            var plugin = Kernel.Plugins.First(p => p.UniqueId == request.Id);
+
+            return ToStaticFileResult(plugin.ConfigurationFilePath);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPluginSecurityInfo request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var result = new PluginSecurityInfo
+            {
+                IsMBSupporter = kernel.PluginSecurityManager.IsMBSupporter,
+                SupporterKey = kernel.PluginSecurityManager.SupporterKey,
+                LegacyKey = kernel.PluginSecurityManager.LegacyKey
+            };
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(UpdatePluginSecurityInfo request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var info = JsonSerializer.DeserializeFromStream<PluginSecurityInfo>(request.RequestStream);
+
+            kernel.PluginSecurityManager.SupporterKey = info.SupporterKey;
+            kernel.PluginSecurityManager.LegacyKey = info.LegacyKey;
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(UpdatePluginConfiguration request)
+        {
+            // We need to parse this manually because we told service stack not to with IRequiresRequestStream
+            // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
+            var pathInfo = PathInfo.Parse(Request.PathInfo);
+            var id = new Guid(pathInfo.GetArgumentValue<string>(1));
+
+            var plugin = Kernel.Plugins.First(p => p.UniqueId == id);
+
+            var configuration = JsonSerializer.DeserializeFromStream(request.RequestStream, plugin.ConfigurationType) as BasePluginConfiguration;
+
+            plugin.UpdateConfiguration(configuration);
+        }
+
+        /// <summary>
+        /// Deletes the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Delete(UninstallPlugin request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var plugin = kernel.Plugins.First(p => p.UniqueId == request.Id);
+
+            kernel.InstallationManager.UninstallPlugin(plugin);
+        }
+    }
+}

+ 34 - 35
MediaBrowser.Api/Properties/AssemblyInfo.cs

@@ -1,35 +1,34 @@
-using System.Reflection;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following 
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("MediaBrowser.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.*")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.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("2.9.*")]

+ 112 - 0
MediaBrowser.Api/Streaming/AudioHandler.cs

@@ -0,0 +1,112 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.DTO;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+
+namespace MediaBrowser.Api.Streaming
+{
+    /// <summary>
+    /// Providers a progressive streaming audio api
+    /// </summary>
+    [Export(typeof(IHttpServerHandler))]
+    public class AudioHandler : BaseProgressiveStreamingHandler<Audio>
+    {
+        /// <summary>
+        /// Handleses the request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return EntityResolutionHelper.AudioFileExtensions.Any(a => ApiService.IsApiUrlMatch("audio" + a, request));
+        }
+
+        /// <summary>
+        /// Gets the audio codec.
+        /// </summary>
+        /// <value>The audio codec.</value>
+        /// <exception cref="InvalidOperationException"></exception>
+        protected override AudioCodecs? AudioCodec
+        {
+            get
+            {
+                var ext = OutputFileExtension;
+
+                if (ext.Equals(".aac", StringComparison.OrdinalIgnoreCase) || ext.Equals(".m4a", StringComparison.OrdinalIgnoreCase))
+                {
+                    return AudioCodecs.Aac;
+                }
+                if (ext.Equals(".mp3", StringComparison.OrdinalIgnoreCase))
+                {
+                    return AudioCodecs.Mp3;
+                }
+                if (ext.Equals(".wma", StringComparison.OrdinalIgnoreCase))
+                {
+                    return AudioCodecs.Wma;
+                }
+                if (ext.Equals(".oga", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase))
+                {
+                    return AudioCodecs.Vorbis;
+                }
+
+                throw new InvalidOperationException();
+            }
+        }
+
+        /// <summary>
+        /// Creates arguments to pass to ffmpeg
+        /// </summary>
+        /// <param name="outputPath">The output path.</param>
+        /// <param name="isoMount">The iso mount.</param>
+        /// <returns>System.String.</returns>
+        /// <exception cref="System.InvalidOperationException">Only aac and mp3 audio codecs are supported.</exception>
+        /// <exception cref="InvalidOperationException">Only aac and mp3 audio codecs are supported.</exception>
+        /// <exception cref="ArgumentException">Only aac and mp3 audio codecs are supported.</exception>
+        protected override string GetCommandLineArguments(string outputPath, IIsoMount isoMount)
+        {
+            var audioTranscodeParams = new List<string>();
+
+            var outputFormat = AudioCodec;
+
+            if (outputFormat != AudioCodecs.Aac && outputFormat != AudioCodecs.Mp3)
+            {
+                throw new InvalidOperationException("Only aac and mp3 audio codecs are supported.");
+            }
+
+            if (AudioBitRate.HasValue)
+            {
+                audioTranscodeParams.Add("-ab " + AudioBitRate.Value);
+            }
+
+            var channels = GetNumAudioChannelsParam();
+
+            if (channels.HasValue)
+            {
+                audioTranscodeParams.Add("-ac " + channels.Value);
+            }
+
+            var sampleRate = GetSampleRateParam();
+
+            if (sampleRate.HasValue)
+            {
+                audioTranscodeParams.Add("-ar " + sampleRate.Value);
+            }
+
+            const string vn = " -vn";
+
+            return string.Format("{0} -i {1}{2} -threads 0{5} {3} -id3v2_version 3 -write_id3v1 1 \"{4}\"",
+                FastSeekCommandLineParameter,
+                GetInputArgument(isoMount),
+                SlowSeekCommandLineParameter,
+                string.Join(" ", audioTranscodeParams.ToArray()),
+                outputPath,
+                vn).Trim();
+        }
+    }
+}

+ 221 - 0
MediaBrowser.Api/Streaming/BaseHlsPlaylistHandler.cs

@@ -0,0 +1,221 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Streaming
+{
+    /// <summary>
+    /// Class BaseHlsPlaylistHandler
+    /// </summary>
+    /// <typeparam name="TBaseItemType">The type of the T base item type.</typeparam>
+    public abstract class BaseHlsPlaylistHandler<TBaseItemType> : BaseStreamingHandler<TBaseItemType>
+        where TBaseItemType : BaseItem, IHasMediaStreams, new()
+    {
+        /// <summary>
+        /// Gets the audio arguments.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        protected abstract string GetAudioArguments();
+        /// <summary>
+        /// Gets the video arguments.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        protected abstract string GetVideoArguments();
+
+        /// <summary>
+        /// Gets the type of the transcoding job.
+        /// </summary>
+        /// <value>The type of the transcoding job.</value>
+        protected override TranscodingJobType TranscodingJobType
+        {
+            get { return TranscodingJobType.Hls; }
+        }
+
+        /// <summary>
+        /// This isn't needed because we're going to override the whole flow using ProcessRequest
+        /// </summary>
+        /// <param name="stream">The stream.</param>
+        /// <param name="responseInfo">The response info.</param>
+        /// <param name="contentLength">Length of the content.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="NotImplementedException"></exception>
+        protected override Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? contentLength)
+        {
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// Gets the segment file extension.
+        /// </summary>
+        /// <value>The segment file extension.</value>
+        protected abstract string SegmentFileExtension { get; }
+
+        /// <summary>
+        /// Processes the request.
+        /// </summary>
+        /// <param name="ctx">The CTX.</param>
+        /// <returns>Task.</returns>
+        public override async Task ProcessRequest(HttpListenerContext ctx)
+        {
+            HttpListenerContext = ctx;
+
+            var playlist = OutputFilePath;
+            var isPlaylistNewlyCreated = false;
+
+            // If the playlist doesn't already exist, startup ffmpeg
+            if (!File.Exists(playlist))
+            {
+                isPlaylistNewlyCreated = true;
+                await StartFFMpeg(playlist).ConfigureAwait(false);
+            }
+            else
+            {
+                Plugin.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls);
+            }
+
+            // Get the current playlist text and convert to bytes
+            var playlistText = await GetPlaylistFileText(playlist, isPlaylistNewlyCreated).ConfigureAwait(false);
+
+            var content = Encoding.UTF8.GetBytes(playlistText);
+
+            var stream = new MemoryStream(content);
+
+            try
+            {
+                // Dump the stream off to the static file handler to serve statically
+                await new StaticFileHandler(Kernel) { ContentType = MimeTypes.GetMimeType("playlist.m3u8"), SourceStream = stream }.ProcessRequest(ctx);
+            }
+            finally
+            {
+                Plugin.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls);
+            }
+        }
+
+        /// <summary>
+        /// Gets the current playlist text
+        /// </summary>
+        /// <param name="playlist">The path to the playlist</param>
+        /// <param name="waitForMinimumSegments">Whether or not we should wait until it contains three segments</param>
+        /// <returns>Task{System.String}.</returns>
+        private async Task<string> GetPlaylistFileText(string playlist, bool waitForMinimumSegments)
+        {
+            string fileText;
+
+            while (true)
+            {
+                // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
+                using (var fileStream = new FileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+                {
+                    using (var reader = new StreamReader(fileStream))
+                    {
+                        fileText = await reader.ReadToEndAsync().ConfigureAwait(false);
+                    }
+                }
+
+                if (!waitForMinimumSegments || CountStringOccurrences(fileText, "#EXTINF:") >= 3)
+                {
+                    break;
+                }
+
+                await Task.Delay(25).ConfigureAwait(false);
+            }
+
+            // The segement paths within the playlist are phsyical, so strip that out to make it relative
+            fileText = fileText.Replace(Path.GetDirectoryName(playlist) + Path.DirectorySeparatorChar, string.Empty);
+
+            // Even though we specify target duration of 9, ffmpeg seems unable to keep all segments under that amount
+            fileText = fileText.Replace("#EXT-X-TARGETDURATION:9", "#EXT-X-TARGETDURATION:10");
+
+            // It's considered live while still encoding (EVENT). Once the encoding has finished, it's video on demand (VOD).
+            var playlistType = fileText.IndexOf("#EXT-X-ENDLIST", StringComparison.OrdinalIgnoreCase) == -1 ? "EVENT" : "VOD";
+
+            // Add event type at the top
+            fileText = fileText.Replace("#EXT-X-ALLOWCACHE", "#EXT-X-PLAYLIST-TYPE:" + playlistType + Environment.NewLine + "#EXT-X-ALLOWCACHE");
+
+            return fileText;
+        }
+
+        /// <summary>
+        /// Count occurrences of strings.
+        /// </summary>
+        /// <param name="text">The text.</param>
+        /// <param name="pattern">The pattern.</param>
+        /// <returns>System.Int32.</returns>
+        private static int CountStringOccurrences(string text, string pattern)
+        {
+            // Loop through all instances of the string 'text'.
+            var count = 0;
+            var i = 0;
+            while ((i = text.IndexOf(pattern, i, StringComparison.OrdinalIgnoreCase)) != -1)
+            {
+                i += pattern.Length;
+                count++;
+            }
+            return count;
+        }
+
+        /// <summary>
+        /// Gets all command line arguments to pass to ffmpeg
+        /// </summary>
+        /// <param name="outputPath">The playlist output path</param>
+        /// <param name="isoMount">The iso mount.</param>
+        /// <returns>System.String.</returns>
+        protected override string GetCommandLineArguments(string outputPath, IIsoMount isoMount)
+        {
+            var segmentOutputPath = Path.GetDirectoryName(outputPath);
+            var segmentOutputName = HlsSegmentHandler.SegmentFilePrefix + Path.GetFileNameWithoutExtension(outputPath);
+
+            segmentOutputPath = Path.Combine(segmentOutputPath, segmentOutputName + "%03d." + SegmentFileExtension.TrimStart('.'));
+
+            var probeSize = Kernel.FFMpegManager.GetProbeSizeArgument(LibraryItem);
+
+            return string.Format("{0} {1} -i {2}{3} -threads 0 {4} {5} {6} -f ssegment -segment_list_flags +live -segment_time 9 -segment_list \"{7}\" \"{8}\"",
+                probeSize,
+                FastSeekCommandLineParameter,
+                GetInputArgument(isoMount),
+                SlowSeekCommandLineParameter,
+                MapArgs,
+                GetVideoArguments(),
+                GetAudioArguments(),
+                outputPath,
+                segmentOutputPath
+                ).Trim();
+        }
+
+        /// <summary>
+        /// Deletes the partial stream files.
+        /// </summary>
+        /// <param name="playlistFilePath">The playlist file path.</param>
+        protected override void DeletePartialStreamFiles(string playlistFilePath)
+        {
+            var directory = Path.GetDirectoryName(playlistFilePath);
+            var name = Path.GetFileNameWithoutExtension(playlistFilePath);
+
+            var filesToDelete = Directory.EnumerateFiles(directory, "*", SearchOption.TopDirectoryOnly)
+                .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1)
+                .ToList();
+
+            foreach (var file in filesToDelete)
+            {
+                try
+                {
+                    Logger.Info("Deleting HLS file {0}", file);
+                    File.Delete(file);
+                }
+                catch (IOException ex)
+                {
+                    Logger.ErrorException("Error deleting HLS file {0}", ex, file);
+                }
+            }
+        }
+    }
+}

+ 152 - 0
MediaBrowser.Api/Streaming/BaseProgressiveStreamingHandler.cs

@@ -0,0 +1,152 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Streaming
+{
+    /// <summary>
+    /// Class BaseMediaHandler
+    /// </summary>
+    /// <typeparam name="TBaseItemType">The type of the T base item type.</typeparam>
+    public abstract class BaseProgressiveStreamingHandler<TBaseItemType> : BaseStreamingHandler<TBaseItemType>
+        where TBaseItemType : BaseItem, IHasMediaStreams, new()
+    {
+        /// <summary>
+        /// Gets the type of the transcoding job.
+        /// </summary>
+        /// <value>The type of the transcoding job.</value>
+        protected override TranscodingJobType TranscodingJobType
+        {
+            get { return TranscodingJobType.Progressive; }
+        }
+
+        /// <summary>
+        /// Processes the request.
+        /// </summary>
+        /// <param name="ctx">The CTX.</param>
+        /// <returns>Task.</returns>
+        public override Task ProcessRequest(HttpListenerContext ctx)
+        {
+            HttpListenerContext = ctx;
+
+            if (!RequiresConversion())
+            {
+                return new StaticFileHandler(Kernel)
+                {
+                    Path = LibraryItem.Path
+
+                }.ProcessRequest(ctx);
+            }
+
+            var outputPath = OutputFilePath;
+
+            if (File.Exists(outputPath) && !Plugin.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive))
+            {
+                return new StaticFileHandler(Kernel)
+                {
+                    Path = outputPath
+
+                }.ProcessRequest(ctx);
+            }
+
+            return base.ProcessRequest(ctx);
+        }
+
+        /// <summary>
+        /// Requireses the conversion.
+        /// </summary>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        protected bool RequiresConversion()
+        {
+            return !GetBoolQueryStringParam("static");
+        }
+
+        /// <summary>
+        /// Writes the response to output stream.
+        /// </summary>
+        /// <param name="stream">The stream.</param>
+        /// <param name="responseInfo">The response info.</param>
+        /// <param name="contentLength">Length of the content.</param>
+        /// <returns>Task.</returns>
+        protected override async Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? contentLength)
+        {
+            // Use the command line args with a dummy playlist path
+            var outputPath = OutputFilePath;
+
+            if (!File.Exists(outputPath))
+            {
+                await StartFFMpeg(outputPath).ConfigureAwait(false);
+            }
+            else
+            {
+                Plugin.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+            }
+
+            try
+            {
+                await StreamFile(outputPath, stream).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                Logger.ErrorException("Error streaming media", ex);
+            }
+            finally
+            {
+                Plugin.Instance.OnTranscodeEndRequest(outputPath, TranscodingJobType.Progressive);
+            }
+        }
+
+        /// <summary>
+        /// Streams the file.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="outputStream">The output stream.</param>
+        /// <returns>Task{System.Boolean}.</returns>
+        private async Task StreamFile(string path, Stream outputStream)
+        {
+            var eofCount = 0;
+            long position = 0;
+
+            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+            {
+                while (eofCount < 15)
+                {
+                    await fs.CopyToAsync(outputStream).ConfigureAwait(false);
+
+                    var fsPosition = fs.Position;
+
+                    var bytesRead = fsPosition - position;
+
+                    //Logger.LogInfo("Streamed {0} bytes from file {1}", bytesRead, path);
+
+                    if (bytesRead == 0)
+                    {
+                        eofCount++;
+                        await Task.Delay(100).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        eofCount = 0;
+                    }
+
+                    position = fsPosition;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Deletes the partial stream files.
+        /// </summary>
+        /// <param name="outputFilePath">The output file path.</param>
+        protected override void DeletePartialStreamFiles(string outputFilePath)
+        {
+            File.Delete(outputFilePath);
+        }
+    }
+}

+ 992 - 0
MediaBrowser.Api/Streaming/BaseStreamingHandler.cs

@@ -0,0 +1,992 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Streaming
+{
+    /// <summary>
+    /// Represents a common base class for both progressive and hls streaming
+    /// </summary>
+    /// <typeparam name="TBaseItemType">The type of the T base item type.</typeparam>
+    public abstract class BaseStreamingHandler<TBaseItemType> : BaseHandler<Kernel>
+        where TBaseItemType : BaseItem, IHasMediaStreams, new()
+    {
+        /// <summary>
+        /// Gets the command line arguments.
+        /// </summary>
+        /// <param name="outputPath">The output path.</param>
+        /// <param name="isoMount">The iso mount.</param>
+        /// <returns>System.String.</returns>
+        protected abstract string GetCommandLineArguments(string outputPath, IIsoMount isoMount);
+
+        /// <summary>
+        /// Gets or sets the log file stream.
+        /// </summary>
+        /// <value>The log file stream.</value>
+        protected Stream LogFileStream { get; set; }
+
+        /// <summary>
+        /// Gets the type of the transcoding job.
+        /// </summary>
+        /// <value>The type of the transcoding job.</value>
+        protected abstract TranscodingJobType TranscodingJobType { get; }
+
+        /// <summary>
+        /// Gets the output file extension.
+        /// </summary>
+        /// <value>The output file extension.</value>
+        protected string OutputFileExtension
+        {
+            get
+            {
+                return Path.GetExtension(HttpListenerContext.Request.Url.LocalPath);
+            }
+        }
+
+        /// <summary>
+        /// Gets the output file path.
+        /// </summary>
+        /// <value>The output file path.</value>
+        protected string OutputFilePath
+        {
+            get
+            {
+                return Path.Combine(Kernel.ApplicationPaths.FFMpegStreamCachePath, GetCommandLineArguments("dummy\\dummy", null).GetMD5() + OutputFileExtension.ToLower());
+            }
+        }
+
+        /// <summary>
+        /// Gets the audio codec to endoce to.
+        /// </summary>
+        /// <value>The audio encoding format.</value>
+        protected virtual AudioCodecs? AudioCodec
+        {
+            get
+            {
+                if (string.IsNullOrEmpty(QueryString["audioCodec"]))
+                {
+                    return null;
+                }
+
+                return (AudioCodecs)Enum.Parse(typeof(AudioCodecs), QueryString["audioCodec"], true);
+            }
+        }
+
+        /// <summary>
+        /// Gets the video encoding codec.
+        /// </summary>
+        /// <value>The video codec.</value>
+        protected VideoCodecs? VideoCodec
+        {
+            get
+            {
+                if (string.IsNullOrEmpty(QueryString["videoCodec"]))
+                {
+                    return null;
+                }
+
+                return (VideoCodecs)Enum.Parse(typeof(VideoCodecs), QueryString["videoCodec"], true);
+            }
+        }
+
+        /// <summary>
+        /// Gets the time, in ticks, in which playback should start
+        /// </summary>
+        /// <value>The start time ticks.</value>
+        protected long? StartTimeTicks
+        {
+            get
+            {
+                string val = QueryString["StartTimeTicks"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return long.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// The fast seek offset seconds
+        /// </summary>
+        private const int FastSeekOffsetSeconds = 1;
+
+        /// <summary>
+        /// Gets the fast seek command line parameter.
+        /// </summary>
+        /// <value>The fast seek command line parameter.</value>
+        protected string FastSeekCommandLineParameter
+        {
+            get
+            {
+                var time = StartTimeTicks;
+
+                if (time.HasValue)
+                {
+                    var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds - FastSeekOffsetSeconds;
+
+                    if (seconds > 0)
+                    {
+                        return string.Format("-ss {0}", seconds);
+                    }
+                }
+
+                return string.Empty;
+            }
+        }
+
+        /// <summary>
+        /// Gets the slow seek command line parameter.
+        /// </summary>
+        /// <value>The slow seek command line parameter.</value>
+        protected string SlowSeekCommandLineParameter
+        {
+            get
+            {
+                var time = StartTimeTicks;
+
+                if (time.HasValue)
+                {
+                    if (TimeSpan.FromTicks(time.Value).TotalSeconds - FastSeekOffsetSeconds > 0)
+                    {
+                        return string.Format(" -ss {0}", FastSeekOffsetSeconds);
+                    }
+                }
+
+                return string.Empty;
+            }
+        }
+
+        /// <summary>
+        /// Gets the map args.
+        /// </summary>
+        /// <value>The map args.</value>
+        protected virtual string MapArgs
+        {
+            get
+            {
+                var args = string.Empty;
+
+                if (VideoStream != null)
+                {
+                    args += string.Format("-map 0:{0}", VideoStream.Index);
+                }
+                else
+                {
+                    args += "-map -0:v";
+                }
+
+                if (AudioStream != null)
+                {
+                    args += string.Format(" -map 0:{0}", AudioStream.Index);
+                }
+                else
+                {
+                    args += " -map -0:a";
+                }
+
+                if (SubtitleStream == null)
+                {
+                    args += " -map -0:s";
+                }
+
+                return args;
+            }
+        }
+
+        /// <summary>
+        /// The _library item
+        /// </summary>
+        private TBaseItemType _libraryItem;
+        /// <summary>
+        /// Gets the library item that will be played, if any
+        /// </summary>
+        /// <value>The library item.</value>
+        protected TBaseItemType LibraryItem
+        {
+            get
+            {
+                return _libraryItem ?? (_libraryItem = (TBaseItemType)DtoBuilder.GetItemByClientId(QueryString["id"]));
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the iso mount.
+        /// </summary>
+        /// <value>The iso mount.</value>
+        private IIsoMount IsoMount { get; set; }
+
+        /// <summary>
+        /// The _audio stream
+        /// </summary>
+        private MediaStream _audioStream;
+        /// <summary>
+        /// Gets the audio stream.
+        /// </summary>
+        /// <value>The audio stream.</value>
+        protected MediaStream AudioStream
+        {
+            get { return _audioStream ?? (_audioStream = GetMediaStream(AudioStreamIndex, MediaStreamType.Audio)); }
+        }
+
+        /// <summary>
+        /// The _video stream
+        /// </summary>
+        private MediaStream _videoStream;
+        /// <summary>
+        /// Gets the video stream.
+        /// </summary>
+        /// <value>The video stream.</value>
+        protected MediaStream VideoStream
+        {
+            get
+            {
+                // No video streams here
+                // Need to make this check to make sure we don't pickup embedded image streams (which are listed in the file as type video)
+                if (LibraryItem is Audio)
+                {
+                    return null;
+                }
+
+                return _videoStream ?? (_videoStream = GetMediaStream(VideoStreamIndex, MediaStreamType.Video));
+            }
+        }
+
+        /// <summary>
+        /// The subtitle stream
+        /// </summary>
+        private MediaStream _subtitleStream;
+        /// <summary>
+        /// Gets the subtitle stream.
+        /// </summary>
+        /// <value>The subtitle stream.</value>
+        protected MediaStream SubtitleStream
+        {
+            get
+            {
+                // No subtitle streams here
+                if (LibraryItem is Audio)
+                {
+                    return null;
+                }
+
+                return _subtitleStream ?? (_subtitleStream = GetMediaStream(SubtitleStreamIndex, MediaStreamType.Subtitle, false));
+            }
+        }
+
+        /// <summary>
+        /// Determines which stream will be used for playback
+        /// </summary>
+        /// <param name="desiredIndex">Index of the desired.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="returnFirstIfNoIndex">if set to <c>true</c> [return first if no index].</param>
+        /// <returns>MediaStream.</returns>
+        private MediaStream GetMediaStream(int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true)
+        {
+            var streams = LibraryItem.MediaStreams.Where(s => s.Type == type).ToList();
+
+            if (desiredIndex.HasValue)
+            {
+                var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value);
+
+                if (stream != null)
+                {
+                    return stream;
+                }
+            }
+
+            // Just return the first one
+            return returnFirstIfNoIndex ? streams.FirstOrDefault() : null;
+        }
+
+        /// <summary>
+        /// Gets the response info.
+        /// </summary>
+        /// <returns>Task{ResponseInfo}.</returns>
+        protected override Task<ResponseInfo> GetResponseInfo()
+        {
+            var info = new ResponseInfo
+            {
+                ContentType = MimeTypes.GetMimeType(OutputFilePath),
+                CompressResponse = false
+            };
+
+            return Task.FromResult(info);
+        }
+
+        /// <summary>
+        /// Gets the client's desired audio bitrate
+        /// </summary>
+        /// <value>The audio bit rate.</value>
+        protected int? AudioBitRate
+        {
+            get
+            {
+                var val = QueryString["AudioBitRate"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the client's desired video bitrate
+        /// </summary>
+        /// <value>The video bit rate.</value>
+        protected int? VideoBitRate
+        {
+            get
+            {
+                var val = QueryString["VideoBitRate"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the desired audio stream index
+        /// </summary>
+        /// <value>The index of the audio stream.</value>
+        private int? AudioStreamIndex
+        {
+            get
+            {
+                var val = QueryString["AudioStreamIndex"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the desired video stream index
+        /// </summary>
+        /// <value>The index of the video stream.</value>
+        private int? VideoStreamIndex
+        {
+            get
+            {
+                var val = QueryString["VideoStreamIndex"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the desired subtitle stream index
+        /// </summary>
+        /// <value>The index of the subtitle stream.</value>
+        private int? SubtitleStreamIndex
+        {
+            get
+            {
+                var val = QueryString["SubtitleStreamIndex"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the audio channels.
+        /// </summary>
+        /// <value>The audio channels.</value>
+        public int? AudioChannels
+        {
+            get
+            {
+                var val = QueryString["audiochannels"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the audio sample rate.
+        /// </summary>
+        /// <value>The audio sample rate.</value>
+        public int? AudioSampleRate
+        {
+            get
+            {
+                var val = QueryString["audiosamplerate"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return 44100;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// If we're going to put a fixed size on the command line, this will calculate it
+        /// </summary>
+        /// <param name="outputVideoCodec">The output video codec.</param>
+        /// <returns>System.String.</returns>
+        protected string GetOutputSizeParam(string outputVideoCodec)
+        {
+            // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
+
+            var assSubtitleParam = string.Empty;
+
+            if (SubtitleStream != null)
+            {
+                if (SubtitleStream.Codec.IndexOf("srt", StringComparison.OrdinalIgnoreCase) != -1 || SubtitleStream.Codec.IndexOf("subrip", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    assSubtitleParam = GetTextSubtitleParam(SubtitleStream);
+                }
+            }
+
+            // If fixed dimensions were supplied
+            if (Width.HasValue && Height.HasValue)
+            {
+                return string.Format(" -vf \"scale={0}:{1}{2}\"", Width.Value, Height.Value, assSubtitleParam);
+            }
+
+            var isH264Output = outputVideoCodec.Equals("libx264", StringComparison.OrdinalIgnoreCase);
+
+            // If a fixed width was requested
+            if (Width.HasValue)
+            {
+                return isH264Output ?
+                    string.Format(" -vf \"scale={0}:trunc(ow/a/2)*2{1}\"", Width.Value, assSubtitleParam) :
+                    string.Format(" -vf \"scale={0}:-1{1}\"", Width.Value, assSubtitleParam);
+            }
+
+            // If a max width was requested
+            if (MaxWidth.HasValue && !MaxHeight.HasValue)
+            {
+                return isH264Output ?
+                    string.Format(" -vf \"scale=min(iw\\,{0}):trunc(ow/a/2)*2{1}\"", MaxWidth.Value, assSubtitleParam) :
+                    string.Format(" -vf \"scale=min(iw\\,{0}):-1{1}\"", MaxWidth.Value, assSubtitleParam);
+            }
+
+            // Need to perform calculations manually
+
+            // Try to account for bad media info
+            var currentHeight = VideoStream.Height ?? MaxHeight ?? Height ?? 0;
+            var currentWidth = VideoStream.Width ?? MaxWidth ?? Width ?? 0;
+
+            var outputSize = DrawingUtils.Resize(currentWidth, currentHeight, Width, Height, MaxWidth, MaxHeight);
+
+            // If we're encoding with libx264, it can't handle odd numbered widths or heights, so we'll have to fix that
+            if (isH264Output)
+            {
+                return string.Format(" -vf \"scale=trunc({0}/2)*2:trunc({1}/2)*2{2}\"", outputSize.Width, outputSize.Height, assSubtitleParam);
+            }
+
+            // Otherwise use -vf scale since ffmpeg will ensure internally that the aspect ratio is preserved
+            return string.Format(" -vf \"scale={0}:-1{1}\"", Convert.ToInt32(outputSize.Width), assSubtitleParam);
+        }
+
+        /// <summary>
+        /// Gets the text subtitle param.
+        /// </summary>
+        /// <param name="subtitleStream">The subtitle stream.</param>
+        /// <returns>System.String.</returns>
+        protected string GetTextSubtitleParam(MediaStream subtitleStream)
+        {
+            var path = subtitleStream.IsExternal ? GetConvertedAssPath(subtitleStream) : GetExtractedAssPath(subtitleStream);
+
+            if (string.IsNullOrEmpty(path))
+            {
+                return string.Empty;
+            }
+
+            var param = string.Format(",ass={0}", path);
+
+            var time = StartTimeTicks;
+
+            if (time.HasValue)
+            {
+                var seconds = Convert.ToInt32(TimeSpan.FromTicks(time.Value).TotalSeconds);
+                param += string.Format(",setpts=PTS-{0}/TB", seconds);
+            }
+
+            return param;
+        }
+
+        /// <summary>
+        /// Gets the extracted ass path.
+        /// </summary>
+        /// <param name="subtitleStream">The subtitle stream.</param>
+        /// <returns>System.String.</returns>
+        private string GetExtractedAssPath(MediaStream subtitleStream)
+        {
+            var video = LibraryItem as Video;
+
+            var path = Kernel.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, ".ass");
+
+            if (!File.Exists(path))
+            {
+                var success = Kernel.FFMpegManager.ExtractTextSubtitle(video, subtitleStream.Index, path, CancellationToken.None).Result;
+
+                if (!success)
+                {
+                    return null;
+                }
+            }
+
+            return path;
+        }
+
+        /// <summary>
+        /// Gets the converted ass path.
+        /// </summary>
+        /// <param name="subtitleStream">The subtitle stream.</param>
+        /// <returns>System.String.</returns>
+        private string GetConvertedAssPath(MediaStream subtitleStream)
+        {
+            var video = LibraryItem as Video;
+
+            var path = Kernel.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, ".ass");
+
+            if (!File.Exists(path))
+            {
+                var success = Kernel.FFMpegManager.ConvertTextSubtitle(subtitleStream, path, CancellationToken.None).Result;
+
+                if (!success)
+                {
+                    return null;
+                }
+            }
+
+            return path;
+        }
+
+        /// <summary>
+        /// Gets the internal graphical subtitle param.
+        /// </summary>
+        /// <param name="subtitleStream">The subtitle stream.</param>
+        /// <param name="videoCodec">The video codec.</param>
+        /// <returns>System.String.</returns>
+        protected string GetInternalGraphicalSubtitleParam(MediaStream subtitleStream, string videoCodec)
+        {
+            var outputSizeParam = string.Empty;
+
+            // Add resolution params, if specified
+            if (Width.HasValue || Height.HasValue || MaxHeight.HasValue || MaxWidth.HasValue)
+            {
+                outputSizeParam = GetOutputSizeParam(videoCodec).TrimEnd('"');
+                outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase));
+            }
+
+            return string.Format(" -filter_complex \"[0:{0}]format=yuva444p,lut=u=128:v=128:y=gammaval(.3)[sub] ; [0:0] [sub] overlay{1}\"", subtitleStream.Index, outputSizeParam);
+        }
+
+        /// <summary>
+        /// Gets the fixed output video height, in pixels
+        /// </summary>
+        /// <value>The height.</value>
+        protected int? Height
+        {
+            get
+            {
+                string val = QueryString["height"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the fixed output video width, in pixels
+        /// </summary>
+        /// <value>The width.</value>
+        protected int? Width
+        {
+            get
+            {
+                string val = QueryString["width"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the maximum output video height, in pixels
+        /// </summary>
+        /// <value>The height of the max.</value>
+        protected int? MaxHeight
+        {
+            get
+            {
+                string val = QueryString["maxheight"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the maximum output video width, in pixels
+        /// </summary>
+        /// <value>The width of the max.</value>
+        protected int? MaxWidth
+        {
+            get
+            {
+                string val = QueryString["maxwidth"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return int.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the output video framerate
+        /// </summary>
+        /// <value>The max frame rate.</value>
+        protected float? FrameRate
+        {
+            get
+            {
+                string val = QueryString["framerate"];
+
+                if (string.IsNullOrEmpty(val))
+                {
+                    return null;
+                }
+
+                return float.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the number of audio channels to specify on the command line
+        /// </summary>
+        /// <returns>System.Nullable{System.Int32}.</returns>
+        protected int? GetSampleRateParam()
+        {
+            // If the user requested a max value
+            if (AudioSampleRate.HasValue)
+            {
+                return AudioSampleRate.Value;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Gets the number of audio channels to specify on the command line
+        /// </summary>
+        /// <param name="audioCodec">The audio codec.</param>
+        /// <returns>System.Nullable{System.Int32}.</returns>
+        protected int? GetNumAudioChannelsParam(string audioCodec)
+        {
+            if (AudioStream.Channels > 2)
+            {
+                if (audioCodec.Equals("libvo_aacenc"))
+                {
+                    // libvo_aacenc currently only supports two channel output
+                    return 2;
+                }
+                if (audioCodec.Equals("wmav2"))
+                {
+                    // wmav2 currently only supports two channel output
+                    return 2;
+                }
+            }
+
+            return GetNumAudioChannelsParam();
+        }
+
+        /// <summary>
+        /// Gets the number of audio channels to specify on the command line
+        /// </summary>
+        /// <returns>System.Nullable{System.Int32}.</returns>
+        protected int? GetNumAudioChannelsParam()
+        {
+            // If the user requested a max number of channels
+            if (AudioChannels.HasValue)
+            {
+                return AudioChannels.Value;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Determines whether the specified stream is H264.
+        /// </summary>
+        /// <param name="stream">The stream.</param>
+        /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
+        protected bool IsH264(MediaStream stream)
+        {
+            return stream.Codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 ||
+                   stream.Codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
+        }
+
+        /// <summary>
+        /// Gets the name of the output audio codec
+        /// </summary>
+        /// <returns>System.String.</returns>
+        protected string GetAudioCodec()
+        {
+            if (AudioCodec.HasValue)
+            {
+                if (AudioCodec == AudioCodecs.Aac)
+                {
+                    return "libvo_aacenc";
+                }
+                if (AudioCodec == AudioCodecs.Mp3)
+                {
+                    return "libmp3lame";
+                }
+                if (AudioCodec == AudioCodecs.Vorbis)
+                {
+                    return "libvorbis";
+                }
+                if (AudioCodec == AudioCodecs.Wma)
+                {
+                    return "wmav2";
+                }
+            }
+
+            return "copy";
+        }
+
+        /// <summary>
+        /// Gets the name of the output video codec
+        /// </summary>
+        /// <returns>System.String.</returns>
+        protected string GetVideoCodec()
+        {
+            if (VideoCodec.HasValue)
+            {
+                if (VideoCodec == VideoCodecs.H264)
+                {
+                    return "libx264";
+                }
+                if (VideoCodec == VideoCodecs.Vpx)
+                {
+                    return "libvpx";
+                }
+                if (VideoCodec == VideoCodecs.Wmv)
+                {
+                    return "wmv2";
+                }
+                if (VideoCodec == VideoCodecs.Theora)
+                {
+                    return "libtheora";
+                }
+            }
+
+            return "copy";
+        }
+
+        /// <summary>
+        /// Gets the input argument.
+        /// </summary>
+        /// <param name="isoMount">The iso mount.</param>
+        /// <returns>System.String.</returns>
+        protected string GetInputArgument(IIsoMount isoMount)
+        {
+            return isoMount == null ?
+                Kernel.FFMpegManager.GetInputArgument(LibraryItem) :
+                Kernel.FFMpegManager.GetInputArgument(LibraryItem as Video, IsoMount);
+        }
+
+        /// <summary>
+        /// Starts the FFMPEG.
+        /// </summary>
+        /// <param name="outputPath">The output path.</param>
+        /// <returns>Task.</returns>
+        protected async Task StartFFMpeg(string outputPath)
+        {
+            var video = LibraryItem as Video;
+
+            if (video != null && video.VideoType == VideoType.Iso &&
+                video.IsoType.HasValue && Kernel.IsoManager.CanMount(video.Path))
+            {
+                IsoMount = await Kernel.IsoManager.Mount(video.Path, CancellationToken.None).ConfigureAwait(false);
+            }
+
+            var process = new Process
+            {
+                StartInfo = new ProcessStartInfo
+                {
+                    CreateNoWindow = true,
+                    UseShellExecute = false,
+
+                    // Must consume both stdout and stderr or deadlocks may occur
+                    RedirectStandardOutput = true,
+                    RedirectStandardError = true,
+
+                    FileName = Kernel.FFMpegManager.FFMpegPath,
+                    WorkingDirectory = Path.GetDirectoryName(Kernel.FFMpegManager.FFMpegPath),
+                    Arguments = GetCommandLineArguments(outputPath, IsoMount),
+
+                    WindowStyle = ProcessWindowStyle.Hidden,
+                    ErrorDialog = false
+                },
+
+                EnableRaisingEvents = true
+            };
+
+            Plugin.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process);
+
+            Logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
+
+            var logFilePath = Path.Combine(Kernel.ApplicationPaths.LogDirectoryPath, "ffmpeg-" + Guid.NewGuid() + ".txt");
+
+            // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
+            LogFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous);
+
+            process.Exited += OnFFMpegProcessExited;
+
+            try
+            {
+                process.Start();
+            }
+            catch (Win32Exception ex)
+            {
+                Logger.ErrorException("Error starting ffmpeg", ex);
+
+                Plugin.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType);
+
+                process.Exited -= OnFFMpegProcessExited;
+                LogFileStream.Dispose();
+
+                throw;
+            }
+
+            // MUST read both stdout and stderr asynchronously or a deadlock may occurr
+            process.BeginOutputReadLine();
+
+            // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
+            process.StandardError.BaseStream.CopyToAsync(LogFileStream);
+
+            // Wait for the file to exist before proceeeding
+            while (!File.Exists(outputPath))
+            {
+                await Task.Delay(100).ConfigureAwait(false);
+            }
+        }
+
+        /// <summary>
+        /// Processes the exited.
+        /// </summary>
+        /// <param name="sender">The sender.</param>
+        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
+        protected void OnFFMpegProcessExited(object sender, EventArgs e)
+        {
+            if (IsoMount != null)
+            {
+                IsoMount.Dispose();
+                IsoMount = null;
+            }
+
+            var outputFilePath = OutputFilePath;
+
+            LogFileStream.Dispose();
+
+            var process = (Process)sender;
+
+            process.Exited -= OnFFMpegProcessExited;
+
+            int? exitCode = null;
+
+            try
+            {
+                exitCode = process.ExitCode;
+                Logger.Info("FFMpeg exited with code {0} for {1}", exitCode.Value, outputFilePath);
+            }
+            catch
+            {
+                Logger.Info("FFMpeg exited with an error for {0}", outputFilePath);
+            }
+
+            process.Dispose();
+
+            Plugin.Instance.OnTranscodingFinished(outputFilePath, TranscodingJobType);
+
+            if (!exitCode.HasValue || exitCode.Value != 0)
+            {
+                Logger.Info("Deleting partial stream file(s) {0}", outputFilePath);
+
+                try
+                {
+                    DeletePartialStreamFiles(outputFilePath);
+                }
+                catch (IOException ex)
+                {
+                    Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, outputFilePath);
+                }
+            }
+            else
+            {
+                Logger.Info("FFMpeg completed and exited normally for {0}", outputFilePath);
+            }
+        }
+
+        /// <summary>
+        /// Deletes the partial stream files.
+        /// </summary>
+        /// <param name="outputFilePath">The output file path.</param>
+        protected abstract void DeletePartialStreamFiles(string outputFilePath);
+    }
+}

+ 102 - 0
MediaBrowser.Api/Streaming/HlsAudioPlaylistHandler.cs

@@ -0,0 +1,102 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.DTO;
+using System;
+using System.ComponentModel.Composition;
+using System.Net;
+
+namespace MediaBrowser.Api.Streaming
+{
+    /// <summary>
+    /// Class HlsAudioPlaylistHandler
+    /// </summary>
+    [Export(typeof(IHttpServerHandler))]
+    public class HlsAudioPlaylistHandler : BaseHlsPlaylistHandler<Audio>
+    {
+        /// <summary>
+        /// Handleses the request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("audio.m3u8", request);
+        }
+
+        /// <summary>
+        /// Gets the segment file extension.
+        /// </summary>
+        /// <value>The segment file extension.</value>
+        /// <exception cref="InvalidOperationException">Only aac and mp3 audio codecs are supported.</exception>
+        protected override string SegmentFileExtension
+        {
+            get
+            {
+                if (AudioCodec == AudioCodecs.Aac)
+                {
+                    return ".aac";
+                }
+                if (AudioCodec == AudioCodecs.Mp3)
+                {
+                    return ".mp3";
+                }
+
+                throw new InvalidOperationException("Only aac and mp3 audio codecs are supported.");
+            }
+        }
+
+        /// <summary>
+        /// Gets the video arguments.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        protected override string GetVideoArguments()
+        {
+            // No video
+            return string.Empty;
+        }
+
+        /// <summary>
+        /// Gets the map args.
+        /// </summary>
+        /// <value>The map args.</value>
+        protected override string MapArgs
+        {
+            get
+            {
+                return string.Format("-map 0:{0}", AudioStream.Index);
+            }
+        }
+
+        /// <summary>
+        /// Gets the audio arguments.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        protected override string GetAudioArguments()
+        {
+            var codec = GetAudioCodec();
+
+            var args = "-codec:a " + codec;
+
+            var channels = GetNumAudioChannelsParam();
+
+            if (channels.HasValue)
+            {
+                args += " -ac " + channels.Value;
+            }
+
+            var sampleRate = GetSampleRateParam();
+
+            if (sampleRate.HasValue)
+            {
+                args += " -ar " + sampleRate.Value;
+            }
+
+            if (AudioBitRate.HasValue)
+            {
+                args += " -ab " + AudioBitRate.Value;
+            }
+
+            return args;
+        }
+    }
+}

+ 87 - 0
MediaBrowser.Api/Streaming/HlsSegmentHandler.cs

@@ -0,0 +1,87 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Streaming
+{
+    /// <summary>
+    /// Class HlsSegmentHandler
+    /// </summary>
+    [Export(typeof(IHttpServerHandler))]
+    public class HlsSegmentHandler : BaseHandler<Kernel>
+    {
+        /// <summary>
+        /// The segment file prefix
+        /// </summary>
+        public const string SegmentFilePrefix = "segment-";
+
+        /// <summary>
+        /// Handleses the request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            const string url = "/api/" + SegmentFilePrefix;
+
+            return request.Url.LocalPath.IndexOf(url, StringComparison.OrdinalIgnoreCase) != -1;
+        }
+
+        /// <summary>
+        /// Writes the response to output stream.
+        /// </summary>
+        /// <param name="stream">The stream.</param>
+        /// <param name="responseInfo">The response info.</param>
+        /// <param name="contentLength">Length of the content.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.NotImplementedException"></exception>
+        /// <exception cref="NotImplementedException"></exception>
+        protected override Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? contentLength)
+        {
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// Gets the response info.
+        /// </summary>
+        /// <returns>Task{ResponseInfo}.</returns>
+        /// <exception cref="System.NotImplementedException"></exception>
+        /// <exception cref="NotImplementedException"></exception>
+        protected override Task<ResponseInfo> GetResponseInfo()
+        {
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// Processes the request.
+        /// </summary>
+        /// <param name="ctx">The CTX.</param>
+        /// <returns>Task.</returns>
+        public override async Task ProcessRequest(HttpListenerContext ctx)
+        {
+            var path = Path.GetFileName(ctx.Request.Url.LocalPath);
+
+            path = Path.Combine(Kernel.ApplicationPaths.FFMpegStreamCachePath, path);
+
+            var playlistFilename = Path.GetFileNameWithoutExtension(path).Substring(SegmentFilePrefix.Length);
+            playlistFilename = playlistFilename.Substring(0, playlistFilename.Length - 3);
+
+            var playlistPath = Path.Combine(Path.GetDirectoryName(path), playlistFilename + ".m3u8");
+
+            Plugin.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
+
+            try
+            {
+                await new StaticFileHandler(Kernel) { Path = path }.ProcessRequest(ctx).ConfigureAwait(false);
+            }
+            finally
+            {
+                Plugin.Instance.OnTranscodeEndRequest(playlistPath, TranscodingJobType.Hls);
+            }
+        }
+    }
+}

+ 134 - 0
MediaBrowser.Api/Streaming/HlsVideoPlaylistHandler.cs

@@ -0,0 +1,134 @@
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.Net;
+
+namespace MediaBrowser.Api.Streaming
+{
+    /// <summary>
+    /// Class HlsVideoPlaylistHandler
+    /// </summary>
+    [Export(typeof(IHttpServerHandler))]
+    public class HlsVideoPlaylistHandler : BaseHlsPlaylistHandler<Video>
+    {
+        /// <summary>
+        /// Handleses the request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return ApiService.IsApiUrlMatch("video.m3u8", request);
+        }
+
+        /// <summary>
+        /// Gets the segment file extension.
+        /// </summary>
+        /// <value>The segment file extension.</value>
+        protected override string SegmentFileExtension
+        {
+            get { return ".ts"; }
+        }
+
+        /// <summary>
+        /// Gets the video arguments.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        protected override string GetVideoArguments()
+        {
+            var codec = GetVideoCodec();
+
+            // Right now all we support is either h264 or copy
+            if (!codec.Equals("copy", StringComparison.OrdinalIgnoreCase) && !codec.Equals("libx264", StringComparison.OrdinalIgnoreCase))
+            {
+                codec = "libx264";
+            }
+
+            // See if we can save come cpu cycles by avoiding encoding
+            if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+            {
+                return IsH264(VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy";
+            }
+
+            var args = "-codec:v:0 " + codec + " -preset superfast";
+
+            if (VideoBitRate.HasValue)
+            {
+                args += string.Format(" -b:v {0}", VideoBitRate.Value);
+            }
+
+            // Add resolution params, if specified
+            if (Width.HasValue || Height.HasValue || MaxHeight.HasValue || MaxWidth.HasValue)
+            {
+                args += GetOutputSizeParam(codec);
+            }
+
+            // Get the output framerate based on the FrameRate param
+            double framerate = FrameRate ?? 0;
+
+            // We have to supply a framerate for hls, so if it's null, account for that here
+            if (framerate.Equals(0))
+            {
+                framerate = VideoStream.AverageFrameRate ?? 0;
+            }
+            if (framerate.Equals(0))
+            {
+                framerate = VideoStream.RealFrameRate ?? 0;
+            }
+            if (framerate.Equals(0))
+            {
+                framerate = 23.976;
+            }
+
+            args += string.Format(" -r {0}", framerate);
+
+            // Needed to ensure segments stay under 10 seconds
+            args += string.Format(" -g {0}", framerate);
+
+            return args;
+        }
+
+        /// <summary>
+        /// Gets the audio arguments to pass to ffmpeg
+        /// </summary>
+        /// <returns>System.String.</returns>
+        protected override string GetAudioArguments()
+        {
+            if (!AudioCodec.HasValue)
+            {
+                return "-codec:a:0 copy";
+            }
+
+            var codec = GetAudioCodec();
+
+            var args = "-codec:a:0 " + codec;
+
+            if (AudioStream != null)
+            {
+                var channels = GetNumAudioChannelsParam();
+
+                if (channels.HasValue)
+                {
+                    args += " -ac " + channels.Value;
+                }
+
+                var sampleRate = GetSampleRateParam();
+
+                if (sampleRate.HasValue)
+                {
+                    args += " -ar " + sampleRate.Value;
+                }
+
+                if (AudioBitRate.HasValue)
+                {
+                    args += " -ab " + AudioBitRate.Value;
+                }
+
+                return args;
+            }
+
+            return args;
+        }
+    }
+}

+ 185 - 0
MediaBrowser.Api/Streaming/VideoHandler.cs

@@ -0,0 +1,185 @@
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net.Handlers;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Resolvers;
+using System;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Net;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Api.Streaming
+{
+    /// <summary>
+    /// Providers a progressive streaming video api
+    /// </summary>
+    [Export(typeof(IHttpServerHandler))]
+    class VideoHandler : BaseProgressiveStreamingHandler<Video>
+    {
+        /// <summary>
+        /// Handleses the request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        public override bool HandlesRequest(HttpListenerRequest request)
+        {
+            return EntityResolutionHelper.VideoFileExtensions.Any(a => ApiService.IsApiUrlMatch("video" + a, request));
+        }
+
+        /// <summary>
+        /// Creates arguments to pass to ffmpeg
+        /// </summary>
+        /// <param name="outputPath">The output path.</param>
+        /// <param name="isoMount">The iso mount.</param>
+        /// <returns>System.String.</returns>
+        protected override string GetCommandLineArguments(string outputPath, IIsoMount isoMount)
+        {
+            var probeSize = Kernel.FFMpegManager.GetProbeSizeArgument(LibraryItem.VideoType, LibraryItem.IsoType);
+
+            // Get the output codec name
+            var videoCodec = GetVideoCodec();
+
+            var graphicalSubtitleParam = string.Empty;
+
+            if (SubtitleStream != null)
+            {
+                // This is for internal graphical subs
+                if (!SubtitleStream.IsExternal && (SubtitleStream.Codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) != -1 || SubtitleStream.Codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1))
+                {
+                    graphicalSubtitleParam = GetInternalGraphicalSubtitleParam(SubtitleStream, videoCodec);
+                }
+            }
+
+            return string.Format("{0} {1} -i {2}{3} -threads 0 {4} {5}{6} {7} \"{8}\"",
+                probeSize,
+                FastSeekCommandLineParameter,
+                GetInputArgument(isoMount),
+                SlowSeekCommandLineParameter,
+                MapArgs,
+                GetVideoArguments(videoCodec),
+                graphicalSubtitleParam,
+                GetAudioArguments(),
+                outputPath
+                ).Trim();
+        }
+
+        /// <summary>
+        /// Gets video arguments to pass to ffmpeg
+        /// </summary>
+        /// <returns>System.String.</returns>
+        private string GetVideoArguments(string videoCodec)
+        {
+            var args = "-vcodec " + videoCodec;
+
+            // If we're encoding video, add additional params
+            if (!videoCodec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+            {
+                // Add resolution params, if specified
+                if (Width.HasValue || Height.HasValue || MaxHeight.HasValue || MaxWidth.HasValue)
+                {
+                    args += GetOutputSizeParam(videoCodec);
+                }
+
+                if (FrameRate.HasValue)
+                {
+                    args += string.Format(" -r {0}", FrameRate.Value);
+                }
+
+                // Add the audio bitrate
+                var qualityParam = GetVideoQualityParam(videoCodec);
+
+                if (!string.IsNullOrEmpty(qualityParam))
+                {
+                    args += " " + qualityParam;
+                }
+            }
+            else if (IsH264(VideoStream))
+            {
+                args += " -bsf h264_mp4toannexb";
+            }
+
+            return args;
+        }
+
+        /// <summary>
+        /// Gets audio arguments to pass to ffmpeg
+        /// </summary>
+        /// <returns>System.String.</returns>
+        private string GetAudioArguments()
+        {
+            // If the video doesn't have an audio stream, return a default.
+            if (AudioStream == null)
+            {
+                return string.Empty;
+            }
+
+            // Get the output codec name
+            var codec = GetAudioCodec();
+
+            var args = "-acodec " + codec;
+
+            // If we're encoding audio, add additional params
+            if (!codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+            {
+                // Add the number of audio channels
+                var channels = GetNumAudioChannelsParam();
+
+                if (channels.HasValue)
+                {
+                    args += " -ac " + channels.Value;
+                }
+
+                // Add the audio sample rate
+                var sampleRate = GetSampleRateParam();
+
+                if (sampleRate.HasValue)
+                {
+                    args += " -ar " + sampleRate.Value;
+                }
+
+                if (AudioBitRate.HasValue)
+                {
+                    args += " -ab " + AudioBitRate.Value;
+                }
+            }
+
+            return args;
+        }
+
+        /// <summary>
+        /// Gets the video bitrate to specify on the command line
+        /// </summary>
+        /// <param name="videoCodec">The video codec.</param>
+        /// <returns>System.String.</returns>
+        private string GetVideoQualityParam(string videoCodec)
+        {
+            var args = string.Empty;
+
+            // webm
+            if (videoCodec.Equals("libvpx", StringComparison.OrdinalIgnoreCase))
+            {
+                args = "-g 120 -cpu-used 1 -lag-in-frames 16 -deadline realtime -slices 4 -vprofile 0";
+            }
+
+            // asf/wmv
+            else if (videoCodec.Equals("wmv2", StringComparison.OrdinalIgnoreCase))
+            {
+                args = "-g 100 -qmax 15";
+            }
+
+            else if (videoCodec.Equals("libx264", StringComparison.OrdinalIgnoreCase))
+            {
+                args = "-preset superfast";
+            }
+
+            if (VideoBitRate.HasValue)
+            {
+                args += " -b:v " + VideoBitRate;
+            }
+
+            return args.Trim();
+        }
+    }
+}

+ 107 - 0
MediaBrowser.Api/SystemService.cs

@@ -0,0 +1,107 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.System;
+using ServiceStack.ServiceHost;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api
+{
+    [Route("/System/Info", "GET")]
+    public class GetSystemInfo : IReturn<SystemInfo>
+    {
+
+    }
+
+    /// <summary>
+    /// Class RestartApplication
+    /// </summary>
+    [Route("/System/Restart", "POST")]
+    [ServiceStack.ServiceHost.Api(("Restarts the application, if needed"))]
+    public class RestartApplication
+    {
+    }
+
+    /// <summary>
+    /// Class GetConfiguration
+    /// </summary>
+    [Route("/System/Configuration", "GET")]
+    public class GetConfiguration : IReturn<ServerConfiguration>
+    {
+
+    }
+
+    /// <summary>
+    /// Class UpdateConfiguration
+    /// </summary>
+    [Route("/System/Configuration", "POST")]
+    public class UpdateConfiguration : IRequiresRequestStream
+    {
+        public Stream RequestStream { get; set; }
+    }
+
+    /// <summary>
+    /// Class SystemInfoService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class SystemService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetSystemInfo request)
+        {
+            var result = Kernel.GetSystemInfo();
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetConfiguration request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var dateModified = File.GetLastWriteTimeUtc(Kernel.ApplicationPaths.SystemConfigurationFilePath);
+
+            var cacheKey = (Kernel.ApplicationPaths.SystemConfigurationFilePath + dateModified.Ticks).GetMD5();
+
+            return ToOptimizedResultUsingCache(cacheKey, dateModified, null, () => kernel.Configuration);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(RestartApplication request)
+        {
+            Task.Run(async () =>
+            {
+                await Task.Delay(100);
+                Kernel.PerformPendingRestart();
+            });
+        }
+
+        /// <summary>
+        /// Posts the specified configuraiton.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(UpdateConfiguration request)
+        {
+            var serverConfig = JsonSerializer.DeserializeFromStream<ServerConfiguration>(request.RequestStream);
+            
+            var kernel = (Kernel)Kernel;
+
+            kernel.UpdateConfiguration(serverConfig);
+        }
+    }
+}

+ 161 - 0
MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs

@@ -0,0 +1,161 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Entities;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.UserLibrary
+{
+    /// <summary>
+    /// Class BaseItemsByNameService
+    /// </summary>
+    /// <typeparam name="TItemType">The type of the T item type.</typeparam>
+    public abstract class BaseItemsByNameService<TItemType> : BaseRestService
+        where TItemType : BaseItem
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Task{ItemsResult}.</returns>
+        protected async Task<ItemsResult> GetResult(GetItemsByName request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var item = string.IsNullOrEmpty(request.Id) ? user.RootFolder : DtoBuilder.GetItemByClientId(request.Id, user.Id);
+
+            IEnumerable<BaseItem> items;
+
+            if (item.IsFolder)
+            {
+                var folder = (Folder)item;
+
+                items = request.Recursive ? folder.GetRecursiveChildren(user) : folder.GetChildren(user);
+            }
+            else
+            {
+                items = new[] { item };
+            }
+
+            var ibnItemsArray = GetAllItems(request, items, user).ToArray();
+            IEnumerable<Tuple<string, Func<int>>> ibnItems = ibnItemsArray;
+
+            var result = new ItemsResult
+            {
+                TotalRecordCount = ibnItemsArray.Length
+            };
+
+            if (request.StartIndex.HasValue || request.PageSize.HasValue)
+            {
+                if (request.StartIndex.HasValue)
+                {
+                    ibnItems = ibnItems.Skip(request.StartIndex.Value);
+                }
+
+                if (request.PageSize.HasValue)
+                {
+                    ibnItems = ibnItems.Take(request.PageSize.Value);
+                }
+
+            }
+
+            var tasks = ibnItems.Select(i => GetDto(i, user, new List<ItemFields>()));
+
+            var resultItems = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+            result.Items = resultItems.Where(i => i != null).OrderByDescending(i => i.SortName ?? i.Name).ToArray();
+
+            return result;
+        }
+
+        /// <summary>
+        /// Gets all items.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="items">The items.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
+        protected abstract IEnumerable<Tuple<string, Func<int>>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items, User user);
+
+        /// <summary>
+        /// Gets the entity.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns>Task{BaseItem}.</returns>
+        protected abstract Task<TItemType> GetEntity(string name);
+
+        /// <summary>
+        /// Gets the dto.
+        /// </summary>
+        /// <param name="stub">The stub.</param>
+        /// <param name="user">The user.</param>
+        /// <param name="fields">The fields.</param>
+        /// <returns>Task{DtoBaseItem}.</returns>
+        private async Task<DtoBaseItem> GetDto(Tuple<string, Func<int>> stub, User user, List<ItemFields> fields)
+        {
+            BaseItem item;
+
+            try
+            {
+                item = await GetEntity(stub.Item1).ConfigureAwait(false);
+            }
+            catch (IOException ex)
+            {
+                Logger.ErrorException("Error getting IBN item {0}", ex, stub.Item1);
+                return null;
+            }
+
+            var dto = await DtoBuilder.GetDtoBaseItem(item, user, fields).ConfigureAwait(false);
+
+            dto.ChildCount = stub.Item2();
+
+            return dto;
+        }
+    }
+
+    /// <summary>
+    /// Class GetItemsByName
+    /// </summary>
+    public class GetItemsByName : IReturn<ItemsResult>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+        /// <summary>
+        /// Gets or sets the start index.
+        /// </summary>
+        /// <value>The start index.</value>
+        public int? StartIndex { get; set; }
+        /// <summary>
+        /// Gets or sets the size of the page.
+        /// </summary>
+        /// <value>The size of the page.</value>
+        public int? PageSize { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether this <see cref="GetItemsByName" /> is recursive.
+        /// </summary>
+        /// <value><c>true</c> if recursive; otherwise, <c>false</c>.</value>
+        public bool Recursive { get; set; }
+        /// <summary>
+        /// Gets or sets the sort order.
+        /// </summary>
+        /// <value>The sort order.</value>
+        public SortOrder? SortOrder { get; set; }
+        /// <summary>
+        /// If specified the search will be localized within a specific item or folder
+        /// </summary>
+        /// <value>The item id.</value>
+        public string Id { get; set; }
+    }
+}

+ 69 - 0
MediaBrowser.Api/UserLibrary/GenresService.cs

@@ -0,0 +1,69 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.UserLibrary
+{
+    /// <summary>
+    /// Class GetGenres
+    /// </summary>
+    [Route("/Users/{UserId}/Items/{Id}/Genres", "GET")]
+    [Route("/Users/{UserId}/Items/Root/Genres", "GET")]
+    public class GetGenres : GetItemsByName
+    {
+    }
+
+    /// <summary>
+    /// Class GenresService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class GenresService : BaseItemsByNameService<Genre>
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetGenres request)
+        {
+            var result = GetResult(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets all items.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="items">The items.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
+        protected override IEnumerable<Tuple<string, Func<int>>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items, User user)
+        {
+            var itemsList = items.Where(i => i.Genres != null).ToList();
+
+            return itemsList
+                .SelectMany(i => i.Genres)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .Select(name => new Tuple<string, Func<int>>(name, () => itemsList.Count(i => i.Genres.Contains(name, StringComparer.OrdinalIgnoreCase))));
+        }
+
+        /// <summary>
+        /// Gets the entity.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns>Task{Genre}.</returns>
+        protected override Task<Genre> GetEntity(string name)
+        {
+            var kernel = (Kernel)Kernel;
+
+            return kernel.LibraryManager.GetGenre(name);
+        }
+    }
+}

+ 788 - 0
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -0,0 +1,788 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Entities;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.UserLibrary
+{
+    /// <summary>
+    /// Class GetItems
+    /// </summary>
+    [Route("/Users/{UserId}/Items", "GET")]
+    public class GetItems : IReturn<ItemsResult>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Specify this to localize the search to a specific item or folder. Omit to use the root.
+        /// </summary>
+        /// <value>The parent id.</value>
+        public string ParentId { get; set; }
+
+        /// <summary>
+        /// Skips over a given number of items within the results. Use for paging.
+        /// </summary>
+        /// <value>The start index.</value>
+        public int? StartIndex { get; set; }
+
+        /// <summary>
+        /// The maximum number of items to return
+        /// </summary>
+        /// <value>The limit.</value>
+        public int? Limit { get; set; }
+
+        /// <summary>
+        /// Whether or not to perform the query recursively
+        /// </summary>
+        /// <value><c>true</c> if recursive; otherwise, <c>false</c>.</value>
+        public bool Recursive { get; set; }
+
+        /// <summary>
+        /// Limit results to items containing a specific person
+        /// </summary>
+        /// <value>The person.</value>
+        public string Person { get; set; }
+
+        /// <summary>
+        /// If the Person filter is used, this can also be used to restrict to a specific person type
+        /// </summary>
+        /// <value>The type of the person.</value>
+        public string PersonType { get; set; }
+
+        /// <summary>
+        /// Search characters used to find items
+        /// </summary>
+        /// <value>The index by.</value>
+        public string SearchTerm { get; set; }
+
+        /// <summary>
+        /// The dynamic, localized index function name
+        /// </summary>
+        /// <value>The index by.</value>
+        public string IndexBy { get; set; }
+
+        /// <summary>
+        /// The dynamic, localized sort function name
+        /// </summary>
+        /// <value>The dynamic sort by.</value>
+        public string DynamicSortBy { get; set; }
+
+        /// <summary>
+        /// What to sort the results by
+        /// </summary>
+        /// <value>The sort by.</value>
+        public string SortBy { get; set; }
+
+        /// <summary>
+        /// The sort order to return results with
+        /// </summary>
+        /// <value>The sort order.</value>
+        public string SortOrder { get; set; }
+
+        /// <summary>
+        /// Filters to apply to the results
+        /// </summary>
+        /// <value>The filters.</value>
+        public string Filters { get; set; }
+
+        /// <summary>
+        /// Fields to return within the items, in addition to basic information
+        /// </summary>
+        /// <value>The fields.</value>
+        public string Fields { get; set; }
+
+        /// <summary>
+        /// Limit results to items containing specific genres
+        /// </summary>
+        /// <value>The genres.</value>
+        public string Genres { get; set; }
+
+        /// <summary>
+        /// Limit results to items containing specific studios
+        /// </summary>
+        /// <value>The studios.</value>
+        public string Studios { get; set; }
+
+        /// <summary>
+        /// Gets or sets the exclude item types.
+        /// </summary>
+        /// <value>The exclude item types.</value>
+        public string ExcludeItemTypes { get; set; }
+
+        /// <summary>
+        /// Gets or sets the include item types.
+        /// </summary>
+        /// <value>The include item types.</value>
+        public string IncludeItemTypes { get; set; }
+
+        /// <summary>
+        /// Limit results to items containing specific years
+        /// </summary>
+        /// <value>The years.</value>
+        public string Years { get; set; }
+
+        /// <summary>
+        /// Gets or sets the image types.
+        /// </summary>
+        /// <value>The image types.</value>
+        public string ImageTypes { get; set; }
+    }
+
+    /// <summary>
+    /// Class ItemsService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class ItemsService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetItems request)
+        {
+            var result = GetItems(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the items.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>Task{ItemsResult}.</returns>
+        private async Task<ItemsResult> GetItems(GetItems request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var items = GetItemsToSerialize(request, user);
+
+            // Apply filters
+            // Run them starting with the ones that are likely to reduce the list the most
+            foreach (var filter in GetFilters(request).OrderByDescending(f => (int)f))
+            {
+                items = ApplyFilter(items, filter, user);
+            }
+
+            items = ApplyAdditionalFilters(request, items);
+
+            items = ApplySearchTerm(request, items);
+
+            items = ApplySortOrder(request, items, user);
+
+            var itemsArray = items.ToArray();
+
+            var pagedItems = ApplyPaging(request, itemsArray);
+
+            var fields = GetItemFields(request).ToList();
+
+            var returnItems = await Task.WhenAll(pagedItems.Select(i => DtoBuilder.GetDtoBaseItem(i, user, fields))).ConfigureAwait(false);
+
+            return new ItemsResult
+            {
+                TotalRecordCount = itemsArray.Length,
+                Items = returnItems
+            };
+        }
+
+        /// <summary>
+        /// Gets the items to serialize.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        /// <exception cref="System.InvalidOperationException"></exception>
+        private IEnumerable<BaseItem> GetItemsToSerialize(GetItems request, User user)
+        {
+            var item = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : DtoBuilder.GetItemByClientId(request.ParentId, user.Id);
+
+            // Default list type = children
+
+            if (request.Recursive)
+            {
+                return ((Folder)item).GetRecursiveChildren(user);
+            }
+
+            return ((Folder)item).GetChildren(user, request.IndexBy, request.DynamicSortBy, GetSortOrder(request));
+        }
+
+        /// <summary>
+        /// Applies sort order
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="items">The items.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        private IEnumerable<BaseItem> ApplySortOrder(GetItems request, IEnumerable<BaseItem> items, User user)
+        {
+            var isFirst = true;
+            var descending = (GetSortOrder(request) ?? SortOrder.Ascending) == SortOrder.Descending;
+
+            IOrderedEnumerable<BaseItem> orderedItems = null;
+
+            foreach (var orderBy in GetOrderBy(request).Select(o => GetComparer(o, user)))
+            {
+                if (isFirst)
+                {
+                    orderedItems = descending ? items.OrderByDescending(i => i, orderBy) : items.OrderBy(i => i, orderBy);
+                }
+                else
+                {
+                    orderedItems = descending ? orderedItems.ThenByDescending(i => i, orderBy) : orderedItems.ThenBy(i => i, orderBy);
+                }
+
+                isFirst = false;
+            }
+
+            return orderedItems ?? items;
+        }
+
+        /// <summary>
+        /// Gets the comparer.
+        /// </summary>
+        /// <param name="sortBy">The sort by.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IComparer{BaseItem}.</returns>
+        /// <exception cref="System.ArgumentException"></exception>
+        private IComparer<BaseItem> GetComparer(ItemSortBy sortBy, User user)
+        {
+            switch (sortBy)
+            {
+                case ItemSortBy.Album:
+                    return new AlbumComparer();
+                case ItemSortBy.AlbumArtist:
+                    return new AlbumArtistComparer();
+                case ItemSortBy.Artist:
+                    return new ArtistComparer();
+                case ItemSortBy.Random:
+                    return new RandomComparer();
+                case ItemSortBy.DateCreated:
+                    return new DateCreatedComparer();
+                case ItemSortBy.SortName:
+                    return new SortNameComparer();
+                case ItemSortBy.PremiereDate:
+                    return new PremiereDateComparer();
+                case ItemSortBy.DatePlayed:
+                    return new DatePlayedComparer { User = user };
+                default:
+                    throw new ArgumentException();
+            }
+        }
+
+        /// <summary>
+        /// Applies filtering
+        /// </summary>
+        /// <param name="items">The items.</param>
+        /// <param name="filter">The filter.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        private IEnumerable<BaseItem> ApplyFilter(IEnumerable<BaseItem> items, ItemFilter filter, User user)
+        {
+            switch (filter)
+            {
+                case ItemFilter.IsFavorite:
+                    return items.Where(item =>
+                    {
+                        var userdata = item.GetUserData(user, false);
+
+                        return userdata != null && userdata.IsFavorite;
+                    });
+
+                case ItemFilter.IsRecentlyAdded:
+                    return items.Where(item => item.IsRecentlyAdded(user));
+
+                case ItemFilter.IsResumable:
+                    return items.Where(item =>
+                    {
+                        var userdata = item.GetUserData(user, false);
+
+                        return userdata != null && userdata.PlaybackPositionTicks > 0;
+                    });
+
+                case ItemFilter.IsPlayed:
+                    return items.Where(item =>
+                    {
+                        var userdata = item.GetUserData(user, false);
+
+                        return userdata != null && userdata.PlayCount > 0;
+                    });
+
+                case ItemFilter.IsRecentlyPlayed:
+                    return items.Where(item => item.IsRecentlyPlayed(user));
+
+                case ItemFilter.IsUnplayed:
+                    return items.Where(item =>
+                    {
+                        var userdata = item.GetUserData(user, false);
+
+                        return userdata == null || userdata.PlayCount == 0;
+                    });
+
+                case ItemFilter.IsFolder:
+                    return items.Where(item => item.IsFolder);
+
+                case ItemFilter.IsNotFolder:
+                    return items.Where(item => !item.IsFolder);
+            }
+
+            return items;
+        }
+
+        /// <summary>
+        /// Applies the additional filters.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="items">The items.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        private IEnumerable<BaseItem> ApplyAdditionalFilters(GetItems request, IEnumerable<BaseItem> items)
+        {
+            var imageTypes = GetImageTypes(request).ToArray();
+            if (imageTypes.Length > 0)
+            {
+                items = items.Where(item => imageTypes.Any(imageType => HasImage(item, imageType)));
+            }
+
+            // Exclude item types
+            var excludeItemTypes = request.ExcludeItemTypes;
+            if (!string.IsNullOrEmpty(excludeItemTypes))
+            {
+                var vals = excludeItemTypes.Split(',');
+                items = items.Where(f => !vals.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase));
+            }
+
+            var includeItemTypes = request.IncludeItemTypes;
+            if (!string.IsNullOrEmpty(includeItemTypes))
+            {
+                var vals = includeItemTypes.Split(',');
+                items = items.Where(f => vals.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase));
+            }
+
+            var genres = request.Genres;
+
+            // Apply genre filter
+            if (!string.IsNullOrEmpty(genres))
+            {
+                var vals = genres.Split(',');
+                items = items.Where(f => f.Genres != null && vals.Any(v => f.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)));
+            }
+
+            var studios = request.Studios;
+
+            // Apply studio filter
+            if (!string.IsNullOrEmpty(studios))
+            {
+                var vals = studios.Split(',');
+                items = items.Where(f => f.Studios != null && vals.Any(v => f.Studios.Contains(v, StringComparer.OrdinalIgnoreCase)));
+            }
+
+            var years = request.Years;
+
+            // Apply year filter
+            if (!string.IsNullOrEmpty(years))
+            {
+                var vals = years.Split(',').Select(int.Parse);
+                items = items.Where(f => f.ProductionYear.HasValue && vals.Contains(f.ProductionYear.Value));
+            }
+
+            var personName = request.Person;
+
+            // Apply person filter
+            if (!string.IsNullOrEmpty(personName))
+            {
+                var personType = request.PersonType;
+
+                items = !string.IsNullOrEmpty(personType)
+                            ? items.Where(item => item.People != null && item.People.Any(p => p.Name.Equals(personName, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(personType, StringComparison.OrdinalIgnoreCase)))
+                            : items.Where(item => item.People != null && item.People.Any(p => p.Name.Equals(personName, StringComparison.OrdinalIgnoreCase)));
+            }
+
+            return items;
+        }
+
+        /// <summary>
+        /// Determines whether the specified item has image.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="imageType">Type of the image.</param>
+        /// <returns><c>true</c> if the specified item has image; otherwise, <c>false</c>.</returns>
+        private bool HasImage(BaseItem item, ImageType imageType)
+        {
+            if (imageType == ImageType.Backdrop)
+            {
+                return item.BackdropImagePaths != null && item.BackdropImagePaths.Count > 0;
+            }
+
+            if (imageType == ImageType.Screenshot)
+            {
+                return item.ScreenshotImagePaths != null && item.ScreenshotImagePaths.Count > 0;
+            }
+
+            if (imageType == ImageType.ChapterImage)
+            {
+                var video = item as Video;
+
+                if (video != null)
+                {
+                    return video.Chapters != null && video.Chapters.Any(c => !string.IsNullOrEmpty(c.ImagePath));
+                }
+
+                return false;
+            }
+
+            return item.HasImage(imageType);
+        }
+
+        /// <summary>
+        /// Applies the search term.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="items">The items.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        private IEnumerable<BaseItem> ApplySearchTerm(GetItems request, IEnumerable<BaseItem> items)
+        {
+            var term = request.SearchTerm;
+
+            if (!string.IsNullOrEmpty(term))
+            {
+                items = items.Where(i => i.Name.StartsWith(term, StringComparison.OrdinalIgnoreCase));
+            }
+
+            return items;
+        }
+
+        /// <summary>
+        /// Applies the paging.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="items">The items.</param>
+        /// <returns>IEnumerable{BaseItem}.</returns>
+        private IEnumerable<BaseItem> ApplyPaging(GetItems request, IEnumerable<BaseItem> items)
+        {
+            // Start at
+            if (request.StartIndex.HasValue)
+            {
+                items = items.Skip(request.StartIndex.Value);
+            }
+
+            // Return limit
+            if (request.Limit.HasValue)
+            {
+                items = items.Take(request.Limit.Value);
+            }
+
+            return items;
+        }
+
+        /// <summary>
+        /// Gets the sort order.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Nullable{SortOrder}.</returns>
+        private SortOrder? GetSortOrder(GetItems request)
+        {
+            if (string.IsNullOrEmpty(request.SortOrder))
+            {
+                return null;
+            }
+
+            return (SortOrder)Enum.Parse(typeof(SortOrder), request.SortOrder, true);
+        }
+
+        /// <summary>
+        /// Gets the filters.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>IEnumerable{ItemFilter}.</returns>
+        private IEnumerable<ItemFilter> GetFilters(GetItems request)
+        {
+            var val = request.Filters;
+
+            if (string.IsNullOrEmpty(val))
+            {
+                return new ItemFilter[] { };
+            }
+
+            return val.Split(',').Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true));
+        }
+
+        /// <summary>
+        /// Gets the item fields.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>IEnumerable{ItemFields}.</returns>
+        private IEnumerable<ItemFields> GetItemFields(GetItems request)
+        {
+            var val = request.Fields;
+
+            if (string.IsNullOrEmpty(val))
+            {
+                return new ItemFields[] { };
+            }
+
+            return val.Split(',').Select(v => (ItemFields)Enum.Parse(typeof(ItemFields), v, true));
+        }
+
+        /// <summary>
+        /// Gets the order by.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>IEnumerable{ItemSortBy}.</returns>
+        private IEnumerable<ItemSortBy> GetOrderBy(GetItems request)
+        {
+            var val = request.SortBy;
+
+            if (string.IsNullOrEmpty(val))
+            {
+                return new ItemSortBy[] { };
+            }
+
+            return val.Split(',').Select(v => (ItemSortBy)Enum.Parse(typeof(ItemSortBy), v, true));
+        }
+
+        /// <summary>
+        /// Gets the image types.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>IEnumerable{ImageType}.</returns>
+        private IEnumerable<ImageType> GetImageTypes(GetItems request)
+        {
+            var val = request.ImageTypes;
+
+            if (string.IsNullOrEmpty(val))
+            {
+                return new ImageType[] { };
+            }
+
+            return val.Split(',').Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true));
+        }
+    }
+
+    /// <summary>
+    /// Class DateCreatedComparer
+    /// </summary>
+    public class DateCreatedComparer : IComparer<BaseItem>
+    {
+        /// <summary>
+        /// Compares the specified x.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <param name="y">The y.</param>
+        /// <returns>System.Int32.</returns>
+        public int Compare(BaseItem x, BaseItem y)
+        {
+            return x.DateCreated.CompareTo(y.DateCreated);
+        }
+    }
+
+    /// <summary>
+    /// Class RandomComparer
+    /// </summary>
+    public class RandomComparer : IComparer<BaseItem>
+    {
+        /// <summary>
+        /// Compares the specified x.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <param name="y">The y.</param>
+        /// <returns>System.Int32.</returns>
+        public int Compare(BaseItem x, BaseItem y)
+        {
+            return Guid.NewGuid().CompareTo(Guid.NewGuid());
+        }
+    }
+
+    /// <summary>
+    /// Class SortNameComparer
+    /// </summary>
+    public class SortNameComparer : IComparer<BaseItem>
+    {
+        /// <summary>
+        /// Compares the specified x.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <param name="y">The y.</param>
+        /// <returns>System.Int32.</returns>
+        public int Compare(BaseItem x, BaseItem y)
+        {
+            return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase);
+        }
+    }
+
+    /// <summary>
+    /// Class AlbumArtistComparer
+    /// </summary>
+    public class AlbumArtistComparer : IComparer<BaseItem>
+    {
+        /// <summary>
+        /// Compares the specified x.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <param name="y">The y.</param>
+        /// <returns>System.Int32.</returns>
+        public int Compare(BaseItem x, BaseItem y)
+        {
+            return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+        }
+
+        /// <summary>
+        /// Gets the value.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <returns>System.String.</returns>
+        private string GetValue(BaseItem x)
+        {
+            var audio = x as Audio;
+
+            return audio == null ? string.Empty : audio.AlbumArtist;
+        }
+    }
+
+    /// <summary>
+    /// Class AlbumComparer
+    /// </summary>
+    public class AlbumComparer : IComparer<BaseItem>
+    {
+        /// <summary>
+        /// Compares the specified x.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <param name="y">The y.</param>
+        /// <returns>System.Int32.</returns>
+        public int Compare(BaseItem x, BaseItem y)
+        {
+            return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+        }
+
+        /// <summary>
+        /// Gets the value.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <returns>System.String.</returns>
+        private string GetValue(BaseItem x)
+        {
+            var audio = x as Audio;
+
+            return audio == null ? string.Empty : audio.Album;
+        }
+    }
+
+    /// <summary>
+    /// Class ArtistComparer
+    /// </summary>
+    public class ArtistComparer : IComparer<BaseItem>
+    {
+        /// <summary>
+        /// Compares the specified x.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <param name="y">The y.</param>
+        /// <returns>System.Int32.</returns>
+        public int Compare(BaseItem x, BaseItem y)
+        {
+            return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+        }
+
+        /// <summary>
+        /// Gets the value.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <returns>System.String.</returns>
+        private string GetValue(BaseItem x)
+        {
+            var audio = x as Audio;
+
+            return audio == null ? string.Empty : audio.Artist;
+        }
+    }
+
+    /// <summary>
+    /// Class PremiereDateComparer
+    /// </summary>
+    public class PremiereDateComparer : IComparer<BaseItem>
+    {
+        /// <summary>
+        /// Compares the specified x.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <param name="y">The y.</param>
+        /// <returns>System.Int32.</returns>
+        public int Compare(BaseItem x, BaseItem y)
+        {
+            return GetDate(x).CompareTo(GetDate(y));
+        }
+
+        /// <summary>
+        /// Gets the date.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <returns>DateTime.</returns>
+        private DateTime GetDate(BaseItem x)
+        {
+            if (x.PremiereDate.HasValue)
+            {
+                return x.PremiereDate.Value;
+            }
+
+            if (x.ProductionYear.HasValue)
+            {
+                return new DateTime(x.ProductionYear.Value, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+            }
+            return DateTime.MaxValue;
+        }
+    }
+
+    /// <summary>
+    /// Class DatePlayedComparer
+    /// </summary>
+    public class DatePlayedComparer : IComparer<BaseItem>
+    {
+        /// <summary>
+        /// Gets or sets the user.
+        /// </summary>
+        /// <value>The user.</value>
+        public User User { get; set; }
+
+        /// <summary>
+        /// Compares the specified x.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <param name="y">The y.</param>
+        /// <returns>System.Int32.</returns>
+        public int Compare(BaseItem x, BaseItem y)
+        {
+            return GetDate(x).CompareTo(GetDate(y));
+        }
+
+        /// <summary>
+        /// Gets the date.
+        /// </summary>
+        /// <param name="x">The x.</param>
+        /// <returns>DateTime.</returns>
+        private DateTime GetDate(BaseItem x)
+        {
+            var userdata = x.GetUserData(User, false);
+
+            if (userdata != null && userdata.LastPlayedDate.HasValue)
+            {
+                return userdata.LastPlayedDate.Value;
+            }
+
+            return DateTime.MaxValue;
+        }
+    }
+}

+ 103 - 0
MediaBrowser.Api/UserLibrary/PersonsService.cs

@@ -0,0 +1,103 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.UserLibrary
+{
+    /// <summary>
+    /// Class GetPersons
+    /// </summary>
+    [Route("/Users/{UserId}/Items/{Id}/Persons", "GET")]
+    [Route("/Users/{UserId}/Items/Root/Persons", "GET")]
+    public class GetPersons : GetItemsByName
+    {
+        /// <summary>
+        /// Gets or sets the person types.
+        /// </summary>
+        /// <value>The person types.</value>
+        public string PersonTypes { get; set; }
+    }
+
+    /// <summary>
+    /// Class PersonsService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class PersonsService : BaseItemsByNameService<Person>
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetPersons request)
+        {
+            var result = GetResult(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets all items.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="items">The items.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
+        protected override IEnumerable<Tuple<string, Func<int>>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items, User user)
+        {
+            var inputPersonTypes = ((GetPersons) request).PersonTypes;
+            var personTypes = string.IsNullOrEmpty(inputPersonTypes) ? new string[] { } : inputPersonTypes.Split(',');
+
+            var itemsList = items.Where(i => i.People != null).ToList();
+
+            // Either get all people, or all people filtered by a specific person type
+            var allPeople = GetAllPeople(itemsList, personTypes);
+
+            return allPeople
+                .Select(i => i.Name)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+
+                .Select(name => new Tuple<string, Func<int>>(name, () =>
+                {
+                    if (personTypes.Length == 0)
+                    {
+                        return itemsList.Count(i => i.People.Any(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
+                    }
+
+                    return itemsList.Count(i => i.People.Any(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && personTypes.Contains(p.Type ?? string.Empty, StringComparer.OrdinalIgnoreCase)));
+                })
+            );
+        }
+
+        /// <summary>
+        /// Gets all people.
+        /// </summary>
+        /// <param name="itemsList">The items list.</param>
+        /// <param name="personTypes">The person types.</param>
+        /// <returns>IEnumerable{PersonInfo}.</returns>
+        private IEnumerable<PersonInfo> GetAllPeople(IEnumerable<BaseItem> itemsList, string[] personTypes)
+        {
+            return personTypes.Length == 0 ?
+                itemsList.SelectMany(i => i.People) :
+                itemsList.SelectMany(i => i.People.Where(p => personTypes.Contains(p.Type ?? string.Empty, StringComparer.OrdinalIgnoreCase)));
+        }
+
+        /// <summary>
+        /// Gets the entity.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns>Task{Genre}.</returns>
+        protected override Task<Person> GetEntity(string name)
+        {
+            var kernel = (Kernel)Kernel;
+
+            return kernel.LibraryManager.GetPerson(name);
+        }
+    }
+}

+ 69 - 0
MediaBrowser.Api/UserLibrary/StudiosService.cs

@@ -0,0 +1,69 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.UserLibrary
+{
+    /// <summary>
+    /// Class GetStudios
+    /// </summary>
+    [Route("/Users/{UserId}/Items/{Id}/Studios", "GET")]
+    [Route("/Users/{UserId}/Items/Root/Studios", "GET")]
+    public class GetStudios : GetItemsByName
+    {
+    }
+
+    /// <summary>
+    /// Class StudiosService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class StudiosService : BaseItemsByNameService<Studio>
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetStudios request)
+        {
+            var result = GetResult(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets all items.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="items">The items.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
+        protected override IEnumerable<Tuple<string, Func<int>>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items, User user)
+        {
+            var itemsList = items.Where(i => i.Studios != null).ToList();
+
+            return itemsList
+                .SelectMany(i => i.Studios)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .Select(name => new Tuple<string, Func<int>>(name, () => itemsList.Count(i => i.Studios.Contains(name, StringComparer.OrdinalIgnoreCase))));
+        }
+
+        /// <summary>
+        /// Gets the entity.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns>Task{Studio}.</returns>
+        protected override Task<Studio> GetEntity(string name)
+        {
+            var kernel = (Kernel)Kernel;
+
+            return kernel.LibraryManager.GetStudio(name);
+        }
+    }
+}

+ 506 - 0
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -0,0 +1,506 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.DTO;
+using MediaBrowser.Model.Entities;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using ServiceStack.Text.Controller;
+
+namespace MediaBrowser.Api.UserLibrary
+{
+    /// <summary>
+    /// Class GetItem
+    /// </summary>
+    [Route("/Users/{UserId}/Items/{Id}", "GET")]
+    [Route("/Users/{UserId}/Items/Root", "GET")]
+    public class GetItem : IReturn<DtoBaseItem>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetIntros
+    /// </summary>
+    [Route("/Users/{UserId}/Items/{Id}/Intros", "GET")]
+    [ServiceStack.ServiceHost.Api(("Gets intros to play before the main media item plays"))]
+    public class GetIntros : IReturn<List<string>>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the item id.
+        /// </summary>
+        /// <value>The item id.</value>
+        public string Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class UpdateDisplayPreferences
+    /// </summary>
+    [Route("/Users/{UserId}/Items/{Id}/DisplayPreferences", "GET")]
+    [ServiceStack.ServiceHost.Api(("Updates a user's display preferences for an item"))]
+    public class UpdateDisplayPreferences : IReturnVoid, IRequiresRequestStream
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// The raw Http Request Input Stream
+        /// </summary>
+        /// <value>The request stream.</value>
+        public Stream RequestStream { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetVirtualFolders
+    /// </summary>
+    [Route("/Users/{UserId}/VirtualFolders", "GET")]
+    public class GetVirtualFolders : IReturn<List<VirtualFolderInfo>>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+    }
+
+    /// <summary>
+    /// Class MarkFavoriteItem
+    /// </summary>
+    [Route("/Users/{UserId}/FavoriteItems/{Id}", "POST")]
+    public class MarkFavoriteItem : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class UnmarkFavoriteItem
+    /// </summary>
+    [Route("/Users/{UserId}/FavoriteItems/{Id}", "DELETE")]
+    public class UnmarkFavoriteItem : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class ClearUserItemRating
+    /// </summary>
+    [Route("/Users/{UserId}/Items/{Id}/Rating", "DELETE")]
+    public class DeleteUserItemRating : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class UpdateUserItemRating
+    /// </summary>
+    [Route("/Users/{UserId}/Items/{Id}/Rating", "POST")]
+    public class UpdateUserItemRating : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes.
+        /// </summary>
+        /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value>
+        public bool Likes { get; set; }
+    }
+
+    /// <summary>
+    /// Class MarkPlayedItem
+    /// </summary>
+    [Route("/Users/{UserId}/PlayedItems/{Id}", "POST")]
+    public class MarkPlayedItem : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class MarkUnplayedItem
+    /// </summary>
+    [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE")]
+    public class MarkUnplayedItem : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+    }
+
+    [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET")]
+    public class GetLocalTrailers : IReturn<List<DtoBaseItem>>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+    }
+
+    [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET")]
+    public class GetSpecialFeatures : IReturn<List<DtoBaseItem>>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        public Guid UserId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+    }
+
+
+    /// <summary>
+    /// Class UserLibraryService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class UserLibraryService : BaseRestService
+    {
+        public object Get(GetSpecialFeatures request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var item = DtoBuilder.GetItemByClientId(request.Id, user.Id);
+            
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList();
+
+            var movie = (Movie)item;
+
+            var items = movie.SpecialFeatures.Select(i => DtoBuilder.GetDtoBaseItem(item, user, fields)).AsParallel().Select(t => t.Result).ToList();
+
+            return ToOptimizedResult(items);
+        }
+        
+        public object Get(GetLocalTrailers request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var item = DtoBuilder.GetItemByClientId(request.Id, user.Id);
+            
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList();
+
+            var items = item.LocalTrailers.Select(i => DtoBuilder.GetDtoBaseItem(item, user, fields)).AsParallel().Select(t => t.Result).ToList();
+
+            return ToOptimizedResult(items);
+        }
+        
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetItem request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var item = string.IsNullOrEmpty(request.Id) ? user.RootFolder : DtoBuilder.GetItemByClientId(request.Id, user.Id);
+            
+            // Get everything
+            var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList();
+
+            var result = DtoBuilder.GetDtoBaseItem(item, user, fields).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetVirtualFolders request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var result = kernel.LibraryManager.GetVirtualFolders(user).ToList();
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetIntros request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var item = DtoBuilder.GetItemByClientId(request.Id, user.Id);
+
+            var result = kernel.IntroProviders.SelectMany(i => i.GetIntros(item, user));
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(UpdateDisplayPreferences request)
+        {
+            // We need to parse this manually because we told service stack not to with IRequiresRequestStream
+            // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
+            var pathInfo = PathInfo.Parse(Request.PathInfo);
+            var userId = new Guid(pathInfo.GetArgumentValue<string>(1));
+            var itemId = pathInfo.GetArgumentValue<string>(3);
+
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(userId);
+
+            var item = (Folder)DtoBuilder.GetItemByClientId(itemId, user.Id);
+
+            var displayPreferences = JsonSerializer.DeserializeFromStream<DisplayPreferences>(request.RequestStream);
+
+            var task = kernel.LibraryManager.SaveDisplayPreferencesForFolder(user, item, displayPreferences);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(MarkFavoriteItem request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var item = (Folder)DtoBuilder.GetItemByClientId(request.Id, user.Id);
+
+            // Get the user data for this item
+            var data = item.GetUserData(user, true);
+
+            // Set favorite status
+            data.IsFavorite = true;
+
+            var task = kernel.UserDataManager.SaveUserDataForItem(user, item, data);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Deletes the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Delete(UnmarkFavoriteItem request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var item = (Folder)DtoBuilder.GetItemByClientId(request.Id, user.Id);
+
+            // Get the user data for this item
+            var data = item.GetUserData(user, true);
+
+            // Set favorite status
+            data.IsFavorite = false;
+
+            var task = kernel.UserDataManager.SaveUserDataForItem(user, item, data);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Deletes the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Delete(DeleteUserItemRating request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var item = (Folder)DtoBuilder.GetItemByClientId(request.Id, user.Id);
+
+            // Get the user data for this item
+            var data = item.GetUserData(user, true);
+
+            data.Rating = null;
+
+            var task = kernel.UserDataManager.SaveUserDataForItem(user, item, data);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(UpdateUserItemRating request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var item = (Folder)DtoBuilder.GetItemByClientId(request.Id, user.Id);
+
+            // Get the user data for this item
+            var data = item.GetUserData(user, true);
+
+            data.Likes = request.Likes;
+
+            var task = kernel.UserDataManager.SaveUserDataForItem(user, item, data);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(MarkPlayedItem request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var task = UpdatePlayedStatus(user, request.Id, true);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Deletes the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Delete(MarkUnplayedItem request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.UserId);
+
+            var task = UpdatePlayedStatus(user, request.Id, false);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Updates the played status.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
+        /// <returns>Task.</returns>
+        private Task UpdatePlayedStatus(User user, string itemId, bool wasPlayed)
+        {
+            var item = DtoBuilder.GetItemByClientId(itemId, user.Id);
+
+            return item.SetPlayedStatus(user, wasPlayed);
+        }
+    }
+}

+ 75 - 0
MediaBrowser.Api/UserLibrary/YearsService.cs

@@ -0,0 +1,75 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.UserLibrary
+{
+    /// <summary>
+    /// Class GetYears
+    /// </summary>
+    [Route("/Users/{UserId}/Items/{Id}/Years", "GET")]
+    [Route("/Users/{UserId}/Items/Root/Years", "GET")]
+    public class GetYears : GetItemsByName
+    {
+    }
+
+    /// <summary>
+    /// Class YearsService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class YearsService : BaseItemsByNameService<Year>
+    {
+        /// <summary>
+        /// The us culture
+        /// </summary>
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetYears request)
+        {
+            var result = GetResult(request).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets all items.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="items">The items.</param>
+        /// <param name="user">The user.</param>
+        /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns>
+        protected override IEnumerable<Tuple<string, Func<int>>> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items, User user)
+        {
+            var itemsList = items.Where(i => i.ProductionYear != null).ToList();
+
+            return itemsList
+                .Select(i => i.ProductionYear.Value)
+                .Distinct()
+                .Select(year => new Tuple<string, Func<int>>(year.ToString(UsCulture), () => itemsList.Count(i => i.ProductionYear.HasValue && i.ProductionYear.Value == year)));
+        }
+
+        /// <summary>
+        /// Gets the entity.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns>Task{Studio}.</returns>
+        protected override Task<Year> GetEntity(string name)
+        {
+            var kernel = (Kernel)Kernel;
+
+            return kernel.LibraryManager.GetYear(int.Parse(name, UsCulture));
+        }
+    }
+}

+ 297 - 0
MediaBrowser.Api/UserService.cs

@@ -0,0 +1,297 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.DTO;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using ServiceStack.Text.Controller;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class GetUsers
+    /// </summary>
+    [Route("/Users", "GET")]
+    public class GetUsers : IReturn<List<DtoUser>>
+    {
+    }
+
+    /// <summary>
+    /// Class GetUser
+    /// </summary>
+    [Route("/Users/{Id}", "GET")]
+    public class GetUser : IReturn<DtoUser>
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class DeleteUser
+    /// </summary>
+    [Route("/Users/{Id}", "DELETE")]
+    public class DeleteUser : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class AuthenticateUser
+    /// </summary>
+    [Route("/Users/{Id}/Authenticate", "POST")]
+    public class AuthenticateUser : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the password.
+        /// </summary>
+        /// <value>The password.</value>
+        public string Password { get; set; }
+    }
+
+    /// <summary>
+    /// Class UpdateUserPassword
+    /// </summary>
+    [Route("/Users/{Id}/Password", "POST")]
+    public class UpdateUserPassword : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the password.
+        /// </summary>
+        /// <value>The password.</value>
+        public string CurrentPassword { get; set; }
+
+        /// <summary>
+        /// Gets or sets the new password.
+        /// </summary>
+        /// <value>The new password.</value>
+        public string NewPassword { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether [reset password].
+        /// </summary>
+        /// <value><c>true</c> if [reset password]; otherwise, <c>false</c>.</value>
+        public bool ResetPassword { get; set; }
+    }
+
+    /// <summary>
+    /// Class UpdateUser
+    /// </summary>
+    [Route("/Users/{Id}", "POST")]
+    public class UpdateUser : IRequiresRequestStream, IReturnVoid
+    {
+        /// <summary>
+        /// The raw Http Request Input Stream
+        /// </summary>
+        /// <value>The request stream.</value>
+        public Stream RequestStream { get; set; }
+
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public Guid Id { get; set; }
+    }
+
+    /// <summary>
+    /// Class CreateUser
+    /// </summary>
+    [Route("/Users", "POST")]
+    public class CreateUser : IRequiresRequestStream, IReturn<DtoUser>
+    {
+        /// <summary>
+        /// The raw Http Request Input Stream
+        /// </summary>
+        /// <value>The request stream.</value>
+        public Stream RequestStream { get; set; }
+    }
+
+    /// <summary>
+    /// Class UsersService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class UserService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetUsers request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var result = kernel.Users.OrderBy(u => u.Name).Select(DtoBuilder.GetDtoUser).ToList();
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetUser request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.Id);
+
+            if (user == null)
+            {
+                throw new ResourceNotFoundException("User not found");
+            }
+
+            var result = DtoBuilder.GetDtoUser(user);
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Deletes the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Delete(DeleteUser request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.Id);
+
+            if (user == null)
+            {
+                throw new ResourceNotFoundException("User not found");
+            }
+
+            var task = kernel.UserManager.DeleteUser(user);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(AuthenticateUser request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.Id);
+
+            if (user == null)
+            {
+                throw new ResourceNotFoundException("User not found");
+            }
+
+            var success = kernel.UserManager.AuthenticateUser(user, request.Password).Result;
+
+            if (!success)
+            {
+                // Unauthorized
+                throw new ResourceNotFoundException("Invalid user or password entered.");
+            }
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(UpdateUserPassword request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var user = kernel.GetUserById(request.Id);
+
+            if (user == null)
+            {
+                throw new ResourceNotFoundException("User not found");
+            }
+
+            if (request.ResetPassword)
+            {
+                var task = user.ResetPassword();
+
+                Task.WaitAll(task);
+            }
+            else
+            {
+                var success = kernel.UserManager.AuthenticateUser(user, request.CurrentPassword).Result;
+
+                if (!success)
+                {
+                    throw new ResourceNotFoundException("Invalid user or password entered.");
+                }
+
+                var task = user.ChangePassword(request.NewPassword);
+
+                Task.WaitAll(task);
+            }
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(UpdateUser request)
+        {
+            // We need to parse this manually because we told service stack not to with IRequiresRequestStream
+            // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
+            var pathInfo = PathInfo.Parse(Request.PathInfo);
+            var id = new Guid(pathInfo.GetArgumentValue<string>(1));
+            
+            var kernel = (Kernel)Kernel;
+
+            var dtoUser = JsonSerializer.DeserializeFromStream<DtoUser>(request.RequestStream);
+
+            var user = kernel.GetUserById(id);
+
+            var task = user.Name.Equals(dtoUser.Name, StringComparison.Ordinal) ? kernel.UserManager.UpdateUser(user) : kernel.UserManager.RenameUser(user, dtoUser.Name);
+
+            Task.WaitAll(task);
+
+            user.UpdateConfiguration(dtoUser.Configuration);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Post(CreateUser request)
+        {
+            var kernel = (Kernel)Kernel;
+
+            var dtoUser = JsonSerializer.DeserializeFromStream<DtoUser>(request.RequestStream);
+
+            var newUser = kernel.UserManager.CreateUser(dtoUser.Name).Result;
+
+            var result = DtoBuilder.GetDtoUser(newUser);
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

+ 46 - 0
MediaBrowser.Api/WeatherService.cs

@@ -0,0 +1,46 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Weather;
+using ServiceStack.ServiceHost;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+
+namespace MediaBrowser.Api
+{
+    /// <summary>
+    /// Class Weather
+    /// </summary>
+    [Route("/Weather", "GET")]
+    public class GetWeather : IReturn<WeatherInfo>
+    {
+        /// <summary>
+        /// Gets or sets the location.
+        /// </summary>
+        /// <value>The location.</value>
+        public string Location { get; set; }
+    }
+
+    /// <summary>
+    /// Class WeatherService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class WeatherService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetWeather request)
+        {
+            var kernel = (Kernel) Kernel;
+
+            var location = string.IsNullOrWhiteSpace(request.Location) ? kernel.Configuration.WeatherLocation : request.Location;
+
+            var result = kernel.WeatherProviders.First().GetWeatherInfoAsync(location, CancellationToken.None).Result;
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

+ 876 - 0
MediaBrowser.Api/options.xml

@@ -0,0 +1,876 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<Options>
+  <!-- ************************************************************* -->
+  <!-- * DO NOT EDIT MANUALLY USE METABROWSER SETTINGS GUI TO EDIT * -->
+  <!-- ************************************************************* -->
+  <Locations>
+    <Location enabled="True" group="">D:\Video\TV\</Location>
+    <Location enabled="True" group="">D:\Video\Movies\</Location>
+    <Location enabled="True" group="Music Videos">D:\Video\Music Videos\</Location>
+  </Locations>
+  <Window>
+    <MetaBrowser Maximized="True" Width="444" Height="2224" X="240" Y="50" />
+    <Options Maximized="False" Width="1120" Height="885" X="470" Y="239" />
+    <AutoUpdate Maximized="False" Width="745" Height="556" X="332" Y="220" />
+    <Search Maximized="False" Width="622" Height="533" X="642" Y="532" />
+    <Poster Maximized="False" Width="994" Height="352" X="1150" Y="542" />
+    <Backdrop Maximized="False" Width="806" Height="440" X="542" Y="332" />
+    <Banner Maximized="False" Width="794" Height="410" X="915" Y="515" />
+    <Episode Maximized="False" Width="798" Height="250" X="0" Y="0" />
+    <Logo Maximized="False" Width="645" Height="400" X="349" Y="312" />
+    <ClearArt Maximized="False" Width="806" Height="440" X="0" Y="0" />
+    <Thumb Maximized="False" Width="806" Height="440" X="0" Y="0" />
+    <Person Maximized="False" Width="994" Height="350" X="0" Y="0" />
+    <ExportList Maximized="False" Width="802" Height="600" X="927" Y="383" />
+    <ManageFilters Maximized="False" Width="860" Height="580" X="157" Y="226" />
+    <DeleteMedia Maximized="False" Width="600" Height="420" X="0" Y="0" />
+    <ProcessProfile Maximized="False" Width="691" Height="868" X="1605" Y="333" />
+    <RealtimeUpdateFields Maximized="False" Width="350" Height="473" X="390" Y="436" />
+    <RenameHistory Maximized="False" Width="1083" Height="677" X="128" Y="252" />
+    <MovieConfirm Maximized="False" Width="800" Height="600" X="0" Y="0" />
+  </Window>
+  <IsFirstRun>False</IsFirstRun>
+  <ShowExpireMessage>True</ShowExpireMessage>
+  <Version>2.2.41</Version>
+  <CacheFolder>C:\ProgramData\MetaBrowser 2.0\Cache\</CacheFolder>
+  <EnableLogging>False</EnableLogging>
+  <LogDeleteDays>10</LogDeleteDays>
+  <RefreshOnStart>True</RefreshOnStart>
+  <FetchOnFirstRefresh>False</FetchOnFirstRefresh>
+  <FetchOnEveryRefresh>False</FetchOnEveryRefresh>
+  <RealTimeMonitoring>True</RealTimeMonitoring>
+  <PollLocations>False</PollLocations>
+  <FetchOnDetect>True</FetchOnDetect>
+  <MinimizeToTray>True</MinimizeToTray>
+  <CloseToTray>False</CloseToTray>
+  <LoadOnStartup>True</LoadOnStartup>
+  <LoadMinimizedToTray>False</LoadMinimizedToTray>
+  <ReplaceMissingOnly>False</ReplaceMissingOnly>
+  <ForceUpdateImages>False</ForceUpdateImages>
+  <FetchIdOnly>False</FetchIdOnly>
+  <AutoSave>False</AutoSave>
+  <ForceSave>False</ForceSave>
+  <ForceAutoSave>False</ForceAutoSave>
+  <ImageAddTypePoster>replace</ImageAddTypePoster>
+  <ImageAddTypeBackdrop>add</ImageAddTypeBackdrop>
+  <ImageAddTypeBanner>replace</ImageAddTypeBanner>
+  <ImageAddTypeLogo>replace</ImageAddTypeLogo>
+  <ImageAddTypeClearArt>replace</ImageAddTypeClearArt>
+  <ImageAddTypeThumb>replace</ImageAddTypeThumb>
+  <ValidVideoExtensions>.iso;.ts;.avi;.mpg;.mkv;.mp4;.mov;.wmv;.dvr-ms;.m4v;.wtv;.flv;.ogm</ValidVideoExtensions>
+  <DynamicFiltering>True</DynamicFiltering>
+  <IgnoreHiddenItems>True</IgnoreHiddenItems>
+  <EnableMapping>True</EnableMapping>
+  <ExtensionAsType>True</ExtensionAsType>
+  <ACD>
+  </ACD>
+  <ValueLists>
+    <MediaTypes>
+      <Value>AVI</Value>
+      <Value>Blu-ray</Value>
+      <Value>DVD</Value>
+      <Value>HD DVD</Value>
+      <Value>MKV</Value>
+    </MediaTypes>
+    <Genres>
+      <Value>Action</Value>
+      <Value>Adventure</Value>
+      <Value>Animation</Value>
+      <Value>Biography</Value>
+      <Value>Comedy</Value>
+      <Value>Crime</Value>
+      <Value>Documentary</Value>
+      <Value>Drama</Value>
+      <Value>Family</Value>
+      <Value>Fantasy</Value>
+      <Value>Film-Noir</Value>
+      <Value>Game-Show</Value>
+      <Value>History</Value>
+      <Value>Horror</Value>
+      <Value>Music</Value>
+      <Value>Musical</Value>
+      <Value>Mystery</Value>
+      <Value>News</Value>
+      <Value>Reality-TV</Value>
+      <Value>Romance</Value>
+      <Value>Sci-Fi</Value>
+      <Value>Short</Value>
+      <Value>Sport</Value>
+      <Value>Talk-Show</Value>
+      <Value>Thriller</Value>
+      <Value>War</Value>
+      <Value>Western</Value>
+    </Genres>
+    <AspectRatio>
+      <Value>1.33:1</Value>
+      <Value>1.78:1</Value>
+      <Value>1.85:1</Value>
+      <Value>2.35:1</Value>
+      <Value>2.40:1</Value>
+    </AspectRatio>
+    <MovieStudios>
+      <Value>20th Century Fox</Value>
+      <Value>20th Century Fox Home Entertainment</Value>
+      <Value>Amblin Entertainment</Value>
+      <Value>Beacon Pictures</Value>
+      <Value>Castle Rock</Value>
+      <Value>Centropolis Entertainment</Value>
+      <Value>Columbia Pictures</Value>
+      <Value>Dimension Films</Value>
+      <Value>Disney</Value>
+      <Value>DreamWorks Pictures</Value>
+      <Value>Hollywood Pictures</Value>
+      <Value>Hyde Park Entertainment</Value>
+      <Value>Imagine Entertainment</Value>
+      <Value>Legendary Pictures</Value>
+      <Value>Lions Gate</Value>
+      <Value>Metro-Goldwyn-Mayer Pictures</Value>
+      <Value>Metro-Goldwyn-Mayer Studios</Value>
+      <Value>MGM Home Entertainment</Value>
+      <Value>Millennium Films</Value>
+      <Value>Miramax Films</Value>
+      <Value>Momentum Pictures</Value>
+      <Value>New Line Cinema</Value>
+      <Value>New Line Home Entertainment</Value>
+      <Value>Paramount Pictures</Value>
+      <Value>Sony Pictures</Value>
+      <Value>Sony Pictures Home Entertainment</Value>
+      <Value>Spyglass Entertainment</Value>
+      <Value>Studio Canal</Value>
+      <Value>Summit Entertainment</Value>
+      <Value>Touchstone Pictures</Value>
+      <Value>Universal Pictures</Value>
+      <Value>Universal Studios</Value>
+      <Value>Universal Studios Home Entertainment</Value>
+      <Value>Valhalla Motion Pictures</Value>
+      <Value>Walt Disney Home Entertainment</Value>
+      <Value>Walt Disney Pictures</Value>
+      <Value>Warner Bros.</Value>
+      <Value>Warner Bros. Entertainment</Value>
+      <Value>Warner Bros. Pictures</Value>
+      <Value>Weinstein Company</Value>
+      <Value>Working Title Productions</Value>
+    </MovieStudios>
+    <MovieRatings>
+      <Value>CS</Value>
+      <Value>G</Value>
+      <Value>NC-17</Value>
+      <Value>NR</Value>
+      <Value>PG</Value>
+      <Value>PG-13</Value>
+      <Value>R</Value>
+      <Value>S</Value>
+    </MovieRatings>
+    <MovieCrewType>
+      <Value>Art Director</Value>
+      <Value>Assistant Director</Value>
+      <Value>Associate Producer</Value>
+      <Value>Background Artist</Value>
+      <Value>Best Boy</Value>
+      <Value>Body Double</Value>
+      <Value>Boom Operator</Value>
+      <Value>Camera Loader</Value>
+      <Value>Casting Director</Value>
+      <Value>Choreographer</Value>
+      <Value>Cinematographer</Value>
+      <Value>Color Consultant</Value>
+      <Value>Composer</Value>
+      <Value>Conductor</Value>
+      <Value>Construction Coordinator</Value>
+      <Value>Costume Designer</Value>
+      <Value>Costumer</Value>
+      <Value>Creator</Value>
+      <Value>Dialog Coach</Value>
+      <Value>Director</Value>
+      <Value>Director of Photography</Value>
+      <Value>Dolly Grip</Value>
+      <Value>Editor</Value>
+      <Value>Executive Producer</Value>
+      <Value>Extra</Value>
+      <Value>Foley Artist</Value>
+      <Value>Gaffer</Value>
+      <Value>Greensman</Value>
+      <Value>Grip</Value>
+      <Value>Key Grip</Value>
+      <Value>Line Producer</Value>
+      <Value>Location Manager</Value>
+      <Value>Matte Artist</Value>
+      <Value>Producer</Value>
+      <Value>Production Assistant</Value>
+      <Value>Production Illustrator</Value>
+      <Value>Production Manager</Value>
+      <Value>Property Master</Value>
+      <Value>Screenwriter</Value>
+      <Value>Set Decorator</Value>
+      <Value>Set Designer</Value>
+      <Value>Sound Designer</Value>
+      <Value>Technical Advisor</Value>
+      <Value>Unit Production Manager</Value>
+      <Value>Wrangler</Value>
+    </MovieCrewType>
+    <TVNetworks>
+      <Value>A&amp;E</Value>
+      <Value>ABC</Value>
+      <Value>AMC</Value>
+      <Value>BET</Value>
+      <Value>BRAVO</Value>
+      <Value>CBS</Value>
+      <Value>CMDY</Value>
+      <Value>DISC</Value>
+      <Value>E!</Value>
+      <Value>FOOD</Value>
+      <Value>FOX</Value>
+      <Value>HBO</Value>
+      <Value>HGTV</Value>
+      <Value>HIST</Value>
+      <Value>LIFE</Value>
+      <Value>MSNBC</Value>
+      <Value>MTV</Value>
+      <Value>MTV2</Value>
+      <Value>NBC</Value>
+      <Value>NICK</Value>
+      <Value>SPIKE</Value>
+      <Value>SPIKE</Value>
+      <Value>SYFY</Value>
+      <Value>TBS</Value>
+      <Value>TLC</Value>
+      <Value>TNT</Value>
+      <Value>TOON</Value>
+      <Value>TOONW</Value>
+      <Value>TRUTV</Value>
+      <Value>TVLND</Value>
+      <Value>USA</Value>
+    </TVNetworks>
+    <TVRatings>
+      <Value>CS</Value>
+      <Value>TV-14</Value>
+      <Value>TV-G</Value>
+      <Value>TV-MA</Value>
+      <Value>TV-PG</Value>
+      <Value>TV-Y</Value>
+      <Value>TV-Y7</Value>
+      <Value>TV-Y7-FV</Value>
+    </TVRatings>
+    <VideoCodecs>
+      <Value>ASF</Value>
+      <Value>AVC</Value>
+      <Value>DivX</Value>
+      <Value>H.264</Value>
+      <Value>MPEG-1</Value>
+      <Value>MPEG-2</Value>
+      <Value>RealVideo</Value>
+      <Value>VC-1</Value>
+      <Value>WMV</Value>
+      <Value>XviD</Value>
+    </VideoCodecs>
+    <AudioCodecs>
+      <Value>AAC</Value>
+      <Value>AC-3</Value>
+      <Value>DTS</Value>
+      <Value>DTS-HD MA</Value>
+      <Value>E-AC-3</Value>
+      <Value>FLAC</Value>
+      <Value>MP2</Value>
+      <Value>MP3</Value>
+      <Value>MPEG AUDIO</Value>
+      <Value>PCM</Value>
+      <Value>RealAudio</Value>
+      <Value>TrueHD</Value>
+      <Value>Vorbis</Value>
+      <Value>WMA</Value>
+    </AudioCodecs>
+  </ValueLists>
+  <Mappings>
+    <Movies>
+    </Movies>
+    <TV>
+    </TV>
+    <Sorting>
+      <Mapping Key="(?i)^shameless.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Shameless (US)</Mapping>
+      <Mapping Key="(?i)^castle.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Castle (2009)</Mapping>
+      <Mapping Key="(?i)^the.river.*?$" MatchCase="False" ExactMatch="False" Type="Regex">The River (2012)</Mapping>
+      <Mapping Key="(?i)^parenthood.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Parenthood (2010)</Mapping>
+      <Mapping Key="(?i)^the.office.*?$" MatchCase="False" ExactMatch="False" Type="Regex">The Office (US)</Mapping>
+      <Mapping Key="(?i)^smash.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Smash (2012)</Mapping>
+      <Mapping Key="(?i)^rob.*?$" MatchCase="False" ExactMatch="False" Type="Regex">¡Rob!</Mapping>
+      <Mapping Key="(?i)^archer.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Archer (2009)</Mapping>
+      <Mapping Key="(?i)^once.upon.a.time.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Once Upon a Time (2011)</Mapping>
+      <Mapping Key="(?i)^lifes.too.short.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Life's Too Short</Mapping>
+      <Mapping Key="(?i)^eastbound.and.down.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Eastbound &amp; Down</Mapping>
+      <Mapping Key="(?i)^the.killing.*?$" MatchCase="False" ExactMatch="False" Type="Regex">The Killing (2011)</Mapping>
+      <Mapping Key="(?i)^touch.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Touch (2012)</Mapping>
+      <Mapping Key="(?i)^missing.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Missing (2012)</Mapping>
+      <Mapping Key="(?i)^scandal.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Scandal (2012)</Mapping>
+      <Mapping Key="(?i)^wilfred.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Wilfred (US)</Mapping>
+      <Mapping Key="(?i)^louie.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Louie (2010)</Mapping>
+      <Mapping Key="(?i)^the.newsroom.*?$" MatchCase="False" ExactMatch="False" Type="Regex">The Newsroom (2012)</Mapping>
+      <Mapping Key="(?i)^boss.*?$" MatchCase="False" ExactMatch="False" Type="Regex">Boss (2011)</Mapping>
+    </Sorting>
+    <SortingFilename>
+    </SortingFilename>
+    <Type>
+      <VIDEO_TS>DVD</VIDEO_TS>
+      <BDMV>Blu-ray</BDMV>
+      <HVDVD_TS>HD DVD</HVDVD_TS>
+    </Type>
+  </Mappings>
+  <WebService>
+    <LoadOnStartup>False</LoadOnStartup>
+    <Port>8085</Port>
+    <PasswordProtected>False</PasswordProtected>
+    <Username>
+    </Username>
+    <SortByDateAdded>False</SortByDateAdded>
+    <Password>
+    </Password>
+    <AllowedPathsEnabled>False</AllowedPathsEnabled>
+  </WebService>
+  <MetadataVisibleFields>
+    <movies>
+      <LocalTitle>True</LocalTitle>
+      <OriginalTitle>True</OriginalTitle>
+      <SortTitle>True</SortTitle>
+      <movieSet>True</movieSet>
+      <DateAdded>True</DateAdded>
+      <ProductionYear>True</ProductionYear>
+      <Runtime>True</Runtime>
+      <Rating>True</Rating>
+      <MPAARating>True</MPAARating>
+      <MPAADescription>True</MPAADescription>
+      <CustomRating>True</CustomRating>
+      <Plot>True</Plot>
+      <Description>True</Description>
+      <Type>True</Type>
+      <AspectRatio>True</AspectRatio>
+      <Watched>True</Watched>
+      <Comment>True</Comment>
+      <AllowRenaming>True</AllowRenaming>
+      <CollectionNumber>True</CollectionNumber>
+      <TrailerUrl>True</TrailerUrl>
+      <IMDbId>True</IMDbId>
+      <TMDbId>True</TMDbId>
+      <MyMoviesId>True</MyMoviesId>
+      <NetflixId>True</NetflixId>
+      <MovieMeterId>True</MovieMeterId>
+      <AlloCineId>True</AlloCineId>
+      <FilmAffinityId>True</FilmAffinityId>
+      <YahooIndiaId>True</YahooIndiaId>
+      <AmazonId>True</AmazonId>
+      <RottenTomatoesId>True</RottenTomatoesId>
+      <CineFactsId>True</CineFactsId>
+      <OFDbId>True</OFDbId>
+      <CSFDId>True</CSFDId>
+      <MoviePlayerId>True</MoviePlayerId>
+      <AdultDVDEmpireId>True</AdultDVDEmpireId>
+      <CDUniverseId>True</CDUniverseId>
+    </movies>
+    <series>
+      <SeriesName>True</SeriesName>
+      <AirDay>True</AirDay>
+      <AirTime>True</AirTime>
+      <Runtime>True</Runtime>
+      <Network>True</Network>
+      <MPAARating>True</MPAARating>
+      <CustomRating>True</CustomRating>
+      <Status>True</Status>
+      <FirstAirDate>True</FirstAirDate>
+      <Description>True</Description>
+      <Rating>True</Rating>
+      <Language>True</Language>
+      <Comment>True</Comment>
+      <AllowRenaming>True</AllowRenaming>
+      <CustomRenamePattern>True</CustomRenamePattern>
+      <DisplayOrder>True</DisplayOrder>
+      <FetchOrder>True</FetchOrder>
+      <IMDbId>True</IMDbId>
+      <TVDbId>True</TVDbId>
+      <TVcomId>True</TVcomId>
+      <TVcom2Id>True</TVcom2Id>
+      <MoviePlayerId>True</MoviePlayerId>
+    </series>
+    <episode>
+      <EpisodeName>True</EpisodeName>
+      <SeasonNumber>True</SeasonNumber>
+      <EpisodeNumber>True</EpisodeNumber>
+      <DVDSeasonNumber>True</DVDSeasonNumber>
+      <DVDEpisodeNumber>True</DVDEpisodeNumber>
+      <AbsoluteEpisodeNumber>True</AbsoluteEpisodeNumber>
+      <AirsAfterSeason>True</AirsAfterSeason>
+      <AirsBeforeEpisode>True</AirsBeforeEpisode>
+      <AirsBeforeSeason>True</AirsBeforeSeason>
+      <DateAdded>True</DateAdded>
+      <FirstAirDate>True</FirstAirDate>
+      <GuestStars>True</GuestStars>
+      <Description>True</Description>
+      <Writer>True</Writer>
+      <Director>True</Director>
+      <Rating>True</Rating>
+      <Type>True</Type>
+      <Watched>True</Watched>
+      <CustomRating>True</CustomRating>
+      <Comment>True</Comment>
+    </episode>
+    <season>
+      <Description>True</Description>
+      <Comment>True</Comment>
+      <CustomRating>True</CustomRating>
+    </season>
+    <music>
+      <Title>True</Title>
+      <Album>True</Album>
+      <AlbumArtist>True</AlbumArtist>
+      <TitleSort>True</TitleSort>
+      <AlbumSort>True</AlbumSort>
+      <Performer>True</Performer>
+      <DateAdded>True</DateAdded>
+      <Year>True</Year>
+      <Genre>True</Genre>
+      <Composer>True</Composer>
+      <Track>True</Track>
+      <TrackCount>True</TrackCount>
+      <Disc>True</Disc>
+      <DiscCount>True</DiscCount>
+      <Comment>True</Comment>
+      <Copyright>True</Copyright>
+      <Publisher>True</Publisher>
+      <Grouping>True</Grouping>
+      <MusicBrainzTrackId>True</MusicBrainzTrackId>
+      <MusicBrainzReleaseId>True</MusicBrainzReleaseId>
+    </music>
+  </MetadataVisibleFields>
+  <Misc>
+    <MediaIconsLocation>Poster</MediaIconsLocation>
+    <MediaIconsPosition>top</MediaIconsPosition>
+    <MediaIconsOpacity>50</MediaIconsOpacity>
+    <FFmpegLocation>
+    </FFmpegLocation>
+    <FFProbeLocation>
+    </FFProbeLocation>
+    <UseDisplayAspectRatio>False</UseDisplayAspectRatio>
+    <ColorDefault>-16777216</ColorDefault>
+    <ColorNoMetadata>-65536</ColorNoMetadata>
+    <ColorIncompleteMetadata>-16776961</ColorIncompleteMetadata>
+    <ColorCompleteMetadata>-16744448</ColorCompleteMetadata>
+    <HighlightParent>True</HighlightParent>
+    <ShowRedDots>True</ShowRedDots>
+    <ShowAsCompleteWhenLocked>True</ShowAsCompleteWhenLocked>
+    <LoadWithCollapsedGroups>False</LoadWithCollapsedGroups>
+    <UseTieredExpandCollapse>False</UseTieredExpandCollapse>
+    <DoNotExceedOriginalImageSizeOnPanel>True</DoNotExceedOriginalImageSizeOnPanel>
+    <SkipCleanupAfterManualSave>False</SkipCleanupAfterManualSave>
+    <ResumePauseSeconds>0</ResumePauseSeconds>
+    <DefaultExternalSubLanguage>
+    </DefaultExternalSubLanguage>
+    <AllowMultiSelect>True</AllowMultiSelect>
+    <AsyncMultiSelectMinimum>50</AsyncMultiSelectMinimum>
+    <AsyncSaveMinimum>1</AsyncSaveMinimum>
+  </Misc>
+  <Movies>
+    <AllowMultipleMoviesPerFolder>False</AllowMultipleMoviesPerFolder>
+    <AppendProductionYear>True</AppendProductionYear>
+    <GroupMovieSets>True</GroupMovieSets>
+    <FlattenFolders>False</FlattenFolders>
+    <UseFolderNameForSortTitle>False</UseFolderNameForSortTitle>
+    <SelectFirstSearchResult>False</SelectFirstSearchResult>
+    <DisplayValuePattern>%lt</DisplayValuePattern>
+    <SortValue>SortTitle</SortValue>
+    <FetchValue>OriginalTitle</FetchValue>
+    <PreferredPosterPlugin>7291961d-21e7-4ee2-a996-4febdb7661eb</PreferredPosterPlugin>
+    <UsePreferredPosterPluginOnly>True</UsePreferredPosterPluginOnly>
+    <PosterMinimumWidth>0</PosterMinimumWidth>
+    <PosterMinimumHeight>0</PosterMinimumHeight>
+    <PostersToDownload>1</PostersToDownload>
+    <PreferredBackdropPlugin>546eda50-7029-4421-9596-09b2bae293f7</PreferredBackdropPlugin>
+    <UsePreferredBackdropPluginOnly>True</UsePreferredBackdropPluginOnly>
+    <BackdropMinimumWidth>1920</BackdropMinimumWidth>
+    <BackdropMinimumHeight>0</BackdropMinimumHeight>
+    <BackdropsToDownload>3</BackdropsToDownload>
+    <PreferredClearArtPlugin>01a001e8-316b-4e49-8e9a-52b5b3179067</PreferredClearArtPlugin>
+    <UsePreferredClearArtPluginOnly>False</UsePreferredClearArtPluginOnly>
+    <PreferredCastPlugin>c422bc7f-2910-4e62-9370-a5ba5ee69514</PreferredCastPlugin>
+    <UsePreferredCastPluginOnly>False</UsePreferredCastPluginOnly>
+    <PreferredTrailerPlugin>d3dd7a50-859f-4bcd-91c9-51e218ce29eb</PreferredTrailerPlugin>
+    <UsePreferredTrailerPluginOnly>False</UsePreferredTrailerPluginOnly>
+    <PreferredTrailerResolution>0</PreferredTrailerResolution>
+    <DownloadNextAvailableTrailerResolution>True</DownloadNextAvailableTrailerResolution>
+    <DeleteTrailerFromCache>True</DeleteTrailerFromCache>
+    <DownloadAllTrailersIncludeLocked>False</DownloadAllTrailersIncludeLocked>
+    <UseMediaInfoRuntime>True</UseMediaInfoRuntime>
+    <RenameMovie>False</RenameMovie>
+    <RenameMovieFolder>False</RenameMovieFolder>
+    <RenameMoviePattern>
+    </RenameMoviePattern>
+    <RenameMovieFolderPattern>
+    </RenameMovieFolderPattern>
+    <UpdateFields>
+      <FetchIdOnly>False</FetchIdOnly>
+      <ReplaceMissingOnly>False</ReplaceMissingOnly>
+      <Information>True</Information>
+      <Posters>True</Posters>
+      <Backdrops>True</Backdrops>
+      <Logos>True</Logos>
+      <ClearArts>True</ClearArts>
+      <Trailers>False</Trailers>
+      <Rename>True</Rename>
+      <LocalTitle>True</LocalTitle>
+      <OriginalTitle>True</OriginalTitle>
+      <SortTitle>True</SortTitle>
+      <ProductionYear>True</ProductionYear>
+      <Runtime>True</Runtime>
+      <Rating>True</Rating>
+      <MPAARating>True</MPAARating>
+      <MPAADescription>True</MPAADescription>
+      <Plot>True</Plot>
+      <Description>True</Description>
+      <Type>True</Type>
+      <AspectRatio>True</AspectRatio>
+      <TrailerUrl>True</TrailerUrl>
+      <Watched>True</Watched>
+      <CastCrew>True</CastCrew>
+      <CastCrewImages>False</CastCrewImages>
+      <Genres>True</Genres>
+      <Studios>True</Studios>
+      <Countries>True</Countries>
+      <Taglines>True</Taglines>
+      <MediaInfoCustom>True</MediaInfoCustom>
+    </UpdateFields>
+    <MetadataCompleteFields>
+      <LocalTitle>True</LocalTitle>
+      <OriginalTitle>True</OriginalTitle>
+      <SortTitle>True</SortTitle>
+      <ProductionYear>True</ProductionYear>
+      <Runtime>True</Runtime>
+      <Rating>True</Rating>
+      <MPAARating>True</MPAARating>
+      <MPAADescription>False</MPAADescription>
+      <CustomRating>False</CustomRating>
+      <Plot>False</Plot>
+      <Description>True</Description>
+      <Type>True</Type>
+      <AspectRatio>True</AspectRatio>
+      <TrailerUrl>True</TrailerUrl>
+      <Watched>False</Watched>
+      <Comment>False</Comment>
+      <CollectionNumber>False</CollectionNumber>
+      <CastCrew>True</CastCrew>
+      <Genres>True</Genres>
+      <Studios>False</Studios>
+      <Countries>False</Countries>
+      <Taglines>True</Taglines>
+      <MediaInfoCustom>True</MediaInfoCustom>
+      <Trailers>True</Trailers>
+      <Posters>True</Posters>
+      <Backdrops>True</Backdrops>
+      <Logos>True</Logos>
+      <ClearArts>False</ClearArts>
+    </MetadataCompleteFields>
+    <Plugins>
+      <Locals>
+        <Id Order="0" State="0">529c3819-0ca4-4741-b6b5-48360ace62ee</Id>
+      </Locals>
+      <Fetchers>
+        <RealTime>6d06642d-d028-4b10-a6fd-3060637e9883</RealTime>
+        <LocalTitle>6d06642d-d028-4b10-a6fd-3060637e9883</LocalTitle>
+        <OriginalTitle>6d06642d-d028-4b10-a6fd-3060637e9883</OriginalTitle>
+        <SortTitle>6d06642d-d028-4b10-a6fd-3060637e9883</SortTitle>
+        <ProductionYear>6d06642d-d028-4b10-a6fd-3060637e9883</ProductionYear>
+        <Runtime>6d06642d-d028-4b10-a6fd-3060637e9883</Runtime>
+        <Rating>6d06642d-d028-4b10-a6fd-3060637e9883</Rating>
+        <MPAARating>6d06642d-d028-4b10-a6fd-3060637e9883</MPAARating>
+        <MPAADescription>6d06642d-d028-4b10-a6fd-3060637e9883</MPAADescription>
+        <Plot>6d06642d-d028-4b10-a6fd-3060637e9883</Plot>
+        <Description>6d06642d-d028-4b10-a6fd-3060637e9883</Description>
+        <AspectRatio>6d06642d-d028-4b10-a6fd-3060637e9883</AspectRatio>
+        <TrailerUrl>6d06642d-d028-4b10-a6fd-3060637e9883</TrailerUrl>
+        <Watched>6d06642d-d028-4b10-a6fd-3060637e9883</Watched>
+        <CastCrew>6d06642d-d028-4b10-a6fd-3060637e9883</CastCrew>
+        <Genres>6d06642d-d028-4b10-a6fd-3060637e9883</Genres>
+        <Studios>6d06642d-d028-4b10-a6fd-3060637e9883</Studios>
+        <Countries>6d06642d-d028-4b10-a6fd-3060637e9883</Countries>
+        <Taglines>6d06642d-d028-4b10-a6fd-3060637e9883</Taglines>
+      </Fetchers>
+      <Savers>
+        <Id Order="0" State="1">a04f5745-d062-4e14-bc36-24a673cfed22</Id>
+        <Id Order="1" State="1">d04f5745-d062-4e14-bd36-24a673cfed22</Id>
+      </Savers>
+    </Plugins>
+    <Trailers>
+      <DownloadPath>D:\Video\Coming Soon\</DownloadPath>
+      <MaxTrailers>1000</MaxTrailers>
+      <Quality>HD</Quality>
+      <OnlyDownloadNewerThanLastPostdate>False</OnlyDownloadNewerThanLastPostdate>
+      <CreateFolders>True</CreateFolders>
+      <FetchMetadata>True</FetchMetadata>
+      <PopulateMPAARating>True</PopulateMPAARating>
+      <DeleteTrailers>True</DeleteTrailers>
+      <DeleteTrailersDays>120</DeleteTrailersDays>
+      <DeleteTrailersLowerLimit>25</DeleteTrailersLowerLimit>
+      <SchedulerEnabled>True</SchedulerEnabled>
+      <SchedulerDay>-1</SchedulerDay>
+      <SchedulerTime>2/1/0001 12:00:00 AM</SchedulerTime>
+      <RenameTrailer>True</RenameTrailer>
+      <RenameTrailerPattern>%ot (%py).%ext</RenameTrailerPattern>
+      <SkipGenres>
+      </SkipGenres>
+    </Trailers>
+  </Movies>
+  <TV>
+    <ValidSeasons>Season;Series;Specials</ValidSeasons>
+    <SortValue>Default</SortValue>
+    <EnableSeasonLevelBanners>False</EnableSeasonLevelBanners>
+    <RefreshLocked>True</RefreshLocked>
+    <PreferredPosterPlugin>fcb4d2d3-c609-432a-8c51-25136d847a32</PreferredPosterPlugin>
+    <UsePreferredPosterPluginOnly>True</UsePreferredPosterPluginOnly>
+    <PosterMinimumWidth>0</PosterMinimumWidth>
+    <PosterMinimumHeight>0</PosterMinimumHeight>
+    <PostersToDownload>1</PostersToDownload>
+    <ExtractEpisodeImages>False</ExtractEpisodeImages>
+    <OnlyExtractEpisodeImages>False</OnlyExtractEpisodeImages>
+    <PreferredBackdropPlugin>621f9839-4750-4ceb-a286-06fe96cd7f98</PreferredBackdropPlugin>
+    <UsePreferredBackdropPluginOnly>True</UsePreferredBackdropPluginOnly>
+    <BackdropMinimumWidth>1920</BackdropMinimumWidth>
+    <BackdropMinimumHeight>0</BackdropMinimumHeight>
+    <BackdropsToDownload>3</BackdropsToDownload>
+    <PreferredBannerPlugin>36e97d3d-fa00-43d6-b809-ef3595f0b5da</PreferredBannerPlugin>
+    <UsePreferredBannerPluginOnly>True</UsePreferredBannerPluginOnly>
+    <BannersToDownload>1</BannersToDownload>
+    <PreferredClearArtPlugin>01a001e8-316b-4e49-8e9a-52b5b3179067</PreferredClearArtPlugin>
+    <UsePreferredClearArtPluginOnly>False</UsePreferredClearArtPluginOnly>
+    <PreferredCastPlugin>c422bc7f-2910-4e62-9370-a5ba5ee69514</PreferredCastPlugin>
+    <UsePreferredCastPluginOnly>False</UsePreferredCastPluginOnly>
+    <RenameTV>True</RenameTV>
+    <RenameTVPattern>%sn - %sx%0e - %en.%ext</RenameTVPattern>
+    <UpdateFields>
+      <Series>
+        <FetchIdOnly>False</FetchIdOnly>
+        <ReplaceMissingOnly>False</ReplaceMissingOnly>
+        <Information>True</Information>
+        <Posters>True</Posters>
+        <Backdrops>True</Backdrops>
+        <Banners>True</Banners>
+        <Logos>True</Logos>
+        <ClearArts>True</ClearArts>
+        <Thumbs>True</Thumbs>
+        <SeriesName>True</SeriesName>
+        <AirDay>True</AirDay>
+        <FirstAirDate>True</FirstAirDate>
+        <AirTime>True</AirTime>
+        <Runtime>True</Runtime>
+        <Rating>True</Rating>
+        <MPAARating>True</MPAARating>
+        <Description>True</Description>
+        <Network>True</Network>
+        <Status>True</Status>
+        <CastCrew>True</CastCrew>
+        <CastCrewImages>False</CastCrewImages>
+        <Genres>True</Genres>
+      </Series>
+      <Season>
+        <Posters>True</Posters>
+        <Backdrops>False</Backdrops>
+        <Banners>True</Banners>
+        <Thumbs>True</Thumbs>
+      </Season>
+      <Episode>
+        <ReplaceMissingOnly>False</ReplaceMissingOnly>
+        <Information>True</Information>
+        <Posters>True</Posters>
+        <Rename>True</Rename>
+        <EpisodeName>True</EpisodeName>
+        <FirstAirDate>True</FirstAirDate>
+        <GuestStars>True</GuestStars>
+        <Description>True</Description>
+        <Writer>True</Writer>
+        <Director>True</Director>
+        <Rating>True</Rating>
+        <Type>True</Type>
+        <Watched>True</Watched>
+        <MediaInfoCustom>True</MediaInfoCustom>
+        <CastCrewImages>True</CastCrewImages>
+      </Episode>
+    </UpdateFields>
+    <MetadataCompleteFields>
+      <Series>
+        <SeriesName>True</SeriesName>
+        <AirDay>False</AirDay>
+        <FirstAirDate>True</FirstAirDate>
+        <AirTime>False</AirTime>
+        <Runtime>True</Runtime>
+        <Rating>True</Rating>
+        <MPAARating>False</MPAARating>
+        <Description>True</Description>
+        <Network>True</Network>
+        <Status>True</Status>
+        <CastCrew>True</CastCrew>
+        <Genres>True</Genres>
+        <Comment>False</Comment>
+        <Posters>True</Posters>
+        <Backdrops>True</Backdrops>
+        <Banners>True</Banners>
+        <Logos>True</Logos>
+        <ClearArts>False</ClearArts>
+        <Thumbs>False</Thumbs>
+      </Series>
+      <Season>
+        <Description>False</Description>
+        <Comment>False</Comment>
+        <Posters>True</Posters>
+        <Backdrops>False</Backdrops>
+        <Banners>False</Banners>
+        <Thumbs>False</Thumbs>
+      </Season>
+      <Episode>
+        <EpisodeName>True</EpisodeName>
+        <FirstAirDate>False</FirstAirDate>
+        <GuestStars>False</GuestStars>
+        <Description>True</Description>
+        <Writer>False</Writer>
+        <Director>False</Director>
+        <Rating>False</Rating>
+        <Type>False</Type>
+        <Watched>False</Watched>
+        <Comment>False</Comment>
+        <MediaInfoCustom>False</MediaInfoCustom>
+        <Posters>True</Posters>
+      </Episode>
+    </MetadataCompleteFields>
+    <Plugins>
+      <Locals>
+        <Id Order="0" State="0">5b118517-cc37-427c-bf03-34dec95db959</Id>
+      </Locals>
+      <Fetchers>
+        <Series>
+          <RealTime>default</RealTime>
+          <SeriesName>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</SeriesName>
+          <AirDay>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</AirDay>
+          <FirstAirDate>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</FirstAirDate>
+          <AirTime>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</AirTime>
+          <Runtime>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Runtime>
+          <Rating>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Rating>
+          <MPAARating>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</MPAARating>
+          <Description>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Description>
+          <Network>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Network>
+          <Status>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Status>
+          <CastCrew>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</CastCrew>
+          <Genres>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Genres>
+        </Series>
+        <Episode>
+          <EpisodeName>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</EpisodeName>
+          <FirstAirDate>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</FirstAirDate>
+          <GuestStars>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</GuestStars>
+          <Description>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Description>
+          <Writer>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Writer>
+          <Director>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Director>
+          <Rating>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Rating>
+          <Watched>29f3e4f0-5a4b-4462-a0fe-45f0593ca5f1</Watched>
+        </Episode>
+      </Fetchers>
+      <Savers>
+        <Id Order="0" State="1">59e2f1dc-8dc7-49a7-9e47-29ce066084c3</Id>
+      </Savers>
+    </Plugins>
+    <Sorting>
+      <MinimumFileSize>50</MinimumFileSize>
+      <AutoPoll>True</AutoPoll>
+      <DeleteEmptyFolders>False</DeleteEmptyFolders>
+      <TransferMethod>move</TransferMethod>
+      <MoveAccompanyingFiles>True</MoveAccompanyingFiles>
+      <NotifyMCEClients>False</NotifyMCEClients>
+      <DeleteLeftOverFiles>False</DeleteLeftOverFiles>
+      <LeftOverFilesExtensions>.nfo;.txt</LeftOverFilesExtensions>
+      <SeasonFolderPattern>Season %s</SeasonFolderPattern>
+      <SeasonZeroFolderPattern>Season %0s</SeasonZeroFolderPattern>
+      <OnlyCreateSeriesFolderForS1E1>True</OnlyCreateSeriesFolderForS1E1>
+      <MoveDuplicateEpisodes>False</MoveDuplicateEpisodes>
+      <MoveDuplicateEpisodesLocation>D:\Temp\</MoveDuplicateEpisodesLocation>
+      <OverwiteExistingEpisodes>True</OverwiteExistingEpisodes>
+      <OverwriteOnResolution>False</OverwriteOnResolution>
+      <OverwriteOnWords>False</OverwriteOnWords>
+      <OverwriteWords>PROPER;REPACK</OverwriteWords>
+      <CreateFolders>True</CreateFolders>
+      <ConfirmBeforeProcessing>True</ConfirmBeforeProcessing>
+      <Monitored location="D:\Temp\_MetaBrowserWatcher\" type="0">
+        <Destination>D:\Video\TV\</Destination>
+      </Monitored>
+    </Sorting>
+    <Schedule enabled="True" autofilterseries="False">
+    </Schedule>
+  </TV>
+  <Music>
+    <Enabled>False</Enabled>
+    <SortValue>Default</SortValue>
+    <RenameMusic>False</RenameMusic>
+    <RenameMusicPattern>
+    </RenameMusicPattern>
+    <UpdateFields>
+      <FetchIdOnly>False</FetchIdOnly>
+      <ReplaceMissingOnly>False</ReplaceMissingOnly>
+      <Information>True</Information>
+      <Posters>True</Posters>
+      <Rename>True</Rename>
+      <Title>True</Title>
+      <TitleSort>True</TitleSort>
+      <Album>True</Album>
+      <AlbumSort>True</AlbumSort>
+      <AlbumArtist>True</AlbumArtist>
+      <Performer>True</Performer>
+      <Track>True</Track>
+      <TrackCount>True</TrackCount>
+      <Disc>False</Disc>
+      <DiscCount>True</DiscCount>
+      <Year>True</Year>
+      <Genre>True</Genre>
+      <Comment>True</Comment>
+      <Composer>True</Composer>
+      <Publisher>True</Publisher>
+      <Copyright>True</Copyright>
+      <Lyrics>True</Lyrics>
+    </UpdateFields>
+    <MetadataCompleteFields>
+      <Title>True</Title>
+      <TitleSort>False</TitleSort>
+      <Album>True</Album>
+      <AlbumSort>False</AlbumSort>
+      <AlbumArtist>True</AlbumArtist>
+      <Performer>False</Performer>
+      <Track>True</Track>
+      <TrackCount>False</TrackCount>
+      <Disc>False</Disc>
+      <DiscCount>False</DiscCount>
+      <Year>True</Year>
+      <Genre>True</Genre>
+      <Comment>False</Comment>
+      <Composer>False</Composer>
+      <Publisher>False</Publisher>
+      <Copyright>False</Copyright>
+      <Lyrics>False</Lyrics>
+      <Posters>True</Posters>
+    </MetadataCompleteFields>
+    <Plugins>
+      <Locals>
+      </Locals>
+      <Fetchers>
+        <RealTime>00000000-0000-0000-0000-000000000000</RealTime>
+        <Title>00000000-0000-0000-0000-000000000000</Title>
+        <TitleSort>00000000-0000-0000-0000-000000000000</TitleSort>
+        <Album>00000000-0000-0000-0000-000000000000</Album>
+        <AlbumSort>00000000-0000-0000-0000-000000000000</AlbumSort>
+        <AlbumArtist>00000000-0000-0000-0000-000000000000</AlbumArtist>
+        <Performer>00000000-0000-0000-0000-000000000000</Performer>
+        <Track>00000000-0000-0000-0000-000000000000</Track>
+        <TrackCount>00000000-0000-0000-0000-000000000000</TrackCount>
+        <Disc>00000000-0000-0000-0000-000000000000</Disc>
+        <DiscCount>00000000-0000-0000-0000-000000000000</DiscCount>
+        <Year>00000000-0000-0000-0000-000000000000</Year>
+        <Genre>00000000-0000-0000-0000-000000000000</Genre>
+        <Comment>00000000-0000-0000-0000-000000000000</Comment>
+        <Composer>00000000-0000-0000-0000-000000000000</Composer>
+        <Publisher>00000000-0000-0000-0000-000000000000</Publisher>
+        <Copyright>00000000-0000-0000-0000-000000000000</Copyright>
+        <Lyrics>00000000-0000-0000-0000-000000000000</Lyrics>
+      </Fetchers>
+      <Savers>
+      </Savers>
+    </Plugins>
+  </Music>
+</Options>

+ 12 - 5
MediaBrowser.Api/packages.config

@@ -1,6 +1,13 @@
-<?xml version="1.0" encoding="utf-8"?>
-<packages>
-  <package id="Rx-Core" version="2.0.20823" targetFramework="net45" />
-  <package id="Rx-Interfaces" version="2.0.20823" targetFramework="net45" />
-  <package id="Rx-Linq" version="2.0.20823" targetFramework="net45" />
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="morelinq" version="1.0.15631-beta" targetFramework="net45" />
+  <package id="protobuf-net" version="2.0.0.621" targetFramework="net45" />
+  <package id="Rx-Core" version="2.0.21114" targetFramework="net45" />
+  <package id="Rx-Interfaces" version="2.0.21114" targetFramework="net45" />
+  <package id="Rx-Linq" version="2.0.21114" targetFramework="net45" />
+  <package id="ServiceStack" version="3.9.37" targetFramework="net45" />
+  <package id="ServiceStack.Common" version="3.9.37" targetFramework="net45" />
+  <package id="ServiceStack.OrmLite.SqlServer" version="3.9.37" targetFramework="net45" />
+  <package id="ServiceStack.Redis" version="3.9.37" targetFramework="net45" />
+  <package id="ServiceStack.Text" version="3.9.37" targetFramework="net45" />
 </packages>

+ 1360 - 0
MediaBrowser.ApiInteraction.Javascript/ApiClient.js

@@ -0,0 +1,1360 @@
+/**
+ * Represents a javascript version of ApiClient.
+ * This should be kept up to date with all possible api methods and parameters
+ */
+var ApiClient = {
+
+    serverProtocol: "http",
+
+    /**
+     * Gets or sets the host name of the server
+     */
+    serverHostName: "localhost",
+
+    serverPortNumber: 8096,
+
+    /**
+     * Detects the hostname and port of MB server based on the current url
+     */
+    inferServerFromUrl: function () {
+
+        var loc = window.location;
+
+        ApiClient.serverProtocol = loc.protocol;
+        ApiClient.serverHostName = loc.hostname;
+        ApiClient.serverPortNumber = loc.port;
+    },
+
+    /**
+     * Creates an api url based on a handler name and query string parameters
+     * @param {String} name
+     * @param {Object} params
+     */
+    getUrl: function (name, params) {
+
+        if (!name) {
+            throw new Error("Url name cannot be empty");
+        }
+
+        params = params || {};
+
+        var url = ApiClient.serverProtocol + "//" + ApiClient.serverHostName + ":" + ApiClient.serverPortNumber + "/mediabrowser/" + name;
+
+        if (params) {
+            url += "?" + $.param(params);
+
+        }
+        return url;
+    },
+
+    /**
+     * Returns the name of the current browser
+     */
+    getDeviceName: function () {
+
+        /*if ($.browser.chrome) {
+            return "Chrome";
+        }
+        if ($.browser.safari) {
+            return "Safari";
+        }
+        if ($.browser.webkit) {
+            return "WebKit";
+        }
+        if ($.browser.msie) {
+            return "Internet Explorer";
+        }
+        if ($.browser.firefox) {
+            return "Firefox";
+        }
+        if ($.browser.mozilla) {
+            return "Firefox";
+        }
+        if ($.browser.opera) {
+            return "Opera";
+        }*/
+
+        return "Web Browser";
+    },
+
+    /**
+     * Creates a custom api url based on a handler name and query string parameters
+     * @param {String} name
+     * @param {Object} params
+     */
+    getCustomUrl: function (name, params) {
+
+        if (!name) {
+            throw new Error("Url name cannot be empty");
+        }
+
+        params = params || {};
+        params.client = "Dashboard";
+        params.device = ApiClient.getDeviceName();
+        params.format = "json";
+
+        var url = ApiClient.serverProtocol + "//" + ApiClient.serverHostName + ":" + ApiClient.serverPortNumber + "/mediabrowser/" + name;
+
+        if (params) {
+            url += "?" + $.param(params);
+
+        }
+        return url;
+    },
+
+    /**
+     * Gets an item from the server
+     * Omit itemId to get the root folder.
+     */
+    getItem: function (userId, itemId) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        var url = ApiClient.getUrl("Users/" + userId + "/Items/" + itemId);
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets the root folder from the server
+     */
+    getRootFolder: function (userId) {
+
+        return ApiClient.getItem(userId);
+    },
+
+    /**
+     * Gets the current server status
+     */
+    getSystemInfo: function () {
+
+        var url = ApiClient.getUrl("System/Info");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets all cultures known to the server
+     */
+    getCultures: function () {
+
+        var url = ApiClient.getUrl("Localization/cultures");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets all countries known to the server
+     */
+    getCountries: function () {
+
+        var url = ApiClient.getUrl("Localization/countries");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets plugin security info
+     */
+    getPluginSecurityInfo: function () {
+
+        var url = ApiClient.getUrl("Plugins/SecurityInfo");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets the directory contents of a path on the server
+     */
+    getDirectoryContents: function (path, options) {
+
+        if (!path) {
+            throw new Error("null path");
+        }
+
+        options = options || {};
+
+        options.path = path;
+
+        var url = ApiClient.getUrl("Environment/DirectoryContents", options);
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets a list of physical drives from the server
+     */
+    getDrives: function () {
+
+        var url = ApiClient.getUrl("Environment/Drives");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets a list of network computers from the server
+     */
+    getNetworkComputers: function () {
+
+        var url = ApiClient.getUrl("Environment/NetworkComputers");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Cancels a package installation
+     */
+    cancelPackageInstallation: function (installationId) {
+
+        if (!installationId) {
+            throw new Error("null installationId");
+        }
+
+        var url = ApiClient.getUrl("Packages/Installing/" + id);
+
+        return $.ajax({
+            type: "DELETE",
+            url: url,
+            dataType: "json"
+        });
+    },
+
+    /**
+     * Installs or updates a new plugin
+     */
+    installPlugin: function (name, updateClass, version) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        if (!updateClass) {
+            throw new Error("null updateClass");
+        }
+
+        var options = {
+            updateClass: updateClass
+        };
+
+        if (version) {
+            options.version = version;
+        }
+
+        var url = ApiClient.getUrl("Packages/Installed/" + name, options);
+
+        return $.post(url);
+    },
+
+    /**
+     * Instructs the server to perform a pending kernel reload or app restart.
+     * If a restart is not currently required, nothing will happen.
+     */
+    performPendingRestart: function () {
+
+        var url = ApiClient.getUrl("System/Restart");
+
+        return $.post(url);
+    },
+
+    /**
+     * Gets information about an installable package
+     */
+    getPackageInfo: function (name) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        var url = ApiClient.getUrl("Packages/" + name);
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets the latest available application update (if any)
+     */
+    getAvailableApplicationUpdate: function () {
+
+        var url = ApiClient.getUrl("Packages/Updates", { PackageType: "System" });
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets the latest available plugin updates (if any)
+     */
+    getAvailablePluginUpdates: function () {
+
+        var url = ApiClient.getUrl("Packages/Updates", { PackageType: "UserInstalled" });
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets the virtual folder for a view. Specify a userId to get a user view, or omit for the default view.
+     */
+    getVirtualFolders: function (userId) {
+
+        var url = userId ? "Users/" + userId + "/VirtualFolders" : "Library/DefaultVirtualFolders";
+
+        url = ApiClient.getUrl(url);
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets all the paths of the locations in the physical root.
+     */
+    getPhysicalPaths: function () {
+
+        var url = ApiClient.getUrl("Library/PhysicalPaths");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets the current server configuration
+     */
+    getServerConfiguration: function () {
+
+        var url = ApiClient.getUrl("System/Configuration");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets the server's scheduled tasks
+     */
+    getScheduledTasks: function () {
+
+        var url = ApiClient.getUrl("ScheduledTasks");
+
+        return $.getJSON(url);
+    },
+
+    /**
+    * Starts a scheduled task
+    */
+    startScheduledTask: function (id) {
+
+        if (!id) {
+            throw new Error("null id");
+        }
+
+        var url = ApiClient.getUrl("ScheduledTasks/Running/" + id);
+
+        return $.post(url);
+    },
+
+    /**
+    * Gets a scheduled task
+    */
+    getScheduledTask: function (id) {
+
+        if (!id) {
+            throw new Error("null id");
+        }
+
+        var url = ApiClient.getUrl("ScheduledTasks/" + id);
+
+        return $.getJSON(url);
+    },
+
+    /**
+   * Stops a scheduled task
+   */
+    stopScheduledTask: function (id) {
+
+        if (!id) {
+            throw new Error("null id");
+        }
+
+        var url = ApiClient.getUrl("ScheduledTasks/Running/" + id);
+
+        return $.ajax({
+            type: "DELETE",
+            url: url,
+            dataType: "json"
+        });
+    },
+
+    /**
+     * Gets the configuration of a plugin
+     * @param {String} uniqueId
+     */
+    getPluginConfiguration: function (uniqueId) {
+
+        if (!uniqueId) {
+            throw new Error("null uniqueId");
+        }
+
+        var url = ApiClient.getUrl("Plugins/" + uniqueId + "/Configuration");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets a list of plugins that are available to be installed
+     */
+    getAvailablePlugins: function () {
+
+        var url = ApiClient.getUrl("Packages", { PackageType: "UserInstalled" });
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Uninstalls a plugin
+     * @param {String} uniqueId
+     */
+    uninstallPlugin: function (uniqueId) {
+
+        if (!uniqueId) {
+            throw new Error("null uniqueId");
+        }
+
+        var url = ApiClient.getUrl("Plugins/" + uniqueId);
+
+        return $.ajax({
+            type: "DELETE",
+            url: url,
+            dataType: "json"
+        });
+    },
+
+    /**
+    * Removes a virtual folder from either the default view or a user view
+    * @param {String} name
+    */
+    removeVirtualFolder: function (name, userId) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        var params = {
+            name: name,
+            action: "RemoveVirtualFolder"
+        };
+
+        if (userId) {
+            params.userId = userId;
+        }
+
+        var url = ApiClient.getUrl("UpdateMediaLibrary", params);
+
+        return $.post(url);
+    },
+
+    /**
+   * Adds a virtual folder to either the default view or a user view
+   * @param {String} name
+   */
+    addVirtualFolder: function (name, userId) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        var params = {
+            name: name,
+            action: "addVirtualFolder"
+        };
+
+        if (userId) {
+            params.userId = userId;
+        }
+
+        var url = ApiClient.getUrl("UpdateMediaLibrary", params);
+
+        return $.post(url);
+    },
+
+    /**
+   * Renames a virtual folder, within either the default view or a user view
+   * @param {String} name
+   */
+    renameVirtualFolder: function (name, newName, userId) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        if (!newName) {
+            throw new Error("null newName");
+        }
+
+        var params = {
+            name: name,
+            newName: newName,
+            action: "RenameVirtualFolder"
+        };
+
+        if (userId) {
+            params.userId = userId;
+        }
+
+        var url = ApiClient.getUrl("UpdateMediaLibrary", params);
+
+        return $.post(url);
+    },
+
+    /**
+    * Adds an additional mediaPath to an existing virtual folder, within either the default view or a user view
+    * @param {String} name
+    */
+    addMediaPath: function (virtualFolderName, mediaPath, userId) {
+
+        if (!virtualFolderName) {
+            throw new Error("null virtualFolderName");
+        }
+
+        if (!mediaPath) {
+            throw new Error("null mediaPath");
+        }
+
+        var params = {
+            virtualFolderName: virtualFolderName,
+            mediaPath: mediaPath,
+            action: "addMediaPath"
+        };
+
+        if (userId) {
+            params.userId = userId;
+        }
+
+        var url = ApiClient.getUrl("UpdateMediaLibrary", params);
+
+        return $.post(url);
+    },
+
+    /**
+    * Removes a media path from a virtual folder, within either the default view or a user view
+    * @param {String} name
+    */
+    removeMediaPath: function (virtualFolderName, mediaPath, userId) {
+
+        if (!virtualFolderName) {
+            throw new Error("null virtualFolderName");
+        }
+
+        if (!mediaPath) {
+            throw new Error("null mediaPath");
+        }
+
+        var params = {
+            virtualFolderName: virtualFolderName,
+            mediaPath: mediaPath,
+            action: "RemoveMediaPath"
+        };
+
+        if (userId) {
+            params.userId = userId;
+        }
+
+        var url = ApiClient.getUrl("UpdateMediaLibrary", params);
+
+        return $.post(url);
+    },
+
+    /**
+     * Deletes a user
+     * @param {String} id
+     */
+    deleteUser: function (id) {
+
+        if (!id) {
+            throw new Error("null id");
+        }
+
+        var url = ApiClient.getUrl("Users/" + id);
+
+        return $.ajax({
+            type: "DELETE",
+            url: url,
+            dataType: "json"
+        });
+    },
+
+    /**
+     * Deletes a user image
+     * @param {String} userId
+     * @param {String} imageType The type of image to delete, based on the server-side ImageType enum.
+     */
+    deleteUserImage: function (userId, imageType) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        if (!imageType) {
+            throw new Error("null imageType");
+        }
+
+        var url = ApiClient.getUrl("Users/" + userId + "/Images/" + imageType);
+
+        return $.ajax({
+            type: "DELETE",
+            url: url,
+            dataType: "json"
+        });
+    },
+
+    /**
+     * Uploads a user image
+     * @param {String} userId
+     * @param {String} imageType The type of image to delete, based on the server-side ImageType enum.
+     * @param {Object} file The file from the input element
+     */
+    uploadUserImage: function (userId, imageType, file) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        if (!imageType) {
+            throw new Error("null imageType");
+        }
+
+        if (!file || !file.type.match('image.*')) {
+            throw new Error("File must be an image.");
+        }
+
+        var deferred = $.Deferred();
+
+        var reader = new FileReader();
+
+        reader.onerror = function () {
+            deferred.reject();
+        };
+
+        reader.onabort = function () {
+            deferred.reject();
+        };
+
+        // Closure to capture the file information.
+        reader.onload = function (e) {
+
+            var data = window.btoa(e.target.result);
+
+            var params = {
+                userId: userId,
+                type: imageType,
+                filename: file.name
+            };
+
+            var url = ApiClient.getUrl("UploadImage", params);
+
+            $.post(url, data).done(function (result) {
+
+                deferred.resolveWith(null, [result]);
+
+            }).fail(function () {
+                deferred.reject();
+            });
+        };
+
+        // Read in the image file as a data URL.
+        reader.readAsBinaryString(file);
+
+        return deferred.promise();
+    },
+
+    /**
+     * Gets the list of installed plugins on the server
+     */
+    getInstalledPlugins: function () {
+
+        var url = ApiClient.getUrl("Plugins");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets a user by id
+     * @param {String} id
+     */
+    getUser: function (id) {
+
+        if (!id) {
+            throw new Error("Must supply a userId");
+        }
+
+        var url = ApiClient.getUrl("Users/" + id);
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets a studio
+     */
+    getStudio: function (name) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        var url = ApiClient.getUrl("Library/Studios/" + name);
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets a genre
+     */
+    getGenre: function (name) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        var url = ApiClient.getUrl("Library/Genres/" + name);
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets a year
+     */
+    getYear: function (year) {
+
+        if (!year) {
+            throw new Error("null year");
+        }
+
+        var url = ApiClient.getUrl("Library/Years/" + year);
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets a Person
+     */
+    getPerson: function (name) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        var url = ApiClient.getUrl("Library/Persons/" + name);
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets weather info
+     * @param {String} location - us zip code / city, state, country / city, country
+     * Omit location to get weather info using stored server configuration value
+     */
+    getWeatherInfo: function (location) {
+
+        var url = ApiClient.getUrl("weather", {
+            location: location
+        });
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets all users from the server
+     */
+    getAllUsers: function () {
+
+        var url = ApiClient.getUrl("users");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets all available parental ratings from the server
+     */
+    getParentalRatings: function () {
+
+        var url = ApiClient.getUrl("Localization/ParentalRatings");
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Gets a list of all available conrete BaseItem types from the server
+     */
+    getItemTypes: function (options) {
+
+        var url = ApiClient.getUrl("Library/ItemTypes", options);
+
+        return $.getJSON(url);
+    },
+
+    /**
+     * Constructs a url for a user image
+     * @param {String} userId
+     * @param {Object} options
+     * Options supports the following properties:
+     * width - download the image at a fixed width
+     * height - download the image at a fixed height
+     * maxWidth - download the image at a maxWidth
+     * maxHeight - download the image at a maxHeight
+     * quality - A scale of 0-100. This should almost always be omitted as the default will suffice.
+     * For best results do not specify both width and height together, as aspect ratio might be altered.
+     */
+    getUserImageUrl: function (userId, options) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        options = options || {
+        };
+
+        var url = "Users/" + userId + "/Images/" + options.type;
+
+        if (options.index != null) {
+            url += "/" + options.index;
+        }
+
+        // Don't put these on the query string
+        options.type = null;
+        options.index = null;
+
+        return ApiClient.getUrl(url, options);
+    },
+
+    /**
+     * Constructs a url for a person image
+     * @param {String} name
+     * @param {Object} options
+     * Options supports the following properties:
+     * width - download the image at a fixed width
+     * height - download the image at a fixed height
+     * maxWidth - download the image at a maxWidth
+     * maxHeight - download the image at a maxHeight
+     * quality - A scale of 0-100. This should almost always be omitted as the default will suffice.
+     * For best results do not specify both width and height together, as aspect ratio might be altered.
+     */
+    getPersonImageUrl: function (name, options) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        options = options || {
+        };
+
+        var url = "Persons/" + name + "/Images/" + options.type;
+
+        if (options.index != null) {
+            url += "/" + options.index;
+        }
+
+        // Don't put these on the query string
+        options.type = null;
+        options.index = null;
+
+        return ApiClient.getUrl(url, options);
+    },
+
+    /**
+     * Constructs a url for a year image
+     * @param {String} year
+     * @param {Object} options
+     * Options supports the following properties:
+     * width - download the image at a fixed width
+     * height - download the image at a fixed height
+     * maxWidth - download the image at a maxWidth
+     * maxHeight - download the image at a maxHeight
+     * quality - A scale of 0-100. This should almost always be omitted as the default will suffice.
+     * For best results do not specify both width and height together, as aspect ratio might be altered.
+     */
+    getYearImageUrl: function (year, options) {
+
+        if (!year) {
+            throw new Error("null year");
+        }
+
+        options = options || {
+        };
+
+        var url = "Years/" + year + "/Images/" + options.type;
+
+        if (options.index != null) {
+            url += "/" + options.index;
+        }
+
+        // Don't put these on the query string
+        options.type = null;
+        options.index = null;
+
+        return ApiClient.getUrl(url, options);
+    },
+
+    /**
+     * Constructs a url for a genre image
+     * @param {String} name
+     * @param {Object} options
+     * Options supports the following properties:
+     * width - download the image at a fixed width
+     * height - download the image at a fixed height
+     * maxWidth - download the image at a maxWidth
+     * maxHeight - download the image at a maxHeight
+     * quality - A scale of 0-100. This should almost always be omitted as the default will suffice.
+     * For best results do not specify both width and height together, as aspect ratio might be altered.
+     */
+    getGenreImageUrl: function (name, options) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        options = options || {
+        };
+
+        var url = "Genres/" + name + "/Images/" + options.type;
+
+        if (options.index != null) {
+            url += "/" + options.index;
+        }
+
+        // Don't put these on the query string
+        options.type = null;
+        options.index = null;
+
+        return ApiClient.getUrl(url, options);
+    },
+
+    /**
+     * Constructs a url for a genre image
+     * @param {String} name
+     * @param {Object} options
+     * Options supports the following properties:
+     * width - download the image at a fixed width
+     * height - download the image at a fixed height
+     * maxWidth - download the image at a maxWidth
+     * maxHeight - download the image at a maxHeight
+     * quality - A scale of 0-100. This should almost always be omitted as the default will suffice.
+     * For best results do not specify both width and height together, as aspect ratio might be altered.
+     */
+    getStudioImageUrl: function (name, options) {
+
+        if (!name) {
+            throw new Error("null name");
+        }
+
+        options = options || {
+        };
+
+        var url = "Studios/" + name + "/Images/" + options.type;
+
+        if (options.index != null) {
+            url += "/" + options.index;
+        }
+
+        // Don't put these on the query string
+        options.type = null;
+        options.index = null;
+
+        return ApiClient.getUrl(url, options);
+    },
+
+    /**
+     * Constructs a url for an item image
+     * @param {String} itemId
+     * @param {Object} options
+     * Options supports the following properties:
+     * type - Primary, logo, backdrop, etc. See the server-side enum ImageType
+     * index - When downloading a backdrop, use this to specify which one (omitting is equivalent to zero)
+     * width - download the image at a fixed width
+     * height - download the image at a fixed height
+     * maxWidth - download the image at a maxWidth
+     * maxHeight - download the image at a maxHeight
+     * quality - A scale of 0-100. This should almost always be omitted as the default will suffice.
+     * For best results do not specify both width and height together, as aspect ratio might be altered.
+     */
+    getImageUrl: function (itemId, options) {
+
+        if (!itemId) {
+            throw new Error("itemId cannot be empty");
+        }
+
+        options = options || {
+        };
+
+        var url = "Items/" + itemId + "/Images/" + options.type;
+
+        if (options.index != null) {
+            url += "/" + options.index;
+        }
+
+        // Don't put these on the query string
+        options.type = null;
+        options.index = null;
+
+        return ApiClient.getUrl(url, options);
+    },
+
+    /**
+     * Constructs a url for an item logo image
+     * If the item doesn't have a logo, it will inherit a logo from a parent
+     * @param {Object} item A BaseItem
+     * @param {Object} options
+     * Options supports the following properties:
+     * width - download the image at a fixed width
+     * height - download the image at a fixed height
+     * maxWidth - download the image at a maxWidth
+     * maxHeight - download the image at a maxHeight
+     * quality - A scale of 0-100. This should almost always be omitted as the default will suffice.
+     * For best results do not specify both width and height together, as aspect ratio might be altered.
+     */
+    getLogoImageUrl: function (item, options) {
+
+        if (!item) {
+            throw new Error("null item");
+        }
+
+        options = options || {
+        };
+
+        options.imageType = "logo";
+
+        var logoItemId = item.HasLogo ? item.Id : item.ParentLogoItemId;
+
+        return logoItemId ? ApiClient.getImageUrl(logoItemId, options) : null;
+    },
+
+    /**
+     * Constructs an array of backdrop image url's for an item
+     * If the item doesn't have any backdrops, it will inherit them from a parent
+     * @param {Object} item A BaseItem
+     * @param {Object} options
+     * Options supports the following properties:
+     * width - download the image at a fixed width
+     * height - download the image at a fixed height
+     * maxWidth - download the image at a maxWidth
+     * maxHeight - download the image at a maxHeight
+     * quality - A scale of 0-100. This should almost always be omitted as the default will suffice.
+     * For best results do not specify both width and height together, as aspect ratio might be altered.
+     */
+    getBackdropImageUrl: function (item, options) {
+
+        if (!item) {
+            throw new Error("null item");
+        }
+
+        options = options || {
+        };
+
+        options.imageType = "backdrop";
+
+        var backdropItemId;
+        var backdropCount;
+
+        if (!item.BackdropCount) {
+            backdropItemId = item.ParentBackdropItemId;
+            backdropCount = item.ParentBackdropCount || 0;
+        } else {
+            backdropItemId = item.Id;
+            backdropCount = item.BackdropCount;
+        }
+
+        if (!backdropItemId) {
+            return [];
+        }
+
+        var files = [];
+
+        for (var i = 0; i < backdropCount; i++) {
+
+            options.imageIndex = i;
+
+            files[i] = ApiClient.getImageUrl(backdropItemId, options);
+        }
+
+        return files;
+    },
+
+    /**
+     * Authenticates a user
+     * @param {String} userId
+     * @param {String} password
+     */
+    authenticateUser: function (userId, password) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        var url = ApiClient.getUrl("Users/" + userId + "/authenticate");
+
+        var postData = {
+        };
+
+        if (password) {
+            postData.password = password;
+        }
+        return $.post(url, postData);
+    },
+
+    /**
+     * Updates a user's password
+     * @param {String} userId
+     * @param {String} currentPassword
+     * @param {String} newPassword
+     */
+    updateUserPassword: function (userId, currentPassword, newPassword) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        var url = ApiClient.getUrl("Users/" + userId + "/Password");
+
+        var postData = {
+        };
+
+        if (currentPassword) {
+            postData.currentPassword = currentPassword;
+        }
+        if (newPassword) {
+            postData.newPassword = newPassword;
+        }
+        return $.post(url, postData);
+    },
+
+    /**
+    * Resets a user's password
+    * @param {String} userId
+    */
+    resetUserPassword: function (userId) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        var url = ApiClient.getUrl("Users/" + userId + "/Password");
+
+        var postData = {
+        };
+
+        postData.resetPassword = 1;
+        return $.post(url, postData);
+    },
+
+    /**
+     * Updates the server's configuration
+     * @param {Object} configuration
+     */
+    updateServerConfiguration: function (configuration) {
+
+        if (!configuration) {
+            throw new Error("null configuration");
+        }
+
+        var url = ApiClient.getUrl("System/Configuration");
+
+        return $.post(url, JSON.stringify(configuration));
+    },
+
+    /**
+     * Creates a user
+     * @param {Object} user
+     */
+    createUser: function (user) {
+
+        if (!user) {
+            throw new Error("null user");
+        }
+
+        var url = ApiClient.getUrl("Users");
+
+        return $.post(url, JSON.stringify(user));
+    },
+
+    /**
+     * Updates a user
+     * @param {Object} user
+     */
+    updateUser: function (user) {
+
+        if (!user) {
+            throw new Error("null user");
+        }
+
+        var url = ApiClient.getUrl("Users/" + user.Id);
+
+        return $.post(url, JSON.stringify(user));
+    },
+
+    /**
+     * Updates the Triggers for a ScheduledTask
+     * @param {String} id
+     * @param {Object} triggers
+     */
+    updateScheduledTaskTriggers: function (id, triggers) {
+
+        if (!id) {
+            throw new Error("null id");
+        }
+
+        if (!triggers) {
+            throw new Error("null triggers");
+        }
+
+        var url = ApiClient.getUrl("ScheduledTasks/" + id + "/Triggers");
+
+        return $.post(url, JSON.stringify(triggers));
+    },
+
+    /**
+     * Updates a plugin's configuration
+     * @param {String} uniqueId
+     * @param {Object} configuration
+     */
+    updatePluginConfiguration: function (uniqueId, configuration) {
+
+        if (!uniqueId) {
+            throw new Error("null uniqueId");
+        }
+
+        if (!configuration) {
+            throw new Error("null configuration");
+        }
+
+        var url = ApiClient.getUrl("Plugins/" + uniqueId + "/Configuration");
+
+        return $.post(url, JSON.stringify(configuration));
+    },
+
+    /**
+     * Gets items based on a query, typicall for children of a folder
+     * @param {String} userId
+     * @param {Object} options
+     * Options accepts the following properties:
+     * itemId - Localize the search to a specific folder (root if omitted)
+     * startIndex - Use for paging
+     * limit - Use to limit results to a certain number of items
+     * filter - Specify one or more ItemFilters, comma delimeted (see server-side enum)
+     * sortBy - Specify an ItemSortBy (comma-delimeted list see server-side enum)
+     * sortOrder - ascending/descending
+     * fields - additional fields to include aside from basic info. This is a comma delimited list. See server-side enum ItemFields.
+     * index - the name of the dynamic, localized index function
+     * dynamicSortBy - the name of the dynamic localized sort function
+     * recursive - Whether or not the query should be recursive
+     * searchTerm - search term to use as a filter
+     */
+    getItems: function (userId, options) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        return $.getJSON(ApiClient.getUrl("Users/" + userId + "/Items", options));
+    },
+
+    /**
+     * Marks an item as played or unplayed
+     * This should not be used to update playstate following playback.
+     * There are separate playstate check-in methods for that. This should be used for a
+     * separate option to reset playstate.
+     * @param {String} userId
+     * @param {String} itemId
+     * @param {Boolean} wasPlayed
+     */
+    updatePlayedStatus: function (userId, itemId, wasPlayed) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        if (!itemId) {
+            throw new Error("null itemId");
+        }
+
+        var url = "Users/" + userId + "/PlayedItems/" + itemId;
+
+        var method = wasPlayed ? "POST" : "DELETE";
+
+        return $.ajax({
+            type: method,
+            url: url,
+            dataType: "json"
+        });
+    },
+
+    /**
+     * Updates a user's favorite status for an item and returns the updated UserItemData object.
+     * @param {String} userId
+     * @param {String} itemId
+     * @param {Boolean} isFavorite
+     */
+    updateFavoriteStatus: function (userId, itemId, isFavorite) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        if (!itemId) {
+            throw new Error("null itemId");
+        }
+
+        var url = "Users/" + userId + "/FavoriteItems/" + itemId;
+
+        var method = isFavorite ? "POST" : "DELETE";
+
+        return $.ajax({
+            type: method,
+            url: url,
+            dataType: "json"
+        });
+    },
+
+    /**
+     * Updates a user's personal rating for an item
+     * @param {String} userId
+     * @param {String} itemId
+     * @param {Boolean} likes
+     */
+    updateUserItemRating: function (userId, itemId, likes) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        if (!itemId) {
+            throw new Error("null itemId");
+        }
+
+        var url = ApiClient.getUrl("Users/" + userId + "/Items/" + itemId + "/Rating", {
+            likes: likes
+        });
+
+        return $.post(url);
+    },
+
+    /**
+     * Clears a user's personal rating for an item
+     * @param {String} userId
+     * @param {String} itemId
+     */
+    clearUserItemRating: function (userId, itemId) {
+
+        if (!userId) {
+            throw new Error("null userId");
+        }
+
+        if (!itemId) {
+            throw new Error("null itemId");
+        }
+
+        var url = ApiClient.getUrl("Users/" + userId + "/Items/" + itemId + "/Rating");
+
+        return $.ajax({
+            type: "DELETE",
+            url: url,
+            dataType: "json"
+        });
+    }
+};
+
+// Do this initially. The consumer can always override later
+ApiClient.inferServerFromUrl();

+ 60 - 0
MediaBrowser.ApiInteraction.Javascript/JavascriptApiClientService.cs

@@ -0,0 +1,60 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using ServiceStack.ServiceHost;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.ApiInteraction.Javascript
+{
+    /// <summary>
+    /// Class GetJavascriptApiClient
+    /// </summary>
+    [Route("/JsApiClient.js", "GET")]
+    [Api(("Gets an api wrapper in Javascript"))]
+    public class GetJavascriptApiClient
+    {
+        /// <summary>
+        /// Version identifier for caching
+        /// </summary>
+        /// <value>The v.</value>
+        public string V { get; set; }
+    }
+
+    /// <summary>
+    /// Class JavascriptApiClientService
+    /// </summary>
+    [Export(typeof(IRestfulService))]
+    public class JavascriptApiClientService : BaseRestService
+    {
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetJavascriptApiClient request)
+        {
+            TimeSpan? cacheDuration = null;
+
+            // If there's a version number in the query string we can cache this unconditionally
+            if (!string.IsNullOrEmpty(request.V))
+            {
+                cacheDuration = TimeSpan.FromDays(365);
+            }
+
+            var assembly = GetType().Assembly.GetName();
+
+            return ToStaticResult(assembly.Version.ToString().GetMD5(), null, cacheDuration, MimeTypes.GetMimeType("script.js"), GetStream);
+        }
+
+        /// <summary>
+        /// Gets the stream.
+        /// </summary>
+        /// <returns>Stream.</returns>
+        private Task<Stream> GetStream()
+        {
+            return Task.FromResult(GetType().Assembly.GetManifestResourceStream("MediaBrowser.ApiInteraction.Javascript.ApiClient.js"));
+        }
+    }
+}

+ 115 - 0
MediaBrowser.ApiInteraction.Javascript/MediaBrowser.ApiInteraction.Javascript.csproj

@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{767B536E-D90C-4D74-A14B-8564B16F3499}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>MediaBrowser.ApiInteraction.Javascript</RootNamespace>
+    <AssemblyName>MediaBrowser.ApiInteraction.Javascript</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
+    <RestorePackages>true</RestorePackages>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup>
+    <RunPostBuildEvent>Always</RunPostBuildEvent>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="ServiceStack, Version=3.9.37.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.3.9.37\lib\net35\ServiceStack.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Common, Version=3.9.37.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Common.3.9.37\lib\net35\ServiceStack.Common.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Interfaces, Version=3.9.37.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Common.3.9.37\lib\net35\ServiceStack.Interfaces.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.OrmLite, Version=3.9.37.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.OrmLite.SqlServer.3.9.37\lib\ServiceStack.OrmLite.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.OrmLite.SqlServer, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.OrmLite.SqlServer.3.9.37\lib\ServiceStack.OrmLite.SqlServer.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Redis, Version=3.9.37.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Redis.3.9.37\lib\net35\ServiceStack.Redis.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.ServiceInterface, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.3.9.37\lib\net35\ServiceStack.ServiceInterface.dll</HintPath>
+    </Reference>
+    <Reference Include="ServiceStack.Text, Version=3.9.37.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\ServiceStack.Text.3.9.37\lib\net35\ServiceStack.Text.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.ComponentModel.Composition" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="JavascriptApiClientService.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+      <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+      <Name>MediaBrowser.Common</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
+      <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
+      <Name>MediaBrowser.Controller</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+      <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+      <Name>MediaBrowser.Model</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="ApiClient.js" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <PropertyGroup>
+    <PostBuildEvent>xcopy "$(TargetPath)" "$(SolutionDir)\MediaBrowser.ServerApplication\" /y</PostBuildEvent>
+  </PropertyGroup>
+  <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 34 - 0
MediaBrowser.ApiInteraction.Javascript/Properties/AssemblyInfo.cs

@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.ApiInteraction.Javascript")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.ApiInteraction.Javascript")]
+[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("97f9d4da-d7de-47d9-ae68-06d78679d327")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("2.9.*")]

+ 8 - 0
MediaBrowser.ApiInteraction.Javascript/packages.config

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="ServiceStack" version="3.9.37" targetFramework="net45" />
+  <package id="ServiceStack.Common" version="3.9.37" targetFramework="net45" />
+  <package id="ServiceStack.OrmLite.SqlServer" version="3.9.37" targetFramework="net45" />
+  <package id="ServiceStack.Redis" version="3.9.37" targetFramework="net45" />
+  <package id="ServiceStack.Text" version="3.9.37" targetFramework="net45" />
+</packages>

+ 0 - 12
MediaBrowser.ApiInteraction.Metro/ApiClient.cs

@@ -1,12 +0,0 @@
-using System.Net.Http;
-
-namespace MediaBrowser.ApiInteraction
-{
-    public class ApiClient : BaseHttpApiClient
-    {
-        public ApiClient(HttpClientHandler handler)
-            : base(handler)
-        {
-        }
-    }
-}

+ 0 - 78
MediaBrowser.ApiInteraction.Metro/DataSerializer.cs

@@ -1,78 +0,0 @@
-using Newtonsoft.Json;
-using System;
-using System.IO;
-
-namespace MediaBrowser.ApiInteraction
-{
-    public static class DataSerializer
-    {
-        /// <summary>
-        /// This is an auto-generated Protobuf Serialization assembly for best performance.
-        /// It is created during the Model project's post-build event.
-        /// This means that this class can currently only handle types within the Model project.
-        /// If we need to, we can always add a param indicating whether or not the model serializer should be used.
-        /// </summary>
-        private static readonly ProtobufModelSerializer ProtobufModelSerializer = new ProtobufModelSerializer();
-        
-        public static T DeserializeFromStream<T>(Stream stream, SerializationFormats format)
-            where T : class
-        {
-            if (format == ApiInteraction.SerializationFormats.Protobuf)
-            {
-                return ProtobufModelSerializer.Deserialize(stream, null, typeof(T)) as T;
-            }
-            else if (format == ApiInteraction.SerializationFormats.Jsv)
-            {
-                throw new NotImplementedException();
-            }
-            else if (format == ApiInteraction.SerializationFormats.Json)
-            {
-                using (StreamReader streamReader = new StreamReader(stream))
-                {
-                    using (JsonReader jsonReader = new JsonTextReader(streamReader))
-                    {
-                        return JsonSerializer.Create(new JsonSerializerSettings()).Deserialize<T>(jsonReader);
-                    }
-                }
-            }
-
-            throw new NotImplementedException();
-        }
-
-        public static object DeserializeFromStream(Stream stream, SerializationFormats format, Type type)
-        {
-            if (format == ApiInteraction.SerializationFormats.Protobuf)
-            {
-                return ProtobufModelSerializer.Deserialize(stream, null, type);
-            }
-            else if (format == ApiInteraction.SerializationFormats.Jsv)
-            {
-                throw new NotImplementedException();
-            }
-            else if (format == ApiInteraction.SerializationFormats.Json)
-            {
-                using (StreamReader streamReader = new StreamReader(stream))
-                {
-                    using (JsonReader jsonReader = new JsonTextReader(streamReader))
-                    {
-                        return JsonSerializer.Create(new JsonSerializerSettings()).Deserialize(jsonReader, type);
-                    }
-                }
-            }
-
-            throw new NotImplementedException();
-        }
-
-        public static void Configure()
-        {
-        }
-
-        public static bool CanDeSerializeJsv
-        {
-            get
-            {
-                return false;
-            }
-        }
-    }
-}

+ 76 - 0
MediaBrowser.ApiInteraction.Portable/DataSerializer.cs

@@ -0,0 +1,76 @@
+using Newtonsoft.Json;
+using ProtoBuf;
+using ProtoBuf.Meta;
+using System;
+using System.IO;
+
+namespace MediaBrowser.ApiInteraction
+{
+    /// <summary>
+    /// Class DataSerializer
+    /// </summary>
+    public static class DataSerializer
+    {
+        /// <summary>
+        /// Gets or sets the dynamically created serializer.
+        /// </summary>
+        /// <value>The dynamic serializer.</value>
+        public static TypeModel DynamicSerializer { get; set; }
+
+        /// <summary>
+        /// Deserializes an object
+        /// </summary>
+        /// <param name="stream">The stream.</param>
+        /// <param name="format">The format.</param>
+        /// <param name="type">The type.</param>
+        /// <returns>System.Object.</returns>
+        /// <exception cref="System.NotImplementedException"></exception>
+        public static object DeserializeFromStream(Stream stream, SerializationFormats format, Type type)
+        {
+            if (format == SerializationFormats.Protobuf)
+            {
+                if (DynamicSerializer != null)
+                {
+                    return DynamicSerializer.Deserialize(stream, null, type);
+                }
+                return Serializer.NonGeneric.Deserialize(type, stream);
+            }
+            if (format == SerializationFormats.Json)
+            {
+                using (var streamReader = new StreamReader(stream))
+                {
+                    using (var jsonReader = new JsonTextReader(streamReader))
+                    {
+                        return JsonSerializer.Create(new JsonSerializerSettings()).Deserialize(jsonReader, type);
+                    }
+                }
+            }
+
+            throw new NotImplementedException();
+        }
+
+        /// <summary>
+        /// Serializes to json.
+        /// </summary>
+        /// <param name="obj">The obj.</param>
+        /// <returns>System.String.</returns>
+        public static string SerializeToJsonString(object obj)
+        {
+            using (var streamWriter = new StringWriter())
+            {
+                using (var jsonWriter = new JsonTextWriter((streamWriter)))
+                {
+                    JsonSerializer.Create(new JsonSerializerSettings()).Serialize(jsonWriter, obj);
+                }
+                return streamWriter.ToString();
+            }
+        }
+
+        /// <summary>
+        /// Configures this instance.
+        /// </summary>
+        public static void Configure()
+        {
+        }
+    }
+}

+ 105 - 0
MediaBrowser.ApiInteraction.Portable/MediaBrowser.ApiInteraction.Portable.csproj

@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <MinimumVisualStudioVersion>10.0</MinimumVisualStudioVersion>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{52E0C440-85C0-4A99-ACFE-07C87B2600BE}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>MediaBrowser.ApiInteraction.Portable</RootNamespace>
+    <AssemblyName>MediaBrowser.ApiInteraction.Portable</AssemblyName>
+    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+    <TargetFrameworkProfile>Profile104</TargetFrameworkProfile>
+    <FileAlignment>512</FileAlignment>
+    <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+    <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
+    <RestorePackages>true</RestorePackages>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="..\MediaBrowser.ApiInteraction\AsyncHttpClient.cs">
+      <Link>AsyncHttpClient.cs</Link>
+    </Compile>
+    <Compile Include="DataSerializer.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="..\MediaBrowser.ApiInteraction\BaseApiClient.cs">
+      <Link>BaseApiClient.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.ApiInteraction\ApiClient.cs">
+      <Link>ApiClient.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.ApiInteraction\IAsyncHttpClient.cs">
+      <Link>IAsyncHttpClient.cs</Link>
+    </Compile>
+    <Compile Include="..\MediaBrowser.ApiInteraction\SerializationFormats.cs">
+      <Link>SerializationFormats.cs</Link>
+    </Compile>
+  </ItemGroup>
+  <ItemGroup>
+    <Reference Include="Microsoft.Threading.Tasks">
+      <HintPath>..\packages\Microsoft.Bcl.Async.1.0.14-rc\lib\portable-net40+sl4+win8+wp71\Microsoft.Threading.Tasks.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.Threading.Tasks.Extensions">
+      <HintPath>..\packages\Microsoft.Bcl.Async.1.0.14-rc\lib\portable-net40+sl4+win8+wp71\Microsoft.Threading.Tasks.Extensions.dll</HintPath>
+    </Reference>
+    <Reference Include="Newtonsoft.Json">
+      <HintPath>..\packages\Newtonsoft.Json.4.5.11\lib\portable-net40+sl4+wp7+win8\Newtonsoft.Json.dll</HintPath>
+    </Reference>
+    <Reference Include="protobuf-net">
+      <HintPath>..\packages\protobuf-net.2.0.0.621\lib\portable-sl4+net40+wp7+windows8\protobuf-net.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Net.Http">
+      <HintPath>..\packages\Microsoft.Net.Http.2.1.3-beta\lib\portable-net40+sl4+win8+wp71\System.Net.Http.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Net.Http.Extensions">
+      <HintPath>..\packages\Microsoft.Net.Http.2.1.3-beta\lib\portable-net40+sl4+win8+wp71\System.Net.Http.Extensions.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Net.Http.Primitives">
+      <HintPath>..\packages\Microsoft.Net.Http.2.1.3-beta\lib\portable-net40+sl4+win8+wp71\System.Net.Http.Primitives.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Runtime">
+      <HintPath>..\packages\Microsoft.Bcl.1.0.16-rc\lib\portable-net40+sl4+win8+wp71\System.Runtime.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Threading.Tasks">
+      <HintPath>..\packages\Microsoft.Bcl.1.0.16-rc\lib\portable-net40+sl4+win8+wp71\System.Threading.Tasks.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+      <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+      <Name>MediaBrowser.Model</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="app.config" />
+    <None Include="packages.config" />
+  </ItemGroup>
+  <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
+  <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
+  <Import Project="..\packages\Microsoft.Bcl.Build.1.0.0-rc\tools\Microsoft.Bcl.Build.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

+ 27 - 30
MediaBrowser.ApiInteraction.Metro/Properties/AssemblyInfo.cs → MediaBrowser.ApiInteraction.Portable/Properties/AssemblyInfo.cs

@@ -1,30 +1,27 @@
-using System.Resources;
-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.ApiInteraction.Metro")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("MediaBrowser.ApiInteraction.Metro")]
-[assembly: AssemblyCopyright("Copyright ©  2012")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-[assembly: NeutralResourcesLanguage("en")]
-
-// Version information for an assembly consists of the following four values:
-//
-//      Major Version
-//      Minor Version 
-//      Build Number
-//      Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers 
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
+using System.Reflection;
+using System.Resources;
+
+// 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.ApiInteraction.Portable")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.ApiInteraction.Portable")]
+[assembly: AssemblyCopyright("Copyright ©  2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+[assembly: NeutralResourcesLanguage("en")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("2.9.*")]

Some files were not shown because too many files changed in this diff