beginning remote subtitle downloading

This commit is contained in:
Luke Pulverenti 2014-05-06 22:28:19 -04:00
parent e1dd361c7b
commit 0d025f7fb6
49 changed files with 1035 additions and 299 deletions

View file

@ -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)]

View file

@ -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);

View file

@ -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;

View file

@ -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)

View file

@ -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>

View file

@ -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;

View file

@ -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);
}

View file

@ -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)
{

View file

@ -52,6 +52,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>
@ -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
}
}

View file

@ -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" />

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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

View file

@ -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)

View file

@ -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">

View file

@ -4,6 +4,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
public interface ISubtitleParser
{
SubtitleInfo Parse(Stream stream);
SubtitleTrackInfo Parse(Stream stream);
}
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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>();
}

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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[] { };
}
}
}

View file

@ -159,7 +159,7 @@
public string DeviceId { get; set; }
}
public class SubtitleOptions
public class SubtitleDownloadOptions
{
/// <summary>
/// Gets or sets the item identifier.

View file

@ -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" />

View file

@ -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; }
}
}

View file

@ -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,

View file

@ -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" />

View file

@ -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);
}

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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))
// Avoid implicitly captured closure
var hasCopy = hash;
return 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();
.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,
if (!bestResult.Any())
{
_logger.Debug("No Subtitles");
return response;
}
Id = i.SubFormat + "-" + i.SubLanguageID + "-" + i.IDSubtitle,
_logger.Debug("Found " + bestResult.Count + " subtitles.");
var subtitle = bestResult.First();
var downloadsList = new[] { int.Parse(subtitle.IDSubtitleFile) };
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;
Name = i.SubFileName,
DateCreated = DateTime.Parse(i.SubAddDate, _usCulture),
IsHashMatch = i.MovieHash == hasCopy
});
}
}
}

View file

@ -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);
}
}
}

View file

@ -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)

View file

@ -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;
}
}
}

View file

@ -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>

View file

@ -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);
}
}
}

View file

@ -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>

View file

@ -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."
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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>

View file

@ -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>());

View file

@ -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
}

View file

@ -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 =>
// ffmpeg not present. See if there's an older version we can start with
var existingVersion = GetExistingVersion(info, rootEncoderPath);
// No older version. Need to download and block until complete
if (existingVersion == null)
{
ffmpegPercent = p;
lock (syncLock)
{
progress.Report((ffmpegPercent / 2) + (fontPercent / 2));
}
});
tasks.Add(DownloadFFMpeg(info, ffmpegProgress));
}
else
{
ffmpegPercent = 100;
progress.Report(50);
}
var fontProgress = new ActionableProgress<double>();
fontProgress.RegisterAction(p =>
{
fontPercent = p;
lock (syncLock)
{
progress.Report((ffmpegPercent / 2) + (fontPercent / 2));
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(DownloadFonts(versionedDirectoryPath, fontProgress));
info = existingVersion;
versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath);
}
}
await Task.WhenAll(tasks).ConfigureAwait(false);
await DownloadFonts(versionedDirectoryPath).ConfigureAwait(false);
return info;
}
private async Task DownloadFFMpeg(FFMpegInfo info, IProgress<double> progress)
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())
{
var allFiles = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories).ToList();
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))
{
return new FFMpegInfo
{
EncoderPath = encoder,
ProbePath = probe,
Version = Path.GetFileNameWithoutExtension(Path.GetDirectoryName(probe))
};
}
}
return null;
}
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(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;
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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;