Luke Pulverenti vor 10 Jahren
Ursprung
Commit
1292eccfb5
27 geänderte Dateien mit 820 neuen und 593 gelöschten Zeilen
  1. 1 1
      MediaBrowser.Api/ItemUpdateService.cs
  2. 91 2
      MediaBrowser.Api/Movies/TrailersService.cs
  3. 6 1
      MediaBrowser.Api/Playback/StreamState.cs
  4. 3 1
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  5. 3 1
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  6. 4 33
      MediaBrowser.Controller/Entities/MusicVideo.cs
  7. 3 3
      MediaBrowser.Dlna/Didl/DidlBuilder.cs
  8. 14 4
      MediaBrowser.LocalMetadata/Parsers/MusicVideoXmlParser.cs
  9. 2 2
      MediaBrowser.LocalMetadata/Savers/MovieXmlSaver.cs
  10. 3 1
      MediaBrowser.Model/ApiClient/IConnectionManager.cs
  11. 2 0
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  12. 2 1
      MediaBrowser.Model/Dto/BaseItemDto.cs
  13. 3 5
      MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
  14. 0 4
      MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs
  15. 10 17
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  16. 6 1
      MediaBrowser.Server.Implementations/Localization/Server/server.json
  17. 1 5
      MediaBrowser.Server.Implementations/Session/SessionManager.cs
  18. 35 0
      MediaBrowser.ServerApplication/ApplicationHost.cs
  19. 47 502
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  20. 574 0
      MediaBrowser.WebDashboard/Api/PackageCreator.cs
  21. 1 0
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  22. 1 1
      MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
  23. 2 2
      MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
  24. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  25. 1 1
      Nuget/MediaBrowser.Common.nuspec
  26. 1 1
      Nuget/MediaBrowser.Model.Signed.nuspec
  27. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec

+ 1 - 1
MediaBrowser.Api/ItemUpdateService.cs

@@ -237,7 +237,7 @@ namespace MediaBrowser.Api
 
 
             if (musicVideo != null)
             if (musicVideo != null)
             {
             {
-                musicVideo.Artist = request.Artists[0];
+                musicVideo.Artists = request.Artists.ToList();
                 musicVideo.Album = request.Album;
                 musicVideo.Album = request.Album;
             }
             }
 
 

+ 91 - 2
MediaBrowser.Api/Movies/TrailersService.cs

@@ -1,10 +1,20 @@
-using MediaBrowser.Controller.Dto;
+using MediaBrowser.Api.UserLibrary;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
 using ServiceStack;
 using ServiceStack;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Api.Movies
 namespace MediaBrowser.Api.Movies
 {
 {
@@ -16,6 +26,17 @@ namespace MediaBrowser.Api.Movies
     {
     {
     }
     }
 
 
+    [Route("/Trailers", "GET", Summary = "Finds movies and trailers similar to a given trailer.")]
+    public class Getrailers : BaseItemsRequest, IReturn<ItemsResult>
+    {
+        /// <summary>
+        /// Gets or sets the user id.
+        /// </summary>
+        /// <value>The user id.</value>
+        [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public Guid? UserId { get; set; }
+    }
+
     /// <summary>
     /// <summary>
     /// Class TrailersService
     /// Class TrailersService
     /// </summary>
     /// </summary>
@@ -38,6 +59,7 @@ namespace MediaBrowser.Api.Movies
 
 
         private readonly IItemRepository _itemRepo;
         private readonly IItemRepository _itemRepo;
         private readonly IDtoService _dtoService;
         private readonly IDtoService _dtoService;
+        private readonly IChannelManager _channelManager;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="TrailersService"/> class.
         /// Initializes a new instance of the <see cref="TrailersService"/> class.
@@ -45,13 +67,14 @@ namespace MediaBrowser.Api.Movies
         /// <param name="userManager">The user manager.</param>
         /// <param name="userManager">The user manager.</param>
         /// <param name="userDataRepository">The user data repository.</param>
         /// <param name="userDataRepository">The user data repository.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="libraryManager">The library manager.</param>
-        public TrailersService(IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, IItemRepository itemRepo, IDtoService dtoService)
+        public TrailersService(IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, IItemRepository itemRepo, IDtoService dtoService, IChannelManager channelManager)
         {
         {
             _userManager = userManager;
             _userManager = userManager;
             _userDataRepository = userDataRepository;
             _userDataRepository = userDataRepository;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _itemRepo = itemRepo;
             _itemRepo = itemRepo;
             _dtoService = dtoService;
             _dtoService = dtoService;
+            _channelManager = channelManager;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -75,5 +98,71 @@ namespace MediaBrowser.Api.Movies
 
 
             return ToOptimizedSerializedResultUsingCache(result);
             return ToOptimizedSerializedResultUsingCache(result);
         }
         }
+
+        public async Task<object> Get(Getrailers request)
+        {
+            var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null;
+            var result = await GetAllTrailers(user).ConfigureAwait(false);
+
+            IEnumerable<BaseItem> items = result.Items;
+
+            // Apply filters
+            // Run them starting with the ones that are likely to reduce the list the most
+            foreach (var filter in request.GetFilters().OrderByDescending(f => (int)f))
+            {
+                items = ItemsService.ApplyFilter(items, filter, user, _userDataRepository);
+            }
+
+            items = _libraryManager.Sort(items, user, request.GetOrderBy(), request.SortOrder ?? SortOrder.Ascending);
+
+            var itemsArray = items.ToList();
+
+            var pagedItems = ApplyPaging(request, itemsArray);
+
+            var fields = request.GetItemFields().ToList();
+
+            var returnItems = pagedItems.Select(i => _dtoService.GetBaseItemDto(i, fields, user)).ToArray();
+
+            return new ItemsResult
+            {
+                TotalRecordCount = itemsArray.Count,
+                Items = returnItems
+            };
+        }
+
+        private IEnumerable<BaseItem> ApplyPaging(Getrailers 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;
+        }
+
+        private async Task<QueryResult<BaseItem>> GetAllTrailers(User user)
+        {
+            var trailerResult = await _channelManager.GetAllMediaInternal(new AllChannelMediaQuery
+            {
+                ContentTypes = new[] { ChannelMediaContentType.MovieExtra },
+                ExtraTypes = new[] { ExtraType.Trailer },
+                UserId = user.Id.ToString("N")
+
+            }, CancellationToken.None).ConfigureAwait(false);
+
+
+            return new QueryResult<BaseItem>
+            {
+                Items = trailerResult.Items,
+                TotalRecordCount = trailerResult.TotalRecordCount
+            };
+        }
     }
     }
 }
 }

+ 6 - 1
MediaBrowser.Api/Playback/StreamState.cs

@@ -97,7 +97,12 @@ namespace MediaBrowser.Api.Playback
 
 
         public bool ReadInputAtNativeFramerate
         public bool ReadInputAtNativeFramerate
         {
         {
-            get { return InputProtocol == MediaProtocol.Rtmp || string.Equals(InputContainer, "wtv", StringComparison.OrdinalIgnoreCase); }
+            get { 
+                
+                return InputProtocol == MediaProtocol.Rtmp || 
+                    string.Equals(InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) ||
+                    !string.IsNullOrEmpty(LiveTvStreamId); 
+            }
         }
         }
 
 
         public TransportStreamTimestamp InputTimestamp { get; set; }
         public TransportStreamTimestamp InputTimestamp { get; set; }

+ 3 - 1
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Collections;
+using System.Threading;
+using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
@@ -8,6 +9,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Channels;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using ServiceStack;
 using ServiceStack;

+ 3 - 1
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -226,7 +226,9 @@ namespace MediaBrowser.Controller.Entities.Audio
 
 
         public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
         public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
         {
         {
-            return inputItems.OfType<IHasArtist>().Where(i => i.HasArtist(Name)).Cast<BaseItem>();
+            return inputItems.OfType<IHasArtist>()
+                .Where(i => i.HasArtist(Name))
+                .Cast<BaseItem>();
         }
         }
     }
     }
 }
 }

+ 4 - 33
MediaBrowser.Controller/Entities/MusicVideo.cs

