aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--MediaBrowser.Api/ChannelService.cs2
-rw-r--r--MediaBrowser.Api/Images/ImageByNameService.cs90
-rw-r--r--MediaBrowser.Api/Images/ImageService.cs4
-rw-r--r--MediaBrowser.Api/ItemLookupService.cs27
-rw-r--r--MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs2
-rw-r--r--MediaBrowser.Api/UserLibrary/ItemsService.cs3
-rw-r--r--MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs13
-rw-r--r--MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs6
-rw-r--r--MediaBrowser.Common/Net/HttpRequestOptions.cs16
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj3
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleManager.cs50
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs (renamed from MediaBrowser.Controller/Providers/ISubtitleProvider.cs)21
-rw-r--r--MediaBrowser.Dlna/PlayTo/Device.cs4
-rw-r--r--MediaBrowser.Dlna/PlayTo/DlnaController.cs31
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleTrackInfo.cs (renamed from MediaBrowser.MediaEncoding/Subtitles/SubtitleInfo.cs)4
-rw-r--r--MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj3
-rw-r--r--MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj3
-rw-r--r--MediaBrowser.Model/ApiClient/IApiClient.cs2
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs17
-rw-r--r--MediaBrowser.Model/Dto/StreamOptions.cs2
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj1
-rw-r--r--MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs19
-rw-r--r--MediaBrowser.Model/Querying/ItemFilter.cs4
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj2
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs14
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs57
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs140
-rw-r--r--MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs138
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs141
-rw-r--r--MediaBrowser.Server.Implementations/Channels/ChannelManager.cs2
-rw-r--r--MediaBrowser.Server.Implementations/Collections/CollectionManager.cs8
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/ServerLogger.cs6
-rw-r--r--MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs44
-rw-r--r--MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs47
-rw-r--r--MediaBrowser.Server.Implementations/Localization/Server/server.json81
-rw-r--r--MediaBrowser.Server.Implementations/Localization/countries.json1
-rw-r--r--MediaBrowser.Server.Implementations/Localization/cultures.json1
-rw-r--r--MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj3
-rw-r--r--MediaBrowser.ServerApplication/ApplicationHost.cs11
-rw-r--r--MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs81
-rw-r--r--MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs135
-rw-r--r--MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs2
-rw-r--r--MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj3
-rw-r--r--OpenSubtitlesHandler/OpenSubtitles.cs52
-rw-r--r--OpenSubtitlesHandler/Utilities.cs20
49 files changed, 1032 insertions, 296 deletions
diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs
index a0795c14d..9fa4eec4a 100644
--- a/MediaBrowser.Api/ChannelService.cs
+++ b/MediaBrowser.Api/ChannelService.cs
@@ -67,7 +67,7 @@ namespace MediaBrowser.Api
[ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public SortOrder? SortOrder { get; set; }
- [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsRecentlyAdded, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+ [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string Filters { get; set; }
[ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
diff --git a/MediaBrowser.Api/Images/ImageByNameService.cs b/MediaBrowser.Api/Images/ImageByNameService.cs
index 44a69f6de..6dda2ae7a 100644
--- a/MediaBrowser.Api/Images/ImageByNameService.cs
+++ b/MediaBrowser.Api/Images/ImageByNameService.cs
@@ -1,8 +1,10 @@
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using ServiceStack;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -70,6 +72,32 @@ namespace MediaBrowser.Api.Images
public string Theme { get; set; }
}
+ [Route("/Images/MediaInfo", "GET")]
+ [Api(Description = "Gets all media info image by name")]
+ public class GetMediaInfoImages : IReturn<List<ImageByNameInfo>>
+ {
+ }
+
+ [Route("/Images/Ratings", "GET")]
+ [Api(Description = "Gets all rating images by name")]
+ public class GetRatingImages : IReturn<List<ImageByNameInfo>>
+ {
+ }
+
+ [Route("/Images/General", "GET")]
+ [Api(Description = "Gets all general images by name")]
+ public class GetGeneralImages : IReturn<List<ImageByNameInfo>>
+ {
+ }
+
+ public class ImageByNameInfo
+ {
+ public string Name { get; set; }
+ public string Theme { get; set; }
+ public long FileLength { get; set; }
+ public string Format { get; set; }
+ }
+
/// <summary>
/// Class ImageByNameService
/// </summary>
@@ -89,6 +117,60 @@ namespace MediaBrowser.Api.Images
_appPaths = appPaths;
}
+ public object Get(GetMediaInfoImages request)
+ {
+ return ToOptimizedResult(GetImageList(_appPaths.MediaInfoImagesPath));
+ }
+
+ public object Get(GetRatingImages request)
+ {
+ return ToOptimizedResult(GetImageList(_appPaths.RatingsPath));
+ }
+
+ public object Get(GetGeneralImages request)
+ {
+ return ToOptimizedResult(GetImageList(_appPaths.GeneralPath));
+ }
+
+ private List<ImageByNameInfo> GetImageList(string path)
+ {
+ try
+ {
+ return new DirectoryInfo(path)
+ .GetFiles("*", SearchOption.AllDirectories)
+ .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.Ordinal))
+ .Select(i => new ImageByNameInfo
+ {
+ Name = Path.GetFileNameWithoutExtension(i.FullName),
+ FileLength = i.Length,
+ Theme = GetThemeName(i.FullName, path),
+ Format = i.Extension.ToLower().TrimStart('.')
+ })
+ .OrderBy(i => i.Name)
+ .ToList();
+ }
+ catch (DirectoryNotFoundException)
+ {
+ return new List<ImageByNameInfo>();
+ }
+ }
+
+ private string GetThemeName(string path, string rootImagePath)
+ {
+ var parentName = Path.GetDirectoryName(path);
+
+ if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ parentName = Path.GetFileName(parentName);
+
+ return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ?
+ null :
+ parentName;
+ }
+
/// <summary>
/// Gets the specified request.
/// </summary>
@@ -118,7 +200,8 @@ namespace MediaBrowser.Api.Images
if (Directory.Exists(themeFolder))
{
- var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, request.Name + i))
+ var path = BaseItem.SupportedImageExtensions
+ .Select(i => Path.Combine(themeFolder, request.Name + i))
.FirstOrDefault(File.Exists);
if (!string.IsNullOrEmpty(path))
@@ -134,7 +217,8 @@ namespace MediaBrowser.Api.Images
// Avoid implicitly captured closure
var currentRequest = request;
- var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i))
+ var path = BaseItem.SupportedImageExtensions
+ .Select(i => Path.Combine(allFolder, currentRequest.Name + i))
.FirstOrDefault(File.Exists);
if (!string.IsNullOrEmpty(path))
@@ -175,7 +259,7 @@ namespace MediaBrowser.Api.Images
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i))
.FirstOrDefault(File.Exists);
-
+
if (!string.IsNullOrEmpty(path))
{
return ToStaticFileResult(path);
diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs
index a5bb291ae..ce3eaf053 100644
--- a/MediaBrowser.Api/Images/ImageService.cs
+++ b/MediaBrowser.Api/Images/ImageService.cs
@@ -1,5 +1,4 @@
-using System.Globalization;
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@@ -14,6 +13,7 @@ using ServiceStack.Text.Controller;
using ServiceStack.Web;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs
index b600c3b46..86fdd6da8 100644
--- a/MediaBrowser.Api/ItemLookupService.cs
+++ b/MediaBrowser.Api/ItemLookupService.cs
@@ -1,13 +1,13 @@
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using ServiceStack;
@@ -32,6 +32,16 @@ namespace MediaBrowser.Api
public string Id { get; set; }
}
+ [Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")]
+ public class SearchRemoteSubtitles : IReturn<List<RemoteSubtitleInfo>>
+ {
+ [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+ public string Id { get; set; }
+
+ [ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+ public string Language { get; set; }
+ }
+
[Route("/Items/RemoteSearch/Movie", "POST")]
[Api(Description = "Gets external id infos for an item")]
public class GetMovieRemoteSearchResults : RemoteSearchQuery<MovieInfo>, IReturn<List<RemoteSearchResult>>
@@ -107,19 +117,28 @@ namespace MediaBrowser.Api
public class ItemLookupService : BaseApiService
{
- private readonly IDtoService _dtoService;
private readonly IProviderManager _providerManager;
private readonly IServerApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly ILibraryManager _libraryManager;
+ private readonly ISubtitleManager _subtitleManager;
- public ItemLookupService(IDtoService dtoService, IProviderManager providerManager, IServerApplicationPaths appPaths, IFileSystem fileSystem, ILibraryManager libraryManager)
+ public ItemLookupService(IProviderManager providerManager, IServerApplicationPaths appPaths, IFileSystem fileSystem, ILibraryManager libraryManager, ISubtitleManager subtitleManager)
{
- _dtoService = dtoService;
_providerManager = providerManager;
_appPaths = appPaths;
_fileSystem = fileSystem;
_libraryManager = libraryManager;
+ _subtitleManager = subtitleManager;
+ }
+
+ public object Get(SearchRemoteSubtitles request)
+ {
+ var video = (Video)_libraryManager.GetItemById(request.Id);
+
+ var response = _subtitleManager.SearchSubtitles(video, request.Language, CancellationToken.None).Result;
+
+ return ToOptimizedResult(response);
}
public object Get(GetExternalIdInfos request)
diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
index f1fe904f3..cc76ee95f 100644
--- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
+++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
@@ -69,7 +69,7 @@ namespace MediaBrowser.Api.UserLibrary
/// Filters to apply to the results
/// </summary>
/// <value>The filters.</value>
- [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsRecentlyAdded, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+ [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string Filters { get; set; }
/// <summary>
diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs
index 1cd819197..c7f36e6ac 100644
--- a/MediaBrowser.Api/UserLibrary/ItemsService.cs
+++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs
@@ -521,6 +521,9 @@ namespace MediaBrowser.Api.UserLibrary
case ItemFilter.IsNotFolder:
return items.Where(item => !item.IsFolder);
+
+ case ItemFilter.IsRecentlyAdded:
+ return items.Where(item => (DateTime.UtcNow - item.DateCreated).TotalDays <= 10);
}
return items;
diff --git a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
index a88f457c5..0a9f0ff8a 100644
--- a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
+++ b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
@@ -114,9 +114,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
request.AutomaticDecompression = enableHttpCompression ? DecompressionMethods.Deflate : DecompressionMethods.None;
- request.CachePolicy = options.CachePolicy == Net.HttpRequestCachePolicy.None ?
- new RequestCachePolicy(RequestCacheLevel.BypassCache) :
- new RequestCachePolicy(RequestCacheLevel.Revalidate);
+ request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
request.ConnectionGroupName = GetHostFromUrl(options.Url);
request.KeepAlive = true;
@@ -124,6 +122,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
request.Pipelined = true;
request.Timeout = 20000;
+ if (!string.IsNullOrEmpty(options.Host))
+ {
+ request.Host = options.Host;
+ }
+
#if !__MonoCS__
// This is a hack to prevent KeepAlive from getting disabled internally by the HttpWebRequest
// May need to remove this for mono
@@ -234,9 +237,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
!string.IsNullOrEmpty(options.RequestContent) ||
string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase))
{
- var bytes = options.RequestContentBytes ?? Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty);
+ var bytes = options.RequestContentBytes ??
+ Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty);
httpWebRequest.ContentType = options.RequestContentType ?? "application/x-www-form-urlencoded";
+
httpWebRequest.ContentLength = bytes.Length;
httpWebRequest.GetRequestStream().Write(bytes, 0, bytes.Length);
}
diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index a5b8de554..dbeedfed5 100644
--- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
-
+
/// <summary>
/// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
/// </summary>
@@ -74,9 +74,11 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
progress.Report(90);
+ minDateModified = DateTime.UtcNow.AddDays(-3);
+
try
{
- DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, DateTime.MaxValue, progress);
+ DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
}
catch (DirectoryNotFoundException)
{
diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs
index 11152b30f..192264eed 100644
--- a/MediaBrowser.Common/Net/HttpRequestOptions.cs
+++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs
@@ -53,6 +53,12 @@ namespace MediaBrowser.Common.Net
}
/// <summary>
+ /// Gets or sets the host.
+ /// </summary>
+ /// <value>The host.</value>
+ public string Host { get; set; }
+
+ /// <summary>
/// Gets or sets the progress.
/// </summary>
/// <value>The progress.</value>
@@ -76,8 +82,6 @@ namespace MediaBrowser.Common.Net
public bool LogRequest { get; set; }
public bool LogErrorResponseBody { get; set; }
-
- public HttpRequestCachePolicy CachePolicy { get; set; }
private string GetHeaderValue(string name)
{
@@ -96,17 +100,9 @@ namespace MediaBrowser.Common.Net
EnableHttpCompression = true;
BufferContent = true;
- CachePolicy = HttpRequestCachePolicy.None;
-
RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
LogRequest = true;
}
}
-
- public enum HttpRequestCachePolicy
- {
- None = 1,
- Validate = 2
- }
}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 5e259359f..6a3709dda 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -191,7 +191,8 @@
<Compile Include="Providers\IMetadataProvider.cs" />
<Compile Include="Providers\IMetadataService.cs" />
<Compile Include="Providers\IRemoteMetadataProvider.cs" />
- <Compile Include="Providers\ISubtitleProvider.cs" />
+ <Compile Include="Subtitles\ISubtitleManager.cs" />
+ <Compile Include="Subtitles\ISubtitleProvider.cs" />
<Compile Include="Providers\ItemLookupInfo.cs" />
<Compile Include="Providers\MetadataRefreshOptions.cs" />
<Compile Include="Providers\NameParser.cs" />
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
new file mode 100644
index 000000000..8b0ef223c
--- /dev/null
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
@@ -0,0 +1,50 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Providers;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Subtitles
+{
+ public interface ISubtitleManager
+ {
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="subtitleProviders">The subtitle providers.</param>
+ void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders);
+
+ /// <summary>
+ /// Searches the subtitles.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="language">The language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns>
+ Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(Video video,
+ string language,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Searches the subtitles.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns>
+ Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Downloads the subtitles.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="subtitleId">The subtitle identifier.</param>
+ /// <param name="providerName">Name of the provider.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task DownloadSubtitles(Video video,
+ string subtitleId,
+ string providerName,
+ CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ISubtitleProvider.cs b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
index 09ca27e30..1409b7d50 100644
--- a/MediaBrowser.Controller/Providers/ISubtitleProvider.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
@@ -1,11 +1,12 @@
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
-namespace MediaBrowser.Controller.Providers
+namespace MediaBrowser.Controller.Subtitles
{
public interface ISubtitleProvider
{
@@ -22,12 +23,20 @@ namespace MediaBrowser.Controller.Providers
IEnumerable<SubtitleMediaType> SupportedMediaTypes { get; }
/// <summary>
- /// Gets the subtitles.
+ /// Searches the subtitles.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns>
+ Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the subtitles.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{SubtitleResponse}.</returns>
- Task<SubtitleResponse> GetSubtitles(SubtitleRequest request, CancellationToken cancellationToken);
+ Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken);
}
public enum SubtitleMediaType
@@ -38,12 +47,12 @@ namespace MediaBrowser.Controller.Providers
public class SubtitleResponse
{
+ public string Language { get; set; }
public string Format { get; set; }
- public bool HasContent { get; set; }
public Stream Stream { get; set; }
}
- public class SubtitleRequest : IHasProviderIds
+ public class SubtitleSearchRequest : IHasProviderIds
{
public string Language { get; set; }
@@ -58,7 +67,7 @@ namespace MediaBrowser.Controller.Providers
public int? ProductionYear { get; set; }
public Dictionary<string, string> ProviderIds { get; set; }
- public SubtitleRequest()
+ public SubtitleSearchRequest()
{
ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
diff --git a/MediaBrowser.Dlna/PlayTo/Device.cs b/MediaBrowser.Dlna/PlayTo/Device.cs
index 1c7ed13b6..3e5e877cd 100644
--- a/MediaBrowser.Dlna/PlayTo/Device.cs
+++ b/MediaBrowser.Dlna/PlayTo/Device.cs
@@ -77,6 +77,8 @@ namespace MediaBrowser.Dlna.PlayTo
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
+ public DateTime DateLastActivity { get; private set; }
+
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
{
Properties = deviceProperties;
@@ -386,6 +388,8 @@ namespace MediaBrowser.Dlna.PlayTo
{
var transportState = await GetTransportInfo().ConfigureAwait(false);
+ DateLastActivity = DateTime.UtcNow;
+
if (transportState.HasValue)
{
// If we're not playing anything no need to get additional data
diff --git a/MediaBrowser.Dlna/PlayTo/DlnaController.cs b/MediaBrowser.Dlna/PlayTo/DlnaController.cs
index fb5e0bf34..673a7c245 100644
--- a/MediaBrowser.Dlna/PlayTo/DlnaController.cs
+++ b/MediaBrowser.Dlna/PlayTo/DlnaController.cs
@@ -51,6 +51,8 @@ namespace MediaBrowser.Dlna.PlayTo
}
}
+ private Timer _updateTimer;
+
public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, IDlnaManager dlnaManager, IUserManager userManager, IDtoService dtoService, IImageProcessor imageProcessor, SsdpHandler ssdpHandler, string serverAddress)
{
_session = session;
@@ -75,6 +77,24 @@ namespace MediaBrowser.Dlna.PlayTo
_device.Start();
_ssdpHandler.MessageReceived += _SsdpHandler_MessageReceived;
+
+ _updateTimer = new Timer(updateTimer_Elapsed, null, 60000, 60000);
+ }
+
+ private async void updateTimer_Elapsed(object state)
+ {
+ if (DateTime.UtcNow >= _device.DateLastActivity.AddSeconds(60))
+ {
+ try
+ {
+ // Session is inactive, mark it for Disposal and don't start the elapsed timer.
+ await _sessionManager.ReportSessionEnded(_session.Id).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in ReportSessionEnded", ex);
+ }
+ }
}
private string GetServerAddress()
@@ -571,10 +591,21 @@ namespace MediaBrowser.Dlna.PlayTo
_device.PlaybackStopped -= _device_PlaybackStopped;
_ssdpHandler.MessageReceived -= _SsdpHandler_MessageReceived;
+ DisposeUpdateTimer();
+
_device.Dispose();
}
}
+ private void DisposeUpdateTimer()
+ {
+ if (_updateTimer != null)
+ {
+ _updateTimer.Dispose();
+ _updateTimer = null;
+ }
+ }
+
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 291bb0222..19287b0cb 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -64,7 +64,7 @@
<Compile Include="Subtitles\ISubtitleParser.cs" />
<Compile Include="Subtitles\SrtParser.cs" />
<Compile Include="Subtitles\SsaParser.cs" />
- <Compile Include="Subtitles\SubtitleInfo.cs" />
+ <Compile Include="Subtitles\SubtitleTrackInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
index 5e7ad6699..b983bc5d4 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
@@ -4,6 +4,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
public interface ISubtitleParser
{
- SubtitleInfo Parse(Stream stream);
+ SubtitleTrackInfo Parse(Stream stream);
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
index af0009a82..410c0bbdd 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
@@ -7,9 +7,9 @@ using System.Threading.Tasks;
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public class SrtParser
+ public class SrtParser : ISubtitleParser
{
- public SubtitleInfo Parse(Stream stream)
+ public SubtitleTrackInfo Parse(Stream stream)
{
throw new NotImplementedException();
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
index e134416b1..ca7e58371 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
@@ -7,9 +7,9 @@ using System.Threading.Tasks;
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public class SsaParser
+ public class SsaParser : ISubtitleParser
{
- public SubtitleInfo Parse(Stream stream)
+ public SubtitleTrackInfo Parse(Stream stream)
{
throw new NotImplementedException();
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleInfo.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleTrackInfo.cs
index 812b0c7d4..67d70ed6e 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleInfo.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleTrackInfo.cs
@@ -2,11 +2,11 @@
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public class SubtitleInfo
+ public class SubtitleTrackInfo
{
public List<SubtitleTrackEvent> TrackEvents { get; set; }
- public SubtitleInfo()
+ public SubtitleTrackInfo()
{
TrackEvents = new List<SubtitleTrackEvent>();
}
diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
index e8a802725..991fe3b2a 100644
--- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
+++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
@@ -416,6 +416,9 @@
<Compile Include="..\MediaBrowser.Model\Providers\RemoteSearchResult.cs">
<Link>Providers\RemoteSearchResult.cs</Link>
</Compile>
+ <Compile Include="..\MediaBrowser.Model\Providers\RemoteSubtitleInfo.cs">
+ <Link>Providers\RemoteSubtitleInfo.cs</Link>
+ </Compile>
<Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
<Link>Querying\ArtistsQuery.cs</Link>
</Compile>
diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
index 5fb5fae74..771e739bc 100644
--- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
+++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
@@ -403,6 +403,9 @@
<Compile Include="..\MediaBrowser.Model\Providers\RemoteSearchResult.cs">
<Link>Providers\RemoteSearchResult.cs</Link>
</Compile>
+ <Compile Include="..\MediaBrowser.Model\Providers\RemoteSubtitleInfo.cs">
+ <Link>Providers\RemoteSubtitleInfo.cs</Link>
+ </Compile>
<Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
<Link>Querying\ArtistsQuery.cs</Link>
</Compile>
diff --git a/MediaBrowser.Model/ApiClient/IApiClient.cs b/MediaBrowser.Model/ApiClient/IApiClient.cs
index dd1603d01..c9f5f3ae7 100644
--- a/MediaBrowser.Model/ApiClient/IApiClient.cs
+++ b/MediaBrowser.Model/ApiClient/IApiClient.cs
@@ -760,7 +760,7 @@ namespace MediaBrowser.Model.ApiClient
/// </summary>
/// <param name="options">The options.</param>
/// <returns>System.String.</returns>
- string GetSubtitleUrl(SubtitleOptions options);
+ string GetSubtitleUrl(SubtitleDownloadOptions options);
/// <summary>
/// Gets an image url that can be used to download an image from the api
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 0fb9db6c0..486268e2b 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -221,6 +221,8 @@ namespace MediaBrowser.Model.Configuration
public NotificationOptions NotificationOptions { get; set; }
+ public SubtitleOptions SubtitleOptions { get; set; }
+
/// <summary>
/// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
/// </summary>
@@ -284,6 +286,8 @@ namespace MediaBrowser.Model.Configuration
UICulture = "en-us";
NotificationOptions = new NotificationOptions();
+
+ SubtitleOptions = new SubtitleOptions();
}
}
@@ -311,4 +315,17 @@ namespace MediaBrowser.Model.Configuration
public string From { get; set; }
public string To { get; set; }
}
+
+ public class SubtitleOptions
+ {
+ public bool RequireExternalSubtitles { get; set; }
+ public string[] SubtitleDownloadLanguages { get; set; }
+ public bool DownloadMovieSubtitles { get; set; }
+ public bool DownloadEpisodeSubtitles { get; set; }
+
+ public SubtitleOptions()
+ {
+ SubtitleDownloadLanguages = new string[] { };
+ }
+ }
}
diff --git a/MediaBrowser.Model/Dto/StreamOptions.cs b/MediaBrowser.Model/Dto/StreamOptions.cs
index b1ead2ca3..861fa4e01 100644
--- a/MediaBrowser.Model/Dto/StreamOptions.cs
+++ b/MediaBrowser.Model/Dto/StreamOptions.cs
@@ -159,7 +159,7 @@
public string DeviceId { get; set; }
}
- public class SubtitleOptions
+ public class SubtitleDownloadOptions
{
/// <summary>
/// Gets or sets the item identifier.
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 877eb5444..aaa29d21c 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -139,6 +139,7 @@
<Compile Include="Notifications\NotificationsSummary.cs" />
<Compile Include="Providers\RemoteImageResult.cs" />
<Compile Include="Providers\RemoteSearchResult.cs" />
+ <Compile Include="Providers\RemoteSubtitleInfo.cs" />
<Compile Include="Querying\ArtistsQuery.cs" />
<Compile Include="Querying\EpisodeQuery.cs" />
<Compile Include="Querying\ItemCountsQuery.cs" />
diff --git a/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs b/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs
new file mode 100644
index 000000000..dab9a57a8
--- /dev/null
+++ b/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace MediaBrowser.Model.Providers
+{
+ public class RemoteSubtitleInfo
+ {
+ public string Language { get; set; }
+ public string Id { get; set; }
+ public string ProviderName { get; set; }
+ public string Name { get; set; }
+ public string Format { get; set; }
+ public string Author { get; set; }
+ public string Comment { get; set; }
+ public DateTime? DateCreated { get; set; }
+ public float? CommunityRating { get; set; }
+ public int? DownloadCount { get; set; }
+ public bool? IsHashMatch { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/ItemFilter.cs b/MediaBrowser.Model/Querying/ItemFilter.cs
index 2e88a98c9..d30978ebf 100644
--- a/MediaBrowser.Model/Querying/ItemFilter.cs
+++ b/MediaBrowser.Model/Querying/ItemFilter.cs
@@ -27,6 +27,10 @@ namespace MediaBrowser.Model.Querying
/// </summary>
IsFavorite = 5,
/// <summary>
+ /// The is recently added
+ /// </summary>
+ IsRecentlyAdded = 6,
+ /// <summary>
/// The item is resumable
/// </summary>
IsResumable = 7,
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index b19966718..43402123c 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -108,6 +108,7 @@
<Compile Include="MediaInfo\FFProbeHelpers.cs" />
<Compile Include="MediaInfo\FFProbeProvider.cs" />
<Compile Include="MediaInfo\FFProbeVideoInfo.cs" />
+ <Compile Include="MediaInfo\SubtitleDownloader.cs" />
<Compile Include="Movies\MovieDbTrailerProvider.cs" />
<Compile Include="Movies\MovieExternalIds.cs" />
<Compile Include="Movies\TrailerMetadataService.cs" />
@@ -187,6 +188,7 @@
<Compile Include="Studios\StudiosImageProvider.cs" />
<Compile Include="Studios\StudioMetadataService.cs" />
<Compile Include="Subtitles\OpenSubtitleDownloader.cs" />
+ <Compile Include="Subtitles\SubtitleManager.cs" />
<Compile Include="TV\EpisodeLocalImageProvider.cs" />
<Compile Include="TV\EpisodeMetadataService.cs" />
<Compile Include="TV\EpisodeXmlProvider.cs" />
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
index 7a71a7551..3c03e11b3 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
@@ -1,5 +1,6 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
@@ -10,15 +11,16 @@ using MediaBrowser.Controller.Localization;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using System.Linq;
namespace MediaBrowser.Providers.MediaInfo
{
@@ -45,6 +47,8 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly IJsonSerializer _json;
private readonly IEncodingManager _encodingManager;
private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _config;
+ private readonly ISubtitleManager _subtitleManager;
public string Name
{
@@ -96,7 +100,7 @@ namespace MediaBrowser.Providers.MediaInfo
return FetchAudioInfo(item, cancellationToken);
}
- public FFProbeProvider(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem)
+ public FFProbeProvider(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager)
{
_logger = logger;
_isoManager = isoManager;
@@ -108,6 +112,8 @@ namespace MediaBrowser.Providers.MediaInfo
_json = json;
_encodingManager = encodingManager;
_fileSystem = fileSystem;
+ _config = config;
+ _subtitleManager = subtitleManager;
}
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
@@ -134,7 +140,7 @@ namespace MediaBrowser.Providers.MediaInfo
return _cachedTask;
}
- var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem);
+ var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager);
return prober.ProbeVideo(item, directoryService, cancellationToken);
}
@@ -165,7 +171,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (video != null && !video.IsPlaceHolder)
{
- var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem);
+ var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager);
return !video.SubtitleFiles.SequenceEqual(prober.GetSubtitleFiles(video, directoryService).Select(i => i.FullName).OrderBy(i => i), StringComparer.OrdinalIgnoreCase);
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index a7d4a480e..a2897ef9c 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -2,12 +2,16 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Localization;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
@@ -35,10 +39,12 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly IJsonSerializer _json;
private readonly IEncodingManager _encodingManager;
private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _config;
+ private readonly ISubtitleManager _subtitleManager;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
- public FFProbeVideoInfo(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem)
+ public FFProbeVideoInfo(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager)
{
_logger = logger;
_isoManager = isoManager;
@@ -50,6 +56,8 @@ namespace MediaBrowser.Providers.MediaInfo
_json = json;
_encodingManager = encodingManager;
_fileSystem = fileSystem;
+ _config = config;
+ _subtitleManager = subtitleManager;
}
public async Task<ItemUpdateType> ProbeVideo<T>(T item, IDirectoryService directoryService, CancellationToken cancellationToken)
@@ -118,7 +126,7 @@ namespace MediaBrowser.Providers.MediaInfo
cancellationToken.ThrowIfCancellationRequested();
var idString = item.Id.ToString("N");
- var cachePath = Path.Combine(_appPaths.CachePath,
+ var cachePath = Path.Combine(_appPaths.CachePath,
"ffprobe-video",
idString.Substring(0, 2), idString, "v" + SchemaVersion + _mediaEncoder.Version + item.DateModified.Ticks.ToString(_usCulture) + ".json");
@@ -200,7 +208,7 @@ namespace MediaBrowser.Providers.MediaInfo
FetchBdInfo(video, chapters, mediaStreams, blurayInfo);
}
- AddExternalSubtitles(video, mediaStreams, directoryService);
+ await AddExternalSubtitles(video, mediaStreams, directoryService, cancellationToken).ConfigureAwait(false);
FetchWtvInfo(video, data);
@@ -247,7 +255,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- info.StartPositionTicks = chapter.start/100;
+ info.StartPositionTicks = chapter.start / 100;
return info;
}
@@ -450,11 +458,42 @@ namespace MediaBrowser.Providers.MediaInfo
/// </summary>
/// <param name="video">The video.</param>
/// <param name="currentStreams">The current streams.</param>
- private void AddExternalSubtitles(Video video, List<MediaStream> currentStreams, IDirectoryService directoryService)
+ private async Task AddExternalSubtitles(Video video, List<MediaStream> currentStreams, IDirectoryService directoryService, CancellationToken cancellationToken)
+ {
+ var externalSubtitleStreams = GetExternalSubtitleStreams(video, currentStreams.Count, directoryService).ToList();
+
+ if ((_config.Configuration.SubtitleOptions.DownloadEpisodeSubtitles &&
+ video is Episode) ||
+ (_config.Configuration.SubtitleOptions.DownloadMovieSubtitles &&
+ video is Movie))
+ {
+ var downloadedLanguages = await new SubtitleDownloader(_logger,
+ _subtitleManager)
+ .DownloadSubtitles(video,
+ currentStreams,
+ externalSubtitleStreams,
+ _config.Configuration.SubtitleOptions.RequireExternalSubtitles,
+ _config.Configuration.SubtitleOptions.SubtitleDownloadLanguages,
+ cancellationToken).ConfigureAwait(false);
+
+ // Rescan
+ if (downloadedLanguages.Count > 0)
+ {
+ externalSubtitleStreams = GetExternalSubtitleStreams(video, currentStreams.Count, directoryService).ToList();
+ }
+ }
+
+ video.SubtitleFiles = externalSubtitleStreams.Select(i => i.Path).OrderBy(i => i).ToList();
+
+ currentStreams.AddRange(externalSubtitleStreams);
+ }
+
+ private IEnumerable<MediaStream> GetExternalSubtitleStreams(Video video,
+ int startIndex,
+ IDirectoryService directoryService)
{
var files = GetSubtitleFiles(video, directoryService);
- var startIndex = currentStreams.Count;
var streams = new List<MediaStream>();
var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path);
@@ -504,9 +543,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- video.SubtitleFiles = streams.Select(i => i.Path).OrderBy(i => i).ToList();
-
- currentStreams.AddRange(streams);
+ return streams;
}
/// <summary>
@@ -627,7 +664,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
var path = mount == null ? item.Path : mount.MountedPath;
var dvd = new Dvd(path);
-
+
var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault();
byte? titleNumber = null;
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
new file mode 100644
index 000000000..7f7ccda19
--- /dev/null
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
@@ -0,0 +1,140 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+ public class SubtitleDownloader
+ {
+ private readonly ILogger _logger;
+ private readonly ISubtitleManager _subtitleManager;
+
+ public SubtitleDownloader(ILogger logger, ISubtitleManager subtitleManager)
+ {
+ _logger = logger;
+ _subtitleManager = subtitleManager;
+ }
+
+ public async Task<List<string>> DownloadSubtitles(Video video,
+ List<MediaStream> internalSubtitleStreams,
+ List<MediaStream> externalSubtitleStreams,
+ bool forceExternal,
+ IEnumerable<string> languages,
+ CancellationToken cancellationToken)
+ {
+ if (video.LocationType != LocationType.FileSystem ||
+ video.VideoType != VideoType.VideoFile)
+ {
+ return new List<string>();
+ }
+
+ SubtitleMediaType mediaType;
+
+ if (video is Episode)
+ {
+ mediaType = SubtitleMediaType.Episode;
+ }
+ else if (video is Movie)
+ {
+ mediaType = SubtitleMediaType.Movie;
+ }
+ else
+ {
+ // These are the only supported types
+ return new List<string>();
+ }
+
+ var downloadedLanguages = new List<string>();
+
+ foreach (var lang in languages)
+ {
+ try
+ {
+ var downloaded = await DownloadSubtitles(video, internalSubtitleStreams, externalSubtitleStreams, forceExternal, lang, mediaType, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (downloaded)
+ {
+ downloadedLanguages.Add(lang);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error downloading subtitles", ex);
+ }
+ }
+
+ return downloadedLanguages;
+ }
+
+ private async Task<bool> DownloadSubtitles(Video video,
+ IEnumerable<MediaStream> internalSubtitleStreams,
+ IEnumerable<MediaStream> externalSubtitleStreams,
+ bool forceExternal,
+ string language,
+ SubtitleMediaType mediaType,
+ CancellationToken cancellationToken)
+ {
+ // There's already subtitles for this language
+ if (externalSubtitleStreams.Any(i => string.Equals(i.Language, language, StringComparison.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+
+ // There's an internal subtitle stream for this language
+ if (!forceExternal && internalSubtitleStreams.Any(i => string.Equals(i.Language, language, StringComparison.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+
+ var request = new SubtitleSearchRequest
+ {
+ ContentType = mediaType,
+ IndexNumber = video.IndexNumber,
+ Language = language,
+ MediaPath = video.Path,
+ Name = video.Name,
+ ParentIndexNumber = video.ParentIndexNumber,
+ ProductionYear = video.ProductionYear,
+ ProviderIds = video.ProviderIds
+ };
+
+ var episode = video as Episode;
+
+ if (episode != null)
+ {
+ request.IndexNumberEnd = episode.IndexNumberEnd;
+ request.SeriesName = episode.SeriesName;
+ }
+
+ try
+ {
+ var searchResults = await _subtitleManager.SearchSubtitles(request, cancellationToken).ConfigureAwait(false);
+
+ var result = searchResults.FirstOrDefault();
+
+ if (result != null)
+ {
+ await _subtitleManager.DownloadSubtitles(video, result.Id, result.ProviderName, cancellationToken)
+ .ConfigureAwait(false);
+
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error downloading subtitles", ex);
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs b/MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs
index 7309513d6..929cccd5a 100644
--- a/MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs
+++ b/MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs
@@ -1,8 +1,10 @@
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Providers;
using OpenSubtitlesHandler;
using System;
using System.Collections.Generic;
@@ -20,9 +22,9 @@ namespace MediaBrowser.Providers.Subtitles
private readonly IHttpClient _httpClient;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
- public OpenSubtitleDownloader(ILogger logger, IHttpClient httpClient)
+ public OpenSubtitleDownloader(ILogManager logManager, IHttpClient httpClient)
{
- _logger = logger;
+ _logger = logManager.GetLogger(GetType().Name);
_httpClient = httpClient;
}
@@ -36,39 +38,71 @@ namespace MediaBrowser.Providers.Subtitles
get { return new[] { SubtitleMediaType.Episode, SubtitleMediaType.Movie }; }
}
- public Task<SubtitleResponse> GetSubtitles(SubtitleRequest request, CancellationToken cancellationToken)
+ public Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
{
- return GetSubtitlesInternal(request, cancellationToken);
+ return GetSubtitlesInternal(id, cancellationToken);
}
- private async Task<SubtitleResponse> GetSubtitlesInternal(SubtitleRequest request,
+ private async Task<SubtitleResponse> GetSubtitlesInternal(string id,
CancellationToken cancellationToken)
{
- var response = new SubtitleResponse();
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ var idParts = id.Split(new[] { '-' }, 3);
+
+ var format = idParts[0];
+ var language = idParts[1];
+ var ossId = idParts[2];
+
+ var downloadsList = new[] { int.Parse(ossId, _usCulture) };
+
+ var resultDownLoad = OpenSubtitles.DownloadSubtitles(downloadsList);
+ if (!(resultDownLoad is MethodResponseSubtitleDownload))
+ {
+ throw new ApplicationException("Invalid response type");
+ }
+ var res = ((MethodResponseSubtitleDownload)resultDownLoad).Results.First();
+ var data = Convert.FromBase64String(res.Data);
+
+ return new SubtitleResponse
+ {
+ Format = format,
+ Language = language,
+
+ Stream = new MemoryStream(Utilities.Decompress(new MemoryStream(data)))
+ };
+ }
+
+ public async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken)
+ {
var imdbIdText = request.GetProviderId(MetadataProviders.Imdb);
long imdbId;
if (string.IsNullOrWhiteSpace(imdbIdText) ||
- long.TryParse(imdbIdText.TrimStart('t'), NumberStyles.Any, _usCulture, out imdbId))
+ !long.TryParse(imdbIdText.TrimStart('t'), NumberStyles.Any, _usCulture, out imdbId))
{
- return response;
+ _logger.Debug("Imdb id missing");
+ return new List<RemoteSubtitleInfo>();
}
-
+
switch (request.ContentType)
{
case SubtitleMediaType.Episode:
if (!request.IndexNumber.HasValue || !request.ParentIndexNumber.HasValue || string.IsNullOrEmpty(request.SeriesName))
{
- _logger.Debug("Information Missing");
- return response;
+ _logger.Debug("Episode information missing");
+ return new List<RemoteSubtitleInfo>();
}
break;
case SubtitleMediaType.Movie:
if (string.IsNullOrEmpty(request.Name))
{
- _logger.Debug("Information Missing");
- return response;
+ _logger.Debug("Movie name missing");
+ return new List<RemoteSubtitleInfo>();
}
break;
}
@@ -76,16 +110,18 @@ namespace MediaBrowser.Providers.Subtitles
if (string.IsNullOrEmpty(request.MediaPath))
{
_logger.Debug("Path Missing");
- return response;
+ return new List<RemoteSubtitleInfo>();
}
Utilities.HttpClient = _httpClient;
OpenSubtitles.SetUserAgent("OS Test User Agent");
- var loginResponse = OpenSubtitles.LogIn("", "", "en");
+
+ var loginResponse = await OpenSubtitles.LogInAsync("", "", "en", cancellationToken).ConfigureAwait(false);
+
if (!(loginResponse is MethodResponseLogIn))
{
_logger.Debug("Login error");
- return response;
+ return new List<RemoteSubtitleInfo>();
}
var subLanguageId = request.Language;
@@ -105,54 +141,42 @@ namespace MediaBrowser.Providers.Subtitles
var result = OpenSubtitles.SearchSubtitles(parms.ToArray());
if (!(result is MethodResponseSubtitleSearch))
{
- _logger.Debug("invalid response type");
- return null;
+ _logger.Debug("Invalid response type");
+ return new List<RemoteSubtitleInfo>();
}
Predicate<SubtitleSearchResult> mediaFilter =
x =>
request.ContentType == SubtitleMediaType.Episode
- ? int.Parse(x.SeriesSeason) == request.ParentIndexNumber && int.Parse(x.SeriesEpisode) == request.IndexNumber
- : long.Parse(x.IDMovieImdb) == imdbId;
+ ? int.Parse(x.SeriesSeason, _usCulture) == request.ParentIndexNumber && int.Parse(x.SeriesEpisode, _usCulture) == request.IndexNumber
+ : long.Parse(x.IDMovieImdb, _usCulture) == imdbId;
var results = ((MethodResponseSubtitleSearch)result).Results;
- var bestResult = results.Where(x => x.SubBad == "0" && mediaFilter(x))
- .OrderBy(x => x.MovieHash == hash)
- .ThenBy(x => Math.Abs(long.Parse(x.MovieByteSize) - movieByteSize))
- .ThenByDescending(x => int.Parse(x.SubDownloadsCnt))
- .ThenByDescending(x => double.Parse(x.SubRating))
- .ToList();
-
- if (!bestResult.Any())
- {
- _logger.Debug("No Subtitles");
- return response;
- }
-
- _logger.Debug("Found " + bestResult.Count + " subtitles.");
- var subtitle = bestResult.First();
- var downloadsList = new[] { int.Parse(subtitle.IDSubtitleFile) };
+ // Avoid implicitly captured closure
+ var hasCopy = hash;
- var resultDownLoad = OpenSubtitles.DownloadSubtitles(downloadsList);
- if (!(resultDownLoad is MethodResponseSubtitleDownload))
- {
- _logger.Debug("invalid response type");
- return response;
- }
- if (!((MethodResponseSubtitleDownload)resultDownLoad).Results.Any())
- {
- _logger.Debug("No Subtitle Downloads");
- return response;
- }
-
- var res = ((MethodResponseSubtitleDownload)resultDownLoad).Results.First();
- var data = Convert.FromBase64String(res.Data);
-
- response.HasContent = true;
- response.Format = subtitle.SubFormat.ToUpper();
- response.Stream = new MemoryStream(Utilities.Decompress(new MemoryStream(data)));
- return response;
+ return results.Where(x => x.SubBad == "0" && mediaFilter(x))
+ .OrderBy(x => x.MovieHash == hash)
+ .ThenBy(x => Math.Abs(long.Parse(x.MovieByteSize, _usCulture) - movieByteSize))
+ .ThenByDescending(x => int.Parse(x.SubDownloadsCnt, _usCulture))
+ .ThenByDescending(x => double.Parse(x.SubRating, _usCulture))
+ .Select(i => new RemoteSubtitleInfo
+ {
+ Author = i.UserNickName,
+ Comment = i.SubAuthorComment,
+ CommunityRating = float.Parse(i.SubRating, _usCulture),
+ DownloadCount = int.Parse(i.SubDownloadsCnt, _usCulture),
+ Format = i.SubFormat,
+ ProviderName = Name,
+ Language = i.SubLanguageID,
+
+ Id = i.SubFormat + "-" + i.SubLanguageID + "-" + i.IDSubtitle,
+
+ Name = i.SubFileName,
+ DateCreated = DateTime.Parse(i.SubAddDate, _usCulture),
+ IsHashMatch = i.MovieHash == hasCopy
+ });
}
}
}
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
new file mode 100644
index 000000000..6951e8bd0
--- /dev/null
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -0,0 +1,141 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Providers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.Subtitles
+{
+ public class SubtitleManager : ISubtitleManager
+ {
+ private ISubtitleProvider[] _subtitleProviders;
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryMonitor _monitor;
+
+ public SubtitleManager(ILogger logger, IFileSystem fileSystem, ILibraryMonitor monitor)
+ {
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _monitor = monitor;
+ }
+
+ public void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders)
+ {
+ _subtitleProviders = subtitleProviders.ToArray();
+ }
+
+ public async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken)
+ {
+ var providers = _subtitleProviders
+ .Where(i => i.SupportedMediaTypes.Contains(request.ContentType))
+ .ToList();
+
+ var tasks = providers.Select(async i =>
+ {
+ try
+ {
+ return await i.SearchSubtitles(request, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error downloading subtitles from {0}", ex, i.Name);
+ return new List<RemoteSubtitleInfo>();
+ }
+ });
+
+ var results = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ return results.SelectMany(i => i);
+ }
+
+ public async Task DownloadSubtitles(Video video,
+ string subtitleId,
+ string providerName,
+ CancellationToken cancellationToken)
+ {
+ var provider = _subtitleProviders.First(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
+
+ var response = await provider.GetSubtitles(subtitleId, cancellationToken).ConfigureAwait(false);
+
+ using (var stream = response.Stream)
+ {
+ var savePath = Path.Combine(Path.GetDirectoryName(video.Path),
+ Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLower() + "." + response.Format.ToLower());
+
+ _logger.Info("Saving subtitles to {0}", savePath);
+
+ _monitor.ReportFileSystemChangeBeginning(savePath);
+
+ try
+ {
+ using (var fs = _fileSystem.GetFileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
+ {
+ await stream.CopyToAsync(fs).ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ _monitor.ReportFileSystemChangeComplete(savePath, false);
+ }
+ }
+ }
+
+ public Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(Video video, string language, CancellationToken cancellationToken)
+ {
+ if (video.LocationType != LocationType.FileSystem ||
+ video.VideoType != VideoType.VideoFile)
+ {
+ return Task.FromResult<IEnumerable<RemoteSubtitleInfo>>(new List<RemoteSubtitleInfo>());
+ }
+
+ SubtitleMediaType mediaType;
+
+ if (video is Episode)
+ {
+ mediaType = SubtitleMediaType.Episode;
+ }
+ else if (video is Movie)
+ {
+ mediaType = SubtitleMediaType.Movie;
+ }
+ else
+ {
+ // These are the only supported types
+ return Task.FromResult<IEnumerable<RemoteSubtitleInfo>>(new List<RemoteSubtitleInfo>());
+ }
+
+ var request = new SubtitleSearchRequest
+ {
+ ContentType = mediaType,
+ IndexNumber = video.IndexNumber,
+ Language = language,
+ MediaPath = video.Path,
+ Name = video.Name,
+ ParentIndexNumber = video.ParentIndexNumber,
+ ProductionYear = video.ProductionYear,
+ ProviderIds = video.ProviderIds
+ };
+
+ var episode = video as Episode;
+
+ if (episode != null)
+ {
+ request.IndexNumberEnd = episode.IndexNumberEnd;
+ request.SeriesName = episode.SeriesName;
+ }
+
+ return SearchSubtitles(request, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
index f516c0878..748bc4b9c 100644
--- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
+++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
@@ -327,7 +327,7 @@ namespace MediaBrowser.Server.Implementations.Channels
var categoryKey = string.IsNullOrWhiteSpace(categoryId) ? "root" : categoryId.GetMD5().ToString("N");
- return Path.Combine(_config.ApplicationPaths.CachePath, channelId, categoryKey, user.Id.ToString("N") + ".json");
+ return Path.Combine(_config.ApplicationPaths.CachePath, "channels", channelId, categoryKey, user.Id.ToString("N") + ".json");
}
private async Task<QueryResult<BaseItemDto>> GetReturnItems(IEnumerable<BaseItem> items, User user, ChannelItemQuery query, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs
index 653cbacb6..adcf3edba 100644
--- a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs
+++ b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs
@@ -93,7 +93,13 @@ namespace MediaBrowser.Server.Implementations.Collections
// Find an actual physical folder
if (folder is CollectionFolder)
{
- return _libraryManager.RootFolder.Children.OfType<Folder>().First(i => folder.PhysicalLocations.Contains(i.Path, StringComparer.OrdinalIgnoreCase));
+ var child = _libraryManager.RootFolder.Children.OfType<Folder>()
+ .FirstOrDefault(i => folder.PhysicalLocations.Contains(i.Path, StringComparer.OrdinalIgnoreCase));
+
+ if (child != null)
+ {
+ return child;
+ }
}
}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/ServerLogger.cs b/MediaBrowser.Server.Implementations/HttpServer/ServerLogger.cs
index 011e64df2..7a4f922ed 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/ServerLogger.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/ServerLogger.cs
@@ -206,7 +206,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// <param name="message">The message.</param>
public void Warn(object message)
{
- _logger.Warn(GetMesssage(message));
+ // Hide StringMapTypeDeserializer messages
+ // _logger.Warn(GetMesssage(message));
}
/// <summary>
@@ -216,7 +217,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// <param name="args">The args.</param>
public void WarnFormat(string format, params object[] args)
{
- _logger.Warn(format, args);
+ // Hide StringMapTypeDeserializer messages
+ // _logger.Warn(format, args);
}
/// <summary>
diff --git a/MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs b/MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs
deleted file mode 100644
index d11e62a1a..000000000
--- a/MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Logging;
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Server.Implementations.Library.Validators
-{
- class PeoplePostScanTask : ILibraryPostScanTask
- {
- /// <summary>
- /// The _library manager
- /// </summary>
- private readonly ILibraryManager _libraryManager;
-
- /// <summary>
- /// The _logger
- /// </summary>
- private readonly ILogger _logger;
-
- public PeoplePostScanTask(ILibraryManager libraryManager, ILogger logger)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- }
-
- /// <summary>
- /// Runs the specified progress.
- /// </summary>
- /// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return new PeopleValidator(_libraryManager, _logger).ValidatePeople(cancellationToken, new MetadataRefreshOptions
- {
- ImageRefreshMode = ImageRefreshMode.ValidationOnly,
- MetadataRefreshMode = MetadataRefreshMode.None
-
- }, progress);
- }
- }
-}
diff --git a/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs b/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs
index 629d21df6..8eaaaacc0 100644
--- a/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs
+++ b/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs
@@ -5,7 +5,6 @@ using MediaBrowser.Controller.Localization;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Serialization;
-using MoreLinq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -106,16 +105,13 @@ namespace MediaBrowser.Server.Implementations.Localization
/// <returns>IEnumerable{CultureDto}.</returns>
public IEnumerable<CultureDto> GetCultures()
{
- return CultureInfo.GetCultures(CultureTypes.AllCultures)
- .OrderBy(c => c.DisplayName)
- .DistinctBy(c => c.TwoLetterISOLanguageName + c.ThreeLetterISOLanguageName)
- .Select(c => new CultureDto
- {
- Name = c.Name,
- DisplayName = c.DisplayName,
- ThreeLetterISOLanguageName = c.ThreeLetterISOLanguageName,
- TwoLetterISOLanguageName = c.TwoLetterISOLanguageName
- });
+ var type = GetType();
+ var path = type.Namespace + ".cultures.json";
+
+ using (var stream = type.Assembly.GetManifestResourceStream(path))
+ {
+ return _jsonSerializer.DeserializeFromStream<List<CultureDto>>(stream);
+ }
}
/// <summary>
@@ -124,28 +120,13 @@ namespace MediaBrowser.Server.Implementations.Localization
/// <returns>IEnumerable{CountryInfo}.</returns>
public IEnumerable<CountryInfo> GetCountries()
{
- return CultureInfo.GetCultures(CultureTypes.SpecificCultures)
- .Select(c =>
- {
- try
- {
- return new RegionInfo(c.LCID);
- }
- catch (CultureNotFoundException)
- {
- return null;
- }
- })
- .Where(i => i != null)
- .OrderBy(c => c.DisplayName)
- .DistinctBy(c => c.TwoLetterISORegionName)
- .Select(c => new CountryInfo
- {
- Name = c.Name,
- DisplayName = c.DisplayName,
- TwoLetterISORegionName = c.TwoLetterISORegionName,
- ThreeLetterISORegionName = c.ThreeLetterISORegionName
- });
+ var type = GetType();
+ var path = type.Namespace + ".countries.json";
+
+ using (var stream = type.Assembly.GetManifestResourceStream(path))
+ {
+ return _jsonSerializer.DeserializeFromStream<List<CountryInfo>>(stream);
+ }
}
/// <summary>
diff --git a/MediaBrowser.Server.Implementations/Localization/Server/server.json b/MediaBrowser.Server.Implementations/Localization/Server/server.json
index 0c99b3a57..c2e29649f 100644
--- a/MediaBrowser.Server.Implementations/Localization/Server/server.json
+++ b/MediaBrowser.Server.Implementations/Localization/Server/server.json
@@ -627,5 +627,84 @@
"OptionSpecialFeatures": "Special Features",
"HeaderCollections": "Collections",
"HeaderChannels": "Channels",
- "HeaderMyLibrary": "My Library"
+ "HeaderMyLibrary": "My Library",
+ "LabelProfileCodecsHelp": "Separated by comma. This can be left empty to apply to all codecs.",
+ "LabelProfileContainersHelp": "Separated by comma. This can be left empty to apply to all containers.",
+ "HeaderResponseProfile": "Response Profile",
+ "LabelType": "Type:",
+ "LabelProfileContainer": "Container:",
+ "LabelProfileVideoCodecs": "Video codecs:",
+ "LabelProfileAudioCodecs": "Audio codecs:",
+ "LabelProfileCodecs": "Codecs:",
+ "HeaderDirectPlayProfile": "Direct Play Profile",
+ "HeaderTranscodingProfile": "Transcoding Profile",
+ "HeaderCodecProfile": "Codec Profile",
+ "HeaderCodecProfileHelp": "Define additional conditions that must be met in order for a codec to be direct played.",
+ "HeaderContainerProfile": "Container Profile",
+ "HeaderContainerProfileHelp": "Define additional conditions that must be met in order for a file to be direct played.",
+ "OptionProfileVideo": "Video",
+ "OptionProfileAudio": "Audio",
+ "OptionProfileVideoAudio": "Video Audio",
+ "OptionProfilePhoto": "Photo",
+ "LabelUserLibrary": "User library:",
+ "LabelUserLibraryHelp": "Select which user library to display to the device. Leave empty to inherit the default setting.",
+ "OptionPlainStorageFolders": "Display all folders as plain storage folders",
+ "OptionPlainStorageFoldersHelp": "If enabled, all folders are represented in DIDL as \"object.container.storageFolder\" instead of a more specific type, such as \"object.container.person.musicArtist\".",
+ "OptionPlainVideoItems": "Display all videos as plain video items",
+ "OptionPlainVideoItemsHelp": "If enabled, all videos are represented in DIDL as \"object.item.videoItem\" instead of a more specific type, such as \"object.item.videoItem.movie\".",
+ "LabelSupportedMediaTypes": "Supported Media Types:",
+ "TabIdentification": "Identification",
+ "TabDirectPlay": "Direct Play",
+ "TabContainers": "Containers",
+ "TabCodecs": "Codecs",
+ "TabResponses": "Responses",
+ "HeaderProfileInformation": "Profile Information",
+ "LabelEmbedAlbumArtDidl": "Embed album art in Didl",
+ "LabelEmbedAlbumArtDidlHelp": "Some devices prefer this method for obtaining album art. Others may fail to play with this option enabled.",
+ "LabelAlbumArtPN": "Album art PN:",
+ "LabelAlbumArtHelp": "PN used for album art, within the dlna:profileID attribute on upnp:albumArtURI. Some clients require a specific value, regardless of the size of the image.",
+ "LabelAlbumArtMaxWidth": "Album art max width:",
+ "LabelAlbumArtMaxWidthHelp": "Max resolution of album art exposed via upnp:albumArtURI.",
+ "LabelAlbumArtMaxHeight": "Album art max height:",
+ "LabelAlbumArtMaxHeightHelp": "Max resolution of album art exposed via upnp:albumArtURI.",
+ "LabelIconMaxWidth": "Icon max width:",
+ "LabelIconMaxWidthHelp": "Max resolution of icons exposed via upnp:icon.",
+ "LabelIconMaxHeight": "Icon max height:",
+ "LabelIconMaxHeightHelp": "Max resolution of icons exposed via upnp:icon.",
+ "LabelIdentificationFieldHelp": "A case-insensitive substring or regex expression.",
+ "HeaderProfileServerSettingsHelp": "These values control how Media Browser will present itself to the device.",
+ "LabelMaxBitrate": "Max bitrate:",
+ "LabelMaxBitrateHelp": "Specify a max bitrate in bandwidth constrained environments, or if the device imposes it's own limit.",
+ "OptionIgnoreTranscodeByteRangeRequests": "Ignore transcode byte range requests",
+ "OptionIgnoreTranscodeByteRangeRequestsHelp": "If enabled, these requests will be honored but will ignore the byte range header.",
+ "LabelFriendlyName": "Friendly name",
+ "LabelManufacturer": "Manufacturer",
+ "LabelManufacturerUrl": "Manufacturer url",
+ "LabelModelName": "Model name",
+ "LabelModelNumber": "Model number",
+ "LabelModelDescription": "Model description",
+ "LabelModelUrl": "Model url",
+ "LabelSerialNumber": "Serial number",
+ "LabelDeviceDescription": "Device description",
+ "HeaderIdentificationCriteriaHelp": "Enter at least one identification criteria.",
+ "HeaderDirectPlayProfileHelp": "Add direct play profiles to indicate which formats the device can handle natively.",
+ "HeaderTranscodingProfileHelp": "Add transcoding profiles to indicate which formats should be used when transcoding is required.",
+ "HeaderContainerProfileHelp": "Container profiles indicate the limitations of a device when playing specific formats. If a limitation applies then the media will be transcoded, even if the format is configured for direct play.",
+ "HeaderCodecProfileHelp": "Codec profiles indicate the limitations of a device when playing specific codecs. If a limitation applies then the media will be transcoded, even if the codec is configured for direct play.",
+ "HeaderResponseProfileHelp": "Response profiles provide a way to customize information sent to the device when playing certain kinds of media.",
+ "LabelXDlnaCap": "X-Dlna cap:",
+ "LabelXDlnaCapHelp": "Determines the content of the X_DLNACAP element in the urn:schemas-dlna-org:device-1-0 namespace.",
+ "LabelXDlnaDoc": "X-Dlna doc:",
+ "LabelXDlnaDocHelp": "Determines the content of the X_DLNADOC element in the urn:schemas-dlna-org:device-1-0 namespace.",
+ "LabelSonyAggregationFlags": "Sony aggregation flags:",
+ "LabelSonyAggregationFlagsHelp": "Determines the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace.",
+ "LabelTranscodingContainer": "Container:",
+ "LabelTranscodingVideoCodec": "Video codec:",
+ "LabelTranscodingVideoProfile": "Video profile:",
+ "LabelTranscodingAudioCodec": "Audio codec:",
+ "OptionEnableM2tsMode": "Enable M2ts mode",
+ "OptionEnableM2tsModeHelp": "Enable m2ts mode when encoding to mpegts.",
+ "OptionEstimateContentLength": "Estimate content length when transcoding",
+ "OptionReportByteRangeSeekingWhenTranscoding": "Report that the server supports byte seeking when transcoding",
+ "OptionReportByteRangeSeekingWhenTranscodingHelp": "This is required for some devices that don't time seek very well."
} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/Localization/countries.json b/MediaBrowser.Server.Implementations/Localization/countries.json
new file mode 100644
index 000000000..e671b3685
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/Localization/countries.json
@@ -0,0 +1 @@
+[{"Name":"AF","DisplayName":"Afghanistan","TwoLetterISORegionName":"AF","ThreeLetterISORegionName":"AFG"},{"Name":"AL","DisplayName":"Albania","TwoLetterISORegionName":"AL","ThreeLetterISORegionName":"ALB"},{"Name":"DZ","DisplayName":"Algeria","TwoLetterISORegionName":"DZ","ThreeLetterISORegionName":"DZA"},{"Name":"AR","DisplayName":"Argentina","TwoLetterISORegionName":"AR","ThreeLetterISORegionName":"ARG"},{"Name":"AM","DisplayName":"Armenia","TwoLetterISORegionName":"AM","ThreeLetterISORegionName":"ARM"},{"Name":"AU","DisplayName":"Australia","TwoLetterISORegionName":"AU","ThreeLetterISORegionName":"AUS"},{"Name":"AT","DisplayName":"Austria","TwoLetterISORegionName":"AT","ThreeLetterISORegionName":"AUT"},{"Name":"AZ","DisplayName":"Azerbaijan","TwoLetterISORegionName":"AZ","ThreeLetterISORegionName":"AZE"},{"Name":"BH","DisplayName":"Bahrain","TwoLetterISORegionName":"BH","ThreeLetterISORegionName":"BHR"},{"Name":"BD","DisplayName":"Bangladesh","TwoLetterISORegionName":"BD","ThreeLetterISORegionName":"BGD"},{"Name":"BY","DisplayName":"Belarus","TwoLetterISORegionName":"BY","ThreeLetterISORegionName":"BLR"},{"Name":"BE","DisplayName":"Belgium","TwoLetterISORegionName":"BE","ThreeLetterISORegionName":"BEL"},{"Name":"BZ","DisplayName":"Belize","TwoLetterISORegionName":"BZ","ThreeLetterISORegionName":"BLZ"},{"Name":"VE","DisplayName":"Bolivarian Republic of Venezuela","TwoLetterISORegionName":"VE","ThreeLetterISORegionName":"VEN"},{"Name":"BO","DisplayName":"Bolivia","TwoLetterISORegionName":"BO","ThreeLetterISORegionName":"BOL"},{"Name":"BA","DisplayName":"Bosnia and Herzegovina","TwoLetterISORegionName":"BA","ThreeLetterISORegionName":"BIH"},{"Name":"BW","DisplayName":"Botswana","TwoLetterISORegionName":"BW","ThreeLetterISORegionName":"BWA"},{"Name":"BR","DisplayName":"Brazil","TwoLetterISORegionName":"BR","ThreeLetterISORegionName":"BRA"},{"Name":"BN","DisplayName":"Brunei Darussalam","TwoLetterISORegionName":"BN","ThreeLetterISORegionName":"BRN"},{"Name":"BG","DisplayName":"Bulgaria","TwoLetterISORegionName":"BG","ThreeLetterISORegionName":"BGR"},{"Name":"KH","DisplayName":"Cambodia","TwoLetterISORegionName":"KH","ThreeLetterISORegionName":"KHM"},{"Name":"CM","DisplayName":"Cameroon","TwoLetterISORegionName":"CM","ThreeLetterISORegionName":"CMR"},{"Name":"CA","DisplayName":"Canada","TwoLetterISORegionName":"CA","ThreeLetterISORegionName":"CAN"},{"Name":"029","DisplayName":"Caribbean","TwoLetterISORegionName":"029","ThreeLetterISORegionName":"029"},{"Name":"CL","DisplayName":"Chile","TwoLetterISORegionName":"CL","ThreeLetterISORegionName":"CHL"},{"Name":"CO","DisplayName":"Colombia","TwoLetterISORegionName":"CO","ThreeLetterISORegionName":"COL"},{"Name":"CD","DisplayName":"Congo [DRC]","TwoLetterISORegionName":"CD","ThreeLetterISORegionName":"COD"},{"Name":"CR","DisplayName":"Costa Rica","TwoLetterISORegionName":"CR","ThreeLetterISORegionName":"CRI"},{"Name":"HR","DisplayName":"Croatia","TwoLetterISORegionName":"HR","ThreeLetterISORegionName":"HRV"},{"Name":"CZ","DisplayName":"Czech Republic","TwoLetterISORegionName":"CZ","ThreeLetterISORegionName":"CZE"},{"Name":"DK","DisplayName":"Denmark","TwoLetterISORegionName":"DK","ThreeLetterISORegionName":"DNK"},{"Name":"DO","DisplayName":"Dominican Republic","TwoLetterISORegionName":"DO","ThreeLetterISORegionName":"DOM"},{"Name":"EC","DisplayName":"Ecuador","TwoLetterISORegionName":"EC","ThreeLetterISORegionName":"ECU"},{"Name":"EG","DisplayName":"Egypt","TwoLetterISORegionName":"EG","ThreeLetterISORegionName":"EGY"},{"Name":"SV","DisplayName":"El Salvador","TwoLetterISORegionName":"SV","ThreeLetterISORegionName":"SLV"},{"Name":"ER","DisplayName":"Eritrea","TwoLetterISORegionName":"ER","ThreeLetterISORegionName":"ERI"},{"Name":"EE","DisplayName":"Estonia","TwoLetterISORegionName":"EE","ThreeLetterISORegionName":"EST"},{"Name":"ET","DisplayName":"Ethiopia","TwoLetterISORegionName":"ET","ThreeLetterISORegionName":"ETH"},{"Name":"FO","DisplayName":"Faroe Islands","TwoLetterISORegionName":"FO","ThreeLetterISORegionName":"FRO"},{"Name":"FI","DisplayName":"Finland","TwoLetterISORegionName":"FI","ThreeLetterISORegionName":"FIN"},{"Name":"FR","DisplayName":"France","TwoLetterISORegionName":"FR","ThreeLetterISORegionName":"FRA"},{"Name":"GE","DisplayName":"Georgia","TwoLetterISORegionName":"GE","ThreeLetterISORegionName":"GEO"},{"Name":"DE","DisplayName":"Germany","TwoLetterISORegionName":"DE","ThreeLetterISORegionName":"DEU"},{"Name":"GR","DisplayName":"Greece","TwoLetterISORegionName":"GR","ThreeLetterISORegionName":"GRC"},{"Name":"GL","DisplayName":"Greenland","TwoLetterISORegionName":"GL","ThreeLetterISORegionName":"GRL"},{"Name":"GT","DisplayName":"Guatemala","TwoLetterISORegionName":"GT","ThreeLetterISORegionName":"GTM"},{"Name":"HT","DisplayName":"Haiti","TwoLetterISORegionName":"HT","ThreeLetterISORegionName":"HTI"},{"Name":"HN","DisplayName":"Honduras","TwoLetterISORegionName":"HN","ThreeLetterISORegionName":"HND"},{"Name":"HK","DisplayName":"Hong Kong S.A.R.","TwoLetterISORegionName":"HK","ThreeLetterISORegionName":"HKG"},{"Name":"HU","DisplayName":"Hungary","TwoLetterISORegionName":"HU","ThreeLetterISORegionName":"HUN"},{"Name":"IS","DisplayName":"Iceland","TwoLetterISORegionName":"IS","ThreeLetterISORegionName":"ISL"},{"Name":"IN","DisplayName":"India","TwoLetterISORegionName":"IN","ThreeLetterISORegionName":"IND"},{"Name":"ID","DisplayName":"Indonesia","TwoLetterISORegionName":"ID","ThreeLetterISORegionName":"IDN"},{"Name":"IR","DisplayName":"Iran","TwoLetterISORegionName":"IR","ThreeLetterISORegionName":"IRN"},{"Name":"IQ","DisplayName":"Iraq","TwoLetterISORegionName":"IQ","ThreeLetterISORegionName":"IRQ"},{"Name":"IE","DisplayName":"Ireland","TwoLetterISORegionName":"IE","ThreeLetterISORegionName":"IRL"},{"Name":"PK","DisplayName":"Islamic Republic of Pakistan","TwoLetterISORegionName":"PK","ThreeLetterISORegionName":"PAK"},{"Name":"IL","DisplayName":"Israel","TwoLetterISORegionName":"IL","ThreeLetterISORegionName":"ISR"},{"Name":"IT","DisplayName":"Italy","TwoLetterISORegionName":"IT","ThreeLetterISORegionName":"ITA"},{"Name":"CI","DisplayName":"Ivory Coast","TwoLetterISORegionName":"CI","ThreeLetterISORegionName":"CIV"},{"Name":"JM","DisplayName":"Jamaica","TwoLetterISORegionName":"JM","ThreeLetterISORegionName":"JAM"},{"Name":"JP","DisplayName":"Japan","TwoLetterISORegionName":"JP","ThreeLetterISORegionName":"JPN"},{"Name":"JO","DisplayName":"Jordan","TwoLetterISORegionName":"JO","ThreeLetterISORegionName":"JOR"},{"Name":"KZ","DisplayName":"Kazakhstan","TwoLetterISORegionName":"KZ","ThreeLetterISORegionName":"KAZ"},{"Name":"KE","DisplayName":"Kenya","TwoLetterISORegionName":"KE","ThreeLetterISORegionName":"KEN"},{"Name":"KR","DisplayName":"Korea","TwoLetterISORegionName":"KR","ThreeLetterISORegionName":"KOR"},{"Name":"KW","DisplayName":"Kuwait","TwoLetterISORegionName":"KW","ThreeLetterISORegionName":"KWT"},{"Name":"KG","DisplayName":"Kyrgyzstan","TwoLetterISORegionName":"KG","ThreeLetterISORegionName":"KGZ"},{"Name":"LA","DisplayName":"Lao P.D.R.","TwoLetterISORegionName":"LA","ThreeLetterISORegionName":"LAO"},{"Name":"419","DisplayName":"Latin America","TwoLetterISORegionName":"419","ThreeLetterISORegionName":"419"},{"Name":"LV","DisplayName":"Latvia","TwoLetterISORegionName":"LV","ThreeLetterISORegionName":"LVA"},{"Name":"LB","DisplayName":"Lebanon","TwoLetterISORegionName":"LB","ThreeLetterISORegionName":"LBN"},{"Name":"LY","DisplayName":"Libya","TwoLetterISORegionName":"LY","ThreeLetterISORegionName":"LBY"},{"Name":"LI","DisplayName":"Liechtenstein","TwoLetterISORegionName":"LI","ThreeLetterISORegionName":"LIE"},{"Name":"LT","DisplayName":"Lithuania","TwoLetterISORegionName":"LT","ThreeLetterISORegionName":"LTU"},{"Name":"LU","DisplayName":"Luxembourg","TwoLetterISORegionName":"LU","ThreeLetterISORegionName":"LUX"},{"Name":"MO","DisplayName":"Macao S.A.R.","TwoLetterISORegionName":"MO","ThreeLetterISORegionName":"MAC"},{"Name":"MK","DisplayName":"Macedonia (FYROM)","TwoLetterISORegionName":"MK","ThreeLetterISORegionName":"MKD"},{"Name":"MY","DisplayName":"Malaysia","TwoLetterISORegionName":"MY","ThreeLetterISORegionName":"MYS"},{"Name":"MV","DisplayName":"Maldives","TwoLetterISORegionName":"MV","ThreeLetterISORegionName":"MDV"},{"Name":"ML","DisplayName":"Mali","TwoLetterISORegionName":"ML","ThreeLetterISORegionName":"MLI"},{"Name":"MT","DisplayName":"Malta","TwoLetterISORegionName":"MT","ThreeLetterISORegionName":"MLT"},{"Name":"MX","DisplayName":"Mexico","TwoLetterISORegionName":"MX","ThreeLetterISORegionName":"MEX"},{"Name":"MN","DisplayName":"Mongolia","TwoLetterISORegionName":"MN","ThreeLetterISORegionName":"MNG"},{"Name":"ME","DisplayName":"Montenegro","TwoLetterISORegionName":"ME","ThreeLetterISORegionName":"MNE"},{"Name":"MA","DisplayName":"Morocco","TwoLetterISORegionName":"MA","ThreeLetterISORegionName":"MAR"},{"Name":"NP","DisplayName":"Nepal","TwoLetterISORegionName":"NP","ThreeLetterISORegionName":"NPL"},{"Name":"NL","DisplayName":"Netherlands","TwoLetterISORegionName":"NL","ThreeLetterISORegionName":"NLD"},{"Name":"NZ","DisplayName":"New Zealand","TwoLetterISORegionName":"NZ","ThreeLetterISORegionName":"NZL"},{"Name":"NI","DisplayName":"Nicaragua","TwoLetterISORegionName":"NI","ThreeLetterISORegionName":"NIC"},{"Name":"NG","DisplayName":"Nigeria","TwoLetterISORegionName":"NG","ThreeLetterISORegionName":"NGA"},{"Name":"NO","DisplayName":"Norway","TwoLetterISORegionName":"NO","ThreeLetterISORegionName":"NOR"},{"Name":"OM","DisplayName":"Oman","TwoLetterISORegionName":"OM","ThreeLetterISORegionName":"OMN"},{"Name":"PA","DisplayName":"Panama","TwoLetterISORegionName":"PA","ThreeLetterISORegionName":"PAN"},{"Name":"PY","DisplayName":"Paraguay","TwoLetterISORegionName":"PY","ThreeLetterISORegionName":"PRY"},{"Name":"CN","DisplayName":"People's Republic of China","TwoLetterISORegionName":"CN","ThreeLetterISORegionName":"CHN"},{"Name":"PE","DisplayName":"Peru","TwoLetterISORegionName":"PE","ThreeLetterISORegionName":"PER"},{"Name":"PH","DisplayName":"Philippines","TwoLetterISORegionName":"PH","ThreeLetterISORegionName":"PHL"},{"Name":"PL","DisplayName":"Poland","TwoLetterISORegionName":"PL","ThreeLetterISORegionName":"POL"},{"Name":"PT","DisplayName":"Portugal","TwoLetterISORegionName":"PT","ThreeLetterISORegionName":"PRT"},{"Name":"MC","DisplayName":"Principality of Monaco","TwoLetterISORegionName":"MC","ThreeLetterISORegionName":"MCO"},{"Name":"PR","DisplayName":"Puerto Rico","TwoLetterISORegionName":"PR","ThreeLetterISORegionName":"PRI"},{"Name":"QA","DisplayName":"Qatar","TwoLetterISORegionName":"QA","ThreeLetterISORegionName":"QAT"},{"Name":"MD","DisplayName":"Republica Moldova","TwoLetterISORegionName":"MD","ThreeLetterISORegionName":"MDA"},{"Name":"RE","DisplayName":"Réunion","TwoLetterISORegionName":"RE","ThreeLetterISORegionName":"REU"},{"Name":"RO","DisplayName":"Romania","TwoLetterISORegionName":"RO","ThreeLetterISORegionName":"ROU"},{"Name":"RU","DisplayName":"Russia","TwoLetterISORegionName":"RU","ThreeLetterISORegionName":"RUS"},{"Name":"RW","DisplayName":"Rwanda","TwoLetterISORegionName":"RW","ThreeLetterISORegionName":"RWA"},{"Name":"SA","DisplayName":"Saudi Arabia","TwoLetterISORegionName":"SA","ThreeLetterISORegionName":"SAU"},{"Name":"SN","DisplayName":"Senegal","TwoLetterISORegionName":"SN","ThreeLetterISORegionName":"SEN"},{"Name":"RS","DisplayName":"Serbia","TwoLetterISORegionName":"RS","ThreeLetterISORegionName":"SRB"},{"Name":"CS","DisplayName":"Serbia and Montenegro (Former)","TwoLetterISORegionName":"CS","ThreeLetterISORegionName":"SCG"},{"Name":"SG","DisplayName":"Singapore","TwoLetterISORegionName":"SG","ThreeLetterISORegionName":"SGP"},{"Name":"SK","DisplayName":"Slovakia","TwoLetterISORegionName":"SK","ThreeLetterISORegionName":"SVK"},{"Name":"SI","DisplayName":"Slovenia","TwoLetterISORegionName":"SI","ThreeLetterISORegionName":"SVN"},{"Name":"SO","DisplayName":"Soomaaliya","TwoLetterISORegionName":"SO","ThreeLetterISORegionName":"SOM"},{"Name":"ZA","DisplayName":"South Africa","TwoLetterISORegionName":"ZA","ThreeLetterISORegionName":"ZAF"},{"Name":"ES","DisplayName":"Spain","TwoLetterISORegionName":"ES","ThreeLetterISORegionName":"ESP"},{"Name":"LK","DisplayName":"Sri Lanka","TwoLetterISORegionName":"LK","ThreeLetterISORegionName":"LKA"},{"Name":"SE","DisplayName":"Sweden","TwoLetterISORegionName":"SE","ThreeLetterISORegionName":"SWE"},{"Name":"CH","DisplayName":"Switzerland","TwoLetterISORegionName":"CH","ThreeLetterISORegionName":"CHE"},{"Name":"SY","DisplayName":"Syria","TwoLetterISORegionName":"SY","ThreeLetterISORegionName":"SYR"},{"Name":"TW","DisplayName":"Taiwan","TwoLetterISORegionName":"TW","ThreeLetterISORegionName":"TWN"},{"Name":"TJ","DisplayName":"Tajikistan","TwoLetterISORegionName":"TJ","ThreeLetterISORegionName":"TAJ"},{"Name":"TH","DisplayName":"Thailand","TwoLetterISORegionName":"TH","ThreeLetterISORegionName":"THA"},{"Name":"TT","DisplayName":"Trinidad and Tobago","TwoLetterISORegionName":"TT","ThreeLetterISORegionName":"TTO"},{"Name":"TN","DisplayName":"Tunisia","TwoLetterISORegionName":"TN","ThreeLetterISORegionName":"TUN"},{"Name":"TR","DisplayName":"Turkey","TwoLetterISORegionName":"TR","ThreeLetterISORegionName":"TUR"},{"Name":"TM","DisplayName":"Turkmenistan","TwoLetterISORegionName":"TM","ThreeLetterISORegionName":"TKM"},{"Name":"AE","DisplayName":"U.A.E.","TwoLetterISORegionName":"AE","ThreeLetterISORegionName":"ARE"},{"Name":"UA","DisplayName":"Ukraine","TwoLetterISORegionName":"UA","ThreeLetterISORegionName":"UKR"},{"Name":"GB","DisplayName":"United Kingdom","TwoLetterISORegionName":"GB","ThreeLetterISORegionName":"GBR"},{"Name":"US","DisplayName":"United States","TwoLetterISORegionName":"US","ThreeLetterISORegionName":"USA"},{"Name":"UY","DisplayName":"Uruguay","TwoLetterISORegionName":"UY","ThreeLetterISORegionName":"URY"},{"Name":"UZ","DisplayName":"Uzbekistan","TwoLetterISORegionName":"UZ","ThreeLetterISORegionName":"UZB"},{"Name":"VN","DisplayName":"Vietnam","TwoLetterISORegionName":"VN","ThreeLetterISORegionName":"VNM"},{"Name":"YE","DisplayName":"Yemen","TwoLetterISORegionName":"YE","ThreeLetterISORegionName":"YEM"},{"Name":"ZW","DisplayName":"Zimbabwe","TwoLetterISORegionName":"ZW","ThreeLetterISORegionName":"ZWE"}] \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/Localization/cultures.json b/MediaBrowser.Server.Implementations/Localization/cultures.json
new file mode 100644
index 000000000..9d98b664b
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/Localization/cultures.json
@@ -0,0 +1 @@
+[{"Name":"af","DisplayName":"Afrikaans","TwoLetterISOLanguageName":"af","ThreeLetterISOLanguageName":"afr"},{"Name":"sq","DisplayName":"Albanian","TwoLetterISOLanguageName":"sq","ThreeLetterISOLanguageName":"sqi"},{"Name":"gsw","DisplayName":"Alsatian","TwoLetterISOLanguageName":"gsw","ThreeLetterISOLanguageName":"gsw"},{"Name":"am","DisplayName":"Amharic","TwoLetterISOLanguageName":"am","ThreeLetterISOLanguageName":"amh"},{"Name":"ar","DisplayName":"Arabic","TwoLetterISOLanguageName":"ar","ThreeLetterISOLanguageName":"ara"},{"Name":"hy","DisplayName":"Armenian","TwoLetterISOLanguageName":"hy","ThreeLetterISOLanguageName":"hye"},{"Name":"as","DisplayName":"Assamese","TwoLetterISOLanguageName":"as","ThreeLetterISOLanguageName":"asm"},{"Name":"az","DisplayName":"Azeri","TwoLetterISOLanguageName":"az","ThreeLetterISOLanguageName":"aze"},{"Name":"jv","DisplayName":"Basa Jawa","TwoLetterISOLanguageName":"jv","ThreeLetterISOLanguageName":"jav"},{"Name":"ba","DisplayName":"Bashkir","TwoLetterISOLanguageName":"ba","ThreeLetterISOLanguageName":"bak"},{"Name":"eu","DisplayName":"Basque","TwoLetterISOLanguageName":"eu","ThreeLetterISOLanguageName":"eus"},{"Name":"be","DisplayName":"Belarusian","TwoLetterISOLanguageName":"be","ThreeLetterISOLanguageName":"bel"},{"Name":"bn","DisplayName":"Bengali","TwoLetterISOLanguageName":"bn","ThreeLetterISOLanguageName":"bng"},{"Name":"bs","DisplayName":"Bosnian","TwoLetterISOLanguageName":"bs","ThreeLetterISOLanguageName":"bsb"},{"Name":"bs-Cyrl","DisplayName":"Bosnian (Cyrillic)","TwoLetterISOLanguageName":"bs","ThreeLetterISOLanguageName":"bsc"},{"Name":"br","DisplayName":"Breton","TwoLetterISOLanguageName":"br","ThreeLetterISOLanguageName":"bre"},{"Name":"bg","DisplayName":"Bulgarian","TwoLetterISOLanguageName":"bg","ThreeLetterISOLanguageName":"bul"},{"Name":"my","DisplayName":"Burmese","TwoLetterISOLanguageName":"my","ThreeLetterISOLanguageName":"mya"},{"Name":"ca","DisplayName":"Catalan","TwoLetterISOLanguageName":"ca","ThreeLetterISOLanguageName":"cat"},{"Name":"tzm-Tfng-MA","DisplayName":"Central Atlas Tamazight (Tifinagh, Morocco)","TwoLetterISOLanguageName":"tzm","ThreeLetterISOLanguageName":"tzm"},{"Name":"ku","DisplayName":"Central Kurdish","TwoLetterISOLanguageName":"ku","ThreeLetterISOLanguageName":"kur"},{"Name":"chr","DisplayName":"Cherokee","TwoLetterISOLanguageName":"chr","ThreeLetterISOLanguageName":"chr"},{"Name":"zh","DisplayName":"Chinese","TwoLetterISOLanguageName":"zh","ThreeLetterISOLanguageName":"zho"},{"Name":"sn","DisplayName":"chiShona","TwoLetterISOLanguageName":"sn","ThreeLetterISOLanguageName":"sna"},{"Name":"co","DisplayName":"Corsican","TwoLetterISOLanguageName":"co","ThreeLetterISOLanguageName":"cos"},{"Name":"hr","DisplayName":"Croatian","TwoLetterISOLanguageName":"hr","ThreeLetterISOLanguageName":"hrv"},{"Name":"hr-BA","DisplayName":"Croatian (Latin, Bosnia and Herzegovina)","TwoLetterISOLanguageName":"hr","ThreeLetterISOLanguageName":"hrb"},{"Name":"cs","DisplayName":"Czech","TwoLetterISOLanguageName":"cs","ThreeLetterISOLanguageName":"ces"},{"Name":"da","DisplayName":"Danish","TwoLetterISOLanguageName":"da","ThreeLetterISOLanguageName":"dan"},{"Name":"prs","DisplayName":"Dari","TwoLetterISOLanguageName":"prs","ThreeLetterISOLanguageName":"prs"},{"Name":"dv","DisplayName":"Divehi","TwoLetterISOLanguageName":"dv","ThreeLetterISOLanguageName":"div"},{"Name":"nl","DisplayName":"Dutch","TwoLetterISOLanguageName":"nl","ThreeLetterISOLanguageName":"nld"},{"Name":"en","DisplayName":"English","TwoLetterISOLanguageName":"en","ThreeLetterISOLanguageName":"eng"},{"Name":"et","DisplayName":"Estonian","TwoLetterISOLanguageName":"et","ThreeLetterISOLanguageName":"est"},{"Name":"fo","DisplayName":"Faroese","TwoLetterISOLanguageName":"fo","ThreeLetterISOLanguageName":"fao"},{"Name":"fil","DisplayName":"Filipino","TwoLetterISOLanguageName":"fil","ThreeLetterISOLanguageName":"fil"},{"Name":"fi","DisplayName":"Finnish","TwoLetterISOLanguageName":"fi","ThreeLetterISOLanguageName":"fin"},{"Name":"fr","DisplayName":"French","TwoLetterISOLanguageName":"fr","ThreeLetterISOLanguageName":"fra"},{"Name":"fy","DisplayName":"Frisian","TwoLetterISOLanguageName":"fy","ThreeLetterISOLanguageName":"fry"},{"Name":"ff","DisplayName":"Fulah","TwoLetterISOLanguageName":"ff","ThreeLetterISOLanguageName":"ful"},{"Name":"gl","DisplayName":"Galician","TwoLetterISOLanguageName":"gl","ThreeLetterISOLanguageName":"glg"},{"Name":"ka","DisplayName":"Georgian","TwoLetterISOLanguageName":"ka","ThreeLetterISOLanguageName":"kat"},{"Name":"de","DisplayName":"German","TwoLetterISOLanguageName":"de","ThreeLetterISOLanguageName":"deu"},{"Name":"el","DisplayName":"Greek","TwoLetterISOLanguageName":"el","ThreeLetterISOLanguageName":"ell"},{"Name":"kl","DisplayName":"Greenlandic","TwoLetterISOLanguageName":"kl","ThreeLetterISOLanguageName":"kal"},{"Name":"gn","DisplayName":"Guarani","TwoLetterISOLanguageName":"gn","ThreeLetterISOLanguageName":"grn"},{"Name":"gu","DisplayName":"Gujarati","TwoLetterISOLanguageName":"gu","ThreeLetterISOLanguageName":"guj"},{"Name":"ha","DisplayName":"Hausa","TwoLetterISOLanguageName":"ha","ThreeLetterISOLanguageName":"hau"},{"Name":"haw","DisplayName":"Hawaiian","TwoLetterISOLanguageName":"haw","ThreeLetterISOLanguageName":"haw"},{"Name":"he","DisplayName":"Hebrew","TwoLetterISOLanguageName":"he","ThreeLetterISOLanguageName":"heb"},{"Name":"hi","DisplayName":"Hindi","TwoLetterISOLanguageName":"hi","ThreeLetterISOLanguageName":"hin"},{"Name":"hu","DisplayName":"Hungarian","TwoLetterISOLanguageName":"hu","ThreeLetterISOLanguageName":"hun"},{"Name":"is","DisplayName":"Icelandic","TwoLetterISOLanguageName":"is","ThreeLetterISOLanguageName":"isl"},{"Name":"ig","DisplayName":"Igbo","TwoLetterISOLanguageName":"ig","ThreeLetterISOLanguageName":"ibo"},{"Name":"id","DisplayName":"Indonesian","TwoLetterISOLanguageName":"id","ThreeLetterISOLanguageName":"ind"},{"Name":"iu","DisplayName":"Inuktitut","TwoLetterISOLanguageName":"iu","ThreeLetterISOLanguageName":"iku"},{"Name":"","DisplayName":"Invariant Language (Invariant Country)","TwoLetterISOLanguageName":"iv","ThreeLetterISOLanguageName":"ivl"},{"Name":"ga","DisplayName":"Irish","TwoLetterISOLanguageName":"ga","ThreeLetterISOLanguageName":"gle"},{"Name":"xh","DisplayName":"isiXhosa","TwoLetterISOLanguageName":"xh","ThreeLetterISOLanguageName":"xho"},{"Name":"zu","DisplayName":"isiZulu","TwoLetterISOLanguageName":"zu","ThreeLetterISOLanguageName":"zul"},{"Name":"it","DisplayName":"Italian","TwoLetterISOLanguageName":"it","ThreeLetterISOLanguageName":"ita"},{"Name":"ja","DisplayName":"Japanese","TwoLetterISOLanguageName":"ja","ThreeLetterISOLanguageName":"jpn"},{"Name":"kn","DisplayName":"Kannada","TwoLetterISOLanguageName":"kn","ThreeLetterISOLanguageName":"kan"},{"Name":"kk","DisplayName":"Kazakh","TwoLetterISOLanguageName":"kk","ThreeLetterISOLanguageName":"kaz"},{"Name":"km","DisplayName":"Khmer","TwoLetterISOLanguageName":"km","ThreeLetterISOLanguageName":"khm"},{"Name":"qut","DisplayName":"K'iche","TwoLetterISOLanguageName":"qut","ThreeLetterISOLanguageName":"qut"},{"Name":"rw","DisplayName":"Kinyarwanda","TwoLetterISOLanguageName":"rw","ThreeLetterISOLanguageName":"kin"},{"Name":"sw","DisplayName":"Kiswahili","TwoLetterISOLanguageName":"sw","ThreeLetterISOLanguageName":"swa"},{"Name":"kok","DisplayName":"Konkani","TwoLetterISOLanguageName":"kok","ThreeLetterISOLanguageName":"kok"},{"Name":"ko","DisplayName":"Korean","TwoLetterISOLanguageName":"ko","ThreeLetterISOLanguageName":"kor"},{"Name":"ky","DisplayName":"Kyrgyz","TwoLetterISOLanguageName":"ky","ThreeLetterISOLanguageName":"kir"},{"Name":"lo","DisplayName":"Lao","TwoLetterISOLanguageName":"lo","ThreeLetterISOLanguageName":"lao"},{"Name":"lv","DisplayName":"Latvian","TwoLetterISOLanguageName":"lv","ThreeLetterISOLanguageName":"lav"},{"Name":"lt","DisplayName":"Lithuanian","TwoLetterISOLanguageName":"lt","ThreeLetterISOLanguageName":"lit"},{"Name":"dsb","DisplayName":"Lower Sorbian","TwoLetterISOLanguageName":"dsb","ThreeLetterISOLanguageName":"dsb"},{"Name":"lb","DisplayName":"Luxembourgish","TwoLetterISOLanguageName":"lb","ThreeLetterISOLanguageName":"ltz"},{"Name":"mk-MK","DisplayName":"Macedonian (Former Yugoslav Republic of Macedonia)","TwoLetterISOLanguageName":"mk","ThreeLetterISOLanguageName":"mkd"},{"Name":"mg","DisplayName":"Malagasy","TwoLetterISOLanguageName":"mg","ThreeLetterISOLanguageName":"mlg"},{"Name":"ms","DisplayName":"Malay","TwoLetterISOLanguageName":"ms","ThreeLetterISOLanguageName":"msa"},{"Name":"ml","DisplayName":"Malayalam","TwoLetterISOLanguageName":"ml","ThreeLetterISOLanguageName":"mym"},{"Name":"mt","DisplayName":"Maltese","TwoLetterISOLanguageName":"mt","ThreeLetterISOLanguageName":"mlt"},{"Name":"mi","DisplayName":"Maori","TwoLetterISOLanguageName":"mi","ThreeLetterISOLanguageName":"mri"},{"Name":"arn","DisplayName":"Mapudungun","TwoLetterISOLanguageName":"arn","ThreeLetterISOLanguageName":"arn"},{"Name":"mr","DisplayName":"Marathi","TwoLetterISOLanguageName":"mr","ThreeLetterISOLanguageName":"mar"},{"Name":"moh","DisplayName":"Mohawk","TwoLetterISOLanguageName":"moh","ThreeLetterISOLanguageName":"moh"},{"Name":"mn","DisplayName":"Mongolian","TwoLetterISOLanguageName":"mn","ThreeLetterISOLanguageName":"mon"},{"Name":"ne","DisplayName":"Nepali","TwoLetterISOLanguageName":"ne","ThreeLetterISOLanguageName":"nep"},{"Name":"no","DisplayName":"Norwegian","TwoLetterISOLanguageName":"nb","ThreeLetterISOLanguageName":"nob"},{"Name":"nn","DisplayName":"Norwegian (Nynorsk)","TwoLetterISOLanguageName":"nn","ThreeLetterISOLanguageName":"nno"},{"Name":"oc","DisplayName":"Occitan","TwoLetterISOLanguageName":"oc","ThreeLetterISOLanguageName":"oci"},{"Name":"or","DisplayName":"Oriya","TwoLetterISOLanguageName":"or","ThreeLetterISOLanguageName":"ori"},{"Name":"om","DisplayName":"Oromo","TwoLetterISOLanguageName":"om","ThreeLetterISOLanguageName":"orm"},{"Name":"ps","DisplayName":"Pashto","TwoLetterISOLanguageName":"ps","ThreeLetterISOLanguageName":"pus"},{"Name":"fa","DisplayName":"Persian","TwoLetterISOLanguageName":"fa","ThreeLetterISOLanguageName":"fas"},{"Name":"pl","DisplayName":"Polish","TwoLetterISOLanguageName":"pl","ThreeLetterISOLanguageName":"pol"},{"Name":"pt-AO","DisplayName":"português (Angola)","TwoLetterISOLanguageName":"pt","ThreeLetterISOLanguageName":"por"},{"Name":"pa","DisplayName":"Punjabi","TwoLetterISOLanguageName":"pa","ThreeLetterISOLanguageName":"pan"},{"Name":"quz","DisplayName":"Quechua","TwoLetterISOLanguageName":"quz","ThreeLetterISOLanguageName":"qub"},{"Name":"quz-EC","DisplayName":"Quechua (Ecuador)","TwoLetterISOLanguageName":"quz","ThreeLetterISOLanguageName":"que"},{"Name":"quz-PE","DisplayName":"Quechua (Peru)","TwoLetterISOLanguageName":"quz","ThreeLetterISOLanguageName":"qup"},{"Name":"ro","DisplayName":"Romanian","TwoLetterISOLanguageName":"ro","ThreeLetterISOLanguageName":"ron"},{"Name":"rm","DisplayName":"Romansh","TwoLetterISOLanguageName":"rm","ThreeLetterISOLanguageName":"roh"},{"Name":"ru","DisplayName":"Russian","TwoLetterISOLanguageName":"ru","ThreeLetterISOLanguageName":"rus"},{"Name":"sah","DisplayName":"Sakha","TwoLetterISOLanguageName":"sah","ThreeLetterISOLanguageName":"sah"},{"Name":"smn","DisplayName":"Sami (Inari)","TwoLetterISOLanguageName":"smn","ThreeLetterISOLanguageName":"smn"},{"Name":"smj","DisplayName":"Sami (Lule)","TwoLetterISOLanguageName":"smj","ThreeLetterISOLanguageName":"smj"},{"Name":"se","DisplayName":"Sami (Northern)","TwoLetterISOLanguageName":"se","ThreeLetterISOLanguageName":"sme"},{"Name":"sms","DisplayName":"Sami (Skolt)","TwoLetterISOLanguageName":"sms","ThreeLetterISOLanguageName":"sms"},{"Name":"sma","DisplayName":"Sami (Southern)","TwoLetterISOLanguageName":"sma","ThreeLetterISOLanguageName":"sma"},{"Name":"sa","DisplayName":"Sanskrit","TwoLetterISOLanguageName":"sa","ThreeLetterISOLanguageName":"san"},{"Name":"gd","DisplayName":"Scottish Gaelic","TwoLetterISOLanguageName":"gd","ThreeLetterISOLanguageName":"gla"},{"Name":"sr","DisplayName":"Serbian","TwoLetterISOLanguageName":"sr","ThreeLetterISOLanguageName":"srp"},{"Name":"sr-Cyrl-BA","DisplayName":"Serbian (Cyrillic, Bosnia and Herzegovina)","TwoLetterISOLanguageName":"sr","ThreeLetterISOLanguageName":"srn"},{"Name":"sr-Latn-BA","DisplayName":"Serbian (Latin, Bosnia and Herzegovina)","TwoLetterISOLanguageName":"sr","ThreeLetterISOLanguageName":"srs"},{"Name":"nso","DisplayName":"Sesotho sa Leboa","TwoLetterISOLanguageName":"nso","ThreeLetterISOLanguageName":"nso"},{"Name":"tn","DisplayName":"Setswana","TwoLetterISOLanguageName":"tn","ThreeLetterISOLanguageName":"tsn"},{"Name":"sd","DisplayName":"Sindhi","TwoLetterISOLanguageName":"sd","ThreeLetterISOLanguageName":"sin"},{"Name":"si","DisplayName":"Sinhala","TwoLetterISOLanguageName":"si","ThreeLetterISOLanguageName":"sin"},{"Name":"sk","DisplayName":"Slovak","TwoLetterISOLanguageName":"sk","ThreeLetterISOLanguageName":"slk"},{"Name":"sl","DisplayName":"Slovenian","TwoLetterISOLanguageName":"sl","ThreeLetterISOLanguageName":"slv"},{"Name":"so","DisplayName":"Somali","TwoLetterISOLanguageName":"so","ThreeLetterISOLanguageName":"som"},{"Name":"st","DisplayName":"Southern Sotho","TwoLetterISOLanguageName":"st","ThreeLetterISOLanguageName":"sot"},{"Name":"es","DisplayName":"Spanish","TwoLetterISOLanguageName":"es","ThreeLetterISOLanguageName":"spa"},{"Name":"zgh","DisplayName":"Standard Morrocan Tamazight","TwoLetterISOLanguageName":"zgh","ThreeLetterISOLanguageName":"zgh"},{"Name":"sv","DisplayName":"Swedish","TwoLetterISOLanguageName":"sv","ThreeLetterISOLanguageName":"swe"},{"Name":"syr","DisplayName":"Syriac","TwoLetterISOLanguageName":"syr","ThreeLetterISOLanguageName":"syr"},{"Name":"tg","DisplayName":"Tajik","TwoLetterISOLanguageName":"tg","ThreeLetterISOLanguageName":"tgk"},{"Name":"ta","DisplayName":"Tamil","TwoLetterISOLanguageName":"ta","ThreeLetterISOLanguageName":"tam"},{"Name":"tt","DisplayName":"Tatar","TwoLetterISOLanguageName":"tt","ThreeLetterISOLanguageName":"tat"},{"Name":"te","DisplayName":"Telugu","TwoLetterISOLanguageName":"te","ThreeLetterISOLanguageName":"tel"},{"Name":"th","DisplayName":"Thai","TwoLetterISOLanguageName":"th","ThreeLetterISOLanguageName":"tha"},{"Name":"bo","DisplayName":"Tibetan","TwoLetterISOLanguageName":"bo","ThreeLetterISOLanguageName":"bod"},{"Name":"ti","DisplayName":"Tigrinya","TwoLetterISOLanguageName":"ti","ThreeLetterISOLanguageName":"tir"},{"Name":"ts","DisplayName":"Tsonga","TwoLetterISOLanguageName":"ts","ThreeLetterISOLanguageName":"tso"},{"Name":"tr","DisplayName":"Turkish","TwoLetterISOLanguageName":"tr","ThreeLetterISOLanguageName":"tur"},{"Name":"tk","DisplayName":"Turkmen","TwoLetterISOLanguageName":"tk","ThreeLetterISOLanguageName":"tuk"},{"Name":"uk","DisplayName":"Ukrainian","TwoLetterISOLanguageName":"uk","ThreeLetterISOLanguageName":"ukr"},{"Name":"hsb","DisplayName":"Upper Sorbian","TwoLetterISOLanguageName":"hsb","ThreeLetterISOLanguageName":"hsb"},{"Name":"ur","DisplayName":"Urdu","TwoLetterISOLanguageName":"ur","ThreeLetterISOLanguageName":"urd"},{"Name":"ug","DisplayName":"Uyghur","TwoLetterISOLanguageName":"ug","ThreeLetterISOLanguageName":"uig"},{"Name":"uz","DisplayName":"Uzbek","TwoLetterISOLanguageName":"uz","ThreeLetterISOLanguageName":"uzb"},{"Name":"vi","DisplayName":"Vietnamese","TwoLetterISOLanguageName":"vi","ThreeLetterISOLanguageName":"vie"},{"Name":"cy","DisplayName":"Welsh","TwoLetterISOLanguageName":"cy","ThreeLetterISOLanguageName":"cym"},{"Name":"wo","DisplayName":"Wolof","TwoLetterISOLanguageName":"wo","ThreeLetterISOLanguageName":"wol"},{"Name":"ii","DisplayName":"Yi","TwoLetterISOLanguageName":"ii","ThreeLetterISOLanguageName":"iii"},{"Name":"yo","DisplayName":"Yoruba","TwoLetterISOLanguageName":"yo","ThreeLetterISOLanguageName":"yor"}] \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
index 6e5e58d26..3532ee370 100644
--- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
+++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
@@ -169,7 +169,6 @@
<Compile Include="Library\Validators\GenresValidator.cs" />
<Compile Include="Library\Validators\MusicGenresPostScanTask.cs" />
<Compile Include="Library\Validators\MusicGenresValidator.cs" />
- <Compile Include="Library\Validators\PeoplePostScanTask.cs" />
<Compile Include="Library\Validators\PeopleValidator.cs" />
<Compile Include="Library\Validators\StudiosPostScanTask.cs" />
<Compile Include="Library\Validators\StudiosValidator.cs" />
@@ -328,6 +327,8 @@
<EmbeddedResource Include="Localization\Server\ms.json" />
<EmbeddedResource Include="Localization\JavaScript\kk.json" />
<EmbeddedResource Include="Localization\Server\kk.json" />
+ <EmbeddedResource Include="Localization\countries.json" />
+ <EmbeddedResource Include="Localization\cultures.json" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs
index af400f850..c79d84e5a 100644
--- a/MediaBrowser.ServerApplication/ApplicationHost.cs
+++ b/MediaBrowser.ServerApplication/ApplicationHost.cs
@@ -31,11 +31,11 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.Themes;
using MediaBrowser.Dlna;
using MediaBrowser.Dlna.Eventing;
using MediaBrowser.Dlna.Main;
-using MediaBrowser.Dlna.PlayTo;
using MediaBrowser.Dlna.Server;
using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Encoder;
@@ -44,6 +44,7 @@ using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Updates;
using MediaBrowser.Providers.Manager;
+using MediaBrowser.Providers.Subtitles;
using MediaBrowser.Server.Implementations;
using MediaBrowser.Server.Implementations.Channels;
using MediaBrowser.Server.Implementations.Collections;
@@ -193,6 +194,7 @@ namespace MediaBrowser.ServerApplication
private IProviderRepository ProviderRepository { get; set; }
private INotificationManager NotificationManager { get; set; }
+ private ISubtitleManager SubtitleManager { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@@ -531,6 +533,9 @@ namespace MediaBrowser.ServerApplication
NotificationManager = new NotificationManager(LogManager, UserManager, ServerConfigurationManager);
RegisterSingleInstance(NotificationManager);
+ SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor);
+ RegisterSingleInstance(SubtitleManager);
+
var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false));
var itemsTask = Task.Run(async () => await ConfigureItemRepositories().ConfigureAwait(false));
var userdataTask = Task.Run(async () => await ConfigureUserDataRepositories().ConfigureAwait(false));
@@ -566,7 +571,7 @@ namespace MediaBrowser.ServerApplication
{
var info = await new FFMpegDownloader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager).GetFFMpegInfo(progress).ConfigureAwait(false);
- MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), ApplicationPaths, JsonSerializer, info.Path, info.ProbePath, info.Version, FileSystemManager);
+ MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), ApplicationPaths, JsonSerializer, info.EncoderPath, info.ProbePath, info.Version, FileSystemManager);
RegisterSingleInstance(MediaEncoder);
}
@@ -710,6 +715,8 @@ namespace MediaBrowser.ServerApplication
LiveTvManager.AddParts(GetExports<ILiveTvService>());
+ SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
+
SessionManager.AddParts(GetExports<ISessionControllerFactory>());
ChannelManager.AddParts(GetExports<IChannel>(), GetExports<IChannelFactory>());
diff --git a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs
index c4f529754..19aa7a684 100644
--- a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs
+++ b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs
@@ -4,6 +4,8 @@ using Mono.Unix.Native;
using System.Text.RegularExpressions;
using System.IO;
#endif
+using System.IO;
+using System.Text.RegularExpressions;
namespace MediaBrowser.ServerApplication.FFMpeg
{
@@ -32,7 +34,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
switch (arg)
{
case "Version":
- return "20140304";
+ return "20140506";
case "FFMpegFilename":
return "ffmpeg.exe";
case "FFProbeFilename":
@@ -42,7 +44,6 @@ namespace MediaBrowser.ServerApplication.FFMpeg
}
break;
- #if __MonoCS__
case PlatformID.Unix:
if (PlatformDetection.IsMac)
{
@@ -69,7 +70,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
switch (arg)
{
case "Version":
- return "20140304";
+ return "20140506";
case "FFMpegFilename":
return "ffmpeg";
case "FFProbeFilename":
@@ -85,7 +86,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
switch (arg)
{
case "Version":
- return "20140304";
+ return "20140505";
case "FFMpegFilename":
return "ffmpeg";
case "FFProbeFilename":
@@ -98,7 +99,6 @@ namespace MediaBrowser.ServerApplication.FFMpeg
}
// Unsupported Unix platform
return "";
-#endif
}
return "";
}
@@ -106,18 +106,17 @@ namespace MediaBrowser.ServerApplication.FFMpeg
private static string[] GetDownloadUrls()
{
var pid = Environment.OSVersion.Platform;
-
+
switch (pid)
{
case PlatformID.Win32NT:
return new[]
{
- "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20140304-git-f34cceb-win32-static.7z",
- "https://www.dropbox.com/s/6brdetuzbld93jk/ffmpeg-20140304-git-f34cceb-win32-static.7z?dl=1"
+ "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20140506-git-2baf1c8-win32-static.7z",
+ "https://www.dropbox.com/s/lxlzxs0r83iatsv/ffmpeg-20140506-git-2baf1c8-win32-static.7z?dl=1"
};
-
- #if __MonoCS__
- case PlatformID.Unix:
+
+ case PlatformID.Unix:
if (PlatformDetection.IsMac && PlatformDetection.IsX86_64)
{
return new[]
@@ -132,8 +131,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
{
return new[]
{
- "http://ffmpeg.gusari.org/static/32bit/ffmpeg.static.32bit.2014-03-04.tar.gz",
- "https://www.dropbox.com/s/0l76mcauqqkta31/ffmpeg.static.32bit.2014-03-04.tar.gz?dl=1"
+ "http://ffmpeg.gusari.org/static/32bit/ffmpeg.static.32bit.latest.tar.gz",
+ "https://www.dropbox.com/s/k9s02pv5to6slfb/ffmpeg.static.32bit.2014-05-06.tar.gz?dl=1"
};
}
@@ -141,22 +140,20 @@ namespace MediaBrowser.ServerApplication.FFMpeg
{
return new[]
{
- "http://ffmpeg.gusari.org/static/64bit/ffmpeg.static.64bit.2014-03-04.tar.gz",
- "https://www.dropbox.com/s/9wlxz440mdejuqe/ffmpeg.static.64bit.2014-03-04.tar.gz?dl=1"
+ "http://ffmpeg.gusari.org/static/64bit/ffmpeg.static.64bit.latest.tar.gz",
+ "https://www.dropbox.com/s/onuregwghywnzjo/ffmpeg.static.64bit.2014-05-05.tar.gz?dl=1"
};
}
}
//No Unix version available
- return new string[] {};
-#endif
+ return new string[] { };
}
- return new string[] {};
+ return new string[] { };
}
}
- #if __MonoCS__
public static class PlatformDetection
{
public readonly static bool IsWindows;
@@ -166,34 +163,52 @@ namespace MediaBrowser.ServerApplication.FFMpeg
public readonly static bool IsX86_64;
public readonly static bool IsArm;
- static PlatformDetection ()
+ static PlatformDetection()
{
IsWindows = Path.DirectorySeparatorChar == '\\';
//Don't call uname on windows
if (!IsWindows)
{
- Utsname uname;
- var callResult = Syscall.uname(out uname);
- if (callResult == 0)
- {
- IsMac = uname.sysname == "Darwin";
- IsLinux = !IsMac && uname.sysname == "Linux";
+ var uname = GetUnixName();
- Regex archX86 = new Regex("(i|I)[3-6]86");
- IsX86 = archX86.IsMatch(uname.machine);
- IsX86_64 = !IsX86 && uname.machine == "x86_64";
- IsArm = !IsX86 && !IsX86 && uname.machine.StartsWith("arm");
- }
+ IsMac = uname.sysname == "Darwin";
+ IsLinux = uname.sysname == "Linux";
+
+ var archX86 = new Regex("(i|I)[3-6]86");
+ IsX86 = archX86.IsMatch(uname.machine);
+ IsX86_64 = !IsX86 && uname.machine == "x86_64";
+ IsArm = !IsX86 && !IsX86_64 && uname.machine.StartsWith("arm");
}
else
{
- if (System.Environment.Is64BitOperatingSystem)
+ if (Environment.Is64BitOperatingSystem)
IsX86_64 = true;
else
IsX86 = true;
}
}
+
+ private static Uname GetUnixName()
+ {
+ var uname = new Uname();
+
+#if __MonoCS__
+ Utsname uname;
+ var callResult = Syscall.uname(out uname);
+ if (callResult == 0)
+ {
+ uname.sysname= uname.sysname;
+ uname.machine= uname.machine;
+ }
+#endif
+ return uname;
+ }
+ }
+
+ public class Uname
+ {
+ public string sysname = string.Empty;
+ public string machine = string.Empty;
}
- #endif
}
diff --git a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs
index b9c45e0d9..c550cb27f 100644
--- a/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs
+++ b/MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs
@@ -42,63 +42,86 @@ namespace MediaBrowser.ServerApplication.FFMpeg
public async Task<FFMpegInfo> GetFFMpegInfo(IProgress<double> progress)
{
- var versionedDirectoryPath = Path.Combine(GetMediaToolsPath(true), FFMpegDownloadInfo.Version);
+ var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
+ var versionedDirectoryPath = Path.Combine(rootEncoderPath, FFMpegDownloadInfo.Version);
var info = new FFMpegInfo
{
ProbePath = Path.Combine(versionedDirectoryPath, FFMpegDownloadInfo.FFProbeFilename),
- Path = Path.Combine(versionedDirectoryPath, FFMpegDownloadInfo.FFMpegFilename),
+ EncoderPath = Path.Combine(versionedDirectoryPath, FFMpegDownloadInfo.FFMpegFilename),
Version = FFMpegDownloadInfo.Version
};
Directory.CreateDirectory(versionedDirectoryPath);
- var tasks = new List<Task>();
-
- double ffmpegPercent = 0;
- double fontPercent = 0;
- var syncLock = new object();
-
- if (!File.Exists(info.ProbePath) || !File.Exists(info.Path))
+ if (!File.Exists(info.ProbePath) || !File.Exists(info.EncoderPath))
{
- var ffmpegProgress = new ActionableProgress<double>();
- ffmpegProgress.RegisterAction(p =>
- {
- ffmpegPercent = p;
+ // ffmpeg not present. See if there's an older version we can start with
+ var existingVersion = GetExistingVersion(info, rootEncoderPath);
- lock (syncLock)
- {
- progress.Report((ffmpegPercent / 2) + (fontPercent / 2));
- }
- });
+ // No older version. Need to download and block until complete
+ if (existingVersion == null)
+ {
+ await DownloadFFMpeg(versionedDirectoryPath, progress).ConfigureAwait(false);
+ }
+ else
+ {
+ // Older version found.
+ // Start with that. Download new version in the background.
+ var newPath = versionedDirectoryPath;
+ Task.Run(() => DownloadFFMpegInBackground(newPath));
- tasks.Add(DownloadFFMpeg(info, ffmpegProgress));
- }
- else
- {
- ffmpegPercent = 100;
- progress.Report(50);
+ info = existingVersion;
+ versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath);
+ }
}
- var fontProgress = new ActionableProgress<double>();
- fontProgress.RegisterAction(p =>
+ await DownloadFonts(versionedDirectoryPath).ConfigureAwait(false);
+
+ return info;
+ }
+
+ private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath)
+ {
+ var encoderFilename = Path.GetFileName(info.EncoderPath);
+ var probeFilename = Path.GetFileName(info.ProbePath);
+
+ foreach (var directory in Directory.EnumerateDirectories(rootEncoderPath, "*", SearchOption.TopDirectoryOnly)
+ .ToList())
{
- fontPercent = p;
+ var allFiles = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories).ToList();
- lock (syncLock)
+ var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase));
+ var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase));
+
+ if (!string.IsNullOrWhiteSpace(encoder) &&
+ !string.IsNullOrWhiteSpace(probe))
{
- progress.Report((ffmpegPercent / 2) + (fontPercent / 2));
+ return new FFMpegInfo
+ {
+ EncoderPath = encoder,
+ ProbePath = probe,
+ Version = Path.GetFileNameWithoutExtension(Path.GetDirectoryName(probe))
+ };
}
- });
-
- tasks.Add(DownloadFonts(versionedDirectoryPath, fontProgress));
+ }
- await Task.WhenAll(tasks).ConfigureAwait(false);
+ return null;
+ }
- return info;
+ private async void DownloadFFMpegInBackground(string directory)
+ {
+ try
+ {
+ await DownloadFFMpeg(directory, new Progress<double>()).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error downloading ffmpeg", ex);
+ }
}
- private async Task DownloadFFMpeg(FFMpegInfo info, IProgress<double> progress)
+ private async Task DownloadFFMpeg(string directory, IProgress<double> progress)
{
foreach (var url in FFMpegDownloadInfo.FfMpegUrls)
{
@@ -114,7 +137,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
}).ConfigureAwait(false);
- ExtractFFMpeg(tempFile, Path.GetDirectoryName(info.Path));
+ ExtractFFMpeg(tempFile, directory);
return;
}
catch (HttpException ex)
@@ -132,7 +155,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
private void ExtractFFMpeg(string tempFile, string targetFolder)
{
- _logger.Debug("Extracting ffmpeg from {0}", tempFile);
+ _logger.Info("Extracting ffmpeg from {0}", tempFile);
var tempFolder = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString());
@@ -171,6 +194,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
private void ExtractArchive(string archivePath, string targetPath)
{
+ _logger.Info("Extracting {0} to {1}", archivePath, targetPath);
+
if (string.Equals(FFMpegDownloadInfo.ArchiveType, "7z", StringComparison.OrdinalIgnoreCase))
{
_zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
@@ -182,6 +207,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
}
private void Extract7zArchive(string archivePath, string targetPath)
{
+ _logger.Info("Extracting {0} to {1}", archivePath, targetPath);
+
_zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
}
@@ -201,7 +228,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
/// Extracts the fonts.
/// </summary>
/// <param name="targetPath">The target path.</param>
- private async Task DownloadFonts(string targetPath, IProgress<double> progress)
+ /// <returns>Task.</returns>
+ private async Task DownloadFonts(string targetPath)
{
try
{
@@ -213,12 +241,19 @@ namespace MediaBrowser.ServerApplication.FFMpeg
var fontFile = Path.Combine(fontsDirectory, fontFilename);
- if (!File.Exists(fontFile))
+ if (File.Exists(fontFile))
{
- await DownloadFontFile(fontsDirectory, fontFilename, progress).ConfigureAwait(false);
+ await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false);
+ }
+ else
+ {
+ // Kick this off, but no need to wait on it
+ Task.Run(async () =>
+ {
+ await DownloadFontFile(fontsDirectory, fontFilename, new Progress<double>()).ConfigureAwait(false);
+ await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false);
+ });
}
-
- await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false);
}
catch (HttpException ex)
{
@@ -230,8 +265,6 @@ namespace MediaBrowser.ServerApplication.FFMpeg
// Don't let the server crash because of this
_logger.ErrorException("Error writing ffmpeg font files", ex);
}
-
- progress.Report(100);
}
/// <summary>
@@ -325,19 +358,5 @@ namespace MediaBrowser.ServerApplication.FFMpeg
}
}
}
-
- /// <summary>
- /// Gets the media tools path.
- /// </summary>
- /// <param name="create">if set to <c>true</c> [create].</param>
- /// <returns>System.String.</returns>
- private string GetMediaToolsPath(bool create)
- {
- var path = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
-
- Directory.CreateDirectory(path);
-
- return path;
- }
}
}
diff --git a/MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs b/MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs
index 147a9f771..1361277aa 100644
--- a/MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs
+++ b/MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs
@@ -9,7 +9,7 @@
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
- public string Path { get; set; }
+ public string EncoderPath { get; set; }
/// <summary>
/// Gets or sets the probe path.
/// </summary>
diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
index 4b9dad90a..3072413f9 100644
--- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
+++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
@@ -217,6 +217,9 @@
<Content Include="dashboard-ui\css\images\items\folders\channels.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\css\images\items\folders\folder.png">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\css\images\items\folders\games.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
diff --git a/OpenSubtitlesHandler/OpenSubtitles.cs b/OpenSubtitlesHandler/OpenSubtitles.cs
index ba3c461a1..5353586c8 100644
--- a/OpenSubtitlesHandler/OpenSubtitles.cs
+++ b/OpenSubtitlesHandler/OpenSubtitles.cs
@@ -20,6 +20,8 @@ using System;
using System.Text;
using System.Collections.Generic;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using OpenSubtitlesHandler.Console;
using XmlRpcHandler;
@@ -96,6 +98,56 @@ namespace OpenSubtitlesHandler
}
return new MethodResponseError("Fail", "Log in failed !");
}
+
+ public static async Task<IMethodResponse> LogInAsync(string userName, string password, string language, CancellationToken cancellationToken)
+ {
+ // Method call ..
+ List<IXmlRpcValue> parms = new List<IXmlRpcValue>();
+ parms.Add(new XmlRpcValueBasic(userName));
+ parms.Add(new XmlRpcValueBasic(password));
+ parms.Add(new XmlRpcValueBasic(language));
+ parms.Add(new XmlRpcValueBasic(XML_PRC_USERAGENT));
+ XmlRpcMethodCall call = new XmlRpcMethodCall("LogIn", parms);
+ OSHConsole.WriteLine("Sending LogIn request to the server ...", DebugCode.Good);
+
+ //File.WriteAllText(".\\request.txt", Encoding.UTF8.GetString(XmlRpcGenerator.Generate(call)));
+ // Send the request to the server
+ var stream = await Utilities.SendRequestAsync(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT, cancellationToken)
+ .ConfigureAwait(false);
+
+ string response = Utilities.GetStreamString(stream);
+
+ if (!response.Contains("ERROR:"))
+ {
+ // No error occur, get and decode the response. We expect Struct here.
+ XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response);
+ if (calls.Length > 0)
+ {
+ if (calls[0].Parameters.Count > 0)
+ {
+ XmlRpcValueStruct mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0];
+ MethodResponseLogIn re = new MethodResponseLogIn("Success", "Log in successful.");
+ foreach (XmlRpcStructMember MEMBER in mainStruct.Members)
+ {
+ switch (MEMBER.Name)
+ {
+ case "token": re.Token = TOKEN = MEMBER.Data.Data.ToString(); OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break;
+ case "seconds": re.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break;
+ case "status": re.Status = MEMBER.Data.Data.ToString(); OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break;
+ }
+ }
+ return re;
+ }
+ }
+ }
+ else
+ {
+ OSHConsole.WriteLine(response, DebugCode.Error);
+ return new MethodResponseError("Fail", response);
+ }
+ return new MethodResponseError("Fail", "Log in failed !");
+ }
+
/// <summary>
/// Log out from the server. Call this to terminate the session.
/// </summary>
diff --git a/OpenSubtitlesHandler/Utilities.cs b/OpenSubtitlesHandler/Utilities.cs
index 5c72f4fde..7f0f93009 100644
--- a/OpenSubtitlesHandler/Utilities.cs
+++ b/OpenSubtitlesHandler/Utilities.cs
@@ -24,6 +24,7 @@ using System.IO;
using System.IO.Compression;
using System.Net;
using System.Security.Cryptography;
+using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
@@ -161,7 +162,7 @@ namespace OpenSubtitlesHandler
/// <returns>Response of the server or stream of error message as string started with 'ERROR:' keyword.</returns>
public static Stream SendRequest(byte[] request, string userAgent)
{
- return SendRequestAsync(request, userAgent).Result;
+ return SendRequestAsync(request, userAgent, CancellationToken.None).Result;
//HttpWebRequest req = (HttpWebRequest)WebRequest.Create(XML_RPC_SERVER);
//req.ContentType = "text/xml";
@@ -190,16 +191,27 @@ namespace OpenSubtitlesHandler
//}
}
- public static async Task<Stream> SendRequestAsync(byte[] request, string userAgent)
+ public static async Task<Stream> SendRequestAsync(byte[] request, string userAgent, CancellationToken cancellationToken)
{
var options = new HttpRequestOptions
{
RequestContentBytes = request,
RequestContentType = "text/xml",
- UserAgent = "xmlrpc-epi-php/0.2 (PHP)",
- Url = XML_RPC_SERVER
+ UserAgent = userAgent,
+ Host = "api.opensubtitles.org:80",
+ Url = XML_RPC_SERVER,
+
+ // Response parsing will fail with this enabled
+ EnableHttpCompression = false,
+
+ CancellationToken = cancellationToken
};
+ if (string.IsNullOrEmpty(options.UserAgent))
+ {
+ options.UserAgent = "xmlrpc-epi-php/0.2 (PHP)";
+ }
+
var result = await HttpClient.Post(options).ConfigureAwait(false);
return result.Content;