@@ -11,12 +11,6 @@ namespace MediaBrowser.Controller.Entities
 {
 {
     public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasProductionLocations, IHasBudget, IHasLookupInfo<MusicVideoInfo>
     public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasProductionLocations, IHasBudget, IHasLookupInfo<MusicVideoInfo>
     {
     {
-        /// <summary>
-        /// Gets or sets the artist.
-        /// </summary>
-        /// <value>The artist.</value>
-        public string Artist { get; set; }
-
         /// <summary>
         /// <summary>
         /// Gets or sets the album.
         /// Gets or sets the album.
         /// </summary>
         /// </summary>
@@ -35,27 +29,12 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The revenue.</value>
         /// <value>The revenue.</value>
         public double? Revenue { get; set; }
         public double? Revenue { get; set; }
         public List<string> ProductionLocations { get; set; }
         public List<string> ProductionLocations { get; set; }
+        public List<string> Artists { get; set; }
 
 
         public MusicVideo()
         public MusicVideo()
         {
         {
             ProductionLocations = new List<string>();
             ProductionLocations = new List<string>();
-        }
-
-        [IgnoreDataMember]
-        public List<string> Artists
-        {
-            get
-            {
-                var list = new List<string>();
-
-                if (!string.IsNullOrEmpty(Artist))
-                {
-                    list.Add(Artist);
-                }
-
-                return list;
-
-            }
+            Artists = new List<string>();
         }
         }
 
 
         [IgnoreDataMember]
         [IgnoreDataMember]
@@ -63,15 +42,7 @@ namespace MediaBrowser.Controller.Entities
         {
         {
             get
             get
             {
             {
-                var list = new List<string>();
-
-                if (!string.IsNullOrEmpty(Artist))
-                {
-                    list.Add(Artist);
-                }
-
-                return list;
-
+                return Artists;
             }
             }
         }
         }
 
 
@@ -82,7 +53,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns><c>true</c> if the specified name has artist; otherwise, <c>false</c>.</returns>
         /// <returns><c>true</c> if the specified name has artist; otherwise, <c>false</c>.</returns>
         public bool HasArtist(string name)
         public bool HasArtist(string name)
         {
         {
-            return string.Equals(Artist, name, StringComparison.OrdinalIgnoreCase);
+            return AllArtists.Contains(name, StringComparer.OrdinalIgnoreCase);
         }
         }
         
         
         /// <summary>
         /// <summary>

+ 3 - 3
MediaBrowser.Dlna/Didl/DidlBuilder.cs

@@ -671,10 +671,10 @@ namespace MediaBrowser.Dlna.Didl
 
 
             if (musicVideo != null)
             if (musicVideo != null)
             {
             {
-                if (!string.IsNullOrEmpty(musicVideo.Artist))
+                foreach (var artist in musicVideo.Artists)
                 {
                 {
-                    AddValue(element, "upnp", "artist", musicVideo.Artist, NS_UPNP);
-                    AddAlbumArtist(element, musicVideo.Artist);
+                    AddValue(element, "upnp", "artist", artist, NS_UPNP);
+                    AddAlbumArtist(element, artist);
                 }
                 }
 
 
                 if (!string.IsNullOrEmpty(musicVideo.Album))
                 if (!string.IsNullOrEmpty(musicVideo.Album))

+ 14 - 4
MediaBrowser.LocalMetadata/Parsers/MusicVideoXmlParser.cs

@@ -1,7 +1,8 @@
-using System.Xml;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
+using System;
+using System.Xml;
 
 
 namespace MediaBrowser.LocalMetadata.Parsers
 namespace MediaBrowser.LocalMetadata.Parsers
 {
 {
@@ -26,8 +27,17 @@ namespace MediaBrowser.LocalMetadata.Parsers
             switch (reader.Name)
             switch (reader.Name)
             {
             {
                 case "Artist":
                 case "Artist":
-                    item.Artist = reader.ReadElementContentAsString();
-                    break;
+                    {
+                        var val = reader.ReadElementContentAsString();
+
+                        if (!string.IsNullOrWhiteSpace(val))
+                        {
+                            var artists = val.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
+                            item.Artists.AddRange(artists);
+                        }
+
+                        break;
+                    }
 
 
                 case "Album":
                 case "Album":
                     item.Album = reader.ReadElementContentAsString();
                     item.Album = reader.ReadElementContentAsString();

+ 2 - 2
MediaBrowser.LocalMetadata/Savers/MovieXmlSaver.cs

@@ -78,9 +78,9 @@ namespace MediaBrowser.LocalMetadata.Savers
 
 
             if (musicVideo != null)
             if (musicVideo != null)
             {
             {
-                if (!string.IsNullOrEmpty(musicVideo.Artist))
+                if (musicVideo.Artists.Count > 0)
                 {
                 {
-                    builder.Append("<Artist>" + SecurityElement.Escape(musicVideo.Artist) + "</Artist>");
+                    builder.Append("<Artist>" + SecurityElement.Escape(string.Join(";", musicVideo.Artists.ToArray())) + "</Artist>");
                 }
                 }
                 if (!string.IsNullOrEmpty(musicVideo.Album))
                 if (!string.IsNullOrEmpty(musicVideo.Album))
                 {
                 {

+ 3 - 1
MediaBrowser.Model/ApiClient/IConnectionManager.cs

@@ -30,7 +30,9 @@ namespace MediaBrowser.Model.ApiClient
         /// Occurs when [connect user sign out].
         /// Occurs when [connect user sign out].
         /// </summary>
         /// </summary>
         event EventHandler<EventArgs> ConnectUserSignOut;
         event EventHandler<EventArgs> ConnectUserSignOut;
-
+        [Obsolete]
+        event EventHandler<EventArgs> RemoteLoggedOut;
+        
         /// <summary>
         /// <summary>
         /// Gets the connect user.
         /// Gets the connect user.
         /// </summary>
         /// </summary>

+ 2 - 0
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -180,6 +180,8 @@ namespace MediaBrowser.Model.Configuration
 
 
         public bool SaveMetadataHidden { get; set; }
         public bool SaveMetadataHidden { get; set; }
 
 
+        public bool PlaylistImagesDeleted { get; set; }
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>
         /// </summary>

+ 2 - 1
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -43,7 +43,8 @@ namespace MediaBrowser.Model.Dto
         public DateTime? DateCreated { get; set; }
         public DateTime? DateCreated { get; set; }
 
 
         public DateTime? DateLastMediaAdded { get; set; }
         public DateTime? DateLastMediaAdded { get; set; }
-        
+        public ExtraType? ExtraType { get; set; }
+      
         public int? AirsBeforeSeasonNumber { get; set; }
         public int? AirsBeforeSeasonNumber { get; set; }
         public int? AirsAfterSeasonNumber { get; set; }
         public int? AirsAfterSeasonNumber { get; set; }
         public int? AirsBeforeEpisodeNumber { get; set; }
         public int? AirsBeforeEpisodeNumber { get; set; }

+ 3 - 5
MediaBrowser.Providers/Music/MusicVideoMetadataService.cs

@@ -1,15 +1,13 @@
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Manager;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
+using System.Linq;
 
 
 namespace MediaBrowser.Providers.Music
 namespace MediaBrowser.Providers.Music
 {
 {
@@ -36,9 +34,9 @@ namespace MediaBrowser.Providers.Music
                 target.Album = source.Album;
                 target.Album = source.Album;
             }
             }
 
 
-            if (replaceData || string.IsNullOrEmpty(target.Artist))
+            if (replaceData || target.Artists.Count == 0)
             {
             {
-                target.Artist = source.Artist;
+                target.Artists = source.Artists.ToList();
             }
             }
         }
         }
     }
     }

+ 0 - 4
MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -3,8 +3,6 @@ using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Implementations.Configuration;
 using MediaBrowser.Common.Implementations.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
@@ -215,9 +213,7 @@ namespace MediaBrowser.Server.Implementations.Configuration
         {
         {
             DisableMetadataService(typeof(Movie), Configuration, service);
             DisableMetadataService(typeof(Movie), Configuration, service);
             DisableMetadataService(typeof(Episode), Configuration, service);
             DisableMetadataService(typeof(Episode), Configuration, service);
-            DisableMetadataService(typeof(Season), Configuration, service);
             DisableMetadataService(typeof(Series), Configuration, service);
             DisableMetadataService(typeof(Series), Configuration, service);
-            DisableMetadataService(typeof(Video), Configuration, service);
         }
         }
 
 
         private void DisableMetadataService(Type type, ServerConfiguration config, string service)
         private void DisableMetadataService(Type type, ServerConfiguration config, string service)

+ 10 - 17
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -72,22 +72,9 @@ namespace MediaBrowser.Server.Implementations.Dto
 
 
             if (byName != null && !(item is LiveTvChannel))
             if (byName != null && !(item is LiveTvChannel))
             {
             {
-                IEnumerable<BaseItem> libraryItems;
-
-                var artist = item as MusicArtist;
-
-                if (artist == null || artist.IsAccessedByName)
-                {
-                    libraryItems = user != null ?
-                       user.RootFolder.GetRecursiveChildren(user) :
-                       _libraryManager.RootFolder.RecursiveChildren;
-                }
-                else
-                {
-                    libraryItems = user != null ?
-                       artist.GetRecursiveChildren(user) :
-                       artist.RecursiveChildren;
-                }
+                var libraryItems = user != null ?
+                   user.RootFolder.GetRecursiveChildren(user) :
+                   _libraryManager.RootFolder.RecursiveChildren;
 
 
                 SetItemByNameInfo(item, dto, byName.GetTaggedItems(libraryItems).ToList(), user);
                 SetItemByNameInfo(item, dto, byName.GetTaggedItems(libraryItems).ToList(), user);
 
 
@@ -398,7 +385,7 @@ namespace MediaBrowser.Server.Implementations.Dto
             }
             }
 
 
             dto.Album = item.Album;
             dto.Album = item.Album;
-            dto.Artists = string.IsNullOrEmpty(item.Artist) ? new List<string>() : new List<string> { item.Artist };
+            dto.Artists = item.Artists;
         }
         }
 
 
         private void SetGameProperties(BaseItemDto dto, Game item)
         private void SetGameProperties(BaseItemDto dto, Game item)
@@ -1232,6 +1219,12 @@ namespace MediaBrowser.Server.Implementations.Dto
                 dto.ChannelId = channelItem.ChannelId;
                 dto.ChannelId = channelItem.ChannelId;
                 dto.ChannelName = _channelManagerFactory().GetChannel(channelItem.ChannelId).Name;
                 dto.ChannelName = _channelManagerFactory().GetChannel(channelItem.ChannelId).Name;
             }
             }
+
+            var channelMediaItem = item as IChannelMediaItem;
+            if (channelMediaItem != null)
+            {
+                dto.ExtraType = channelMediaItem.ExtraType;
+            }
         }
         }
 
 
         private void AttachLinkedChildImages(BaseItemDto dto, Folder folder, User user)
         private void AttachLinkedChildImages(BaseItemDto dto, Folder folder, User user)

+ 6 - 1
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -1247,5 +1247,10 @@
     "OptionWeekdays": "Weekdays",
     "OptionWeekdays": "Weekdays",
     "OptionWeekends": "Weekends",
     "OptionWeekends": "Weekends",
     "MessageProfileInfoSynced": "User profile information synced with Media Browser Connect.",
     "MessageProfileInfoSynced": "User profile information synced with Media Browser Connect.",
-    "HeaderOptionalLinkMediaBrowserAccount": "Optional: Link your Media Browser account"
+    "HeaderOptionalLinkMediaBrowserAccount": "Optional: Link your Media Browser account",
+    "ButtonTrailerReel": "Trailer reel",
+    "HeaderTrailerReel": "Trailer Reel",
+    "OptionPlayUnwatchedTrailersOnly": "Play only unwatched trailers",
+    "HeaderTrailerReelHelp": "Start a trailer reel to play a long running playlist of trailers.",
+    "MessageNoTrailersFound": "No trailers found. Install the Trailer channel plugin to import a library of internet trailers."
 }
 }

+ 1 - 5
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -1546,11 +1546,7 @@ namespace MediaBrowser.Server.Implementations.Session
             if (musicVideo != null)
             if (musicVideo != null)
             {
             {
                 info.Album = musicVideo.Album;
                 info.Album = musicVideo.Album;
-
-                if (!string.IsNullOrWhiteSpace(musicVideo.Artist))
-                {
-                    info.Artists.Add(musicVideo.Artist);
-                }
+                info.Artists = musicVideo.Artists.ToList();
             }
             }
 
 
             var backropItem = item.HasImage(ImageType.Backdrop) ? item : null;
             var backropItem = item.HasImage(ImageType.Backdrop) ? item : null;

+ 35 - 0
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -324,6 +324,41 @@ namespace MediaBrowser.ServerApplication
         private void PerformVersionMigration()
         private void PerformVersionMigration()
         {
         {
             DeleteDeprecatedModules();
             DeleteDeprecatedModules();
+
+            if (!ServerConfigurationManager.Configuration.PlaylistImagesDeleted)
+            {
+                DeletePlaylistImages();
+                ServerConfigurationManager.Configuration.PlaylistImagesDeleted = true;
+                ServerConfigurationManager.SaveConfiguration();
+            }
+        }
+
+        private void DeletePlaylistImages()
+        {
+            try
+            {
+                var path = Path.Combine(ApplicationPaths.DataPath, "playlists");
+
+                var files = Directory.GetFiles(path, "*", SearchOption.AllDirectories)
+                    .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i) ?? string.Empty))
+                    .ToList();
+
+                foreach (var file in files)
+                {
+                    try
+                    {
+                        File.Delete(file);
+                    }
+                    catch (IOException)
+                    {
+
+                    }
+                }
+            }
+            catch (IOException)
+            {
+                
+            }
         }
         }
 
 
         private void DeleteDeprecatedModules()
         private void DeleteDeprecatedModules()

+ 47 - 502
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -14,10 +14,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using WebMarkupMin.Core.Minifiers;
-using WebMarkupMin.Core.Settings;
 
 
 namespace MediaBrowser.WebDashboard.Api
 namespace MediaBrowser.WebDashboard.Api
 {
 {
@@ -49,6 +46,12 @@ namespace MediaBrowser.WebDashboard.Api
         public string Name { get; set; }
         public string Name { get; set; }
     }
     }
 
 
+    [Route("/web/Package", "GET")]
+    [Route("/dashboard/Package", "GET")]
+    public class GetDashboardPackage
+    {
+    }
+
     /// <summary>
     /// <summary>
     /// Class GetDashboardResource
     /// Class GetDashboardResource
     /// </summary>
     /// </summary>
@@ -120,35 +123,6 @@ namespace MediaBrowser.WebDashboard.Api
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
         }
         }
 
 
-        /// <summary>
-        /// Gets the dashboard UI path.
-        /// </summary>
-        /// <value>The dashboard UI path.</value>
-        public string DashboardUIPath
-        {
-            get
-            {
-                if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.DashboardSourcePath))
-                {
-                    return _serverConfigurationManager.Configuration.DashboardSourcePath;
-                }
-
-                var runningDirectory = Path.GetDirectoryName(_serverConfigurationManager.ApplicationPaths.ApplicationPath);
-
-                return Path.Combine(runningDirectory, "dashboard-ui");
-            }
-        }
-
-        /// <summary>
-        /// Gets the dashboard resource path.
-        /// </summary>
-        /// <param name="virtualPath">The virtual path.</param>
-        /// <returns>System.String.</returns>
-        private string GetDashboardResourcePath(string virtualPath)
-        {
-            return Path.Combine(DashboardUIPath, virtualPath.Replace('/', Path.DirectorySeparatorChar));
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the specified request.
         /// Gets the specified request.
         /// </summary>
         /// </summary>
@@ -158,7 +132,7 @@ namespace MediaBrowser.WebDashboard.Api
         {
         {
             var page = ServerEntryPoint.Instance.PluginConfigurationPages.First(p => p.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase));
             var page = ServerEntryPoint.Instance.PluginConfigurationPages.First(p => p.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase));
 
 
-            return ResultFactory.GetStaticResult(Request, page.Plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => ModifyHtml(page.GetHtmlStream(), null));
+            return ResultFactory.GetStaticResult(Request, page.Plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator().ModifyHtml(page.GetHtmlStream(), null));
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -228,7 +202,7 @@ namespace MediaBrowser.WebDashboard.Api
                 {
                 {
                     Request.Response.Redirect("wizardstart.html");
                     Request.Response.Redirect("wizardstart.html");
                     return null;
                     return null;
-                }    
+                }
             }
             }
 
 
             path = path.Replace("scripts/jquery.mobile-1.4.4.min.map", "thirdparty/jquerymobile-1.4.4/jquery.mobile-1.4.4.min.map", StringComparison.OrdinalIgnoreCase);
             path = path.Replace("scripts/jquery.mobile-1.4.4.min.map", "thirdparty/jquerymobile-1.4.4/jquery.mobile-1.4.4.min.map", StringComparison.OrdinalIgnoreCase);
@@ -241,7 +215,7 @@ namespace MediaBrowser.WebDashboard.Api
                 !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) &&
                 !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) &&
                 !contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase))
                 !contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase))
             {
             {
-                return ResultFactory.GetResult(GetResourceStream(path, isHtml, localizationCulture).Result, contentType);
+                return ResultFactory.GetResult(GetResourceStream(path, localizationCulture).Result, contentType);
             }
             }
 
 
             TimeSpan? cacheDuration = null;
             TimeSpan? cacheDuration = null;
@@ -257,7 +231,7 @@ namespace MediaBrowser.WebDashboard.Api
 
 
             var cacheKey = (assembly.Version + (localizationCulture ?? string.Empty) + path).GetMD5();
             var cacheKey = (assembly.Version + (localizationCulture ?? string.Empty) + path).GetMD5();
 
 
-            return ResultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(path, isHtml, localizationCulture));
+            return ResultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(path, localizationCulture));
         }
         }
 
 
         private string GetLocalizationCulture()
         private string GetLocalizationCulture()
@@ -269,47 +243,17 @@ namespace MediaBrowser.WebDashboard.Api
         /// Gets the resource stream.
         /// Gets the resource stream.
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
-        /// <param name="isHtml">if set to <c>true</c> [is HTML].</param>
         /// <param name="localizationCulture">The localization culture.</param>
         /// <param name="localizationCulture">The localization culture.</param>
         /// <returns>Task{Stream}.</returns>
         /// <returns>Task{Stream}.</returns>
-        private async Task<Stream> GetResourceStream(string path, bool isHtml, string localizationCulture)
+        private Task<Stream> GetResourceStream(string path, string localizationCulture)
         {
         {
-            Stream resourceStream;
-
-            if (path.Equals("scripts/all.js", StringComparison.OrdinalIgnoreCase))
-            {
-                resourceStream = await GetAllJavascript().ConfigureAwait(false);
-            }
-            else if (path.Equals("css/all.css", StringComparison.OrdinalIgnoreCase))
-            {
-                resourceStream = await GetAllCss().ConfigureAwait(false);
-            }
-            else
-            {
-                resourceStream = GetRawResourceStream(path);
-            }
-
-            if (resourceStream != null)
-            {
-                // Don't apply any caching for html pages
-                // jQuery ajax doesn't seem to handle if-modified-since correctly
-                if (isHtml)
-                {
-                    resourceStream = await ModifyHtml(resourceStream, localizationCulture).ConfigureAwait(false);
-                }
-            }
-
-            return resourceStream;
+            return GetPackageCreator()
+                .GetResource(path, localizationCulture, _appHost.ApplicationVersion.ToString());
         }
         }
 
 
-        /// <summary>
-        /// Gets the raw resource stream.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>Task{Stream}.</returns>
-        private Stream GetRawResourceStream(string path)
+        private PackageCreator GetPackageCreator()
         {
         {
-            return _fileSystem.GetFileStream(GetDashboardResourcePath(path), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
+            return new PackageCreator(_fileSystem, _localization, Logger, _serverConfigurationManager, _jsonSerializer);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -322,466 +266,67 @@ namespace MediaBrowser.WebDashboard.Api
             return Path.GetExtension(path).EndsWith("html", StringComparison.OrdinalIgnoreCase);
             return Path.GetExtension(path).EndsWith("html", StringComparison.OrdinalIgnoreCase);
         }
         }
 
 
-        /// <summary>
-        /// Modifies the HTML by adding common meta tags, css and js.
-        /// </summary>
-        /// <param name="sourceStream">The source stream.</param>
-        /// <param name="userId">The user identifier.</param>
-        /// <param name="localizationCulture">The localization culture.</param>
-        /// <returns>Task{Stream}.</returns>
-        private async Task<Stream> ModifyHtml(Stream sourceStream, string localizationCulture)
+        public async Task<object> Get(GetDashboardPackage request)
         {
         {
-            using (sourceStream)
-            {
-                string html;
-
-                using (var memoryStream = new MemoryStream())
-                {
-                    await sourceStream.CopyToAsync(memoryStream).ConfigureAwait(false);
-
-                    html = Encoding.UTF8.GetString(memoryStream.ToArray());
-
-                    if (!string.IsNullOrWhiteSpace(localizationCulture))
-                    {
-                        var lang = localizationCulture.Split('-').FirstOrDefault();
-
-                        html = _localization.LocalizeDocument(html, localizationCulture, GetLocalizationToken);
-
-                        html = html.Replace("<html>", "<html lang=\"" + lang + "\">");
-                    }
-
-                    //try
-                    //{
-                    //    var minifier = new HtmlMinifier(new HtmlMinificationSettings(true));
-
-                    //    html = minifier.Minify(html).MinifiedContent;
-                    //}
-                    //catch (Exception ex)
-                    //{
-                    //    Logger.ErrorException("Error minifying html", ex);
-                    //}
-                }
-
-                var version = GetType().Assembly.GetName().Version;
-
-                html = html.Replace("<head>", "<head>" + GetMetaTags() + GetCommonCss(version) + GetCommonJavascript(version));
-
-                var bytes = Encoding.UTF8.GetBytes(html);
-
-                return new MemoryStream(bytes);
-            }
-        }
-
-        private string GetLocalizationToken(string phrase)
-        {
-            return "${" + phrase + "}";
-        }
-
-        /// <summary>
-        /// Gets the meta tags.
-        /// </summary>
-        /// <returns>System.String.</returns>
-        private static string GetMetaTags()
-        {
-            var sb = new StringBuilder();
-
-            sb.Append("<meta http-equiv=\"X-UA-Compatibility\" content=\"IE=Edge\">");
-            sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">");
-            //sb.Append("<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">");
-            sb.Append("<meta name=\"mobile-web-app-capable\" content=\"yes\">");
-            sb.Append("<meta name=\"application-name\" content=\"Media Browser\">");
-            //sb.Append("<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">");
-
-            sb.Append("<link rel=\"icon\" sizes=\"114x114\" href=\"css/images/touchicon114.png\" />");
-
-            // http://developer.apple.com/library/ios/#DOCUMENTATION/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
-            sb.Append("<link rel=\"apple-touch-icon\" href=\"css/images/touchicon.png\" />");
-            sb.Append("<link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"css/images/touchicon72.png\" />");
-            sb.Append("<link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"css/images/touchicon114.png\" />");
-            sb.Append("<link rel=\"apple-touch-startup-image\" href=\"css/images/iossplash.png\" />");
-            sb.Append("<link rel=\"shortcut icon\" href=\"css/images/favicon.ico\" />");
+            var path = Path.Combine(_serverConfigurationManager.ApplicationPaths.ProgramDataPath,
+                "webclient-dump");
 
 
-            return sb.ToString();
-        }
-
-        /// <summary>
-        /// Gets the common CSS.
-        /// </summary>
-        /// <param name="version">The version.</param>
-        /// <returns>System.String.</returns>
-        private static string GetCommonCss(Version version)
-        {
-            var versionString = "?v=" + version;
-
-            var files = new[]
-                            {
-                                "thirdparty/jquerymobile-1.4.4/jquery.mobile-1.4.4.min.css",
-                                "thirdparty/swipebox-master/css/swipebox.min.css" + versionString,
-                                "css/all.css" + versionString
-                            };
-
-            var tags = files.Select(s => string.Format("<link rel=\"stylesheet\" href=\"{0}\" />", s)).ToArray();
-
-            return string.Join(string.Empty, tags);
-        }
-
-        /// <summary>
-        /// Gets the common javascript.
-        /// </summary>
-        /// <param name="version">The version.</param>
-        /// <returns>System.String.</returns>
-        private static string GetCommonJavascript(Version version)
-        {
-            var builder = new StringBuilder();
-
-            var versionString = "?v=" + version;
-
-            var files = new[]
-                            {
-                                "scripts/all.js" + versionString,
-                                "thirdparty/jstree1.0/jquery.jstree.min.js",
-                                "thirdparty/swipebox-master/js/jquery.swipebox.min.js" + versionString
-            };
-
-            var tags = files.Select(s => string.Format("<script src=\"{0}\"></script>", s)).ToArray();
-
-            builder.Append(string.Join(string.Empty, tags));
-
-            return builder.ToString();
-        }
-
-        /// <summary>
-        /// Gets a stream containing all concatenated javascript
-        /// </summary>
-        /// <returns>Task{Stream}.</returns>
-        private async Task<Stream> GetAllJavascript()
-        {
-            var memoryStream = new MemoryStream();
-            var newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine);
-
-            // jQuery + jQuery mobile
-            await AppendResource(memoryStream, "thirdparty/jquery-2.1.1.min.js", newLineBytes).ConfigureAwait(false);
-            await AppendResource(memoryStream, "thirdparty/jquerymobile-1.4.4/jquery.mobile-1.4.4.min.js", newLineBytes).ConfigureAwait(false);
-
-            await AppendResource(memoryStream, "thirdparty/jquery.unveil-custom.js", newLineBytes).ConfigureAwait(false);
-
-            // This script produces errors in older versions of safari
-            if ((Request.UserAgent ?? string.Empty).IndexOf("chrome/", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                await AppendResource(memoryStream, "thirdparty/cast_sender.js", newLineBytes).ConfigureAwait(false);
-            }
-            
-            await AppendLocalization(memoryStream).ConfigureAwait(false);
-            await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
-
-            // Write the version string for the dashboard comparison function
-            var versionString = string.Format("window.dashboardVersion='{0}';", _appHost.ApplicationVersion);
-            var versionBytes = Encoding.UTF8.GetBytes(versionString);
-
-            await memoryStream.WriteAsync(versionBytes, 0, versionBytes.Length).ConfigureAwait(false);
-            await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
-
-            var builder = new StringBuilder();
-
-            foreach (var file in new[]
-            {
-                "thirdparty/apiclient/sha1.js",
-                "thirdparty/apiclient/mediabrowser.apiclient.js",
-                "thirdparty/apiclient/connectionmanager.js"
-            })
+            try
             {
             {
-                using (var fs = _fileSystem.GetFileStream(GetDashboardResourcePath(file), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
-                {
-                    using (var streamReader = new StreamReader(fs))
-                    {
-                        var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
-                        builder.Append(text);
-                        builder.Append(Environment.NewLine);
-                    }
-                }
+                Directory.Delete(path, true);
             }
             }
-
-            foreach (var file in GetScriptFiles())
+            catch (IOException)
             {
             {
-                var path = GetDashboardResourcePath("scripts/" + file);
 
 
-                using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
-                {
-                    using (var streamReader = new StreamReader(fs))
-                    {
-                        var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
-                        builder.Append(text);
-                        builder.Append(Environment.NewLine);
-                    }
-                }
             }
             }
 
 
-            var js = builder.ToString();
+            var creator = GetPackageCreator();
 
 
-            try
-            {
-                var result = new CrockfordJsMinifier().Minify(js, false, Encoding.UTF8);
+            CopyDirectory(creator.DashboardUIPath, path);
 
 
-                js = result.MinifiedContent;
-            }
-            catch (Exception ex)
-            {
-                Logger.ErrorException("Error minifying javascript", ex);
-            }
+            var culture = "en-US";
 
 
-            var bytes = Encoding.UTF8.GetBytes(js);
-            await memoryStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+            await DumpHtml(creator.DashboardUIPath, path, culture, _appHost.ApplicationVersion.ToString());
 
 
-            memoryStream.Position = 0;
-            return memoryStream;
+            return "";
         }
         }
 
 
-        private IEnumerable<string> GetScriptFiles()
+        private async Task DumpHtml(string source, string destination, string culture, string appVersion)
         {
         {
-            return new[]
-                            {
-                                "extensions.js",
-                                "site.js",
-                                "librarybrowser.js",
-                                "librarylist.js",
-                                "editorsidebar.js",
-                                "librarymenu.js",
-                                "mediacontroller.js",
-                                "chromecast.js",
-                                "backdrops.js",
-                                "sync.js",
-                                "playlistmanager.js",
-
-                                "mediaplayer.js",
-                                "mediaplayer-video.js",
-                                "nowplayingbar.js",
-                                "nowplayingpage.js",
-
-                                "ratingdialog.js",
-                                "aboutpage.js",
-                                "alphapicker.js",
-                                "addpluginpage.js",
-                                "advancedconfigurationpage.js",
-                                "metadataadvanced.js",
-                                "autoorganizetv.js",
-                                "autoorganizelog.js",
-                                "channels.js",
-                                "channelslatest.js",
-                                "channelitems.js",
-                                "channelsettings.js",
-                                "dashboardgeneral.js",
-                                "dashboardpage.js",
-                                "dashboardsync.js",
-                                "device.js",
-                                "devices.js",
-                                "devicesupload.js",
-                                "directorybrowser.js",
-                                "dlnaprofile.js",
-                                "dlnaprofiles.js",
-                                "dlnasettings.js",
-                                "dlnaserversettings.js",
-                                "editcollectionitems.js",
-                                "edititemmetadata.js",
-                                "edititemimages.js",
-                                "edititemsubtitles.js",
-
-                                "playbackconfiguration.js",
-                                "cinemamodeconfiguration.js",
-                                "encodingsettings.js",
-
-                                "externalplayer.js",
-                                "favorites.js",
-                                "gamesrecommendedpage.js",
-                                "gamesystemspage.js",
-                                "gamespage.js",
-                                "gamegenrepage.js",
-                                "gamestudiospage.js",
-                                "homelatest.js",
-                                "indexpage.js",
-                                "itembynamedetailpage.js",
-                                "itemdetailpage.js",
-                                "itemgallery.js",
-                                "itemlistpage.js",
-                                "librarypathmapping.js",
-                                "reports.js",
-                                "librarysettings.js",
-                                "livetvchannel.js",
-                                "livetvchannels.js",
-                                "livetvguide.js",
-                                "livetvnewrecording.js",
-                                "livetvprogram.js",
-                                "livetvrecording.js",
-                                "livetvrecordinglist.js",
-                                "livetvrecordings.js",
-                                "livetvtimer.js",
-                                "livetvseriestimer.js",
-                                "livetvseriestimers.js",
-                                "livetvsettings.js",
-                                "livetvsuggested.js",
-                                "livetvstatus.js",
-                                "livetvtimers.js",
-
-                                "loginpage.js",
-                                "logpage.js",
-                                "medialibrarypage.js",
-                                "metadataconfigurationpage.js",
-                                "metadataimagespage.js",
-                                "metadatasubtitles.js",
-                                "metadatakodi.js",
-                                "moviegenres.js",
-                                "moviecollections.js",
-                                "movies.js",
-                                "movieslatest.js",
-                                "moviepeople.js",
-                                "moviesrecommended.js",
-                                "moviestudios.js",
-                                "movietrailers.js",
-                                "musicalbums.js",
-                                "musicalbumartists.js",
-                                "musicartists.js",
-                                "musicgenres.js",
-                                "musicrecommended.js",
-                                "musicvideos.js",
-
-                                "mypreferencesdisplay.js",
-                                "mypreferenceslanguages.js",
-                                "mypreferenceswebclient.js",
-
-                                "notifications.js",
-                                "notificationlist.js",
-                                "notificationsetting.js",
-                                "notificationsettings.js",
-                                "playlist.js",
-                                "playlists.js",
-                                "playlistedit.js",
-
-                                "plugincatalogpage.js",
-                                "pluginspage.js",
-                                "remotecontrol.js",
-                                "scheduledtaskpage.js",
-                                "scheduledtaskspage.js",
-                                "search.js",
-                                "serversecurity.js",
-                                "songs.js",
-                                "supporterkeypage.js",
-                                "supporterpage.js",
-                                "episodes.js",
-                                "thememediaplayer.js",
-                                "tvgenres.js",
-                                "tvlatest.js",
-                                "tvpeople.js",
-                                "tvrecommended.js",
-                                "tvshows.js",
-                                "tvstudios.js",
-                                "tvupcoming.js",
-                                "useredit.js",
-                                "userpassword.js",
-                                "myprofile.js",
-                                "userprofilespage.js",
-                                "userparentalcontrol.js",
-                                "userlibraryaccess.js",
-                                "wizardfinishpage.js",
-                                "wizardservice.js",
-                                "wizardstartpage.js",
-                                "wizardsettings.js",
-                                "wizarduserpage.js"
-                            };
+            foreach (var file in Directory.GetFiles(source, "*.html", SearchOption.TopDirectoryOnly))
+            {
+                await DumpHtmlFile(file, destination, culture, appVersion).ConfigureAwait(false);
+            }
         }
         }
 
 
-        private async Task AppendLocalization(Stream stream)
+        private async Task DumpHtmlFile(string file, string destination, string culture, string appVersion)
         {
         {
-            var js = "window.localizationGlossary=" + _jsonSerializer.SerializeToString(_localization.GetJavaScriptLocalizationDictionary(GetLocalizationCulture()));
+            var filename = Path.GetFileName(file);
 
 
-            var bytes = Encoding.UTF8.GetBytes(js);
-            await stream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
-        }
+            var targetPath = Path.Combine(destination, filename);
 
 
-        /// <summary>
-        /// Gets all CSS.
-        /// </summary>
-        /// <returns>Task{Stream}.</returns>
-        private async Task<Stream> GetAllCss()
-        {
-            var files = new[]
-                                  {
-                                      "site.css",
-                                      "chromecast.css",
-                                      "mediaplayer.css",
-                                      "mediaplayer-video.css",
-                                      "librarymenu.css",
-                                      "librarybrowser.css",
-                                      "detailtable.css",
-                                      "card.css",
-                                      "tileitem.css",
-                                      "metadataeditor.css",
-                                      "notifications.css",
-                                      "search.css",
-                                      "pluginupdates.css",
-                                      "remotecontrol.css",
-                                      "userimage.css",
-                                      "livetv.css",
-                                      "nowplaying.css",
-                                      "icons.css"
-                                  };
-
-            var builder = new StringBuilder();
-
-            foreach (var file in files)
+            using (var stream = await GetPackageCreator().GetResource(filename, culture, appVersion).ConfigureAwait(false))
             {
             {
-                var path = GetDashboardResourcePath("css/" + file);
-
-                using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+                using (var fs = _fileSystem.GetFileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.Read))
                 {
                 {
-                    using (var streamReader = new StreamReader(fs))
-                    {
-                        var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
-                        builder.Append(text);
-                        builder.Append(Environment.NewLine);
-                    }
+                    stream.CopyTo(fs);
                 }
                 }
             }
             }
-
-            var css = builder.ToString();
-
-            //try
-            //{
-            //    var result = new KristensenCssMinifier().Minify(builder.ToString(), false, Encoding.UTF8);
-
-            //    css = result.MinifiedContent;
-            //}
-            //catch (Exception ex)
-            //{
-            //    Logger.ErrorException("Error minifying css", ex);
-            //}
-
-            var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(css));
-
-            memoryStream.Position = 0;
-            return memoryStream;
         }
         }
 
 
-        /// <summary>
-        /// Appends the resource.
-        /// </summary>
-        /// <param name="outputStream">The output stream.</param>
-        /// <param name="path">The path.</param>
-        /// <param name="newLineBytes">The new line bytes.</param>
-        /// <returns>Task.</returns>
-        private async Task AppendResource(Stream outputStream, string path, byte[] newLineBytes)
+        private void CopyDirectory(string source, string destination)
         {
         {
-            path = GetDashboardResourcePath(path);
+            Directory.CreateDirectory(destination);
 
 
-            using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
-            {
-                using (var streamReader = new StreamReader(fs))
-                {
-                    var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
-                    var bytes = Encoding.UTF8.GetBytes(text);
-                    await outputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
-                }
-            }
+            //Now Create all of the directories
+            foreach (string dirPath in Directory.GetDirectories(source, "*",
+                SearchOption.AllDirectories))
+                Directory.CreateDirectory(dirPath.Replace(source, destination));
 
 
-            await outputStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
+            //Copy all the files & Replaces any files with the same name
+            foreach (string newPath in Directory.GetFiles(source, "*.*",
+                SearchOption.AllDirectories))
+                File.Copy(newPath, newPath.Replace(source, destination), true);
         }
         }
     }
     }
 
 

+ 574 - 0
MediaBrowser.WebDashboard/Api/PackageCreator.cs

@@ -0,0 +1,574 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Localization;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using WebMarkupMin.Core.Minifiers;
+
+namespace MediaBrowser.WebDashboard.Api
+{
+    public class PackageCreator
+    {
+        private readonly IFileSystem _fileSystem;
+        private readonly ILocalizationManager _localization;
+        private readonly ILogger _logger;
+        private readonly IServerConfigurationManager _config;
+        private readonly IJsonSerializer _jsonSerializer;
+
+        public PackageCreator(IFileSystem fileSystem, ILocalizationManager localization, ILogger logger, IServerConfigurationManager config, IJsonSerializer jsonSerializer)
+        {
+            _fileSystem = fileSystem;
+            _localization = localization;
+            _logger = logger;
+            _config = config;
+            _jsonSerializer = jsonSerializer;
+        }
+
+        public async Task<Stream> GetResource(string path, 
+            string localizationCulture,
+            string appVersion)
+        {
+            var isHtml = IsHtml(path);
+
+            Stream resourceStream;
+
+            if (path.Equals("scripts/all.js", StringComparison.OrdinalIgnoreCase))
+            {
+                resourceStream = await GetAllJavascript(localizationCulture, appVersion).ConfigureAwait(false);
+            }
+            else if (path.Equals("css/all.css", StringComparison.OrdinalIgnoreCase))
+            {
+                resourceStream = await GetAllCss().ConfigureAwait(false);
+            }
+            else
+            {
+                resourceStream = GetRawResourceStream(path);
+            }
+
+            if (resourceStream != null)
+            {
+                // Don't apply any caching for html pages
+                // jQuery ajax doesn't seem to handle if-modified-since correctly
+                if (isHtml)
+                {
+                    resourceStream = await ModifyHtml(resourceStream, localizationCulture).ConfigureAwait(false);
+                }
+            }
+
+            return resourceStream;
+        }
+
+        /// <summary>
+        /// Determines whether the specified path is HTML.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns><c>true</c> if the specified path is HTML; otherwise, <c>false</c>.</returns>
+        private bool IsHtml(string path)
+        {
+            return Path.GetExtension(path).EndsWith("html", StringComparison.OrdinalIgnoreCase);
+        }
+
+        /// <summary>
+        /// Gets the dashboard UI path.
+        /// </summary>
+        /// <value>The dashboard UI path.</value>
+        public string DashboardUIPath
+        {
+            get
+            {
+                if (!string.IsNullOrEmpty(_config.Configuration.DashboardSourcePath))
+                {
+                    return _config.Configuration.DashboardSourcePath;
+                }
+
+                var runningDirectory = Path.GetDirectoryName(_config.ApplicationPaths.ApplicationPath);
+
+                return Path.Combine(runningDirectory, "dashboard-ui");
+            }
+        }
+
+        /// <summary>
+        /// Gets the dashboard resource path.
+        /// </summary>
+        /// <param name="virtualPath">The virtual path.</param>
+        /// <returns>System.String.</returns>
+        private string GetDashboardResourcePath(string virtualPath)
+        {
+            return Path.Combine(DashboardUIPath, virtualPath.Replace('/', Path.DirectorySeparatorChar));
+        }
+
+        /// <summary>
+        /// Modifies the HTML by adding common meta tags, css and js.
+        /// </summary>
+        /// <param name="sourceStream">The source stream.</param>
+        /// <param name="localizationCulture">The localization culture.</param>
+        /// <returns>Task{Stream}.</returns>
+        public async Task<Stream> ModifyHtml(Stream sourceStream, string localizationCulture)
+        {
+            using (sourceStream)
+            {
+                string html;
+
+                using (var memoryStream = new MemoryStream())
+                {
+                    await sourceStream.CopyToAsync(memoryStream).ConfigureAwait(false);
+
+                    html = Encoding.UTF8.GetString(memoryStream.ToArray());
+
+                    if (!string.IsNullOrWhiteSpace(localizationCulture))
+                    {
+                        var lang = localizationCulture.Split('-').FirstOrDefault();
+
+                        html = _localization.LocalizeDocument(html, localizationCulture, GetLocalizationToken);
+
+                        html = html.Replace("<html>", "<html lang=\"" + lang + "\">");
+                    }
+
+                    //try
+                    //{
+                    //    var minifier = new HtmlMinifier(new HtmlMinificationSettings(true));
+
+                    //    html = minifier.Minify(html).MinifiedContent;
+                    //}
+                    //catch (Exception ex)
+                    //{
+                    //    Logger.ErrorException("Error minifying html", ex);
+                    //}
+                }
+
+                var version = GetType().Assembly.GetName().Version;
+
+                html = html.Replace("<head>", "<head>" + GetMetaTags() + GetCommonCss(version) + GetCommonJavascript(version));
+
+                var bytes = Encoding.UTF8.GetBytes(html);
+
+                return new MemoryStream(bytes);
+            }
+        }
+
+        private string GetLocalizationToken(string phrase)
+        {
+            return "${" + phrase + "}";
+        }
+
+        /// <summary>
+        /// Gets the meta tags.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        private static string GetMetaTags()
+        {
+            var sb = new StringBuilder();
+
+            sb.Append("<meta http-equiv=\"X-UA-Compatibility\" content=\"IE=Edge\">");
+            sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">");
+            //sb.Append("<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">");
+            sb.Append("<meta name=\"mobile-web-app-capable\" content=\"yes\">");
+            sb.Append("<meta name=\"application-name\" content=\"Media Browser\">");
+            //sb.Append("<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">");
+
+            sb.Append("<link rel=\"icon\" sizes=\"114x114\" href=\"css/images/touchicon114.png\" />");
+
+            // http://developer.apple.com/library/ios/#DOCUMENTATION/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
+            sb.Append("<link rel=\"apple-touch-icon\" href=\"css/images/touchicon.png\" />");
+            sb.Append("<link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"css/images/touchicon72.png\" />");
+            sb.Append("<link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"css/images/touchicon114.png\" />");
+            sb.Append("<link rel=\"apple-touch-startup-image\" href=\"css/images/iossplash.png\" />");
+            sb.Append("<link rel=\"shortcut icon\" href=\"css/images/favicon.ico\" />");
+
+            return sb.ToString();
+        }
+
+        /// <summary>
+        /// Gets the common CSS.
+        /// </summary>
+        /// <param name="version">The version.</param>
+        /// <returns>System.String.</returns>
+        private string GetCommonCss(Version version)
+        {
+            var versionString = "?v=" + version;
+
+            var files = new[]
+                            {
+                                "thirdparty/jquerymobile-1.4.4/jquery.mobile-1.4.4.min.css",
+                                "thirdparty/swipebox-master/css/swipebox.min.css" + versionString,
+                                "css/all.css" + versionString
+                            };
+
+            var tags = files.Select(s => string.Format("<link rel=\"stylesheet\" href=\"{0}\" />", s)).ToArray();
+
+            return string.Join(string.Empty, tags);
+        }
+
+        /// <summary>
+        /// Gets the common javascript.
+        /// </summary>
+        /// <param name="version">The version.</param>
+        /// <returns>System.String.</returns>
+        private string GetCommonJavascript(Version version)
+        {
+            var builder = new StringBuilder();
+
+            var versionString = "?v=" + version;
+
+            var files = new[]
+                            {
+                                "scripts/all.js" + versionString,
+                                "thirdparty/jstree1.0/jquery.jstree.min.js",
+                                "thirdparty/swipebox-master/js/jquery.swipebox.min.js" + versionString
+            };
+
+            var tags = files.Select(s => string.Format("<script src=\"{0}\"></script>", s)).ToArray();
+
+            builder.Append(string.Join(string.Empty, tags));
+
+            return builder.ToString();
+        }
+
+        /// <summary>
+        /// Gets a stream containing all concatenated javascript
+        /// </summary>
+        /// <returns>Task{Stream}.</returns>
+        private async Task<Stream> GetAllJavascript(string culture, string version)
+        {
+            var memoryStream = new MemoryStream();
+            var newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine);
+
+            // jQuery + jQuery mobile
+            await AppendResource(memoryStream, "thirdparty/jquery-2.1.1.min.js", newLineBytes).ConfigureAwait(false);
+            await AppendResource(memoryStream, "thirdparty/jquerymobile-1.4.4/jquery.mobile-1.4.4.min.js", newLineBytes).ConfigureAwait(false);
+
+            await AppendResource(memoryStream, "thirdparty/jquery.unveil-custom.js", newLineBytes).ConfigureAwait(false);
+
+            await AppendResource(memoryStream, "thirdparty/cast_sender.js", newLineBytes).ConfigureAwait(false);
+
+            await AppendLocalization(memoryStream, culture).ConfigureAwait(false);
+            await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
+
+            // Write the version string for the dashboard comparison function
+            var versionString = string.Format("window.dashboardVersion='{0}';", version);
+            var versionBytes = Encoding.UTF8.GetBytes(versionString);
+
+            await memoryStream.WriteAsync(versionBytes, 0, versionBytes.Length).ConfigureAwait(false);
+            await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
+
+            var builder = new StringBuilder();
+
+            foreach (var file in new[]
+            {
+                "thirdparty/apiclient/sha1.js",
+                "thirdparty/apiclient/mediabrowser.apiclient.js",
+                "thirdparty/apiclient/connectionmanager.js"
+            })
+            {
+                using (var fs = _fileSystem.GetFileStream(GetDashboardResourcePath(file), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+                {
+                    using (var streamReader = new StreamReader(fs))
+                    {
+                        var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
+                        builder.Append(text);
+                        builder.Append(Environment.NewLine);
+                    }
+                }
+            }
+
+            foreach (var file in GetScriptFiles())
+            {
+                var path = GetDashboardResourcePath("scripts/" + file);
+
+                using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+                {
+                    using (var streamReader = new StreamReader(fs))
+                    {
+                        var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
+                        builder.Append(text);
+                        builder.Append(Environment.NewLine);
+                    }
+                }
+            }
+
+            var js = builder.ToString();
+
+            try
+            {
+                var result = new CrockfordJsMinifier().Minify(js, false, Encoding.UTF8);
+
+                js = result.MinifiedContent;
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error minifying javascript", ex);
+            }
+
+            var bytes = Encoding.UTF8.GetBytes(js);
+            await memoryStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+
+            memoryStream.Position = 0;
+            return memoryStream;
+        }
+        private IEnumerable<string> GetScriptFiles()
+        {
+            return new[]
+                            {
+                                "extensions.js",
+                                "site.js",
+                                "librarybrowser.js",
+                                "librarylist.js",
+                                "editorsidebar.js",
+                                "librarymenu.js",
+                                "mediacontroller.js",
+                                "chromecast.js",
+                                "backdrops.js",
+                                "sync.js",
+                                "playlistmanager.js",
+
+                                "mediaplayer.js",
+                                "mediaplayer-video.js",
+                                "nowplayingbar.js",
+                                "nowplayingpage.js",
+
+                                "ratingdialog.js",
+                                "aboutpage.js",
+                                "alphapicker.js",
+                                "addpluginpage.js",
+                                "advancedconfigurationpage.js",
+                                "metadataadvanced.js",
+                                "autoorganizetv.js",
+                                "autoorganizelog.js",
+                                "channels.js",
+                                "channelslatest.js",
+                                "channelitems.js",
+                                "channelsettings.js",
+                                "dashboardgeneral.js",
+                                "dashboardpage.js",
+                                "dashboardsync.js",
+                                "device.js",
+                                "devices.js",
+                                "devicesupload.js",
+                                "directorybrowser.js",
+                                "dlnaprofile.js",
+                                "dlnaprofiles.js",
+                                "dlnasettings.js",
+                                "dlnaserversettings.js",
+                                "editcollectionitems.js",
+                                "edititemmetadata.js",
+                                "edititemimages.js",
+                                "edititemsubtitles.js",
+
+                                "playbackconfiguration.js",
+                                "cinemamodeconfiguration.js",
+                                "encodingsettings.js",
+
+                                "externalplayer.js",
+                                "favorites.js",
+                                "gamesrecommendedpage.js",
+                                "gamesystemspage.js",
+                                "gamespage.js",
+                                "gamegenrepage.js",
+                                "gamestudiospage.js",
+                                "homelatest.js",
+                                "indexpage.js",
+                                "itembynamedetailpage.js",
+                                "itemdetailpage.js",
+                                "itemgallery.js",
+                                "itemlistpage.js",
+                                "librarypathmapping.js",
+                                "reports.js",
+                                "librarysettings.js",
+                                "livetvchannel.js",
+                                "livetvchannels.js",
+                                "livetvguide.js",
+                                "livetvnewrecording.js",
+                                "livetvprogram.js",
+                                "livetvrecording.js",
+                                "livetvrecordinglist.js",
+                                "livetvrecordings.js",
+                                "livetvtimer.js",
+                                "livetvseriestimer.js",
+                                "livetvseriestimers.js",
+                                "livetvsettings.js",
+                                "livetvsuggested.js",
+                                "livetvstatus.js",
+                                "livetvtimers.js",
+
+                                "loginpage.js",
+                                "logpage.js",
+                                "medialibrarypage.js",
+                                "metadataconfigurationpage.js",
+                                "metadataimagespage.js",
+                                "metadatasubtitles.js",
+                                "metadatakodi.js",
+                                "moviegenres.js",
+                                "moviecollections.js",
+                                "movies.js",
+                                "movieslatest.js",
+                                "moviepeople.js",
+                                "moviesrecommended.js",
+                                "moviestudios.js",
+                                "movietrailers.js",
+                                "musicalbums.js",
+                                "musicalbumartists.js",
+                                "musicartists.js",
+                                "musicgenres.js",
+                                "musicrecommended.js",
+                                "musicvideos.js",
+
+                                "mypreferencesdisplay.js",
+                                "mypreferenceslanguages.js",
+                                "mypreferenceswebclient.js",
+
+                                "notifications.js",
+                                "notificationlist.js",
+                                "notificationsetting.js",
+                                "notificationsettings.js",
+                                "playlist.js",
+                                "playlists.js",
+                                "playlistedit.js",
+
+                                "plugincatalogpage.js",
+                                "pluginspage.js",
+                                "remotecontrol.js",
+                                "scheduledtaskpage.js",
+                                "scheduledtaskspage.js",
+                                "search.js",
+                                "serversecurity.js",
+                                "songs.js",
+                                "supporterkeypage.js",
+                                "supporterpage.js",
+                                "episodes.js",
+                                "thememediaplayer.js",
+                                "tvgenres.js",
+                                "tvlatest.js",
+                                "tvpeople.js",
+                                "tvrecommended.js",
+                                "tvshows.js",
+                                "tvstudios.js",
+                                "tvupcoming.js",
+                                "useredit.js",
+                                "userpassword.js",
+                                "myprofile.js",
+                                "userprofilespage.js",
+                                "userparentalcontrol.js",
+                                "userlibraryaccess.js",
+                                "wizardfinishpage.js",
+                                "wizardservice.js",
+                                "wizardstartpage.js",
+                                "wizardsettings.js",
+                                "wizarduserpage.js"
+                            };
+        }
+
+        private async Task AppendLocalization(Stream stream, string culture)
+        {
+            var js = "window.localizationGlossary=" + _jsonSerializer.SerializeToString(_localization.GetJavaScriptLocalizationDictionary(culture));
+
+            var bytes = Encoding.UTF8.GetBytes(js);
+            await stream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Appends the resource.
+        /// </summary>
+        /// <param name="outputStream">The output stream.</param>
+        /// <param name="path">The path.</param>
+        /// <param name="newLineBytes">The new line bytes.</param>
+        /// <returns>Task.</returns>
+        private async Task AppendResource(Stream outputStream, string path, byte[] newLineBytes)
+        {
+            path = GetDashboardResourcePath(path);
+
+            using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+            {
+                using (var streamReader = new StreamReader(fs))
+                {
+                    var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
+                    var bytes = Encoding.UTF8.GetBytes(text);
+                    await outputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+                }
+            }
+
+            await outputStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
+        }
+
+
+        /// <summary>
+        /// Gets all CSS.
+        /// </summary>
+        /// <returns>Task{Stream}.</returns>
+        private async Task<Stream> GetAllCss()
+        {
+            var files = new[]
+                                  {
+                                      "site.css",
+                                      "chromecast.css",
+                                      "mediaplayer.css",
+                                      "mediaplayer-video.css",
+                                      "librarymenu.css",
+                                      "librarybrowser.css",
+                                      "detailtable.css",
+                                      "card.css",
+                                      "tileitem.css",
+                                      "metadataeditor.css",
+                                      "notifications.css",
+                                      "search.css",
+                                      "pluginupdates.css",
+                                      "remotecontrol.css",
+                                      "userimage.css",
+                                      "livetv.css",
+                                      "nowplaying.css",
+                                      "icons.css"
+                                  };
+
+            var builder = new StringBuilder();
+
+            foreach (var file in files)
+            {
+                var path = GetDashboardResourcePath("css/" + file);
+
+                using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
+                {
+                    using (var streamReader = new StreamReader(fs))
+                    {
+                        var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
+                        builder.Append(text);
+                        builder.Append(Environment.NewLine);
+                    }
+                }
+            }
+
+            var css = builder.ToString();
+
+            //try
+            //{
+            //    var result = new KristensenCssMinifier().Minify(builder.ToString(), false, Encoding.UTF8);
+
+            //    css = result.MinifiedContent;
+            //}
+            //catch (Exception ex)
+            //{
+            //    Logger.ErrorException("Error minifying css", ex);
+            //}
+
+            var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(css));
+
+            memoryStream.Position = 0;
+            return memoryStream;
+        }
+
+        /// <summary>
+        /// Gets the raw resource stream.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>Task{Stream}.</returns>
+        private Stream GetRawResourceStream(string path)
+        {
+            return _fileSystem.GetFileStream(GetDashboardResourcePath(path), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
+        }
+
+    }
+}

+ 1 - 0
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -67,6 +67,7 @@
     </Compile>
     </Compile>
     <Compile Include="Api\ConfigurationPageInfo.cs" />
     <Compile Include="Api\ConfigurationPageInfo.cs" />
     <Compile Include="Api\DashboardService.cs" />
     <Compile Include="Api\DashboardService.cs" />
+    <Compile Include="Api\PackageCreator.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="ServerEntryPoint.cs" />
     <Compile Include="ServerEntryPoint.cs" />
   </ItemGroup>
   </ItemGroup>

+ 1 - 1
MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs

@@ -66,7 +66,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
 
 
                         if (!string.IsNullOrWhiteSpace(val) && movie != null)
                         if (!string.IsNullOrWhiteSpace(val) && movie != null)
                         {
                         {
-                            movie.Artist = val;
+                            movie.Artists.Add(val);
                         }
                         }
 
 
                         break;
                         break;

+ 2 - 2
MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs

@@ -74,9 +74,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
 
             if (musicVideo != null)
             if (musicVideo != null)
             {
             {
-                if (!string.IsNullOrEmpty(musicVideo.Artist))
+                foreach (var artist in musicVideo.Artists)
                 {
                 {
-                    writer.WriteElementString("artist", musicVideo.Artist);
+                    writer.WriteElementString("artist", artist);
                 }
                 }
                 if (!string.IsNullOrEmpty(musicVideo.Album))
                 if (!string.IsNullOrEmpty(musicVideo.Album))
                 {
                 {

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.496</version>
+        <version>3.0.497</version>
         <title>MediaBrowser.Common.Internal</title>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.496" />
+            <dependency id="MediaBrowser.Common" version="3.0.497" />
             <dependency id="NLog" version="3.1.0.0" />
             <dependency id="NLog" version="3.1.0.0" />
             <dependency id="SimpleInjector" version="2.6.0" />
             <dependency id="SimpleInjector" version="2.6.0" />
             <dependency id="sharpcompress" version="0.10.2" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Common</id>
         <id>MediaBrowser.Common</id>
-        <version>3.0.496</version>
+        <version>3.0.497</version>
         <title>MediaBrowser.Common</title>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>

+ 1 - 1
Nuget/MediaBrowser.Model.Signed.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Model.Signed</id>
         <id>MediaBrowser.Model.Signed</id>
-        <version>3.0.496</version>
+        <version>3.0.497</version>
         <title>MediaBrowser.Model - Signed Edition</title>
         <title>MediaBrowser.Model - Signed Edition</title>
         <authors>Media Browser Team</authors>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Server.Core</id>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.496</version>
+        <version>3.0.497</version>
         <title>Media Browser.Server.Core</title>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.496" />
+            <dependency id="MediaBrowser.Common" version="3.0.497" />
         </dependencies>
         </dependencies>
     </metadata>
     </metadata>
     <files>
     <files>