commit
9926be0d9d
|
@ -132,6 +132,7 @@ namespace MediaBrowser.Api
|
|||
/// Called when [transcode beginning].
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="streamId">The stream identifier.</param>
|
||||
/// <param name="transcodingJobId">The transcoding job identifier.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="process">The process.</param>
|
||||
|
@ -140,6 +141,7 @@ namespace MediaBrowser.Api
|
|||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
||||
/// <returns>TranscodingJob.</returns>
|
||||
public TranscodingJob OnTranscodeBeginning(string path,
|
||||
string streamId,
|
||||
string transcodingJobId,
|
||||
TranscodingJobType type,
|
||||
Process process,
|
||||
|
@ -157,7 +159,8 @@ namespace MediaBrowser.Api
|
|||
ActiveRequestCount = 1,
|
||||
DeviceId = deviceId,
|
||||
CancellationTokenSource = cancellationTokenSource,
|
||||
Id = transcodingJobId
|
||||
Id = transcodingJobId,
|
||||
StreamId = streamId
|
||||
};
|
||||
|
||||
_activeTranscodingJobs.Add(job);
|
||||
|
@ -316,17 +319,26 @@ namespace MediaBrowser.Api
|
|||
/// Kills the single transcoding job.
|
||||
/// </summary>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <param name="streamId">The stream identifier.</param>
|
||||
/// <param name="deleteFiles">The delete files.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="ArgumentNullException">deviceId</exception>
|
||||
internal void KillTranscodingJobs(string deviceId, Func<string, bool> deleteFiles)
|
||||
internal void KillTranscodingJobs(string deviceId, string streamId, Func<string, bool> deleteFiles)
|
||||
{
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
throw new ArgumentNullException("deviceId");
|
||||
}
|
||||
|
||||
KillTranscodingJobs(j => string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase), deleteFiles);
|
||||
KillTranscodingJobs(j =>
|
||||
{
|
||||
if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(streamId) || string.Equals(streamId, j.StreamId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}, deleteFiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -335,7 +347,7 @@ namespace MediaBrowser.Api
|
|||
/// <param name="killJob">The kill job.</param>
|
||||
/// <param name="deleteFiles">The delete files.</param>
|
||||
/// <returns>Task.</returns>
|
||||
internal void KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles)
|
||||
private void KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles)
|
||||
{
|
||||
var jobs = new List<TranscodingJob>();
|
||||
|
||||
|
@ -516,6 +528,11 @@ namespace MediaBrowser.Api
|
|||
/// </summary>
|
||||
public class TranscodingJob
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the stream identifier.
|
||||
/// </summary>
|
||||
/// <value>The stream identifier.</value>
|
||||
public string StreamId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using MediaBrowser.Controller.Dto;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -72,6 +73,29 @@ namespace MediaBrowser.Api
|
|||
return ResultFactory.GetOptimizedResultUsingCache(Request, cacheKey, lastDateModified, cacheDuration, factoryFn);
|
||||
}
|
||||
|
||||
protected void AssertCanUpdateUser(IUserManager userManager, string userId)
|
||||
{
|
||||
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||
|
||||
var authenticatedUser = userManager.GetUserById(auth.UserId);
|
||||
|
||||
// If they're going to update the record of another user, they must be an administrator
|
||||
if (!string.Equals(userId, auth.UserId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!authenticatedUser.Policy.IsAdministrator)
|
||||
{
|
||||
throw new SecurityException("Unauthorized access.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!authenticatedUser.Policy.EnableUserPreferenceAccess)
|
||||
{
|
||||
throw new SecurityException("Unauthorized access.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To the optimized serialized result using cache.
|
||||
/// </summary>
|
||||
|
@ -88,9 +112,9 @@ namespace MediaBrowser.Api
|
|||
/// Gets the session.
|
||||
/// </summary>
|
||||
/// <returns>SessionInfo.</returns>
|
||||
protected SessionInfo GetSession()
|
||||
protected async Task<SessionInfo> GetSession()
|
||||
{
|
||||
var session = SessionContext.GetSession(Request);
|
||||
var session = await SessionContext.GetSession(Request).ConfigureAwait(false);
|
||||
|
||||
if (session == null)
|
||||
{
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using MediaBrowser.Common.Extensions;
|
||||
using System;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Connect;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Connect;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using ServiceStack;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
@ -73,6 +75,28 @@ namespace MediaBrowser.Api
|
|||
public string ConnectUserId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Connect/Supporters", "GET")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class GetConnectSupporterSummary : IReturn<ConnectSupporterSummary>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Connect/Supporters", "DELETE")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class RemoveConnectSupporter : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Connect/Supporters", "POST")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class AddConnectSupporter : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
public class ConnectService : BaseApiService
|
||||
{
|
||||
private readonly IConnectManager _connectManager;
|
||||
|
@ -84,6 +108,35 @@ namespace MediaBrowser.Api
|
|||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetConnectSupporterSummary request)
|
||||
{
|
||||
var result = await _connectManager.GetConnectSupporterSummary().ConfigureAwait(false);
|
||||
var existingConnectUserIds = result.Users.Select(i => i.Id).ToList();
|
||||
|
||||
result.EligibleUsers = _userManager.Users
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i.ConnectUserId))
|
||||
.Where(i => !existingConnectUserIds.Contains(i.ConnectUserId, StringComparer.OrdinalIgnoreCase))
|
||||
.OrderBy(i => i.Name)
|
||||
.Select(i => _userManager.GetUserDto(i))
|
||||
.ToList();
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public void Delete(RemoveConnectSupporter request)
|
||||
{
|
||||
var task = _connectManager.RemoveConnectSupporter(request.Id);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public void Post(AddConnectSupporter request)
|
||||
{
|
||||
var task = _connectManager.AddConnectSupporter(request.Id);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public object Post(CreateConnectLink request)
|
||||
{
|
||||
return _connectManager.LinkUser(request.Id, request.ConnectUsername);
|
||||
|
|
|
@ -56,7 +56,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// Class UpdateItemImageIndex
|
||||
/// </summary>
|
||||
[Route("/Items/{Id}/Images/{Type}/{Index}/Index", "POST", Summary = "Updates the index for an item image")]
|
||||
[Authenticated]
|
||||
[Authenticated(Roles = "admin")]
|
||||
public class UpdateItemImageIndex : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -64,7 +64,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public Guid Id { get; set; }
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the image.
|
||||
|
@ -143,7 +143,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public Guid Id { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -151,7 +151,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// </summary>
|
||||
[Route("/Items/{Id}/Images/{Type}", "DELETE")]
|
||||
[Route("/Items/{Id}/Images/{Type}/{Index}", "DELETE")]
|
||||
[Authenticated]
|
||||
[Authenticated(Roles = "admin")]
|
||||
public class DeleteItemImage : DeleteImageRequest, IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -159,7 +159,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public Guid Id { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -175,7 +175,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public Guid Id { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -191,7 +191,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
[ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public Guid Id { get; set; }
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw Http Request Input Stream
|
||||
|
@ -206,7 +206,7 @@ namespace MediaBrowser.Api.Images
|
|||
[Route("/Items/{Id}/Images/{Type}", "POST")]
|
||||
[Route("/Items/{Id}/Images/{Type}/{Index}", "POST")]
|
||||
[Api(Description = "Posts an item image")]
|
||||
[Authenticated]
|
||||
[Authenticated(Roles = "admin")]
|
||||
public class PostItemImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -318,7 +318,7 @@ namespace MediaBrowser.Api.Images
|
|||
|
||||
try
|
||||
{
|
||||
var size = _imageProcessor.GetImageSize(info.Path, info.DateModified);
|
||||
var size = _imageProcessor.GetImageSize(info);
|
||||
|
||||
width = Convert.ToInt32(size.Width);
|
||||
height = Convert.ToInt32(size.Height);
|
||||
|
@ -417,11 +417,12 @@ namespace MediaBrowser.Api.Images
|
|||
/// <param name="request">The request.</param>
|
||||
public void Post(PostUserImage request)
|
||||
{
|
||||
var id = new Guid(GetPathValue(1));
|
||||
var userId = GetPathValue(1);
|
||||
AssertCanUpdateUser(_userManager, userId);
|
||||
|
||||
request.Type = (ImageType)Enum.Parse(typeof(ImageType), GetPathValue(3), true);
|
||||
|
||||
var item = _userManager.GetUserById(id);
|
||||
var item = _userManager.GetUserById(userId);
|
||||
|
||||
var task = PostImage(item, request.RequestStream, request.Type, Request.ContentType);
|
||||
|
||||
|
@ -434,7 +435,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// <param name="request">The request.</param>
|
||||
public void Post(PostItemImage request)
|
||||
{
|
||||
var id = new Guid(GetPathValue(1));
|
||||
var id = GetPathValue(1);
|
||||
|
||||
request.Type = (ImageType)Enum.Parse(typeof(ImageType), GetPathValue(3), true);
|
||||
|
||||
|
@ -451,7 +452,10 @@ namespace MediaBrowser.Api.Images
|
|||
/// <param name="request">The request.</param>
|
||||
public void Delete(DeleteUserImage request)
|
||||
{
|
||||
var item = _userManager.GetUserById(request.Id);
|
||||
var userId = request.Id;
|
||||
AssertCanUpdateUser(_userManager, userId);
|
||||
|
||||
var item = _userManager.GetUserById(userId);
|
||||
|
||||
var task = item.DeleteImage(request.Type, request.Index ?? 0);
|
||||
|
||||
|
@ -492,7 +496,6 @@ namespace MediaBrowser.Api.Images
|
|||
/// <param name="currentIndex">Index of the current.</param>
|
||||
/// <param name="newIndex">The new index.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.ArgumentException">The change index operation is only applicable to backdrops and screenshots</exception>
|
||||
private Task UpdateItemIndex(IHasImages item, ImageType type, int currentIndex, int newIndex)
|
||||
{
|
||||
return item.SwapImages(type, currentIndex, newIndex);
|
||||
|
|
|
@ -200,24 +200,15 @@ namespace MediaBrowser.Api
|
|||
//}
|
||||
item.ProviderIds = request.ProviderIds;
|
||||
|
||||
var service = new ItemRefreshService(_libraryManager)
|
||||
var task = _providerManager.RefreshFullItem(item, new MetadataRefreshOptions
|
||||
{
|
||||
Logger = Logger,
|
||||
Request = Request,
|
||||
ResultFactory = ResultFactory,
|
||||
SessionContext = SessionContext,
|
||||
AuthorizationContext = AuthorizationContext
|
||||
};
|
||||
|
||||
service.Post(new RefreshItem
|
||||
{
|
||||
Id = request.Id,
|
||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||
ImageRefreshMode = ImageRefreshMode.FullRefresh,
|
||||
ReplaceAllMetadata = true,
|
||||
ReplaceAllImages = request.ReplaceAllImages,
|
||||
Recursive = true
|
||||
});
|
||||
ReplaceAllImages = request.ReplaceAllImages
|
||||
|
||||
}, CancellationToken.None);
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Api
|
||||
{
|
||||
|
@ -40,41 +34,12 @@ namespace MediaBrowser.Api
|
|||
public class ItemRefreshService : BaseApiService
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
|
||||
public ItemRefreshService(ILibraryManager libraryManager)
|
||||
public ItemRefreshService(ILibraryManager libraryManager, IProviderManager providerManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
private async Task RefreshArtist(RefreshItem request, MusicArtist item)
|
||||
{
|
||||
var cancellationToken = CancellationToken.None;
|
||||
|
||||
var albums = _libraryManager.RootFolder
|
||||
.GetRecursiveChildren()
|
||||
.OfType<MusicAlbum>()
|
||||
.Where(i => i.HasArtist(item.Name))
|
||||
.ToList();
|
||||
|
||||
var musicArtists = albums
|
||||
.Select(i => i.Parent)
|
||||
.OfType<MusicArtist>()
|
||||
.ToList();
|
||||
|
||||
var options = GetRefreshOptions(request);
|
||||
|
||||
var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress<double>(), cancellationToken, options, true));
|
||||
|
||||
await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error refreshing library", ex);
|
||||
}
|
||||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -85,68 +50,9 @@ namespace MediaBrowser.Api
|
|||
{
|
||||
var item = _libraryManager.GetItemById(request.Id);
|
||||
|
||||
var task = item is MusicArtist ? RefreshArtist(request, (MusicArtist)item) : RefreshItem(request, item);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the item.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task RefreshItem(RefreshItem request, BaseItem item)
|
||||
{
|
||||
var options = GetRefreshOptions(request);
|
||||
|
||||
try
|
||||
{
|
||||
await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (item.IsFolder)
|
||||
{
|
||||
// Collection folders don't validate their children so we'll have to simulate that here
|
||||
var collectionFolder = item as CollectionFolder;
|
||||
|
||||
if (collectionFolder != null)
|
||||
{
|
||||
await RefreshCollectionFolderChildren(request, collectionFolder).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var folder = (Folder)item;
|
||||
|
||||
await folder.ValidateChildren(new Progress<double>(), CancellationToken.None, options, request.Recursive).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error refreshing library", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the collection folder children.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="collectionFolder">The collection folder.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task RefreshCollectionFolderChildren(RefreshItem request, CollectionFolder collectionFolder)
|
||||
{
|
||||
var options = GetRefreshOptions(request);
|
||||
|
||||
foreach (var child in collectionFolder.Children.ToList())
|
||||
{
|
||||
await child.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (child.IsFolder)
|
||||
{
|
||||
var folder = (Folder)child;
|
||||
|
||||
await folder.ValidateChildren(new Progress<double>(), CancellationToken.None, options, request.Recursive).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
_providerManager.QueueRefresh(item.Id, options);
|
||||
}
|
||||
|
||||
private MetadataRefreshOptions GetRefreshOptions(BaseRefreshRequest request)
|
||||
|
|
|
@ -41,8 +41,8 @@ namespace MediaBrowser.Api
|
|||
[ApiMember(Name = "ContentType", Description = "The content type of the item", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string ContentType { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
|
||||
[Authenticated(Roles = "admin")]
|
||||
public class ItemUpdateService : BaseApiService
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
@ -61,7 +61,7 @@ namespace MediaBrowser.Api
|
|||
public object Get(GetMetadataEditorInfo request)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(request.ItemId);
|
||||
|
||||
|
||||
var info = new MetadataEditorInfo
|
||||
{
|
||||
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(),
|
||||
|
@ -131,7 +131,7 @@ namespace MediaBrowser.Api
|
|||
Value = ""
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "FolderTypeMovies",
|
||||
|
@ -389,20 +389,33 @@ namespace MediaBrowser.Api
|
|||
game.PlayersSupported = request.Players;
|
||||
}
|
||||
|
||||
var song = item as Audio;
|
||||
var hasAlbumArtists = item as IHasAlbumArtist;
|
||||
if (hasAlbumArtists != null)
|
||||
{
|
||||
hasAlbumArtists.AlbumArtists = request
|
||||
.AlbumArtists
|
||||
.Select(i => i.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var hasArtists = item as IHasArtist;
|
||||
if (hasArtists != null)
|
||||
{
|
||||
hasArtists.Artists = request
|
||||
.ArtistItems
|
||||
.Select(i => i.Name)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var song = item as Audio;
|
||||
if (song != null)
|
||||
{
|
||||
song.Album = request.Album;
|
||||
song.AlbumArtists = string.IsNullOrWhiteSpace(request.AlbumArtist) ? new List<string>() : new List<string> { request.AlbumArtist };
|
||||
song.Artists = request.Artists.ToList();
|
||||
}
|
||||
|
||||
var musicVideo = item as MusicVideo;
|
||||
|
||||
if (musicVideo != null)
|
||||
{
|
||||
musicVideo.Artists = request.Artists.ToList();
|
||||
musicVideo.Album = request.Album;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using MediaBrowser.Controller.Activity;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
|
@ -9,11 +8,9 @@ using MediaBrowser.Controller.Library;
|
|||
using MediaBrowser.Controller.Localization;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
|
|
|
@ -212,24 +212,26 @@ namespace MediaBrowser.Api.Library
|
|||
|
||||
File.Create(path);
|
||||
}
|
||||
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (!request.RefreshLibrary)
|
||||
Task.Run(() =>
|
||||
{
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
}
|
||||
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,24 +281,26 @@ namespace MediaBrowser.Api.Library
|
|||
}
|
||||
|
||||
Directory.Move(currentPath, newPath);
|
||||
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (!request.RefreshLibrary)
|
||||
Task.Run(() =>
|
||||
{
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
}
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,24 +329,26 @@ namespace MediaBrowser.Api.Library
|
|||
try
|
||||
{
|
||||
_fileSystem.DeleteDirectory(path, true);
|
||||
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var delayTask = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(delayTask);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (!request.RefreshLibrary)
|
||||
Task.Run(() =>
|
||||
{
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
}
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,24 +368,26 @@ namespace MediaBrowser.Api.Library
|
|||
try
|
||||
{
|
||||
LibraryHelpers.AddMediaPath(_fileSystem, request.Name, request.Path, _appPaths);
|
||||
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (!request.RefreshLibrary)
|
||||
Task.Run(() =>
|
||||
{
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
}
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -399,24 +407,26 @@ namespace MediaBrowser.Api.Library
|
|||
try
|
||||
{
|
||||
LibraryHelpers.RemoveMediaPath(_fileSystem, request.Name, request.Path, _appPaths);
|
||||
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (!request.RefreshLibrary)
|
||||
Task.Run(() =>
|
||||
{
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
}
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,6 +171,9 @@ namespace MediaBrowser.Api.LiveTv
|
|||
[ApiMember(Name = "MinStartDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
|
||||
public string MinStartDate { get; set; }
|
||||
|
||||
[ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool? HasAired { get; set; }
|
||||
|
||||
[ApiMember(Name = "MaxStartDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
|
||||
public string MaxStartDate { get; set; }
|
||||
|
||||
|
@ -179,6 +182,24 @@ namespace MediaBrowser.Api.LiveTv
|
|||
|
||||
[ApiMember(Name = "MaxEndDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
|
||||
public string MaxEndDate { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
|
||||
public bool? IsMovie { get; set; }
|
||||
|
||||
[ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
[ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
[ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Name, StartDate", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string SortBy { get; set; }
|
||||
|
||||
[ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public SortOrder? SortOrder { get; set; }
|
||||
|
||||
[ApiMember(Name = "Genres", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")]
|
||||
public string Genres { get; set; }
|
||||
}
|
||||
|
||||
[Route("/LiveTv/Programs/Recommended", "GET", Summary = "Gets available live tv epgs..")]
|
||||
|
@ -196,6 +217,9 @@ namespace MediaBrowser.Api.LiveTv
|
|||
|
||||
[ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool? HasAired { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool? IsMovie { get; set; }
|
||||
}
|
||||
|
||||
[Route("/LiveTv/Programs/{Id}", "GET", Summary = "Gets a live tv program")]
|
||||
|
@ -312,7 +336,7 @@ namespace MediaBrowser.Api.LiveTv
|
|||
|
||||
private void AssertUserCanManageLiveTv()
|
||||
{
|
||||
var user = SessionContext.GetUser(Request);
|
||||
var user = SessionContext.GetUser(Request).Result;
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
|
@ -368,8 +392,9 @@ namespace MediaBrowser.Api.LiveTv
|
|||
{
|
||||
var query = new ProgramQuery
|
||||
{
|
||||
ChannelIdList = (request.ChannelIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToArray(),
|
||||
UserId = request.UserId
|
||||
ChannelIds = (request.ChannelIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToArray(),
|
||||
UserId = request.UserId,
|
||||
HasAired = request.HasAired
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(request.MinStartDate))
|
||||
|
@ -392,6 +417,13 @@ namespace MediaBrowser.Api.LiveTv
|
|||
query.MaxEndDate = DateTime.Parse(request.MaxEndDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
|
||||
}
|
||||
|
||||
query.StartIndex = request.StartIndex;
|
||||
query.Limit = request.Limit;
|
||||
query.SortBy = (request.SortBy ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
query.SortOrder = request.SortOrder;
|
||||
query.IsMovie = request.IsMovie;
|
||||
query.Genres = (request.Genres ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var result = await _liveTvManager.GetPrograms(query, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
|
@ -404,7 +436,8 @@ namespace MediaBrowser.Api.LiveTv
|
|||
UserId = request.UserId,
|
||||
IsAiring = request.IsAiring,
|
||||
Limit = request.Limit,
|
||||
HasAired = request.HasAired
|
||||
HasAired = request.HasAired,
|
||||
IsMovie = request.IsMovie
|
||||
};
|
||||
|
||||
var result = await _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).ConfigureAwait(false);
|
||||
|
|
|
@ -79,8 +79,10 @@
|
|||
<Compile Include="FilterService.cs" />
|
||||
<Compile Include="IHasDtoOptions.cs" />
|
||||
<Compile Include="Library\ChapterService.cs" />
|
||||
<Compile Include="Playback\Hls\MpegDashService.cs" />
|
||||
<Compile Include="Playback\Dash\ManifestBuilder.cs" />
|
||||
<Compile Include="Playback\Dash\MpegDashService.cs" />
|
||||
<Compile Include="Playback\MediaInfoService.cs" />
|
||||
<Compile Include="Playback\TranscodingThrottler.cs" />
|
||||
<Compile Include="PlaylistService.cs" />
|
||||
<Compile Include="Reports\ReportFieldType.cs" />
|
||||
<Compile Include="Reports\ReportResult.cs" />
|
||||
|
@ -134,6 +136,7 @@
|
|||
<Compile Include="SearchService.cs" />
|
||||
<Compile Include="Session\SessionsService.cs" />
|
||||
<Compile Include="SimilarItemsHelper.cs" />
|
||||
<Compile Include="Sync\SyncHelper.cs" />
|
||||
<Compile Include="Sync\SyncJobWebSocketListener.cs" />
|
||||
<Compile Include="Sync\SyncJobsWebSocketListener.cs" />
|
||||
<Compile Include="Sync\SyncService.cs" />
|
||||
|
|
|
@ -77,15 +77,13 @@ namespace MediaBrowser.Api.Music
|
|||
var album1 = (MusicAlbum)item1;
|
||||
var album2 = (MusicAlbum)item2;
|
||||
|
||||
var artists1 = album1.GetRecursiveChildren(i => i is IHasArtist)
|
||||
.Cast<IHasArtist>()
|
||||
.SelectMany(i => i.AllArtists)
|
||||
var artists1 = album1
|
||||
.AllArtists
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var artists2 = album2.GetRecursiveChildren(i => i is IHasArtist)
|
||||
.Cast<IHasArtist>()
|
||||
.SelectMany(i => i.AllArtists)
|
||||
var artists2 = album2
|
||||
.AllArtists
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Diagnostics;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -14,6 +14,7 @@ using MediaBrowser.Model.Dlna;
|
|||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using System;
|
||||
|
@ -68,17 +69,21 @@ namespace MediaBrowser.Api.Playback
|
|||
protected ILiveTvManager LiveTvManager { get; private set; }
|
||||
protected IDlnaManager DlnaManager { get; private set; }
|
||||
protected IDeviceManager DeviceManager { get; private set; }
|
||||
protected IChannelManager ChannelManager { get; private set; }
|
||||
protected ISubtitleEncoder SubtitleEncoder { get; private set; }
|
||||
protected IProcessManager ProcessManager { get; private set; }
|
||||
protected IMediaSourceManager MediaSourceManager { get; private set; }
|
||||
protected IZipClient ZipClient { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
|
||||
/// </summary>
|
||||
protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager)
|
||||
protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient)
|
||||
{
|
||||
ZipClient = zipClient;
|
||||
MediaSourceManager = mediaSourceManager;
|
||||
ProcessManager = processManager;
|
||||
DeviceManager = deviceManager;
|
||||
SubtitleEncoder = subtitleEncoder;
|
||||
ChannelManager = channelManager;
|
||||
DlnaManager = dlnaManager;
|
||||
LiveTvManager = liveTvManager;
|
||||
FileSystem = fileSystem;
|
||||
|
@ -129,9 +134,21 @@ namespace MediaBrowser.Api.Playback
|
|||
var data = GetCommandLineArguments("dummy\\dummy", "dummyTranscodingId", state, false);
|
||||
|
||||
data += "-" + (state.Request.DeviceId ?? string.Empty);
|
||||
data += "-" + (state.Request.ClientTime ?? string.Empty);
|
||||
data += "-" + (state.Request.StreamId ?? state.Request.ClientTime ?? string.Empty);
|
||||
|
||||
return Path.Combine(folder, data.GetMD5().ToString("N") + (outputFileExtension ?? string.Empty).ToLower());
|
||||
var dataHash = data.GetMD5().ToString("N");
|
||||
|
||||
if (EnableOutputInSubFolder)
|
||||
{
|
||||
return Path.Combine(folder, dataHash, dataHash + (outputFileExtension ?? string.Empty).ToLower());
|
||||
}
|
||||
|
||||
return Path.Combine(folder, dataHash + (outputFileExtension ?? string.Empty).ToLower());
|
||||
}
|
||||
|
||||
protected virtual bool EnableOutputInSubFolder
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
@ -877,14 +894,6 @@ namespace MediaBrowser.Api.Playback
|
|||
return "copy";
|
||||
}
|
||||
|
||||
private bool SupportsThrottleWithStream
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input argument.
|
||||
/// </summary>
|
||||
|
@ -908,23 +917,15 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
private string GetInputPathArgument(string transcodingJobId, StreamState state)
|
||||
{
|
||||
if (state.InputProtocol == MediaProtocol.File &&
|
||||
state.RunTimeTicks.HasValue &&
|
||||
state.VideoType == VideoType.VideoFile &&
|
||||
!string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
|
||||
{
|
||||
if (SupportsThrottleWithStream)
|
||||
{
|
||||
var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/videos/" + state.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + state.Request.MediaSourceId;
|
||||
|
||||
url += "&transcodingJobId=" + transcodingJobId;
|
||||
|
||||
return string.Format("\"{0}\"", url);
|
||||
}
|
||||
}
|
||||
}
|
||||
//if (state.InputProtocol == MediaProtocol.File &&
|
||||
// state.RunTimeTicks.HasValue &&
|
||||
// state.VideoType == VideoType.VideoFile &&
|
||||
// !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
//{
|
||||
// if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
|
||||
// {
|
||||
// }
|
||||
//}
|
||||
|
||||
var protocol = state.InputProtocol;
|
||||
|
||||
|
@ -1053,6 +1054,7 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
|
||||
var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
|
||||
state.Request.StreamId ?? state.Request.ClientTime,
|
||||
transcodingId,
|
||||
TranscodingJobType,
|
||||
process,
|
||||
|
@ -1094,7 +1096,7 @@ namespace MediaBrowser.Api.Playback
|
|||
StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream);
|
||||
|
||||
// Wait for the file to exist before proceeeding
|
||||
while (!File.Exists(outputPath) && !transcodingJob.HasExited)
|
||||
while (!File.Exists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
|
||||
{
|
||||
await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
@ -1109,9 +1111,26 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
}
|
||||
|
||||
StartThrottler(state, transcodingJob);
|
||||
|
||||
return transcodingJob;
|
||||
}
|
||||
|
||||
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
|
||||
{
|
||||
if (state.InputProtocol == MediaProtocol.File &&
|
||||
state.RunTimeTicks.HasValue &&
|
||||
state.VideoType == VideoType.VideoFile &&
|
||||
!string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
|
||||
{
|
||||
state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ProcessManager);
|
||||
state.TranscodingThrottler.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target)
|
||||
{
|
||||
try
|
||||
|
@ -1505,7 +1524,7 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
else if (i == 16)
|
||||
{
|
||||
request.ClientTime = val;
|
||||
request.StreamId = val;
|
||||
}
|
||||
else if (i == 17)
|
||||
{
|
||||
|
@ -1640,6 +1659,9 @@ namespace MediaBrowser.Api.Playback
|
|||
List<MediaStream> mediaStreams = null;
|
||||
|
||||
state.ItemType = item.GetType().Name;
|
||||
state.ItemId = item.Id.ToString("N");
|
||||
var archivable = item as IArchivable;
|
||||
state.IsInputArchive = archivable != null && archivable.IsArchive;
|
||||
|
||||
if (item is ILiveTvRecording)
|
||||
{
|
||||
|
@ -1653,7 +1675,7 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
var source = string.IsNullOrEmpty(request.MediaSourceId)
|
||||
? recording.GetMediaSources(false).First()
|
||||
: recording.GetMediaSources(false).First(i => string.Equals(i.Id, request.MediaSourceId));
|
||||
: MediaSourceManager.GetStaticMediaSource(recording, request.MediaSourceId, false);
|
||||
|
||||
mediaStreams = source.MediaStreams;
|
||||
|
||||
|
@ -1692,25 +1714,13 @@ namespace MediaBrowser.Api.Playback
|
|||
// Just to prevent this from being null and causing other methods to fail
|
||||
state.MediaPath = string.Empty;
|
||||
}
|
||||
else if (item is IChannelMediaItem)
|
||||
{
|
||||
var mediaSource = await GetChannelMediaInfo(request.Id, request.MediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
|
||||
state.InputProtocol = mediaSource.Protocol;
|
||||
state.MediaPath = mediaSource.Path;
|
||||
state.RunTimeTicks = item.RunTimeTicks;
|
||||
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
|
||||
state.InputBitrate = mediaSource.Bitrate;
|
||||
state.InputFileSize = mediaSource.Size;
|
||||
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
|
||||
mediaStreams = mediaSource.MediaStreams;
|
||||
}
|
||||
else
|
||||
{
|
||||
var hasMediaSources = (IHasMediaSources)item;
|
||||
var mediaSources = await MediaSourceManager.GetPlayackMediaSources(request.Id, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
|
||||
? hasMediaSources.GetMediaSources(false).First()
|
||||
: hasMediaSources.GetMediaSources(false).First(i => string.Equals(i.Id, request.MediaSourceId));
|
||||
? mediaSources.First()
|
||||
: mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
|
||||
|
||||
mediaStreams = mediaSource.MediaStreams;
|
||||
|
||||
|
@ -1720,6 +1730,8 @@ namespace MediaBrowser.Api.Playback
|
|||
state.InputFileSize = mediaSource.Size;
|
||||
state.InputBitrate = mediaSource.Bitrate;
|
||||
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
|
||||
state.RunTimeTicks = mediaSource.RunTimeTicks;
|
||||
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
|
||||
|
||||
var video = item as Video;
|
||||
|
||||
|
@ -1742,7 +1754,6 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
}
|
||||
|
||||
state.RunTimeTicks = mediaSource.RunTimeTicks;
|
||||
}
|
||||
|
||||
var videoRequest = request as VideoStreamRequest;
|
||||
|
@ -1865,29 +1876,6 @@ namespace MediaBrowser.Api.Playback
|
|||
state.AllMediaStreams = mediaStreams;
|
||||
}
|
||||
|
||||
private async Task<MediaSourceInfo> GetChannelMediaInfo(string id,
|
||||
string mediaSourceId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var channelMediaSources = await ChannelManager.GetChannelItemMediaSources(id, true, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var list = channelMediaSources.ToList();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(mediaSourceId))
|
||||
{
|
||||
var source = list
|
||||
.FirstOrDefault(i => string.Equals(mediaSourceId, i.Id));
|
||||
|
||||
if (source != null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
return list.First();
|
||||
}
|
||||
|
||||
private bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream)
|
||||
{
|
||||
if (videoStream.IsInterlaced)
|
||||
|
|
224
MediaBrowser.Api/Playback/Dash/ManifestBuilder.cs
Normal file
224
MediaBrowser.Api/Playback/Dash/ManifestBuilder.cs
Normal file
|
@ -0,0 +1,224 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Dash
|
||||
{
|
||||
public class ManifestBuilder
|
||||
{
|
||||
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
public string GetManifestText(StreamState state, string playlistUrl)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
var time = TimeSpan.FromTicks(state.RunTimeTicks.Value);
|
||||
|
||||
var duration = "PT" + time.Hours.ToString("00", UsCulture) + "H" + time.Minutes.ToString("00", UsCulture) + "M" + time.Seconds.ToString("00", UsCulture) + ".00S";
|
||||
|
||||
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
|
||||
builder.AppendFormat(
|
||||
"<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"urn:mpeg:dash:schema:mpd:2011\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd\" profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" type=\"static\" mediaPresentationDuration=\"{0}\" minBufferTime=\"PT5.0S\">",
|
||||
duration);
|
||||
|
||||
builder.Append("<ProgramInformation>");
|
||||
builder.Append("</ProgramInformation>");
|
||||
|
||||
builder.Append("<Period start=\"PT0S\">");
|
||||
builder.Append(GetVideoAdaptationSet(state, playlistUrl));
|
||||
builder.Append(GetAudioAdaptationSet(state, playlistUrl));
|
||||
builder.Append("</Period>");
|
||||
|
||||
builder.Append("</MPD>");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private string GetVideoAdaptationSet(StreamState state, string playlistUrl)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append("<AdaptationSet id=\"video\" segmentAlignment=\"true\" bitstreamSwitching=\"true\">");
|
||||
builder.Append(GetVideoRepresentationOpenElement(state));
|
||||
|
||||
AppendSegmentList(state, builder, "0", playlistUrl);
|
||||
|
||||
builder.Append("</Representation>");
|
||||
builder.Append("</AdaptationSet>");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private string GetAudioAdaptationSet(StreamState state, string playlistUrl)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append("<AdaptationSet id=\"audio\" segmentAlignment=\"true\" bitstreamSwitching=\"true\">");
|
||||
builder.Append(GetAudioRepresentationOpenElement(state));
|
||||
|
||||
builder.Append("<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"6\" />");
|
||||
|
||||
AppendSegmentList(state, builder, "1", playlistUrl);
|
||||
|
||||
builder.Append("</Representation>");
|
||||
builder.Append("</AdaptationSet>");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private string GetVideoRepresentationOpenElement(StreamState state)
|
||||
{
|
||||
var codecs = GetVideoCodecDescriptor(state);
|
||||
|
||||
var mime = "video/mp4";
|
||||
|
||||
var xml = "<Representation id=\"0\" mimeType=\"" + mime + "\" codecs=\"" + codecs + "\"";
|
||||
|
||||
if (state.OutputWidth.HasValue)
|
||||
{
|
||||
xml += " width=\"" + state.OutputWidth.Value.ToString(UsCulture) + "\"";
|
||||
}
|
||||
if (state.OutputHeight.HasValue)
|
||||
{
|
||||
xml += " height=\"" + state.OutputHeight.Value.ToString(UsCulture) + "\"";
|
||||
}
|
||||
if (state.OutputVideoBitrate.HasValue)
|
||||
{
|
||||
xml += " bandwidth=\"" + state.OutputVideoBitrate.Value.ToString(UsCulture) + "\"";
|
||||
}
|
||||
|
||||
xml += ">";
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
private string GetAudioRepresentationOpenElement(StreamState state)
|
||||
{
|
||||
var codecs = GetAudioCodecDescriptor(state);
|
||||
|
||||
var mime = "audio/mp4";
|
||||
|
||||
var xml = "<Representation id=\"1\" mimeType=\"" + mime + "\" codecs=\"" + codecs + "\"";
|
||||
|
||||
if (state.OutputAudioSampleRate.HasValue)
|
||||
{
|
||||
xml += " audioSamplingRate=\"" + state.OutputAudioSampleRate.Value.ToString(UsCulture) + "\"";
|
||||
}
|
||||
if (state.OutputAudioBitrate.HasValue)
|
||||
{
|
||||
xml += " bandwidth=\"" + state.OutputAudioBitrate.Value.ToString(UsCulture) + "\"";
|
||||
}
|
||||
|
||||
xml += ">";
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
private string GetVideoCodecDescriptor(StreamState state)
|
||||
{
|
||||
// https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html
|
||||
// http://www.chipwreck.de/blog/2010/02/25/html-5-video-tag-and-attributes/
|
||||
|
||||
var level = state.TargetVideoLevel ?? 0;
|
||||
var profile = state.TargetVideoProfile ?? string.Empty;
|
||||
|
||||
if (profile.IndexOf("high", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
if (level >= 4.1)
|
||||
{
|
||||
return "avc1.640028";
|
||||
}
|
||||
|
||||
if (level >= 4)
|
||||
{
|
||||
return "avc1.640028";
|
||||
}
|
||||
|
||||
return "avc1.64001f";
|
||||
}
|
||||
|
||||
if (profile.IndexOf("main", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
if (level >= 4)
|
||||
{
|
||||
return "avc1.4d0028";
|
||||
}
|
||||
|
||||
if (level >= 3.1)
|
||||
{
|
||||
return "avc1.4d001f";
|
||||
}
|
||||
|
||||
return "avc1.4d001e";
|
||||
}
|
||||
|
||||
if (level >= 3.1)
|
||||
{
|
||||
return "avc1.42001f";
|
||||
}
|
||||
|
||||
return "avc1.42E01E";
|
||||
}
|
||||
|
||||
private string GetAudioCodecDescriptor(StreamState state)
|
||||
{
|
||||
// https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html
|
||||
|
||||
if (string.Equals(state.OutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "mp4a.40.34";
|
||||
}
|
||||
|
||||
// AAC 5ch
|
||||
if (state.OutputAudioChannels.HasValue && state.OutputAudioChannels.Value >= 5)
|
||||
{
|
||||
return "mp4a.40.5";
|
||||
}
|
||||
|
||||
// AAC 2ch
|
||||
return "mp4a.40.2";
|
||||
}
|
||||
|
||||
private void AppendSegmentList(StreamState state, StringBuilder builder, string type, string playlistUrl)
|
||||
{
|
||||
var extension = ".m4s";
|
||||
|
||||
var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds;
|
||||
|
||||
var queryStringIndex = playlistUrl.IndexOf('?');
|
||||
var queryString = queryStringIndex == -1 ? string.Empty : playlistUrl.Substring(queryStringIndex);
|
||||
|
||||
var index = 0;
|
||||
var duration = 1000000 * state.SegmentLength;
|
||||
builder.AppendFormat("<SegmentList timescale=\"1000000\" duration=\"{0}\" startNumber=\"1\">", duration.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
while (seconds > 0)
|
||||
{
|
||||
var filename = index == 0
|
||||
? "init"
|
||||
: (index - 1).ToString(UsCulture);
|
||||
|
||||
var segmentUrl = string.Format("dash/{3}/{0}{1}{2}",
|
||||
filename,
|
||||
extension,
|
||||
SecurityElement.Escape(queryString),
|
||||
type);
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
builder.AppendFormat("<Initialization sourceURL=\"{0}\"/>", segmentUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AppendFormat("<SegmentURL media=\"{0}\"/>", segmentUrl);
|
||||
}
|
||||
|
||||
seconds -= state.SegmentLength;
|
||||
index++;
|
||||
}
|
||||
builder.Append("</SegmentList>");
|
||||
}
|
||||
}
|
||||
}
|
561
MediaBrowser.Api/Playback/Dash/MpegDashService.cs
Normal file
561
MediaBrowser.Api/Playback/Dash/MpegDashService.cs
Normal file
|
@ -0,0 +1,561 @@
|
|||
using MediaBrowser.Api.Playback.Hls;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Diagnostics;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.IO;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Dash
|
||||
{
|
||||
/// <summary>
|
||||
/// Options is needed for chromecast. Threw Head in there since it's related
|
||||
/// </summary>
|
||||
[Route("/Videos/{Id}/master.mpd", "GET", Summary = "Gets a video stream using Mpeg dash.")]
|
||||
[Route("/Videos/{Id}/master.mpd", "HEAD", Summary = "Gets a video stream using Mpeg dash.")]
|
||||
public class GetMasterManifest : VideoStreamRequest
|
||||
{
|
||||
public bool EnableAdaptiveBitrateStreaming { get; set; }
|
||||
|
||||
public GetMasterManifest()
|
||||
{
|
||||
EnableAdaptiveBitrateStreaming = true;
|
||||
}
|
||||
}
|
||||
|
||||
[Route("/Videos/{Id}/dash/{RepresentationId}/{SegmentId}.m4s", "GET")]
|
||||
public class GetDashSegment : VideoStreamRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the segment id.
|
||||
/// </summary>
|
||||
/// <value>The segment id.</value>
|
||||
public string SegmentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the representation identifier.
|
||||
/// </summary>
|
||||
/// <value>The representation identifier.</value>
|
||||
public string RepresentationId { get; set; }
|
||||
}
|
||||
|
||||
public class MpegDashService : BaseHlsService
|
||||
{
|
||||
public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, INetworkManager networkManager)
|
||||
: base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient)
|
||||
{
|
||||
NetworkManager = networkManager;
|
||||
}
|
||||
|
||||
protected INetworkManager NetworkManager { get; private set; }
|
||||
|
||||
public object Get(GetMasterManifest request)
|
||||
{
|
||||
var result = GetAsync(request, "GET").Result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public object Head(GetMasterManifest request)
|
||||
{
|
||||
var result = GetAsync(request, "HEAD").Result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override bool EnableOutputInSubFolder
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<object> GetAsync(GetMasterManifest request, string method)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.MediaSourceId))
|
||||
{
|
||||
throw new ArgumentException("MediaSourceId is required");
|
||||
}
|
||||
|
||||
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var playlistText = string.Empty;
|
||||
|
||||
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
playlistText = new ManifestBuilder().GetManifestText(state, Request.RawUrl);
|
||||
}
|
||||
|
||||
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.mpd"), new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
public object Get(GetDashSegment request)
|
||||
{
|
||||
return GetDynamicSegment(request, request.SegmentId, request.RepresentationId).Result;
|
||||
}
|
||||
|
||||
private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId, string representationId)
|
||||
{
|
||||
if ((request.StartTimeTicks ?? 0) > 0)
|
||||
{
|
||||
throw new ArgumentException("StartTimeTicks is not allowed.");
|
||||
}
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
var requestedIndex = string.Equals(segmentId, "init", StringComparison.OrdinalIgnoreCase) ?
|
||||
-1 :
|
||||
int.Parse(segmentId, NumberStyles.Integer, UsCulture);
|
||||
|
||||
var state = await GetState(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".mpd");
|
||||
|
||||
var segmentExtension = GetSegmentFileExtension(state);
|
||||
|
||||
var segmentPath = FindSegment(playlistPath, representationId, segmentExtension, requestedIndex);
|
||||
var segmentLength = state.SegmentLength;
|
||||
|
||||
TranscodingJob job = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(segmentPath))
|
||||
{
|
||||
job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
segmentPath = FindSegment(playlistPath, representationId, segmentExtension, requestedIndex);
|
||||
if (!string.IsNullOrWhiteSpace(segmentPath))
|
||||
{
|
||||
job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.Equals(representationId, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
|
||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
||||
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
|
||||
Logger.Debug("Current transcoding index is {0}. requestedIndex={1}. segmentGapRequiringTranscodingChange={2}", currentTranscodingIndex ?? -2, requestedIndex, segmentGapRequiringTranscodingChange);
|
||||
if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
|
||||
{
|
||||
// If the playlist doesn't already exist, startup ffmpeg
|
||||
try
|
||||
{
|
||||
ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.StreamId, p => false);
|
||||
|
||||
if (currentTranscodingIndex.HasValue)
|
||||
{
|
||||
DeleteLastTranscodedFiles(playlistPath, 0);
|
||||
}
|
||||
|
||||
var positionTicks = GetPositionTicks(state, requestedIndex);
|
||||
request.StartTimeTicks = positionTicks;
|
||||
|
||||
var startNumber = GetStartNumber(state);
|
||||
|
||||
var workingDirectory = Path.Combine(Path.GetDirectoryName(playlistPath), (startNumber == -1 ? 0 : startNumber).ToString(CultureInfo.InvariantCulture));
|
||||
state.WaitForPath = Path.Combine(workingDirectory, Path.GetFileName(playlistPath));
|
||||
Directory.CreateDirectory(workingDirectory);
|
||||
job = await StartFfMpeg(state, playlistPath, cancellationTokenSource, workingDirectory).ConfigureAwait(false);
|
||||
await WaitForMinimumDashSegmentCount(Path.Combine(workingDirectory, Path.GetFileName(playlistPath)), 1, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
state.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ApiEntryPoint.Instance.TranscodingStartLock.Release();
|
||||
}
|
||||
|
||||
while (string.IsNullOrWhiteSpace(segmentPath))
|
||||
{
|
||||
segmentPath = FindSegment(playlistPath, representationId, segmentExtension, requestedIndex);
|
||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.Info("returning {0}", segmentPath);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job ?? ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private long GetPositionTicks(StreamState state, int requestedIndex)
|
||||
{
|
||||
if (requestedIndex <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var startSeconds = requestedIndex * state.SegmentLength;
|
||||
return TimeSpan.FromSeconds(startSeconds).Ticks;
|
||||
}
|
||||
|
||||
protected Task WaitForMinimumDashSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
|
||||
{
|
||||
return WaitForSegment(playlist, "stream0-" + segmentCount.ToString("00000", CultureInfo.InvariantCulture) + ".m4s", cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<object> GetSegmentResult(string playlistPath,
|
||||
string segmentPath,
|
||||
int segmentIndex,
|
||||
int segmentLength,
|
||||
TranscodingJob transcodingJob,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// If all transcoding has completed, just return immediately
|
||||
if (transcodingJob != null && transcodingJob.HasExited)
|
||||
{
|
||||
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
|
||||
}
|
||||
|
||||
// Wait for the file to stop being written to, then stream it
|
||||
var length = new FileInfo(segmentPath).Length;
|
||||
var eofCount = 0;
|
||||
|
||||
while (eofCount < 10)
|
||||
{
|
||||
var info = new FileInfo(segmentPath);
|
||||
|
||||
if (!info.Exists)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var newLength = info.Length;
|
||||
|
||||
if (newLength == length)
|
||||
{
|
||||
eofCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
eofCount = 0;
|
||||
}
|
||||
|
||||
length = newLength;
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
|
||||
}
|
||||
|
||||
private object GetSegmentResult(string segmentPath, int index, int segmentLength, TranscodingJob transcodingJob)
|
||||
{
|
||||
var segmentEndingSeconds = (1 + index) * segmentLength;
|
||||
var segmentEndingPositionTicks = TimeSpan.FromSeconds(segmentEndingSeconds).Ticks;
|
||||
|
||||
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
|
||||
{
|
||||
Path = segmentPath,
|
||||
FileShare = FileShare.ReadWrite,
|
||||
OnComplete = () =>
|
||||
{
|
||||
if (transcodingJob != null)
|
||||
{
|
||||
transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
|
||||
{
|
||||
var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType);
|
||||
|
||||
if (job == null || job.HasExited)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var file = GetLastTranscodingFiles(playlist, segmentExtension, FileSystem, 1).FirstOrDefault();
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetIndex(file.FullName);
|
||||
}
|
||||
|
||||
public int GetIndex(string segmentPath)
|
||||
{
|
||||
var indexString = Path.GetFileNameWithoutExtension(segmentPath).Split('-').LastOrDefault();
|
||||
|
||||
if (string.Equals(indexString, "init", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
var startNumber = int.Parse(Path.GetFileNameWithoutExtension(Path.GetDirectoryName(segmentPath)), NumberStyles.Integer, UsCulture);
|
||||
|
||||
return startNumber + int.Parse(indexString, NumberStyles.Integer, UsCulture) - 1;
|
||||
}
|
||||
|
||||
private void DeleteLastTranscodedFiles(string playlistPath, int retryCount)
|
||||
{
|
||||
if (retryCount >= 5)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<FileInfo> GetLastTranscodingFiles(string playlist, string segmentExtension, IFileSystem fileSystem, int count)
|
||||
{
|
||||
var folder = Path.GetDirectoryName(playlist);
|
||||
|
||||
try
|
||||
{
|
||||
return new DirectoryInfo(folder)
|
||||
.EnumerateFiles("*", SearchOption.AllDirectories)
|
||||
.Where(i => string.Equals(i.Extension, segmentExtension, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(fileSystem.GetLastWriteTimeUtc)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return new List<FileInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
private string FindSegment(string playlist, string representationId, string segmentExtension, int requestedIndex)
|
||||
{
|
||||
var folder = Path.GetDirectoryName(playlist);
|
||||
|
||||
if (requestedIndex == -1)
|
||||
{
|
||||
var path = Path.Combine(folder, "0", "stream" + representationId + "-" + "init" + segmentExtension);
|
||||
return File.Exists(path) ? path : null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var subfolder in new DirectoryInfo(folder).EnumerateDirectories().ToList())
|
||||
{
|
||||
var subfolderName = Path.GetFileNameWithoutExtension(subfolder.FullName);
|
||||
int startNumber;
|
||||
if (int.TryParse(subfolderName, NumberStyles.Any, UsCulture, out startNumber))
|
||||
{
|
||||
var segmentIndex = requestedIndex - startNumber + 1;
|
||||
var path = Path.Combine(folder, subfolderName, "stream" + representationId + "-" + segmentIndex.ToString("00000", CultureInfo.InvariantCulture) + segmentExtension);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override string GetAudioArguments(StreamState state)
|
||||
{
|
||||
var codec = state.OutputAudioCodec;
|
||||
|
||||
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "-codec:a:0 copy";
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + codec;
|
||||
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
if (channels.HasValue)
|
||||
{
|
||||
args += " -ac " + channels.Value;
|
||||
}
|
||||
|
||||
var bitrate = state.OutputAudioBitrate;
|
||||
|
||||
if (bitrate.HasValue)
|
||||
{
|
||||
args += " -ab " + bitrate.Value.ToString(UsCulture);
|
||||
}
|
||||
|
||||
args += " " + GetAudioFilterParam(state, true);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
protected override string GetVideoArguments(StreamState state)
|
||||
{
|
||||
var codec = state.OutputVideoCodec;
|
||||
|
||||
var args = "-codec:v:0 " + codec;
|
||||
|
||||
if (state.EnableMpegtsM2TsMode)
|
||||
{
|
||||
args += " -mpegts_m2ts_mode 1";
|
||||
}
|
||||
|
||||
// See if we can save come cpu cycles by avoiding encoding
|
||||
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return state.VideoStream != null && IsH264(state.VideoStream) ?
|
||||
args + " -bsf:v h264_mp4toannexb" :
|
||||
args;
|
||||
}
|
||||
|
||||
var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
|
||||
state.SegmentLength.ToString(UsCulture));
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
|
||||
|
||||
args += " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg;
|
||||
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
{
|
||||
args += GetOutputSizeParam(state, codec, false);
|
||||
}
|
||||
|
||||
// This is for internal graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
args += GetGraphicalSubtitleParam(state, codec);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding)
|
||||
{
|
||||
// test url http://192.168.1.2:8096/videos/233e8905d559a8f230db9bffd2ac9d6d/master.mpd?mediasourceid=233e8905d559a8f230db9bffd2ac9d6d&videocodec=h264&audiocodec=aac&maxwidth=1280&videobitrate=500000&audiobitrate=128000&profile=baseline&level=3
|
||||
// Good info on i-frames http://blog.streamroot.io/encode-multi-bitrate-videos-mpeg-dash-mse-based-media-players/
|
||||
|
||||
var threads = GetNumberOfThreads(state, false);
|
||||
|
||||
var inputModifier = GetInputModifier(state);
|
||||
|
||||
var initSegmentName = "stream$RepresentationID$-init.m4s";
|
||||
var segmentName = "stream$RepresentationID$-$Number%05d$.m4s";
|
||||
|
||||
var args = string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts {5} -f dash -init_seg_name \"{6}\" -media_seg_name \"{7}\" -use_template 0 -use_timeline 1 -min_seg_duration {8} -y \"{9}\"",
|
||||
inputModifier,
|
||||
GetInputArgument(transcodingJobId, state),
|
||||
threads,
|
||||
GetMapArgs(state),
|
||||
GetVideoArguments(state),
|
||||
GetAudioArguments(state),
|
||||
initSegmentName,
|
||||
segmentName,
|
||||
(state.SegmentLength * 1000000).ToString(CultureInfo.InvariantCulture),
|
||||
state.WaitForPath
|
||||
).Trim();
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
protected override int GetStartNumber(StreamState state)
|
||||
{
|
||||
return GetStartNumber(state.VideoRequest);
|
||||
}
|
||||
|
||||
private int GetStartNumber(VideoStreamRequest request)
|
||||
{
|
||||
var segmentId = "0";
|
||||
|
||||
var segmentRequest = request as GetDashSegment;
|
||||
if (segmentRequest != null)
|
||||
{
|
||||
segmentId = segmentRequest.SegmentId;
|
||||
}
|
||||
|
||||
if (string.Equals(segmentId, "init", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return int.Parse(segmentId, NumberStyles.Integer, UsCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment file extension.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected override string GetSegmentFileExtension(StreamState state)
|
||||
{
|
||||
return ".m4s";
|
||||
}
|
||||
|
||||
protected override TranscodingJobType TranscodingJobType
|
||||
{
|
||||
get
|
||||
{
|
||||
return TranscodingJobType.Dash;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WaitForSegment(string playlist, string segment, CancellationToken cancellationToken)
|
||||
{
|
||||
var tmpPath = playlist + ".tmp";
|
||||
|
||||
var segmentFilename = Path.GetFileName(segment);
|
||||
|
||||
Logger.Debug("Waiting for {0} in {1}", segmentFilename, playlist);
|
||||
|
||||
while (true)
|
||||
{
|
||||
FileStream fileStream;
|
||||
try
|
||||
{
|
||||
fileStream = FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
|
||||
}
|
||||
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
|
||||
using (fileStream)
|
||||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
if (line.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
Logger.Debug("Finished waiting for {0} in {1}", segmentFilename, playlist);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Diagnostics;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
|
@ -23,7 +23,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
/// </summary>
|
||||
public abstract class BaseHlsService : BaseStreamingService
|
||||
{
|
||||
protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
|
||||
protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
return builder.ToString();
|
||||
}
|
||||
|
||||
protected async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
|
||||
protected virtual async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist);
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Diagnostics;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
|
@ -11,6 +10,7 @@ using MediaBrowser.Controller.MediaEncoding;
|
|||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
|
@ -63,7 +63,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
public class DynamicHlsService : BaseHlsService
|
||||
{
|
||||
public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
|
||||
public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient)
|
||||
{
|
||||
NetworkManager = networkManager;
|
||||
}
|
||||
|
@ -100,13 +100,13 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
var index = int.Parse(segmentId, NumberStyles.Integer, UsCulture);
|
||||
var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, UsCulture);
|
||||
|
||||
var state = await GetState(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
|
||||
|
||||
var segmentPath = GetSegmentPath(playlistPath, index);
|
||||
var segmentPath = GetSegmentPath(playlistPath, requestedIndex);
|
||||
var segmentLength = state.SegmentLength;
|
||||
|
||||
var segmentExtension = GetSegmentFileExtension(state);
|
||||
|
@ -115,7 +115,8 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
if (File.Exists(segmentPath))
|
||||
{
|
||||
return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
@ -123,26 +124,26 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
{
|
||||
if (File.Exists(segmentPath))
|
||||
{
|
||||
return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
||||
|
||||
if (currentTranscodingIndex == null || index < currentTranscodingIndex.Value || (index - currentTranscodingIndex.Value) > 4)
|
||||
var segmentGapRequiringTranscodingChange = 24/state.SegmentLength;
|
||||
if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
|
||||
{
|
||||
// If the playlist doesn't already exist, startup ffmpeg
|
||||
try
|
||||
{
|
||||
ApiEntryPoint.Instance.KillTranscodingJobs(j => j.Type == TranscodingJobType && string.Equals(j.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase), p => !string.Equals(p, playlistPath, StringComparison.OrdinalIgnoreCase));
|
||||
ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.StreamId ?? request.ClientTime, p => false);
|
||||
|
||||
if (currentTranscodingIndex.HasValue)
|
||||
{
|
||||
DeleteLastFile(playlistPath, segmentExtension, 0);
|
||||
}
|
||||
|
||||
var startSeconds = index * state.SegmentLength;
|
||||
request.StartTimeTicks = TimeSpan.FromSeconds(startSeconds).Ticks;
|
||||
request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex);
|
||||
|
||||
job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
|
@ -152,7 +153,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
throw;
|
||||
}
|
||||
|
||||
await WaitForMinimumSegmentCount(playlistPath, 2, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -169,11 +170,26 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
Logger.Info("returning {0}", segmentPath);
|
||||
job = job ?? ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private long GetSeekPositionTicks(StreamState state, int requestedIndex)
|
||||
{
|
||||
var startSeconds = requestedIndex * state.SegmentLength;
|
||||
var position = TimeSpan.FromSeconds(startSeconds).Ticks;
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
|
||||
{
|
||||
var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType);
|
||||
|
||||
if (job == null || job.HasExited)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var file = GetLastTranscodingFile(playlist, segmentExtension, FileSystem);
|
||||
|
||||
if (file == null)
|
||||
|
@ -204,7 +220,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
FileSystem.DeleteFile(file.FullName);
|
||||
|
@ -277,7 +293,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
CancellationToken cancellationToken)
|
||||
{
|
||||
// If all transcoding has completed, just return immediately
|
||||
if (!IsTranscoding(playlistPath))
|
||||
if (transcodingJob != null && transcodingJob.HasExited)
|
||||
{
|
||||
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
|
||||
}
|
||||
|
@ -288,12 +304,15 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
// If it appears in the playlist, it's done
|
||||
if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
|
||||
var text = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
// If it appears in the playlist, it's done
|
||||
if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -356,13 +375,6 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
});
|
||||
}
|
||||
|
||||
private bool IsTranscoding(string playlistPath)
|
||||
{
|
||||
var job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
|
||||
|
||||
return job != null && !job.HasExited;
|
||||
}
|
||||
|
||||
private async Task<object> GetAsync(GetMasterHlsVideoStream request, string method)
|
||||
{
|
||||
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
|
||||
|
@ -681,20 +693,36 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
// If isEncoding is true we're actually starting ffmpeg
|
||||
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
|
||||
|
||||
var args = string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
|
||||
inputModifier,
|
||||
GetInputArgument(transcodingJobId, state),
|
||||
threads,
|
||||
GetMapArgs(state),
|
||||
GetVideoArguments(state),
|
||||
GetAudioArguments(state),
|
||||
state.SegmentLength.ToString(UsCulture),
|
||||
startNumberParam,
|
||||
state.HlsListSize.ToString(UsCulture),
|
||||
outputPath
|
||||
).Trim();
|
||||
if (state.EnableGenericHlsSegmenter)
|
||||
{
|
||||
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d.ts";
|
||||
|
||||
return args;
|
||||
return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -f segment -segment_time {6} -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
|
||||
inputModifier,
|
||||
GetInputArgument(transcodingJobId, state),
|
||||
threads,
|
||||
GetMapArgs(state),
|
||||
GetVideoArguments(state),
|
||||
GetAudioArguments(state),
|
||||
state.SegmentLength.ToString(UsCulture),
|
||||
startNumberParam,
|
||||
outputPath,
|
||||
outputTsArg
|
||||
).Trim();
|
||||
}
|
||||
|
||||
return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
|
||||
inputModifier,
|
||||
GetInputArgument(transcodingJobId, state),
|
||||
threads,
|
||||
GetMapArgs(state),
|
||||
GetVideoArguments(state),
|
||||
GetAudioArguments(state),
|
||||
state.SegmentLength.ToString(UsCulture),
|
||||
startNumberParam,
|
||||
state.HlsListSize.ToString(UsCulture),
|
||||
outputPath
|
||||
).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -47,6 +47,9 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
{
|
||||
[ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
[ApiMember(Name = "StreamId", Description = "The stream id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
|
||||
public string StreamId { get; set; }
|
||||
}
|
||||
|
||||
public class HlsSegmentService : BaseApiService
|
||||
|
@ -69,7 +72,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
public void Delete(StopEncodingProcess request)
|
||||
{
|
||||
ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, path => true);
|
||||
ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.StreamId, path => true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,675 +0,0 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.IO;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Hls
|
||||
{
|
||||
/// <summary>
|
||||
/// Options is needed for chromecast. Threw Head in there since it's related
|
||||
/// </summary>
|
||||
[Route("/Videos/{Id}/master.mpd", "GET", Summary = "Gets a video stream using Mpeg dash.")]
|
||||
[Route("/Videos/{Id}/master.mpd", "HEAD", Summary = "Gets a video stream using Mpeg dash.")]
|
||||
public class GetMasterManifest : VideoStreamRequest
|
||||
{
|
||||
public bool EnableAdaptiveBitrateStreaming { get; set; }
|
||||
|
||||
public GetMasterManifest()
|
||||
{
|
||||
EnableAdaptiveBitrateStreaming = true;
|
||||
}
|
||||
}
|
||||
|
||||
[Route("/Videos/{Id}/dash/{SegmentId}.ts", "GET")]
|
||||
[Route("/Videos/{Id}/dash/{SegmentId}.mp4", "GET")]
|
||||
public class GetDashSegment : VideoStreamRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the segment id.
|
||||
/// </summary>
|
||||
/// <value>The segment id.</value>
|
||||
public string SegmentId { get; set; }
|
||||
}
|
||||
|
||||
public class MpegDashService : BaseHlsService
|
||||
{
|
||||
public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
|
||||
{
|
||||
NetworkManager = networkManager;
|
||||
}
|
||||
|
||||
protected INetworkManager NetworkManager { get; private set; }
|
||||
|
||||
public object Get(GetMasterManifest request)
|
||||
{
|
||||
var result = GetAsync(request, "GET").Result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public object Head(GetMasterManifest request)
|
||||
{
|
||||
var result = GetAsync(request, "HEAD").Result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<object> GetAsync(GetMasterManifest request, string method)
|
||||
{
|
||||
if (string.Equals(request.AudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("Audio codec copy is not allowed here.");
|
||||
}
|
||||
|
||||
if (string.Equals(request.VideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("Video codec copy is not allowed here.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.MediaSourceId))
|
||||
{
|
||||
throw new ArgumentException("MediaSourceId is required");
|
||||
}
|
||||
|
||||
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var playlistText = string.Empty;
|
||||
|
||||
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
playlistText = GetManifestText(state);
|
||||
}
|
||||
|
||||
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.mpd"), new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
private string GetManifestText(StreamState state)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
var time = TimeSpan.FromTicks(state.RunTimeTicks.Value);
|
||||
|
||||
var duration = "PT" + time.Hours.ToString("00", UsCulture) + "H" + time.Minutes.ToString("00", UsCulture) + "M" + time.Seconds.ToString("00", UsCulture) + ".00S";
|
||||
|
||||
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
|
||||
var profile = string.Equals(GetSegmentFileExtension(state), ".ts", StringComparison.OrdinalIgnoreCase)
|
||||
? "urn:mpeg:dash:profile:mp2t-simple:2011"
|
||||
: "urn:mpeg:dash:profile:mp2t-simple:2011";
|
||||
|
||||
builder.AppendFormat(
|
||||
"<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd\" minBufferTime=\"PT2.00S\" mediaPresentationDuration=\"{0}\" maxSegmentDuration=\"PT{1}S\" type=\"static\" profiles=\""+profile+"\">",
|
||||
duration,
|
||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
builder.Append("<ProgramInformation moreInformationURL=\"http://gpac.sourceforge.net\">");
|
||||
builder.Append("</ProgramInformation>");
|
||||
|
||||
builder.AppendFormat("<Period start=\"PT0S\" duration=\"{0}\">", duration);
|
||||
builder.Append("<AdaptationSet segmentAlignment=\"true\">");
|
||||
|
||||
builder.Append("<ContentComponent id=\"1\" contentType=\"video\"/>");
|
||||
|
||||
var lang = state.AudioStream != null ? state.AudioStream.Language : null;
|
||||
if (string.IsNullOrWhiteSpace(lang)) lang = "und";
|
||||
|
||||
builder.AppendFormat("<ContentComponent id=\"2\" contentType=\"audio\" lang=\"{0}\"/>", lang);
|
||||
|
||||
builder.Append(GetRepresentationOpenElement(state, lang));
|
||||
|
||||
AppendSegmentList(state, builder);
|
||||
|
||||
builder.Append("</Representation>");
|
||||
builder.Append("</AdaptationSet>");
|
||||
builder.Append("</Period>");
|
||||
|
||||
builder.Append("</MPD>");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private string GetRepresentationOpenElement(StreamState state, string language)
|
||||
{
|
||||
var codecs = GetVideoCodecDescriptor(state) + "," + GetAudioCodecDescriptor(state);
|
||||
|
||||
var mime = string.Equals(GetSegmentFileExtension(state), ".ts", StringComparison.OrdinalIgnoreCase)
|
||||
? "video/mp2t"
|
||||
: "video/mp4";
|
||||
|
||||
var xml = "<Representation id=\"1\" mimeType=\"" + mime + "\" startWithSAP=\"1\" codecs=\"" + codecs + "\"";
|
||||
|
||||
if (state.OutputWidth.HasValue)
|
||||
{
|
||||
xml += " width=\"" + state.OutputWidth.Value.ToString(UsCulture) + "\"";
|
||||
}
|
||||
if (state.OutputHeight.HasValue)
|
||||
{
|
||||
xml += " height=\"" + state.OutputHeight.Value.ToString(UsCulture) + "\"";
|
||||
}
|
||||
if (state.OutputAudioSampleRate.HasValue)
|
||||
{
|
||||
xml += " sampleRate=\"" + state.OutputAudioSampleRate.Value.ToString(UsCulture) + "\"";
|
||||
}
|
||||
|
||||
if (state.TotalOutputBitrate.HasValue)
|
||||
{
|
||||
xml += " bandwidth=\"" + state.TotalOutputBitrate.Value.ToString(UsCulture) + "\"";
|
||||
}
|
||||
|
||||
xml += ">";
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
private string GetVideoCodecDescriptor(StreamState state)
|
||||
{
|
||||
// https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html
|
||||
// http://www.chipwreck.de/blog/2010/02/25/html-5-video-tag-and-attributes/
|
||||
|
||||
var level = state.TargetVideoLevel ?? 0;
|
||||
var profile = state.TargetVideoProfile ?? string.Empty;
|
||||
|
||||
if (profile.IndexOf("high", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
if (level >= 4.1)
|
||||
{
|
||||
return "avc1.640028";
|
||||
}
|
||||
|
||||
if (level >= 4)
|
||||
{
|
||||
return "avc1.640028";
|
||||
}
|
||||
|
||||
return "avc1.64001f";
|
||||
}
|
||||
|
||||
if (profile.IndexOf("main", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
if (level >= 4)
|
||||
{
|
||||
return "avc1.4d0028";
|
||||
}
|
||||
|
||||
if (level >= 3.1)
|
||||
{
|
||||
return "avc1.4d001f";
|
||||
}
|
||||
|
||||
return "avc1.4d001e";
|
||||
}
|
||||
|
||||
if (level >= 3.1)
|
||||
{
|
||||
return "avc1.42001f";
|
||||
}
|
||||
|
||||
return "avc1.42E01E";
|
||||
}
|
||||
|
||||
private string GetAudioCodecDescriptor(StreamState state)
|
||||
{
|
||||
// https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html
|
||||
|
||||
if (string.Equals(state.OutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "mp4a.40.34";
|
||||
}
|
||||
|
||||
// AAC 5ch
|
||||
if (state.OutputAudioChannels.HasValue && state.OutputAudioChannels.Value >= 5)
|
||||
{
|
||||
return "mp4a.40.5";
|
||||
}
|
||||
|
||||
// AAC 2ch
|
||||
return "mp4a.40.2";
|
||||
}
|
||||
|
||||
public object Get(GetDashSegment request)
|
||||
{
|
||||
return GetDynamicSegment(request, request.SegmentId).Result;
|
||||
}
|
||||
|
||||
private void AppendSegmentList(StreamState state, StringBuilder builder)
|
||||
{
|
||||
var extension = GetSegmentFileExtension(state);
|
||||
|
||||
var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds;
|
||||
|
||||
var queryStringIndex = Request.RawUrl.IndexOf('?');
|
||||
var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
|
||||
|
||||
var index = 0;
|
||||
builder.Append("<SegmentList timescale=\"1000\" duration=\"10000\">");
|
||||
|
||||
|
||||
while (seconds > 0)
|
||||
{
|
||||
var segmentUrl = string.Format("dash/{0}{1}{2}",
|
||||
index.ToString(UsCulture),
|
||||
extension,
|
||||
SecurityElement.Escape(queryString));
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
builder.AppendFormat("<Initialization sourceURL=\"{0}\"/>", segmentUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AppendFormat("<SegmentURL media=\"{0}\"/>", segmentUrl);
|
||||
}
|
||||
|
||||
seconds -= state.SegmentLength;
|
||||
index++;
|
||||
}
|
||||
builder.Append("</SegmentList>");
|
||||
}
|
||||
|
||||
private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId)
|
||||
{
|
||||
if ((request.StartTimeTicks ?? 0) > 0)
|
||||
{
|
||||
throw new ArgumentException("StartTimeTicks is not allowed.");
|
||||
}
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
var index = int.Parse(segmentId, NumberStyles.Integer, UsCulture);
|
||||
|
||||
var state = await GetState(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
|
||||
|
||||
var segmentExtension = GetSegmentFileExtension(state);
|
||||
|
||||
var segmentPath = GetSegmentPath(playlistPath, segmentExtension, index);
|
||||
var segmentLength = state.SegmentLength;
|
||||
|
||||
TranscodingJob job = null;
|
||||
|
||||
if (File.Exists(segmentPath))
|
||||
{
|
||||
return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (File.Exists(segmentPath))
|
||||
{
|
||||
return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
||||
|
||||
if (currentTranscodingIndex == null || index < currentTranscodingIndex.Value || (index - currentTranscodingIndex.Value) > 4)
|
||||
{
|
||||
// If the playlist doesn't already exist, startup ffmpeg
|
||||
try
|
||||
{
|
||||
ApiEntryPoint.Instance.KillTranscodingJobs(j => j.Type == TranscodingJobType && string.Equals(j.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase), p => !string.Equals(p, playlistPath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (currentTranscodingIndex.HasValue)
|
||||
{
|
||||
DeleteLastFile(playlistPath, segmentExtension, 0);
|
||||
}
|
||||
|
||||
var startSeconds = index * state.SegmentLength;
|
||||
request.StartTimeTicks = TimeSpan.FromSeconds(startSeconds).Ticks;
|
||||
|
||||
job = await StartFfMpeg(state, playlistPath, cancellationTokenSource, Path.GetDirectoryName(playlistPath)).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
state.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
await WaitForMinimumSegmentCount(playlistPath, 2, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ApiEntryPoint.Instance.TranscodingStartLock.Release();
|
||||
}
|
||||
|
||||
Logger.Info("waiting for {0}", segmentPath);
|
||||
while (!File.Exists(segmentPath))
|
||||
{
|
||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.Info("returning {0}", segmentPath);
|
||||
job = job ?? ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
|
||||
return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<object> GetSegmentResult(string playlistPath,
|
||||
string segmentPath,
|
||||
int segmentIndex,
|
||||
int segmentLength,
|
||||
TranscodingJob transcodingJob,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// If all transcoding has completed, just return immediately
|
||||
if (!IsTranscoding(playlistPath))
|
||||
{
|
||||
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
|
||||
}
|
||||
|
||||
var segmentFilename = Path.GetFileName(segmentPath);
|
||||
|
||||
using (var fileStream = FileSystem.GetFileStream(playlistPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
|
||||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
// If it appears in the playlist, it's done
|
||||
if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if a different file is encoding, it's done
|
||||
//var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath);
|
||||
//if (currentTranscodingIndex > segmentIndex)
|
||||
//{
|
||||
//return GetSegmentResult(segmentPath, segmentIndex);
|
||||
//}
|
||||
|
||||
// Wait for the file to stop being written to, then stream it
|
||||
var length = new FileInfo(segmentPath).Length;
|
||||
var eofCount = 0;
|
||||
|
||||
while (eofCount < 10)
|
||||
{
|
||||
var info = new FileInfo(segmentPath);
|
||||
|
||||
if (!info.Exists)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var newLength = info.Length;
|
||||
|
||||
if (newLength == length)
|
||||
{
|
||||
eofCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
eofCount = 0;
|
||||
}
|
||||
|
||||
length = newLength;
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
|
||||
}
|
||||
|
||||
private object GetSegmentResult(string segmentPath, int index, int segmentLength, TranscodingJob transcodingJob)
|
||||
{
|
||||
var segmentEndingSeconds = (1 + index) * segmentLength;
|
||||
var segmentEndingPositionTicks = TimeSpan.FromSeconds(segmentEndingSeconds).Ticks;
|
||||
|
||||
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
|
||||
{
|
||||
Path = segmentPath,
|
||||
FileShare = FileShare.ReadWrite,
|
||||
OnComplete = () =>
|
||||
{
|
||||
if (transcodingJob != null)
|
||||
{
|
||||
transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool IsTranscoding(string playlistPath)
|
||||
{
|
||||
var job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
|
||||
|
||||
return job != null && !job.HasExited;
|
||||
}
|
||||
|
||||
public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
|
||||
{
|
||||
var file = GetLastTranscodingFile(playlist, segmentExtension, FileSystem);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
|
||||
|
||||
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
|
||||
|
||||
return int.Parse(indexString, NumberStyles.Integer, UsCulture);
|
||||
}
|
||||
|
||||
private void DeleteLastFile(string path, string segmentExtension, int retryCount)
|
||||
{
|
||||
if (retryCount >= 5)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var file = GetLastTranscodingFile(path, segmentExtension, FileSystem);
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileSystem.DeleteFile(file.FullName);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName);
|
||||
|
||||
Thread.Sleep(100);
|
||||
DeleteLastFile(path, segmentExtension, retryCount + 1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static FileInfo GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
|
||||
{
|
||||
var folder = Path.GetDirectoryName(playlist);
|
||||
|
||||
try
|
||||
{
|
||||
return new DirectoryInfo(folder)
|
||||
.EnumerateFiles("*", SearchOption.TopDirectoryOnly)
|
||||
.Where(i => string.Equals(i.Extension, segmentExtension, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(fileSystem.GetLastWriteTimeUtc)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override int GetStartNumber(StreamState state)
|
||||
{
|
||||
return GetStartNumber(state.VideoRequest);
|
||||
}
|
||||
|
||||
private int GetStartNumber(VideoStreamRequest request)
|
||||
{
|
||||
var segmentId = "0";
|
||||
|
||||
var segmentRequest = request as GetDynamicHlsVideoSegment;
|
||||
if (segmentRequest != null)
|
||||
{
|
||||
segmentId = segmentRequest.SegmentId;
|
||||
}
|
||||
|
||||
return int.Parse(segmentId, NumberStyles.Integer, UsCulture);
|
||||
}
|
||||
|
||||
private string GetSegmentPath(string playlist, string segmentExtension, int index)
|
||||
{
|
||||
var folder = Path.GetDirectoryName(playlist);
|
||||
|
||||
var filename = Path.GetFileNameWithoutExtension(playlist);
|
||||
|
||||
return Path.Combine(folder, filename + index.ToString("000", UsCulture) + segmentExtension);
|
||||
}
|
||||
|
||||
protected override string GetAudioArguments(StreamState state)
|
||||
{
|
||||
var codec = state.OutputAudioCodec;
|
||||
|
||||
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "-codec:a:0 copy";
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + codec;
|
||||
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
if (channels.HasValue)
|
||||
{
|
||||
args += " -ac " + channels.Value;
|
||||
}
|
||||
|
||||
var bitrate = state.OutputAudioBitrate;
|
||||
|
||||
if (bitrate.HasValue)
|
||||
{
|
||||
args += " -ab " + bitrate.Value.ToString(UsCulture);
|
||||
}
|
||||
|
||||
args += " " + GetAudioFilterParam(state, true);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
protected override string GetVideoArguments(StreamState state)
|
||||
{
|
||||
var codec = state.OutputVideoCodec;
|
||||
|
||||
var args = "-codec:v:0 " + codec;
|
||||
|
||||
if (state.EnableMpegtsM2TsMode)
|
||||
{
|
||||
args += " -mpegts_m2ts_mode 1";
|
||||
}
|
||||
|
||||
// See if we can save come cpu cycles by avoiding encoding
|
||||
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return state.VideoStream != null && IsH264(state.VideoStream) ?
|
||||
args + " -bsf:v h264_mp4toannexb" :
|
||||
args;
|
||||
}
|
||||
|
||||
var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
|
||||
state.SegmentLength.ToString(UsCulture));
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
|
||||
|
||||
args+= " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg;
|
||||
|
||||
args += " -r 24 -g 24";
|
||||
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
{
|
||||
args += GetOutputSizeParam(state, codec, false);
|
||||
}
|
||||
|
||||
// This is for internal graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
args += GetGraphicalSubtitleParam(state, codec);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding)
|
||||
{
|
||||
// test url http://192.168.1.2:8096/videos/233e8905d559a8f230db9bffd2ac9d6d/master.mpd?mediasourceid=233e8905d559a8f230db9bffd2ac9d6d&videocodec=h264&audiocodec=aac&maxwidth=1280&videobitrate=500000&audiobitrate=128000&profile=baseline&level=3
|
||||
// Good info on i-frames http://blog.streamroot.io/encode-multi-bitrate-videos-mpeg-dash-mse-based-media-players/
|
||||
|
||||
var threads = GetNumberOfThreads(state, false);
|
||||
|
||||
var inputModifier = GetInputModifier(state);
|
||||
|
||||
// If isEncoding is true we're actually starting ffmpeg
|
||||
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
|
||||
|
||||
var segmentFilename = Path.GetFileNameWithoutExtension(outputPath) + "%03d" + GetSegmentFileExtension(state);
|
||||
|
||||
var args = string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts {5} -f ssegment -segment_time {6} -segment_list_size {8} -segment_list \"{9}\" {10}",
|
||||
inputModifier,
|
||||
GetInputArgument(transcodingJobId, state),
|
||||
threads,
|
||||
GetMapArgs(state),
|
||||
GetVideoArguments(state),
|
||||
GetAudioArguments(state),
|
||||
state.SegmentLength.ToString(UsCulture),
|
||||
startNumberParam,
|
||||
state.HlsListSize.ToString(UsCulture),
|
||||
outputPath,
|
||||
segmentFilename
|
||||
).Trim();
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment file extension.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected override string GetSegmentFileExtension(StreamState state)
|
||||
{
|
||||
return ".mp4";
|
||||
}
|
||||
|
||||
protected override TranscodingJobType TranscodingJobType
|
||||
{
|
||||
get
|
||||
{
|
||||
return TranscodingJobType.Dash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Diagnostics;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
|
@ -57,7 +57,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
/// </summary>
|
||||
public class VideoHlsService : BaseHlsService
|
||||
{
|
||||
public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
|
||||
public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using System;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using ServiceStack;
|
||||
|
@ -22,51 +22,54 @@ namespace MediaBrowser.Api.Playback
|
|||
public string UserId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")]
|
||||
public class GetPlaybackInfo : IReturn<LiveMediaInfoResult>
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class MediaInfoService : BaseApiService
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IChannelManager _channelManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
|
||||
public MediaInfoService(ILibraryManager libraryManager, IChannelManager channelManager, IUserManager userManager)
|
||||
public MediaInfoService(IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_channelManager = channelManager;
|
||||
_userManager = userManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetLiveMediaInfo request)
|
||||
public Task<object> Get(GetPlaybackInfo request)
|
||||
{
|
||||
return GetPlaybackInfo(request.Id, request.UserId);
|
||||
}
|
||||
|
||||
public Task<object> Get(GetLiveMediaInfo request)
|
||||
{
|
||||
return GetPlaybackInfo(request.Id, request.UserId);
|
||||
}
|
||||
|
||||
private async Task<object> GetPlaybackInfo(string id, string userId)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(request.Id);
|
||||
IEnumerable<MediaSourceInfo> mediaSources;
|
||||
var result = new LiveMediaInfoResult();
|
||||
|
||||
var channelItem = item as IChannelMediaItem;
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
if (channelItem != null)
|
||||
try
|
||||
{
|
||||
mediaSources = await _channelManager.GetChannelItemMediaSources(request.Id, true, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
catch (PlaybackException ex)
|
||||
{
|
||||
var hasMediaSources = (IHasMediaSources)item;
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
mediaSources = hasMediaSources.GetMediaSources(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaSources = hasMediaSources.GetMediaSources(true, user);
|
||||
}
|
||||
mediaSources = new List<MediaSourceInfo>();
|
||||
result.ErrorCode = ex.ErrorCode;
|
||||
}
|
||||
|
||||
return ToOptimizedResult(new LiveMediaInfoResult
|
||||
{
|
||||
MediaSources = mediaSources.ToList()
|
||||
});
|
||||
result.MediaSources = mediaSources.ToList();
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Diagnostics;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -32,7 +32,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
/// </summary>
|
||||
public class AudioService : BaseProgressiveStreamingService
|
||||
{
|
||||
public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, imageProcessor, httpClient)
|
||||
public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient, imageProcessor, httpClient)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Diagnostics;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -15,7 +15,6 @@ using ServiceStack.Web;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
@ -29,7 +28,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
protected readonly IImageProcessor ImageProcessor;
|
||||
protected readonly IHttpClient HttpClient;
|
||||
|
||||
protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
|
||||
protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient)
|
||||
{
|
||||
ImageProcessor = imageProcessor;
|
||||
HttpClient = httpClient;
|
||||
|
@ -153,49 +152,12 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
|
||||
using (state)
|
||||
{
|
||||
var job = string.IsNullOrEmpty(request.TranscodingJobId) ?
|
||||
null :
|
||||
ApiEntryPoint.Instance.GetTranscodingJob(request.TranscodingJobId);
|
||||
|
||||
var limits = new List<long>();
|
||||
if (state.InputBitrate.HasValue)
|
||||
{
|
||||
// Bytes per second
|
||||
limits.Add((state.InputBitrate.Value / 8));
|
||||
}
|
||||
if (state.InputFileSize.HasValue && state.RunTimeTicks.HasValue)
|
||||
{
|
||||
var totalSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds;
|
||||
|
||||
if (totalSeconds > 1)
|
||||
{
|
||||
var timeBasedLimit = state.InputFileSize.Value / totalSeconds;
|
||||
limits.Add(Convert.ToInt64(timeBasedLimit));
|
||||
}
|
||||
}
|
||||
|
||||
// Take the greater of the above to methods, just to be safe
|
||||
var throttleLimit = limits.Count > 0 ? limits.First() : 0;
|
||||
|
||||
// Pad to play it safe
|
||||
var bytesPerSecond = Convert.ToInt64(1.05 * throttleLimit);
|
||||
|
||||
// Don't even start evaluating this until at least two minutes have content have been consumed
|
||||
var targetGap = throttleLimit * 120;
|
||||
|
||||
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
|
||||
{
|
||||
ResponseHeaders = responseHeaders,
|
||||
ContentType = contentType,
|
||||
IsHeadRequest = isHeadRequest,
|
||||
Path = state.MediaPath,
|
||||
Throttle = request.Throttle,
|
||||
|
||||
ThrottleLimit = bytesPerSecond,
|
||||
|
||||
MinThrottlePosition = targetGap,
|
||||
|
||||
ThrottleCallback = (l1, l2) => ThrottleCallack(l1, l2, bytesPerSecond, job)
|
||||
Path = state.MediaPath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -234,67 +196,6 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
}
|
||||
}
|
||||
|
||||
private readonly long _gapLengthInTicks = TimeSpan.FromMinutes(3).Ticks;
|
||||
|
||||
private long ThrottleCallack(long currentBytesPerSecond, long bytesWritten, long originalBytesPerSecond, TranscodingJob job)
|
||||
{
|
||||
var bytesDownloaded = job.BytesDownloaded ?? 0;
|
||||
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
|
||||
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
|
||||
|
||||
var path = job.Path;
|
||||
|
||||
if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
|
||||
{
|
||||
// Progressive Streaming - byte-based consideration
|
||||
|
||||
try
|
||||
{
|
||||
var bytesTranscoded = job.BytesTranscoded ?? new FileInfo(path).Length;
|
||||
|
||||
// Estimate the bytes the transcoder should be ahead
|
||||
double gapFactor = _gapLengthInTicks;
|
||||
gapFactor /= transcodingPositionTicks;
|
||||
var targetGap = bytesTranscoded * gapFactor;
|
||||
|
||||
var gap = bytesTranscoded - bytesDownloaded;
|
||||
|
||||
if (gap < targetGap)
|
||||
{
|
||||
//Logger.Debug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Logger.Debug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
|
||||
}
|
||||
catch
|
||||
{
|
||||
//Logger.Error("Error getting output size");
|
||||
}
|
||||
}
|
||||
else if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
|
||||
{
|
||||
// HLS - time-based consideration
|
||||
|
||||
var targetGap = _gapLengthInTicks;
|
||||
var gap = transcodingPositionTicks - downloadPositionTicks;
|
||||
|
||||
if (gap < targetGap)
|
||||
{
|
||||
//Logger.Debug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Logger.Debug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Logger.Debug("No throttle data for " + path);
|
||||
}
|
||||
|
||||
return originalBytesPerSecond;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the static remote stream result.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Diagnostics;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -63,7 +63,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
/// </summary>
|
||||
public class VideoService : BaseProgressiveStreamingService
|
||||
{
|
||||
public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, imageProcessor, httpClient)
|
||||
public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient, imageProcessor, httpClient)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -71,8 +71,8 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
public string Params { get; set; }
|
||||
public string ClientTime { get; set; }
|
||||
public string StreamId { get; set; }
|
||||
|
||||
public bool Throttle { get; set; }
|
||||
public string TranscodingJobId { get; set; }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.Net;
|
||||
|
||||
namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
|
@ -23,6 +22,7 @@ namespace MediaBrowser.Api.Playback
|
|||
public string RequestedUrl { get; set; }
|
||||
|
||||
public StreamRequest Request { get; set; }
|
||||
public TranscodingThrottler TranscodingThrottler { get; set; }
|
||||
|
||||
public VideoStreamRequest VideoRequest
|
||||
{
|
||||
|
@ -52,10 +52,12 @@ namespace MediaBrowser.Api.Playback
|
|||
public IIsoMount IsoMount { get; set; }
|
||||
|
||||
public string MediaPath { get; set; }
|
||||
public string WaitForPath { get; set; }
|
||||
|
||||
public MediaProtocol InputProtocol { get; set; }
|
||||
|
||||
public bool IsInputVideo { get; set; }
|
||||
public bool IsInputArchive { get; set; }
|
||||
|
||||
public VideoType VideoType { get; set; }
|
||||
public IsoType? IsoType { get; set; }
|
||||
|
@ -64,8 +66,8 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
public string LiveTvStreamId { get; set; }
|
||||
|
||||
public int SegmentLength = 6;
|
||||
|
||||
public int SegmentLength = 3;
|
||||
public bool EnableGenericHlsSegmenter = false;
|
||||
public int HlsListSize
|
||||
{
|
||||
get
|
||||
|
@ -112,6 +114,7 @@ namespace MediaBrowser.Api.Playback
|
|||
public long? EncodingDurationTicks { get; set; }
|
||||
|
||||
public string ItemType { get; set; }
|
||||
public string ItemId { get; set; }
|
||||
|
||||
public string GetMimeType(string outputPath)
|
||||
{
|
||||
|
@ -125,6 +128,7 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeTranscodingThrottler();
|
||||
DisposeLiveStream();
|
||||
DisposeLogStream();
|
||||
DisposeIsoMount();
|
||||
|
@ -147,6 +151,23 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
}
|
||||
|
||||
private void DisposeTranscodingThrottler()
|
||||
{
|
||||
if (TranscodingThrottler != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
TranscodingThrottler.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error disposing TranscodingThrottler", ex);
|
||||
}
|
||||
|
||||
TranscodingThrottler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeIsoMount()
|
||||
{
|
||||
if (IsoMount != null)
|
||||
|
|
159
MediaBrowser.Api/Playback/TranscodingThrottler.cs
Normal file
159
MediaBrowser.Api/Playback/TranscodingThrottler.cs
Normal file
|
@ -0,0 +1,159 @@
|
|||
using MediaBrowser.Controller.Diagnostics;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
public class TranscodingThrottler : IDisposable
|
||||
{
|
||||
private readonly TranscodingJob _job;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IProcessManager _processManager;
|
||||
private Timer _timer;
|
||||
private bool _isPaused;
|
||||
|
||||
private readonly long _gapLengthInTicks = TimeSpan.FromMinutes(2).Ticks;
|
||||
|
||||
public TranscodingThrottler(TranscodingJob job, ILogger logger, IProcessManager processManager)
|
||||
{
|
||||
_job = job;
|
||||
_logger = logger;
|
||||
_processManager = processManager;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_timer = new Timer(TimerCallback, null, 5000, 5000);
|
||||
}
|
||||
|
||||
private void TimerCallback(object state)
|
||||
{
|
||||
if (_job.HasExited)
|
||||
{
|
||||
DisposeTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsThrottleAllowed(_job))
|
||||
{
|
||||
PauseTranscoding();
|
||||
}
|
||||
else
|
||||
{
|
||||
UnpauseTranscoding();
|
||||
}
|
||||
}
|
||||
|
||||
private void PauseTranscoding()
|
||||
{
|
||||
if (!_isPaused)
|
||||
{
|
||||
_logger.Debug("Sending pause command to ffmpeg");
|
||||
|
||||
try
|
||||
{
|
||||
_job.Process.StandardInput.Write("c");
|
||||
_isPaused = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error pausing transcoding", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UnpauseTranscoding()
|
||||
{
|
||||
if (_isPaused)
|
||||
{
|
||||
_logger.Debug("Sending unpause command to ffmpeg");
|
||||
|
||||
try
|
||||
{
|
||||
_job.Process.StandardInput.WriteLine();
|
||||
_isPaused = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error unpausing transcoding", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsThrottleAllowed(TranscodingJob job)
|
||||
{
|
||||
var bytesDownloaded = job.BytesDownloaded ?? 0;
|
||||
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
|
||||
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
|
||||
|
||||
var path = job.Path;
|
||||
|
||||
if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
|
||||
{
|
||||
// HLS - time-based consideration
|
||||
|
||||
var targetGap = _gapLengthInTicks;
|
||||
var gap = transcodingPositionTicks - downloadPositionTicks;
|
||||
|
||||
if (gap < targetGap)
|
||||
{
|
||||
//_logger.Debug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
|
||||
return false;
|
||||
}
|
||||
|
||||
//_logger.Debug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
|
||||
{
|
||||
// Progressive Streaming - byte-based consideration
|
||||
|
||||
try
|
||||
{
|
||||
var bytesTranscoded = job.BytesTranscoded ?? new FileInfo(path).Length;
|
||||
|
||||
// Estimate the bytes the transcoder should be ahead
|
||||
double gapFactor = _gapLengthInTicks;
|
||||
gapFactor /= transcodingPositionTicks;
|
||||
var targetGap = bytesTranscoded * gapFactor;
|
||||
|
||||
var gap = bytesTranscoded - bytesDownloaded;
|
||||
|
||||
if (gap < targetGap)
|
||||
{
|
||||
//_logger.Debug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
|
||||
return false;
|
||||
}
|
||||
|
||||
//_logger.Debug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
//_logger.Error("Error getting output size");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//_logger.Debug("No throttle data for " + path);
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeTimer();
|
||||
}
|
||||
|
||||
private void DisposeTimer()
|
||||
{
|
||||
if (_timer != null)
|
||||
{
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ using MediaBrowser.Common.Updates;
|
|||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Registration;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using ServiceStack;
|
||||
using ServiceStack.Web;
|
||||
|
@ -106,6 +107,14 @@ namespace MediaBrowser.Api
|
|||
public string Mb2Equivalent { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Registrations/{Name}", "GET", Summary = "Gets registration status for a feature")]
|
||||
[Authenticated]
|
||||
public class GetRegistration : IReturn<RegistrationInfo>
|
||||
{
|
||||
[ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class PluginsService
|
||||
/// </summary>
|
||||
|
@ -144,13 +153,26 @@ namespace MediaBrowser.Api
|
|||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetRegistrationStatus request)
|
||||
public async Task<object> Get(GetRegistrationStatus request)
|
||||
{
|
||||
var result = _securityManager.GetRegistrationStatus(request.Name, request.Mb2Equivalent).Result;
|
||||
var result = await _securityManager.GetRegistrationStatus(request.Name, request.Mb2Equivalent).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetRegistration request)
|
||||
{
|
||||
var result = await _securityManager.GetRegistrationStatus(request.Name).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(new RegistrationInfo
|
||||
{
|
||||
ExpirationDate = result.ExpirationDate,
|
||||
IsRegistered = result.IsRegistered,
|
||||
IsTrial = result.TrialVersion,
|
||||
Name = request.Name
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified request.
|
||||
/// </summary>
|
||||
|
@ -178,7 +200,7 @@ namespace MediaBrowser.Api
|
|||
}
|
||||
catch
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
|
|
|
@ -3,7 +3,6 @@ using MediaBrowser.Common.ScheduledTasks;
|
|||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using ServiceStack;
|
||||
using ServiceStack.Text.Controller;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
|
|
@ -211,7 +211,7 @@ namespace MediaBrowser.Api
|
|||
result.SongCount = album.Tracks.Count();
|
||||
|
||||
result.Artists = album.Artists.ToArray();
|
||||
result.AlbumArtist = album.AlbumArtists.FirstOrDefault();
|
||||
result.AlbumArtist = album.AlbumArtist;
|
||||
}
|
||||
|
||||
var song = item as Audio;
|
||||
|
|
|
@ -418,7 +418,7 @@ namespace MediaBrowser.Api.Session
|
|||
SeekPositionTicks = request.SeekPositionTicks
|
||||
};
|
||||
|
||||
var task = _sessionManager.SendPlaystateCommand(GetSession().Id, request.Id, command, CancellationToken.None);
|
||||
var task = _sessionManager.SendPlaystateCommand(GetSession().Result.Id, request.Id, command, CancellationToken.None);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
@ -436,7 +436,7 @@ namespace MediaBrowser.Api.Session
|
|||
ItemType = request.ItemType
|
||||
};
|
||||
|
||||
var task = _sessionManager.SendBrowseCommand(GetSession().Id, request.Id, command, CancellationToken.None);
|
||||
var task = _sessionManager.SendBrowseCommand(GetSession().Result.Id, request.Id, command, CancellationToken.None);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
@ -455,7 +455,7 @@ namespace MediaBrowser.Api.Session
|
|||
name = commandType.ToString();
|
||||
}
|
||||
|
||||
var currentSession = GetSession();
|
||||
var currentSession = GetSession().Result;
|
||||
|
||||
var command = new GeneralCommand
|
||||
{
|
||||
|
@ -481,7 +481,7 @@ namespace MediaBrowser.Api.Session
|
|||
Text = request.Text
|
||||
};
|
||||
|
||||
var task = _sessionManager.SendMessageCommand(GetSession().Id, request.Id, command, CancellationToken.None);
|
||||
var task = _sessionManager.SendMessageCommand(GetSession().Result.Id, request.Id, command, CancellationToken.None);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
@ -500,14 +500,14 @@ namespace MediaBrowser.Api.Session
|
|||
StartPositionTicks = request.StartPositionTicks
|
||||
};
|
||||
|
||||
var task = _sessionManager.SendPlayCommand(GetSession().Id, request.Id, command, CancellationToken.None);
|
||||
var task = _sessionManager.SendPlayCommand(GetSession().Result.Id, request.Id, command, CancellationToken.None);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public void Post(SendGeneralCommand request)
|
||||
{
|
||||
var currentSession = GetSession();
|
||||
var currentSession = GetSession().Result;
|
||||
|
||||
var command = new GeneralCommand
|
||||
{
|
||||
|
@ -522,7 +522,7 @@ namespace MediaBrowser.Api.Session
|
|||
|
||||
public void Post(SendFullGeneralCommand request)
|
||||
{
|
||||
var currentSession = GetSession();
|
||||
var currentSession = GetSession().Result;
|
||||
|
||||
request.ControllingUserId = currentSession.UserId.HasValue ? currentSession.UserId.Value.ToString("N") : null;
|
||||
|
||||
|
@ -545,7 +545,7 @@ namespace MediaBrowser.Api.Session
|
|||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Id))
|
||||
{
|
||||
request.Id = GetSession().Id;
|
||||
request.Id = GetSession().Result.Id;
|
||||
}
|
||||
_sessionManager.ReportCapabilities(request.Id, new ClientCapabilities
|
||||
{
|
||||
|
@ -569,7 +569,7 @@ namespace MediaBrowser.Api.Session
|
|||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Id))
|
||||
{
|
||||
request.Id = GetSession().Id;
|
||||
request.Id = GetSession().Result.Id;
|
||||
}
|
||||
_sessionManager.ReportCapabilities(request.Id, request);
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ namespace MediaBrowser.Api
|
|||
_config.Configuration.MergeMetadataAndImagesByName = true;
|
||||
_config.Configuration.EnableStandaloneMetadata = true;
|
||||
_config.Configuration.EnableLibraryMetadataSubFolder = true;
|
||||
_config.Configuration.EnableUserSpecificUserViews = true;
|
||||
_config.SaveConfiguration();
|
||||
}
|
||||
|
||||
|
|
|
@ -124,20 +124,23 @@ namespace MediaBrowser.Api.Subtitles
|
|||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ISubtitleManager _subtitleManager;
|
||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
|
||||
public SubtitleService(ILibraryManager libraryManager, ISubtitleManager subtitleManager, ISubtitleEncoder subtitleEncoder)
|
||||
public SubtitleService(ILibraryManager libraryManager, ISubtitleManager subtitleManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IProviderManager providerManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_subtitleManager = subtitleManager;
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
public object Get(GetSubtitlePlaylist request)
|
||||
{
|
||||
var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
|
||||
|
||||
var mediaSource = item.GetMediaSources(false)
|
||||
.First(i => string.Equals(i.Id, request.MediaSourceId ?? request.Id));
|
||||
var mediaSource = _mediaSourceManager.GetStaticMediaSource(item, request.MediaSourceId, false);
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
|
@ -255,7 +258,7 @@ namespace MediaBrowser.Api.Subtitles
|
|||
await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await video.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService()), CancellationToken.None).ConfigureAwait(false);
|
||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Sync;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Model.Sync
|
||||
namespace MediaBrowser.Api.Sync
|
||||
{
|
||||
public static class SyncHelper
|
||||
{
|
||||
|
@ -21,6 +22,7 @@ namespace MediaBrowser.Model.Sync
|
|||
if (item.IsVideo)
|
||||
{
|
||||
options.Add(SyncJobOption.Quality);
|
||||
options.Add(SyncJobOption.Profile);
|
||||
if (items.Count > 1)
|
||||
{
|
||||
options.Add(SyncJobOption.UnwatchedOnly);
|
||||
|
@ -30,6 +32,7 @@ namespace MediaBrowser.Model.Sync
|
|||
if (item.IsFolder && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre)
|
||||
{
|
||||
options.Add(SyncJobOption.Quality);
|
||||
options.Add(SyncJobOption.Profile);
|
||||
options.Add(SyncJobOption.UnwatchedOnly);
|
||||
break;
|
||||
}
|
||||
|
@ -64,6 +67,7 @@ namespace MediaBrowser.Model.Sync
|
|||
|
||||
options.Add(SyncJobOption.Name);
|
||||
options.Add(SyncJobOption.Quality);
|
||||
options.Add(SyncJobOption.Profile);
|
||||
options.Add(SyncJobOption.UnwatchedOnly);
|
||||
options.Add(SyncJobOption.SyncNewContent);
|
||||
options.Add(SyncJobOption.ItemLimit);
|
|
@ -94,6 +94,9 @@ namespace MediaBrowser.Api.Sync
|
|||
[ApiMember(Name = "ParentId", Description = "ParentId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string ParentId { get; set; }
|
||||
|
||||
[ApiMember(Name = "TargetId", Description = "TargetId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string TargetId { get; set; }
|
||||
|
||||
[ApiMember(Name = "Category", Description = "Category", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public SyncCategory? Category { get; set; }
|
||||
}
|
||||
|
@ -226,6 +229,21 @@ namespace MediaBrowser.Api.Sync
|
|||
result.Targets = _syncManager.GetSyncTargets(request.UserId)
|
||||
.ToList();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.TargetId))
|
||||
{
|
||||
result.Targets = result.Targets
|
||||
.Where(i => string.Equals(i.Id, request.TargetId, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
result.QualityOptions = _syncManager
|
||||
.GetQualityOptions(request.TargetId)
|
||||
.ToList();
|
||||
|
||||
result.ProfileOptions = _syncManager
|
||||
.GetProfileOptions(request.TargetId)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (request.Category.HasValue)
|
||||
{
|
||||
result.Options = SyncHelper.GetSyncOptions(request.Category.Value);
|
||||
|
|
|
@ -39,6 +39,9 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
[ApiMember(Name = "Person", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string Person { get; set; }
|
||||
|
||||
[ApiMember(Name = "PersonIds", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string PersonIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the Person filter is used, this can also be used to restrict to a specific person type
|
||||
/// </summary>
|
||||
|
@ -46,9 +49,6 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
[ApiMember(Name = "PersonTypes", Description = "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string PersonTypes { get; set; }
|
||||
|
||||
[ApiMember(Name = "AllGenres", Description = "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string AllGenres { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Limit results to items containing specific studios
|
||||
/// </summary>
|
||||
|
@ -56,6 +56,9 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
[ApiMember(Name = "Studios", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string Studios { get; set; }
|
||||
|
||||
[ApiMember(Name = "StudioIds", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string StudioIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the studios.
|
||||
/// </summary>
|
||||
|
@ -63,6 +66,9 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
[ApiMember(Name = "Artists", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string Artists { get; set; }
|
||||
|
||||
[ApiMember(Name = "ArtistIds", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string ArtistIds { get; set; }
|
||||
|
||||
[ApiMember(Name = "Albums", Description = "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
|
||||
public string Albums { get; set; }
|
||||
|
||||
|
@ -226,22 +232,27 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
[ApiMember(Name = "CollapseBoxSetItems", Description = "Whether or not to hide items behind their boxsets.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool? CollapseBoxSetItems { get; set; }
|
||||
|
||||
public string[] GetAllGenres()
|
||||
{
|
||||
return (AllGenres ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public string[] GetStudios()
|
||||
{
|
||||
return (Studios ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public string[] GetStudioIds()
|
||||
{
|
||||
return (StudioIds ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public string[] GetPersonTypes()
|
||||
{
|
||||
return (PersonTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public IEnumerable<VideoType> GetVideoTypes()
|
||||
public string[] GetPersonIds()
|
||||
{
|
||||
return (PersonIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public VideoType[] GetVideoTypes()
|
||||
{
|
||||
var val = VideoTypes;
|
||||
|
||||
|
@ -250,7 +261,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
return new VideoType[] { };
|
||||
}
|
||||
|
||||
return val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true));
|
||||
return val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -471,9 +482,10 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
Tags = request.GetTags(),
|
||||
OfficialRatings = request.GetOfficialRatings(),
|
||||
Genres = request.GetGenres(),
|
||||
AllGenres = request.GetAllGenres(),
|
||||
Studios = request.GetStudios(),
|
||||
StudioIds = request.GetStudioIds(),
|
||||
Person = request.Person,
|
||||
PersonIds = request.GetPersonIds(),
|
||||
PersonTypes = request.GetPersonTypes(),
|
||||
Years = request.GetYears(),
|
||||
ImageTypes = request.GetImageTypes().ToArray(),
|
||||
|
@ -609,6 +621,8 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
private bool ApplyAdditionalFilters(GetItems request, BaseItem i, User user, bool isPreFiltered, ILibraryManager libraryManager)
|
||||
{
|
||||
var video = i as Video;
|
||||
|
||||
if (!isPreFiltered)
|
||||
{
|
||||
var mediaTypes = request.GetMediaTypes();
|
||||
|
@ -656,7 +670,6 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
if (request.Is3D.HasValue)
|
||||
{
|
||||
var val = request.Is3D.Value;
|
||||
var video = i as Video;
|
||||
|
||||
if (video == null || val != video.Video3DFormat.HasValue)
|
||||
{
|
||||
|
@ -667,7 +680,6 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
if (request.IsHD.HasValue)
|
||||
{
|
||||
var val = request.IsHD.Value;
|
||||
var video = i as Video;
|
||||
|
||||
if (video == null || val != video.IsHD)
|
||||
{
|
||||
|
@ -809,8 +821,6 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
{
|
||||
var val = request.HasSubtitles.Value;
|
||||
|
||||
var video = i as Video;
|
||||
|
||||
if (video == null || val != video.HasSubtitles)
|
||||
{
|
||||
return false;
|
||||
|
@ -930,25 +940,13 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
return false;
|
||||
}
|
||||
|
||||
// Apply genre filter
|
||||
var allGenres = request.GetAllGenres();
|
||||
if (allGenres.Length > 0 && !allGenres.All(v => i.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
|
||||
// Filter by VideoType
|
||||
var videoTypes = request.GetVideoTypes();
|
||||
if (videoTypes.Length > 0 && (video == null || !videoTypes.Contains(video.VideoType)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by VideoType
|
||||
if (!string.IsNullOrEmpty(request.VideoTypes))
|
||||
{
|
||||
var types = request.VideoTypes.Split(',');
|
||||
|
||||
var video = i as Video;
|
||||
if (video == null || !types.Contains(video.VideoType.ToString(), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var imageTypes = request.GetImageTypes().ToList();
|
||||
if (imageTypes.Count > 0)
|
||||
{
|
||||
|
@ -965,11 +963,37 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
return false;
|
||||
}
|
||||
|
||||
// Apply studio filter
|
||||
var studioIds = request.GetStudioIds();
|
||||
if (studioIds.Length > 0 && !studioIds.Any(id =>
|
||||
{
|
||||
var studioItem = libraryManager.GetItemById(id);
|
||||
return studioItem != null && i.Studios.Contains(studioItem.Name, StringComparer.OrdinalIgnoreCase);
|
||||
}))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply year filter
|
||||
var years = request.GetYears();
|
||||
if (years.Length > 0 && !(i.ProductionYear.HasValue && years.Contains(i.ProductionYear.Value)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply person filter
|
||||
var personIds = request.GetPersonIds();
|
||||
if (personIds.Length > 0)
|
||||
{
|
||||
var names = personIds
|
||||
.Select(libraryManager.GetItemById)
|
||||
.Select(p => p == null ? "-1" : p.Name)
|
||||
.ToList();
|
||||
|
||||
if (!(names.Any(v => i.People.Select(p => p.Name).Contains(v, StringComparer.OrdinalIgnoreCase))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply person filter
|
||||
|
@ -1030,6 +1054,23 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
}
|
||||
}
|
||||
|
||||
// Artists
|
||||
if (!string.IsNullOrEmpty(request.ArtistIds))
|
||||
{
|
||||
var artistIds = request.ArtistIds.Split('|');
|
||||
|
||||
var audio = i as IHasArtist;
|
||||
|
||||
if (!(audio != null && artistIds.Any(id =>
|
||||
{
|
||||
var artistItem = libraryManager.GetItemById(id);
|
||||
return artistItem != null && audio.HasAnyArtist(artistItem.Name);
|
||||
})))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Artists
|
||||
if (!string.IsNullOrEmpty(request.Artists))
|
||||
{
|
||||
|
@ -1037,7 +1078,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
var audio = i as IHasArtist;
|
||||
|
||||
if (!(audio != null && artists.Any(audio.HasArtist)))
|
||||
if (!(audio != null && artists.Any(audio.HasAnyArtist)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -231,7 +231,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
|
||||
var session = GetSession();
|
||||
var session = await GetSession().ConfigureAwait(false);
|
||||
|
||||
var dto = await UpdatePlayedStatus(user, request.Id, true, datePlayed).ConfigureAwait(false);
|
||||
|
||||
|
@ -266,7 +266,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
public void Post(ReportPlaybackStart request)
|
||||
{
|
||||
request.SessionId = GetSession().Id;
|
||||
request.SessionId = GetSession().Result.Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackStart(request);
|
||||
|
||||
|
@ -294,7 +294,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
public void Post(ReportPlaybackProgress request)
|
||||
{
|
||||
request.SessionId = GetSession().Id;
|
||||
request.SessionId = GetSession().Result.Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackProgress(request);
|
||||
|
||||
|
@ -317,7 +317,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
public void Post(ReportPlaybackStopped request)
|
||||
{
|
||||
request.SessionId = GetSession().Id;
|
||||
request.SessionId = GetSession().Result.Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackStopped(request);
|
||||
|
||||
|
@ -339,7 +339,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
||||
var session = GetSession();
|
||||
var session = await GetSession().ConfigureAwait(false);
|
||||
|
||||
var dto = await UpdatePlayedStatus(user, request.Id, false, null).ConfigureAwait(false);
|
||||
|
||||
|
|
|
@ -253,18 +253,14 @@ namespace MediaBrowser.Api
|
|||
/// The _user manager
|
||||
/// </summary>
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly ISessionManager _sessionMananger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
|
||||
public IAuthorizationContext AuthorizationContext { get; set; }
|
||||
|
||||
public UserService(IUserManager userManager, IDtoService dtoService, ISessionManager sessionMananger, IServerConfigurationManager config, INetworkManager networkManager, IDeviceManager deviceManager)
|
||||
public UserService(IUserManager userManager, ISessionManager sessionMananger, IServerConfigurationManager config, INetworkManager networkManager, IDeviceManager deviceManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_dtoService = dtoService;
|
||||
_sessionMananger = sessionMananger;
|
||||
_config = config;
|
||||
_networkManager = networkManager;
|
||||
|
@ -464,7 +460,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
public async Task PostAsync(UpdateUserPassword request)
|
||||
{
|
||||
AssertCanUpdateUser(request.Id);
|
||||
AssertCanUpdateUser(_userManager, request.Id);
|
||||
|
||||
var user = _userManager.GetUserById(request.Id);
|
||||
|
||||
|
@ -498,7 +494,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
public async Task PostAsync(UpdateUserEasyPassword request)
|
||||
{
|
||||
AssertCanUpdateUser(request.Id);
|
||||
AssertCanUpdateUser(_userManager, request.Id);
|
||||
|
||||
var user = _userManager.GetUserById(request.Id);
|
||||
|
||||
|
@ -534,7 +530,7 @@ namespace MediaBrowser.Api
|
|||
// https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
|
||||
var id = GetPathValue(1);
|
||||
|
||||
AssertCanUpdateUser(id);
|
||||
AssertCanUpdateUser(_userManager, id);
|
||||
|
||||
var dtoUser = request;
|
||||
|
||||
|
@ -584,29 +580,13 @@ namespace MediaBrowser.Api
|
|||
|
||||
public void Post(UpdateUserConfiguration request)
|
||||
{
|
||||
AssertCanUpdateUser(request.Id);
|
||||
AssertCanUpdateUser(_userManager, request.Id);
|
||||
|
||||
var task = _userManager.UpdateConfiguration(request.Id, request);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
private void AssertCanUpdateUser(string userId)
|
||||
{
|
||||
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||
|
||||
// If they're going to update the record of another user, they must be an administrator
|
||||
if (!string.Equals(userId, auth.UserId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var authenticatedUser = _userManager.GetUserById(auth.UserId);
|
||||
|
||||
if (!authenticatedUser.Policy.IsAdministrator)
|
||||
{
|
||||
throw new SecurityException("Unauthorized access.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Post(UpdateUserPolicy request)
|
||||
{
|
||||
var task = UpdateUserPolicy(request);
|
||||
|
|
|
@ -5,6 +5,7 @@ using SharpCompress.Archive.Tar;
|
|||
using SharpCompress.Common;
|
||||
using SharpCompress.Reader;
|
||||
using SharpCompress.Reader.Zip;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace MediaBrowser.Common.Implementations.Archiving
|
||||
|
|
|
@ -313,7 +313,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
|
||||
async void trigger_Triggered(object sender, EventArgs e)
|
||||
async void trigger_Triggered(object sender, GenericEventArgs<TaskExecutionOptions> e)
|
||||
{
|
||||
var trigger = (ITaskTrigger)sender;
|
||||
|
||||
|
@ -340,11 +340,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
/// <summary>
|
||||
/// Executes the task
|
||||
/// </summary>
|
||||
/// <param name="options">Task options.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception>
|
||||
public async Task Execute()
|
||||
public async Task Execute(TaskExecutionOptions options)
|
||||
{
|
||||
var task = ExecuteInternal();
|
||||
var task = ExecuteInternal(options);
|
||||
|
||||
_currentTask = task;
|
||||
|
||||
|
@ -358,7 +359,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteInternal()
|
||||
private async Task ExecuteInternal(TaskExecutionOptions options)
|
||||
{
|
||||
// Cancel the current execution, if any
|
||||
if (CurrentCancellationTokenSource != null)
|
||||
|
@ -383,7 +384,14 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
|
||||
try
|
||||
{
|
||||
await ExecuteTask(CurrentCancellationTokenSource.Token, progress).ConfigureAwait(false);
|
||||
var localTask = ScheduledTask.Execute(CurrentCancellationTokenSource.Token, progress);
|
||||
|
||||
if (options != null && options.MaxRuntimeMs.HasValue)
|
||||
{
|
||||
CurrentCancellationTokenSource.CancelAfter(options.MaxRuntimeMs.Value);
|
||||
}
|
||||
|
||||
await localTask.ConfigureAwait(false);
|
||||
|
||||
status = TaskCompletionStatus.Completed;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
/// <summary>
|
||||
/// The _task queue
|
||||
/// </summary>
|
||||
private readonly List<Type> _taskQueue = new List<Type>();
|
||||
private readonly SortedDictionary<Type, TaskExecutionOptions> _taskQueue = new SortedDictionary<Type, TaskExecutionOptions>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the json serializer.
|
||||
|
@ -69,13 +69,20 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
/// Cancels if running and queue.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public void CancelIfRunningAndQueue<T>()
|
||||
/// <param name="options">Task options.</param>
|
||||
public void CancelIfRunningAndQueue<T>(TaskExecutionOptions options)
|
||||
where T : IScheduledTask
|
||||
{
|
||||
var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
|
||||
((ScheduledTaskWorker)task).CancelIfRunning();
|
||||
|
||||
QueueScheduledTask<T>();
|
||||
QueueScheduledTask<T>(options);
|
||||
}
|
||||
|
||||
public void CancelIfRunningAndQueue<T>()
|
||||
where T : IScheduledTask
|
||||
{
|
||||
CancelIfRunningAndQueue<T>(new TaskExecutionOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -93,30 +100,39 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
/// Queues the scheduled task.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public void QueueScheduledTask<T>()
|
||||
/// <param name="options">Task options</param>
|
||||
public void QueueScheduledTask<T>(TaskExecutionOptions options)
|
||||
where T : IScheduledTask
|
||||
{
|
||||
var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
|
||||
|
||||
QueueScheduledTask(scheduledTask);
|
||||
QueueScheduledTask(scheduledTask, options);
|
||||
}
|
||||
|
||||
public void QueueScheduledTask<T>()
|
||||
where T : IScheduledTask
|
||||
{
|
||||
QueueScheduledTask<T>(new TaskExecutionOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues the scheduled task.
|
||||
/// </summary>
|
||||
/// <param name="task">The task.</param>
|
||||
public void QueueScheduledTask(IScheduledTask task)
|
||||
/// <param name="options">The task options.</param>
|
||||
public void QueueScheduledTask(IScheduledTask task, TaskExecutionOptions options)
|
||||
{
|
||||
var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == task.GetType());
|
||||
|
||||
QueueScheduledTask(scheduledTask);
|
||||
QueueScheduledTask(scheduledTask, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues the scheduled task.
|
||||
/// </summary>
|
||||
/// <param name="task">The task.</param>
|
||||
private void QueueScheduledTask(IScheduledTaskWorker task)
|
||||
/// <param name="options">The task options.</param>
|
||||
private void QueueScheduledTask(IScheduledTaskWorker task, TaskExecutionOptions options)
|
||||
{
|
||||
var type = task.ScheduledTask.GetType();
|
||||
|
||||
|
@ -125,17 +141,18 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
// If it's idle just execute immediately
|
||||
if (task.State == TaskState.Idle)
|
||||
{
|
||||
Execute(task);
|
||||
Execute(task, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_taskQueue.Contains(type))
|
||||
if (!_taskQueue.ContainsKey(type))
|
||||
{
|
||||
Logger.Info("Queueing task {0}", type.Name);
|
||||
_taskQueue.Add(type);
|
||||
_taskQueue.Add(type, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
_taskQueue[type] = options;
|
||||
Logger.Info("Task already queued: {0}", type.Name);
|
||||
}
|
||||
}
|
||||
|
@ -181,9 +198,9 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
((ScheduledTaskWorker)task).Cancel();
|
||||
}
|
||||
|
||||
public Task Execute(IScheduledTaskWorker task)
|
||||
public Task Execute(IScheduledTaskWorker task, TaskExecutionOptions options)
|
||||
{
|
||||
return ((ScheduledTaskWorker)task).Execute();
|
||||
return ((ScheduledTaskWorker)task).Execute(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -224,15 +241,15 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
// Execute queued tasks
|
||||
lock (_taskQueue)
|
||||
{
|
||||
foreach (var type in _taskQueue.ToList())
|
||||
foreach (var enqueuedType in _taskQueue.ToList())
|
||||
{
|
||||
var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == type);
|
||||
var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Key);
|
||||
|
||||
if (scheduledTask.State == TaskState.Idle)
|
||||
{
|
||||
Execute(scheduledTask);
|
||||
Execute(scheduledTask, enqueuedType.Value);
|
||||
|
||||
_taskQueue.Remove(type);
|
||||
_taskQueue.Remove(enqueuedType.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,9 +118,23 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
|
|||
index++;
|
||||
}
|
||||
|
||||
DeleteEmptyFolders(directory);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
private static void DeleteEmptyFolders(string parent)
|
||||
{
|
||||
foreach (var directory in Directory.GetDirectories(parent))
|
||||
{
|
||||
DeleteEmptyFolders(directory);
|
||||
if (!Directory.EnumerateFileSystemEntries(directory).Any())
|
||||
{
|
||||
Directory.Delete(directory, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteFile(string path)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -59,6 +59,11 @@ namespace MediaBrowser.Common.Implementations.Serialization
|
|||
}
|
||||
}
|
||||
|
||||
private Stream OpenFile(string path)
|
||||
{
|
||||
return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes from file.
|
||||
/// </summary>
|
||||
|
@ -78,7 +83,7 @@ namespace MediaBrowser.Common.Implementations.Serialization
|
|||
throw new ArgumentNullException("file");
|
||||
}
|
||||
|
||||
using (Stream stream = File.OpenRead(file))
|
||||
using (Stream stream = OpenFile(file))
|
||||
{
|
||||
return DeserializeFromStream(stream, type);
|
||||
}
|
||||
|
@ -99,7 +104,7 @@ namespace MediaBrowser.Common.Implementations.Serialization
|
|||
throw new ArgumentNullException("file");
|
||||
}
|
||||
|
||||
using (Stream stream = File.OpenRead(file))
|
||||
using (Stream stream = OpenFile(file))
|
||||
{
|
||||
return DeserializeFromStream<T>(stream);
|
||||
}
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
<Compile Include="ScheduledTasks\DailyTrigger.cs" />
|
||||
<Compile Include="ScheduledTasks\IntervalTrigger.cs" />
|
||||
<Compile Include="ScheduledTasks\TaskCompletionEventArgs.cs" />
|
||||
<Compile Include="ScheduledTasks\TaskExecutionOptions.cs" />
|
||||
<Compile Include="ScheduledTasks\WeeklyTrigger.cs" />
|
||||
<Compile Include="Security\IRequiresRegistration.cs" />
|
||||
<Compile Include="Security\ISecurityManager.cs" />
|
||||
|
|
|
@ -325,7 +325,7 @@ namespace MediaBrowser.Common.Plugins
|
|||
AssemblyFileName = AssemblyFileName,
|
||||
ConfigurationDateLastModified = ConfigurationDateLastModified,
|
||||
Description = Description,
|
||||
Id = Id.ToString("N"),
|
||||
Id = Id.ToString(),
|
||||
ConfigurationFileName = ConfigurationFileName
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.Events;
|
||||
|
||||
namespace MediaBrowser.Common.ScheduledTasks
|
||||
{
|
||||
|
@ -20,6 +21,14 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <value>The timer.</value>
|
||||
private Timer Timer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the execution properties of this task.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The execution properties of this task.
|
||||
/// </value>
|
||||
public TaskExecutionOptions TaskOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stars waiting for the trigger action
|
||||
/// </summary>
|
||||
|
@ -58,7 +67,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <summary>
|
||||
/// Occurs when [triggered].
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs> Triggered;
|
||||
public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
|
||||
|
||||
/// <summary>
|
||||
/// Called when [triggered].
|
||||
|
@ -67,7 +76,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
{
|
||||
if (Triggered != null)
|
||||
{
|
||||
Triggered(this, EventArgs.Empty);
|
||||
Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,14 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <value>The scheduled tasks.</value>
|
||||
IScheduledTaskWorker[] ScheduledTasks { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Cancels if running and queue.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="options">Task options.</param>
|
||||
void CancelIfRunningAndQueue<T>(TaskExecutionOptions options)
|
||||
where T : IScheduledTask;
|
||||
|
||||
/// <summary>
|
||||
/// Cancels if running and queue.
|
||||
/// </summary>
|
||||
|
@ -27,6 +35,14 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
void CancelIfRunning<T>()
|
||||
where T : IScheduledTask;
|
||||
|
||||
/// <summary>
|
||||
/// Queues the scheduled task.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="options">Task options.</param>
|
||||
void QueueScheduledTask<T>(TaskExecutionOptions options)
|
||||
where T : IScheduledTask;
|
||||
|
||||
/// <summary>
|
||||
/// Queues the scheduled task.
|
||||
/// </summary>
|
||||
|
@ -38,7 +54,8 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// Queues the scheduled task.
|
||||
/// </summary>
|
||||
/// <param name="task">The task.</param>
|
||||
void QueueScheduledTask(IScheduledTask task);
|
||||
/// <param name="options">The task run options.</param>
|
||||
void QueueScheduledTask(IScheduledTask task, TaskExecutionOptions options = null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the tasks.
|
||||
|
@ -47,7 +64,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
void AddTasks(IEnumerable<IScheduledTask> tasks);
|
||||
|
||||
void Cancel(IScheduledTaskWorker task);
|
||||
Task Execute(IScheduledTaskWorker task);
|
||||
Task Execute(IScheduledTaskWorker task, TaskExecutionOptions options = null);
|
||||
|
||||
event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
|
||||
event EventHandler<TaskCompletionEventArgs> TaskCompleted;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using MediaBrowser.Model.Events;
|
||||
|
||||
namespace MediaBrowser.Common.ScheduledTasks
|
||||
{
|
||||
|
@ -10,7 +11,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <summary>
|
||||
/// Fires when the trigger condition is satisfied and the task should run
|
||||
/// </summary>
|
||||
event EventHandler<EventArgs> Triggered;
|
||||
event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
|
||||
|
||||
/// <summary>
|
||||
/// Stars waiting for the trigger action
|
||||
|
@ -22,5 +23,13 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// Stops waiting for the trigger action
|
||||
/// </summary>
|
||||
void Stop();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the execution properties of this task.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The execution properties of this task.
|
||||
/// </value>
|
||||
TaskExecutionOptions TaskOptions { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.Events;
|
||||
|
||||
namespace MediaBrowser.Common.ScheduledTasks
|
||||
{
|
||||
|
@ -20,6 +21,14 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <value>The timer.</value>
|
||||
private Timer Timer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the execution properties of this task.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The execution properties of this task.
|
||||
/// </value>
|
||||
public TaskExecutionOptions TaskOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stars waiting for the trigger action
|
||||
/// </summary>
|
||||
|
@ -53,7 +62,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <summary>
|
||||
/// Occurs when [triggered].
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs> Triggered;
|
||||
public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
|
||||
|
||||
/// <summary>
|
||||
/// Called when [triggered].
|
||||
|
@ -62,7 +71,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
{
|
||||
if (Triggered != null)
|
||||
{
|
||||
Triggered(this, EventArgs.Empty);
|
||||
Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,6 +99,11 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
info.SystemEvent = systemEventTrigger.SystemEvent;
|
||||
}
|
||||
|
||||
if (trigger.TaskOptions != null)
|
||||
{
|
||||
info.MaxRuntimeMs = trigger.TaskOptions.MaxRuntimeMs;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
@ -111,6 +116,11 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <exception cref="System.ArgumentException">Invalid trigger type: + info.Type</exception>
|
||||
public static ITaskTrigger GetTrigger(TaskTriggerInfo info)
|
||||
{
|
||||
var options = new TaskExecutionOptions
|
||||
{
|
||||
MaxRuntimeMs = info.MaxRuntimeMs
|
||||
};
|
||||
|
||||
if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!info.TimeOfDayTicks.HasValue)
|
||||
|
@ -120,7 +130,8 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
|
||||
return new DailyTrigger
|
||||
{
|
||||
TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value)
|
||||
TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
|
||||
TaskOptions = options
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -139,7 +150,8 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
return new WeeklyTrigger
|
||||
{
|
||||
TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
|
||||
DayOfWeek = info.DayOfWeek.Value
|
||||
DayOfWeek = info.DayOfWeek.Value,
|
||||
TaskOptions = options
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -152,7 +164,8 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
|
||||
return new IntervalTrigger
|
||||
{
|
||||
Interval = TimeSpan.FromTicks(info.IntervalTicks.Value)
|
||||
Interval = TimeSpan.FromTicks(info.IntervalTicks.Value),
|
||||
TaskOptions = options
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -165,7 +178,8 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
|
||||
return new SystemEventTrigger
|
||||
{
|
||||
SystemEvent = info.SystemEvent.Value
|
||||
SystemEvent = info.SystemEvent.Value,
|
||||
TaskOptions = options
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Events;
|
||||
|
||||
namespace MediaBrowser.Common.ScheduledTasks
|
||||
{
|
||||
|
@ -10,6 +11,14 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
{
|
||||
public int DelayMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the execution properties of this task.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The execution properties of this task.
|
||||
/// </value>
|
||||
public TaskExecutionOptions TaskOptions { get; set; }
|
||||
|
||||
public StartupTrigger()
|
||||
{
|
||||
DelayMs = 3000;
|
||||
|
@ -39,7 +48,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <summary>
|
||||
/// Occurs when [triggered].
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs> Triggered;
|
||||
public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
|
||||
|
||||
/// <summary>
|
||||
/// Called when [triggered].
|
||||
|
@ -48,7 +57,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
{
|
||||
if (Triggered != null)
|
||||
{
|
||||
Triggered(this, EventArgs.Empty);
|
||||
Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Events;
|
||||
|
||||
namespace MediaBrowser.Common.ScheduledTasks
|
||||
{
|
||||
|
@ -16,6 +17,14 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <value>The system event.</value>
|
||||
public SystemEvent SystemEvent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the execution properties of this task.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The execution properties of this task.
|
||||
/// </value>
|
||||
public TaskExecutionOptions TaskOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stars waiting for the trigger action
|
||||
/// </summary>
|
||||
|
@ -57,7 +66,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <summary>
|
||||
/// Occurs when [triggered].
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs> Triggered;
|
||||
public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
|
||||
|
||||
/// <summary>
|
||||
/// Called when [triggered].
|
||||
|
@ -66,7 +75,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
{
|
||||
if (Triggered != null)
|
||||
{
|
||||
Triggered(this, EventArgs.Empty);
|
||||
Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
11
MediaBrowser.Common/ScheduledTasks/TaskExecutionOptions.cs
Normal file
11
MediaBrowser.Common/ScheduledTasks/TaskExecutionOptions.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
namespace MediaBrowser.Common.ScheduledTasks
|
||||
{
|
||||
/// <summary>
|
||||
/// A class that encomposases all common task run properties.
|
||||
/// </summary>
|
||||
public class TaskExecutionOptions
|
||||
{
|
||||
public int? MaxRuntimeMs { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.Events;
|
||||
|
||||
namespace MediaBrowser.Common.ScheduledTasks
|
||||
{
|
||||
|
@ -20,6 +21,14 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <value>The day of week.</value>
|
||||
public DayOfWeek DayOfWeek { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the execution properties of this task.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The execution properties of this task.
|
||||
/// </value>
|
||||
public TaskExecutionOptions TaskOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timer.
|
||||
/// </summary>
|
||||
|
@ -88,7 +97,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
/// <summary>
|
||||
/// Occurs when [triggered].
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs> Triggered;
|
||||
public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
|
||||
|
||||
/// <summary>
|
||||
/// Called when [triggered].
|
||||
|
@ -97,7 +106,7 @@ namespace MediaBrowser.Common.ScheduledTasks
|
|||
{
|
||||
if (Triggered != null)
|
||||
{
|
||||
Triggered(this, EventArgs.Empty);
|
||||
Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
19
MediaBrowser.Controller/Connect/ConnectSupporterSummary.cs
Normal file
19
MediaBrowser.Controller/Connect/ConnectSupporterSummary.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using MediaBrowser.Model.Connect;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Controller.Connect
|
||||
{
|
||||
public class ConnectSupporterSummary
|
||||
{
|
||||
public int MaxUsers { get; set; }
|
||||
public List<ConnectUser> Users { get; set; }
|
||||
public List<UserDto> EligibleUsers { get; set; }
|
||||
|
||||
public ConnectSupporterSummary()
|
||||
{
|
||||
Users = new List<ConnectUser>();
|
||||
EligibleUsers = new List<UserDto>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -69,5 +69,25 @@ namespace MediaBrowser.Controller.Connect
|
|||
/// <param name="token">The token.</param>
|
||||
/// <returns><c>true</c> if [is authorization token valid] [the specified token]; otherwise, <c>false</c>.</returns>
|
||||
bool IsAuthorizationTokenValid(string token);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connect supporter summary.
|
||||
/// </summary>
|
||||
/// <returns>Task<ConnectSupporterSummary>.</returns>
|
||||
Task<ConnectSupporterSummary> GetConnectSupporterSummary();
|
||||
|
||||
/// <summary>
|
||||
/// Removes the connect supporter.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task RemoveConnectSupporter(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the connect supporter.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task AddConnectSupporter(string id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,9 +25,10 @@ namespace MediaBrowser.Controller.Devices
|
|||
/// <param name="reportedId">The reported identifier.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="appName">Name of the application.</param>
|
||||
/// <param name="appVersion">The application version.</param>
|
||||
/// <param name="usedByUserId">The used by user identifier.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task<DeviceInfo> RegisterDevice(string reportedId, string name, string appName, string usedByUserId);
|
||||
Task<DeviceInfo> RegisterDevice(string reportedId, string name, string appName, string appVersion, string usedByUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the capabilities.
|
||||
|
|
28
MediaBrowser.Controller/Diagnostics/IProcessManager.cs
Normal file
28
MediaBrowser.Controller/Diagnostics/IProcessManager.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace MediaBrowser.Controller.Diagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IProcessManager
|
||||
/// </summary>
|
||||
public interface IProcessManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [supports suspension].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [supports suspension]; otherwise, <c>false</c>.</value>
|
||||
bool SupportsSuspension { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Suspends the process.
|
||||
/// </summary>
|
||||
/// <param name="process">The process.</param>
|
||||
void SuspendProcess(Process process);
|
||||
|
||||
/// <summary>
|
||||
/// Resumes the process.
|
||||
/// </summary>
|
||||
/// <param name="process">The process.</param>
|
||||
void ResumeProcess(Process process);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -30,10 +29,9 @@ namespace MediaBrowser.Controller.Drawing
|
|||
/// <summary>
|
||||
/// Gets the size of the image.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="imageDateModified">The image date modified.</param>
|
||||
/// <param name="info">The information.</param>
|
||||
/// <returns>ImageSize.</returns>
|
||||
ImageSize GetImageSize(string path, DateTime imageDateModified);
|
||||
ImageSize GetImageSize(ItemImageInfo info);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the parts.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
@ -22,7 +21,8 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
IHasLookupInfo<SongInfo>,
|
||||
IHasTags,
|
||||
IHasMediaSources,
|
||||
IThemeMedia
|
||||
IThemeMedia,
|
||||
IArchivable
|
||||
{
|
||||
public string FormatName { get; set; }
|
||||
public long? Size { get; set; }
|
||||
|
@ -171,16 +171,6 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
+ (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified name has artist.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns><c>true</c> if the specified name has artist; otherwise, <c>false</c>.</returns>
|
||||
public bool HasArtist(string name)
|
||||
{
|
||||
return AllArtists.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user data key.
|
||||
/// </summary>
|
||||
|
@ -239,7 +229,7 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
{
|
||||
Id = i.Id.ToString("N"),
|
||||
Protocol = locationType == LocationType.Remote ? MediaProtocol.Http : MediaProtocol.File,
|
||||
MediaStreams = MediaSourceManager.GetMediaStreams(new MediaStreamQuery { ItemId = i.Id }).ToList(),
|
||||
MediaStreams = MediaSourceManager.GetMediaStreams(i.Id).ToList(),
|
||||
Name = i.Name,
|
||||
Path = enablePathSubstituion ? GetMappedPath(i.Path, locationType) : i.Path,
|
||||
RunTimeTicks = i.RunTimeTicks,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
|
@ -9,10 +11,20 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
|
||||
public interface IHasArtist
|
||||
{
|
||||
bool HasArtist(string name);
|
||||
|
||||
List<string> AllArtists { get; }
|
||||
|
||||
List<string> Artists { get; }
|
||||
List<string> Artists { get; set; }
|
||||
}
|
||||
|
||||
public static class HasArtistExtensions
|
||||
{
|
||||
public static bool HasArtist(this IHasArtist hasArtist, string artist)
|
||||
{
|
||||
return hasArtist.Artists.Contains(artist, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
public static bool HasAnyArtist(this IHasArtist hasArtist, string artist)
|
||||
{
|
||||
return hasArtist.AllArtists.Contains(artist, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,16 +120,6 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
get { return Parent as MusicArtist ?? UnknwonArtist; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified artist has artist.
|
||||
/// </summary>
|
||||
/// <param name="artist">The artist.</param>
|
||||
/// <returns><c>true</c> if the specified artist has artist; otherwise, <c>false</c>.</returns>
|
||||
public bool HasArtist(string artist)
|
||||
{
|
||||
return AllArtists.Contains(artist, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public List<string> Artists { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
@ -148,12 +149,15 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
var totalItems = songs.Count + others.Count;
|
||||
var numComplete = 0;
|
||||
|
||||
var childUpdateType = ItemUpdateType.None;
|
||||
|
||||
// Refresh songs
|
||||
foreach (var item in songs)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
var updateType = await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
childUpdateType = childUpdateType | updateType;
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
|
@ -161,15 +165,22 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
progress.Report(percent * 100);
|
||||
}
|
||||
|
||||
var parentRefreshOptions = refreshOptions;
|
||||
if (childUpdateType > ItemUpdateType.None)
|
||||
{
|
||||
parentRefreshOptions = new MetadataRefreshOptions(refreshOptions);
|
||||
parentRefreshOptions.MetadataRefreshMode = MetadataRefreshMode.FullRefresh;
|
||||
}
|
||||
|
||||
// Refresh current item
|
||||
await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
await RefreshMetadata(parentRefreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Refresh all non-songs
|
||||
foreach (var item in others)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
var updateType = await item.RefreshMetadata(parentRefreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
|
@ -202,7 +213,7 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
return i =>
|
||||
{
|
||||
var hasArtist = i as IHasArtist;
|
||||
return hasArtist != null && hasArtist.HasArtist(Name);
|
||||
return hasArtist != null && hasArtist.HasAnyArtist(Name);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -239,6 +239,11 @@ namespace MediaBrowser.Controller.Entities
|
|||
get { return this.GetImagePath(ImageType.Primary); }
|
||||
}
|
||||
|
||||
public virtual bool IsInternetMetadataEnabled()
|
||||
{
|
||||
return ConfigurationManager.Configuration.EnableInternetProviders;
|
||||
}
|
||||
|
||||
public virtual bool CanDelete()
|
||||
{
|
||||
var locationType = LocationType;
|
||||
|
@ -717,7 +722,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// <param name="options">The options.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>true if a provider reports we changed</returns>
|
||||
public async Task RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||
public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
var locationType = LocationType;
|
||||
|
||||
|
@ -744,15 +749,16 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
}
|
||||
|
||||
var dateLastSaved = DateLastSaved;
|
||||
var refreshOptions = requiresSave
|
||||
? new MetadataRefreshOptions(options)
|
||||
{
|
||||
ForceSave = true
|
||||
}
|
||||
: options;
|
||||
|
||||
await ProviderManager.RefreshMetadata(this, options, cancellationToken).ConfigureAwait(false);
|
||||
var result = await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// If it wasn't saved by the provider process, save now
|
||||
if (requiresSave && dateLastSaved == DateLastSaved)
|
||||
{
|
||||
await UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
|
@ -1245,13 +1251,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
{
|
||||
if (string.Equals(i.GetType().Name, info.ItemType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (info.ItemYear.HasValue)
|
||||
{
|
||||
if (info.ItemYear.Value != (i.ProductionYear ?? -1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1461,7 +1460,8 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// <returns>Task.</returns>
|
||||
public virtual Task ChangedExternally()
|
||||
{
|
||||
return RefreshMetadata(CancellationToken.None);
|
||||
ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions());
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1135,10 +1135,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
foreach (var child in LinkedChildren)
|
||||
{
|
||||
// Reset the cached value
|
||||
if (child.ItemId.HasValue && child.ItemId.Value == Guid.Empty)
|
||||
{
|
||||
child.ItemId = null;
|
||||
}
|
||||
child.ItemId = null;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
8
MediaBrowser.Controller/Entities/IArchivable.cs
Normal file
8
MediaBrowser.Controller/Entities/IArchivable.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface IArchivable
|
||||
{
|
||||
bool IsArchive { get; }
|
||||
}
|
||||
}
|
|
@ -184,6 +184,12 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// </summary>
|
||||
/// <value><c>true</c> if [always scan internal metadata path]; otherwise, <c>false</c>.</value>
|
||||
bool AlwaysScanInternalMetadataPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is internet metadata enabled].
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if [is internet metadata enabled]; otherwise, <c>false</c>.</returns>
|
||||
bool IsInternetMetadataEnabled();
|
||||
}
|
||||
|
||||
public static class HasImagesExtensions
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
|
@ -22,53 +19,4 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns>
|
||||
IEnumerable<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
|
||||
}
|
||||
|
||||
public static class HasMediaSourceExtensions
|
||||
{
|
||||
public static IEnumerable<MediaSourceInfo> GetMediaSources(this IHasMediaSources item, bool enablePathSubstitution, User user)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
if (!(item is Video))
|
||||
{
|
||||
return item.GetMediaSources(enablePathSubstitution);
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
|
||||
var sources = item.GetMediaSources(enablePathSubstitution).ToList();
|
||||
|
||||
var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
|
||||
? new string[] { }
|
||||
: new[] { user.Configuration.AudioLanguagePreference };
|
||||
|
||||
var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
|
||||
? new List<string> { }
|
||||
: new List<string> { user.Configuration.SubtitleLanguagePreference };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(
|
||||
source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
|
||||
|
||||
var defaultAudioIndex = source.DefaultAudioStreamIndex;
|
||||
var audioLangage = defaultAudioIndex == null
|
||||
? null
|
||||
: source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
|
||||
|
||||
source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams,
|
||||
preferredSubs,
|
||||
user.Configuration.SubtitleMode,
|
||||
audioLangage);
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
8
MediaBrowser.Controller/Entities/IHasOriginalTitle.cs
Normal file
8
MediaBrowser.Controller/Entities/IHasOriginalTitle.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface IHasOriginalTitle
|
||||
{
|
||||
string OriginalTitle { get; set; }
|
||||
}
|
||||
}
|
|
@ -30,7 +30,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
public string[] IncludeItemTypes { get; set; }
|
||||
public string[] ExcludeItemTypes { get; set; }
|
||||
public string[] Genres { get; set; }
|
||||
public string[] AllGenres { get; set; }
|
||||
|
||||
public bool? IsMissing { get; set; }
|
||||
public bool? IsUnaired { get; set; }
|
||||
|
@ -42,6 +41,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
public string NameLessThan { get; set; }
|
||||
|
||||
public string Person { get; set; }
|
||||
public string[] PersonIds { get; set; }
|
||||
public string AdjacentTo { get; set; }
|
||||
public string[] PersonTypes { get; set; }
|
||||
|
||||
|
@ -66,6 +66,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
public bool? HasParentalRating { get; set; }
|
||||
|
||||
public string[] Studios { get; set; }
|
||||
public string[] StudioIds { get; set; }
|
||||
public ImageType[] ImageTypes { get; set; }
|
||||
public VideoType[] VideoTypes { get; set; }
|
||||
public int[] Years { get; set; }
|
||||
|
@ -80,13 +81,14 @@ namespace MediaBrowser.Controller.Entities
|
|||
MediaTypes = new string[] { };
|
||||
IncludeItemTypes = new string[] { };
|
||||
ExcludeItemTypes = new string[] { };
|
||||
AllGenres = new string[] { };
|
||||
Genres = new string[] { };
|
||||
Studios = new string[] { };
|
||||
StudioIds = new string[] { };
|
||||
ImageTypes = new ImageType[] { };
|
||||
VideoTypes = new VideoType[] { };
|
||||
Years = new int[] { };
|
||||
PersonTypes = new string[] { };
|
||||
PersonIds = new string[] { };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,22 @@ namespace MediaBrowser.Controller.Entities
|
|||
{
|
||||
public class ItemImageInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type.
|
||||
/// </summary>
|
||||
/// <value>The type.</value>
|
||||
public ImageType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date modified.
|
||||
/// </summary>
|
||||
/// <value>The date modified.</value>
|
||||
public DateTime DateModified { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
public string ItemName { get; set; }
|
||||
public string ItemType { get; set; }
|
||||
public int? ItemYear { get; set; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public string Id { get; set; }
|
||||
|
|
|
@ -6,7 +6,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
@ -15,10 +14,11 @@ namespace MediaBrowser.Controller.Entities.Movies
|
|||
/// <summary>
|
||||
/// Class Movie
|
||||
/// </summary>
|
||||
public class Movie : Video, IHasCriticRating, IHasSoundtracks, IHasSpecialFeatures, IHasProductionLocations, IHasBudget, IHasKeywords, IHasTrailers, IHasThemeMedia, IHasTaglines, IHasAwards, IHasMetascore, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping
|
||||
public class Movie : Video, IHasCriticRating, IHasSoundtracks, IHasSpecialFeatures, IHasProductionLocations, IHasBudget, IHasKeywords, IHasTrailers, IHasThemeMedia, IHasTaglines, IHasAwards, IHasMetascore, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping, IHasOriginalTitle
|
||||
{
|
||||
public List<Guid> SpecialFeatureIds { get; set; }
|
||||
|
||||
public string OriginalTitle { get; set; }
|
||||
public List<Guid> SoundtrackIds { get; set; }
|
||||
|
||||
public List<Guid> ThemeSongIds { get; set; }
|
||||
|
|
|
@ -47,16 +47,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified name has artist.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns><c>true</c> if the specified name has artist; otherwise, <c>false</c>.</returns>
|
||||
public bool HasArtist(string name)
|
||||
{
|
||||
return AllArtists.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user data key.
|
||||
/// </summary>
|
||||
|
|
|
@ -15,11 +15,12 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||
/// <summary>
|
||||
/// Class Series
|
||||
/// </summary>
|
||||
public class Series : Folder, IHasSoundtracks, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IHasSpecialFeatures, IMetadataContainer
|
||||
public class Series : Folder, IHasSoundtracks, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IHasSpecialFeatures, IMetadataContainer, IHasOriginalTitle
|
||||
{
|
||||
public List<Guid> SpecialFeatureIds { get; set; }
|
||||
public List<Guid> SoundtrackIds { get; set; }
|
||||
|
||||
public string OriginalTitle { get; set; }
|
||||
public int SeasonCount { get; set; }
|
||||
|
||||
public int? AnimeSeriesIndex { get; set; }
|
||||
|
@ -260,7 +261,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||
progress.Report(percent * 100);
|
||||
}
|
||||
|
||||
await ProviderManager.RefreshMetadata(this, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using System;
|
||||
|
@ -15,7 +16,13 @@ namespace MediaBrowser.Controller.Entities
|
|||
public Guid? UserId { get; set; }
|
||||
|
||||
public static ITVSeriesManager TVSeriesManager;
|
||||
public static IPlaylistManager PlaylistManager;
|
||||
|
||||
public bool ContainsDynamicCategories(User user)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override Task<QueryResult<BaseItem>> GetItems(InternalItemsQuery query)
|
||||
{
|
||||
var parent = this as Folder;
|
||||
|
@ -25,7 +32,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent;
|
||||
}
|
||||
|
||||
return new UserViewBuilder(UserViewManager, LiveTvManager, ChannelManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, CollectionManager)
|
||||
return new UserViewBuilder(UserViewManager, LiveTvManager, ChannelManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, CollectionManager, PlaylistManager)
|
||||
.GetUserItems(parent, this, ViewType, query);
|
||||
}
|
||||
|
||||
|
@ -45,6 +52,11 @@ namespace MediaBrowser.Controller.Entities
|
|||
return false;
|
||||
}
|
||||
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override IEnumerable<BaseItem> GetRecursiveChildren(User user, Func<BaseItem, bool> filter)
|
||||
{
|
||||
var result = GetItems(new InternalItemsQuery
|
||||
|
|
|
@ -5,7 +5,7 @@ using MediaBrowser.Controller.Entities.Movies;
|
|||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Channels;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
@ -18,7 +18,6 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MoreLinq;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
|
@ -32,8 +31,9 @@ namespace MediaBrowser.Controller.Entities
|
|||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ITVSeriesManager _tvSeriesManager;
|
||||
private readonly ICollectionManager _collectionManager;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
|
||||
public UserViewBuilder(IUserViewManager userViewManager, ILiveTvManager liveTvManager, IChannelManager channelManager, ILibraryManager libraryManager, ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, ICollectionManager collectionManager)
|
||||
public UserViewBuilder(IUserViewManager userViewManager, ILiveTvManager liveTvManager, IChannelManager channelManager, ILibraryManager libraryManager, ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, ICollectionManager collectionManager, IPlaylistManager playlistManager)
|
||||
{
|
||||
_userViewManager = userViewManager;
|
||||
_liveTvManager = liveTvManager;
|
||||
|
@ -43,6 +43,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
_userDataManager = userDataManager;
|
||||
_tvSeriesManager = tvSeriesManager;
|
||||
_collectionManager = collectionManager;
|
||||
_playlistManager = playlistManager;
|
||||
}
|
||||
|
||||
public async Task<QueryResult<BaseItem>> GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query)
|
||||
|
@ -111,12 +112,21 @@ namespace MediaBrowser.Controller.Entities
|
|||
return GetResult(result, queryParent, query);
|
||||
}
|
||||
|
||||
case CollectionType.Books:
|
||||
case CollectionType.Photos:
|
||||
case CollectionType.HomeVideos:
|
||||
case CollectionType.MusicVideos:
|
||||
return GetResult(queryParent.GetChildren(user, true), queryParent, query);
|
||||
|
||||
case CollectionType.Folders:
|
||||
return GetResult(user.RootFolder.GetChildren(user, true), queryParent, query);
|
||||
|
||||
case CollectionType.Games:
|
||||
return await GetGameView(user, queryParent, query).ConfigureAwait(false);
|
||||
|
||||
case CollectionType.Playlists:
|
||||
return await GetPlaylistsView(queryParent, user, query).ConfigureAwait(false);
|
||||
|
||||
case CollectionType.BoxSets:
|
||||
return await GetBoxsetView(queryParent, user, query).ConfigureAwait(false);
|
||||
|
||||
|
@ -574,6 +584,11 @@ namespace MediaBrowser.Controller.Entities
|
|||
return GetResult(items, queryParent, query);
|
||||
}
|
||||
|
||||
private async Task<QueryResult<BaseItem>> GetPlaylistsView(Folder parent, User user, InternalItemsQuery query)
|
||||
{
|
||||
return GetResult(_playlistManager.GetPlaylists(user.Id.ToString("N")), parent, query);
|
||||
}
|
||||
|
||||
private async Task<QueryResult<BaseItem>> GetBoxsetView(Folder parent, User user, InternalItemsQuery query)
|
||||
{
|
||||
return GetResult(GetMediaFolders(user).SelectMany(i =>
|
||||
|
@ -937,11 +952,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
return false;
|
||||
}
|
||||
|
||||
if (request.AllGenres.Length > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.Genres.Length > 0)
|
||||
{
|
||||
return false;
|
||||
|
@ -1047,11 +1057,21 @@ namespace MediaBrowser.Controller.Entities
|
|||
return false;
|
||||
}
|
||||
|
||||
if (request.PersonIds.Length > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.Studios.Length > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.StudioIds.Length > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.VideoTypes.Length > 0)
|
||||
{
|
||||
return false;
|
||||
|
@ -1571,12 +1591,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
return false;
|
||||
}
|
||||
|
||||
// Apply genre filter
|
||||
if (query.AllGenres.Length > 0 && !query.AllGenres.All(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by VideoType
|
||||
if (query.VideoTypes.Length > 0)
|
||||
{
|
||||
|
@ -1598,6 +1612,16 @@ namespace MediaBrowser.Controller.Entities
|
|||
return false;
|
||||
}
|
||||
|
||||
// Apply studio filter
|
||||
if (query.StudioIds.Length > 0 && !query.StudioIds.Any(id =>
|
||||
{
|
||||
var studioItem = libraryManager.GetItemById(id);
|
||||
return studioItem != null && item.Studios.Contains(studioItem.Name, StringComparer.OrdinalIgnoreCase);
|
||||
}))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply year filter
|
||||
if (query.Years.Length > 0)
|
||||
{
|
||||
|
@ -1614,7 +1638,22 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
|
||||
// Apply person filter
|
||||
if (!string.IsNullOrEmpty(query.Person))
|
||||
if (query.PersonIds.Length > 0)
|
||||
{
|
||||
var names = query.PersonIds
|
||||
.Select(libraryManager.GetItemById)
|
||||
.Select(i => i == null ? "-1" : i.Name)
|
||||
.ToList();
|
||||
|
||||
if (!(names.Any(
|
||||
v => item.People.Select(i => i.Name).Contains(v, StringComparer.OrdinalIgnoreCase))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply person filter
|
||||
if (!string.IsNullOrWhiteSpace(query.Person))
|
||||
{
|
||||
var personTypes = query.PersonTypes;
|
||||
|
||||
|
@ -1716,7 +1755,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
var parent = user.RootFolder;
|
||||
|
||||
//list.Add(await GetUserView(SpecialFolder.LiveTvNowPlaying, user, "0", parent).ConfigureAwait(false));
|
||||
//list.Add(await GetUserSubView(SpecialFolder.LiveTvNowPlaying, user, "0", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.LiveTvChannels, user, string.Empty, parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.LiveTvRecordingGroups, user, string.Empty, parent).ConfigureAwait(false));
|
||||
|
||||
|
@ -1725,7 +1764,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
private async Task<UserView> GetUserView(string name, string type, User user, string sortName, BaseItem parent)
|
||||
{
|
||||
var view = await _userViewManager.GetUserView(name, parent.Id.ToString("N"), type, user, sortName, CancellationToken.None)
|
||||
var view = await _userViewManager.GetUserSubView(name, parent.Id.ToString("N"), type, user, sortName, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return view;
|
||||
|
@ -1733,7 +1772,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
private async Task<UserView> GetUserView(string type, User user, string sortName, BaseItem parent)
|
||||
{
|
||||
var view = await _userViewManager.GetUserView(parent.Id.ToString("N"), type, user, sortName, CancellationToken.None)
|
||||
var view = await _userViewManager.GetUserSubView(parent.Id.ToString("N"), type, user, sortName, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return view;
|
||||
|
|
|
@ -24,7 +24,8 @@ namespace MediaBrowser.Controller.Entities
|
|||
IHasMediaSources,
|
||||
IHasShortOverview,
|
||||
IHasPreferredMetadataLanguage,
|
||||
IThemeMedia
|
||||
IThemeMedia,
|
||||
IArchivable
|
||||
{
|
||||
public Guid? PrimaryVersionId { get; set; }
|
||||
|
||||
|
@ -420,12 +421,17 @@ namespace MediaBrowser.Controller.Entities
|
|||
return base.GetDeletePaths();
|
||||
}
|
||||
|
||||
public virtual IEnumerable<MediaStream> GetMediaStreams()
|
||||
public IEnumerable<MediaStream> GetMediaStreams()
|
||||
{
|
||||
return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
|
||||
var mediaSource = GetMediaSources(false)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (mediaSource == null)
|
||||
{
|
||||
ItemId = Id
|
||||
});
|
||||
return new List<MediaStream>();
|
||||
}
|
||||
|
||||
return mediaSource.MediaStreams;
|
||||
}
|
||||
|
||||
public virtual MediaStream GetDefaultVideoStream()
|
||||
|
@ -455,7 +461,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
return result.OrderBy(i =>
|
||||
{
|
||||
if (item.VideoType == VideoType.VideoFile)
|
||||
if (i.VideoType == VideoType.VideoFile)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
@ -474,7 +480,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
private static MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, Video i, MediaSourceType type)
|
||||
{
|
||||
var mediaStreams = MediaSourceManager.GetMediaStreams(new MediaStreamQuery { ItemId = i.Id })
|
||||
var mediaStreams = MediaSourceManager.GetMediaStreams(i.Id)
|
||||
.ToList();
|
||||
|
||||
var locationType = i.LocationType;
|
||||
|
@ -551,7 +557,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
return info;
|
||||
}
|
||||
|
||||
|
||||
private static string GetMediaSourceName(Video video, List<MediaStream> mediaStreams)
|
||||
{
|
||||
var terms = new List<string>();
|
||||
|
|
|
@ -302,7 +302,7 @@ namespace MediaBrowser.Controller.Library
|
|||
IEnumerable<BaseItem> ReplaceVideosWithPrimaryVersions(IEnumerable<BaseItem> items);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the special folder.
|
||||
/// Gets the named view.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
|
@ -311,7 +311,7 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <param name="sortName">Name of the sort.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<UserView>.</returns>
|
||||
Task<UserView> GetSpecialFolder(User user,
|
||||
Task<UserView> GetNamedView(User user,
|
||||
string name,
|
||||
string parentId,
|
||||
string viewType,
|
||||
|
@ -321,12 +321,14 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <summary>
|
||||
/// Gets the named view.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="viewType">Type of the view.</param>
|
||||
/// <param name="sortName">Name of the sort.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<UserView>.</returns>
|
||||
Task<UserView> GetNamedView(string name,
|
||||
Task<UserView> GetNamedView(User user,
|
||||
string name,
|
||||
string viewType,
|
||||
string sortName,
|
||||
CancellationToken cancellationToken);
|
||||
|
|
|
@ -1,11 +1,76 @@
|
|||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
public interface IMediaSourceManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the parts.
|
||||
/// </summary>
|
||||
/// <param name="providers">The providers.</param>
|
||||
void AddParts(IEnumerable<IMediaSourceProvider> providers);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media streams.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item identifier.</param>
|
||||
/// <returns>IEnumerable<MediaStream>.</returns>
|
||||
IEnumerable<MediaStream> GetMediaStreams(Guid itemId);
|
||||
/// <summary>
|
||||
/// Gets the media streams.
|
||||
/// </summary>
|
||||
/// <param name="mediaSourceId">The media source identifier.</param>
|
||||
/// <returns>IEnumerable<MediaStream>.</returns>
|
||||
IEnumerable<MediaStream> GetMediaStreams(string mediaSourceId);
|
||||
/// <summary>
|
||||
/// Gets the media streams.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>IEnumerable<MediaStream>.</returns>
|
||||
IEnumerable<MediaStream> GetMediaStreams(MediaStreamQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playack media sources.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="userId">The user identifier.</param>
|
||||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>IEnumerable<MediaSourceInfo>.</returns>
|
||||
Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playack media sources.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<IEnumerable<MediaSourceInfo>>.</returns>
|
||||
Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, bool enablePathSubstitution, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the static media sources.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>IEnumerable<MediaSourceInfo>.</returns>
|
||||
IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the static media source.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="mediaSourceId">The media source identifier.</param>
|
||||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||
/// <returns>MediaSourceInfo.</returns>
|
||||
MediaSourceInfo GetStaticMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
|
||||
}
|
||||
}
|
||||
|
|
19
MediaBrowser.Controller/Library/IMediaSourceProvider.cs
Normal file
19
MediaBrowser.Controller/Library/IMediaSourceProvider.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
public interface IMediaSourceProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the media sources.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<IEnumerable<MediaSourceInfo>>.</returns>
|
||||
Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -34,6 +34,7 @@ namespace MediaBrowser.Controller.Library
|
|||
event EventHandler<GenericEventArgs<User>> UserCreated;
|
||||
event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
|
||||
event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
|
||||
event EventHandler<GenericEventArgs<User>> UserLockedOut;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a User by Id
|
||||
|
|
|
@ -12,12 +12,10 @@ namespace MediaBrowser.Controller.Library
|
|||
{
|
||||
Task<IEnumerable<Folder>> GetUserViews(UserViewQuery query, CancellationToken cancellationToken);
|
||||
|
||||
Task<UserView> GetUserView(string name, string parentId, string type, User user, string sortName,
|
||||
Task<UserView> GetUserSubView(string name, string parentId, string type, User user, string sortName,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<UserView> GetUserView(string type, string sortName, CancellationToken cancellationToken);
|
||||
|
||||
Task<UserView> GetUserView(string category, string type, User user, string sortName, CancellationToken cancellationToken);
|
||||
Task<UserView> GetUserSubView(string category, string type, User user, string sortName, CancellationToken cancellationToken);
|
||||
|
||||
List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request);
|
||||
}
|
||||
|
|
8
MediaBrowser.Controller/LiveTv/ILiveTvItem.cs
Normal file
8
MediaBrowser.Controller/LiveTv/ILiveTvItem.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
namespace MediaBrowser.Controller.LiveTv
|
||||
{
|
||||
public interface ILiveTvItem
|
||||
{
|
||||
string ServiceName { get; set; }
|
||||
}
|
||||
}
|
|
@ -14,12 +14,6 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// </summary>
|
||||
public interface ILiveTvManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the active service.
|
||||
/// </summary>
|
||||
/// <value>The active service.</value>
|
||||
ILiveTvService ActiveService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the services.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Library;
|
||||
using System.Threading;
|
||||
|
@ -6,10 +7,8 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace MediaBrowser.Controller.LiveTv
|
||||
{
|
||||
public interface ILiveTvRecording : IHasImages, IHasMediaSources, IHasUserData
|
||||
public interface ILiveTvRecording : IHasImages, IHasMediaSources, IHasUserData, ILiveTvItem
|
||||
{
|
||||
string ServiceName { get; set; }
|
||||
|
||||
string MediaType { get; }
|
||||
|
||||
string Container { get; }
|
||||
|
@ -22,7 +21,7 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
|
||||
bool IsParentalAllowed(User user);
|
||||
|
||||
Task RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken);
|
||||
Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken);
|
||||
|
||||
PlayAccess GetPlayAccess(User user);
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ using System.Runtime.Serialization;
|
|||
|
||||
namespace MediaBrowser.Controller.LiveTv
|
||||
{
|
||||
public class LiveTvChannel : BaseItem, IHasMediaSources
|
||||
public class LiveTvChannel : BaseItem, IHasMediaSources, ILiveTvItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the user data key.
|
||||
|
@ -58,6 +58,10 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <value>The type of the channel.</value>
|
||||
public ChannelType ChannelType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the service.
|
||||
/// </summary>
|
||||
/// <value>The name of the service.</value>
|
||||
public string ServiceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
@ -11,7 +12,7 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace MediaBrowser.Controller.LiveTv
|
||||
{
|
||||
public class LiveTvProgram : BaseItem
|
||||
public class LiveTvProgram : BaseItem, ILiveTvItem, IHasLookupInfo<LiveTvProgramLookupInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the user data key.
|
||||
|
@ -220,5 +221,23 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsInternetMetadataEnabled()
|
||||
{
|
||||
if (IsMovie)
|
||||
{
|
||||
var options = (LiveTvOptions)ConfigurationManager.GetConfiguration("livetv");
|
||||
return options.EnableMovieProviders;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public LiveTvProgramLookupInfo GetLookupInfo()
|
||||
{
|
||||
var info = GetItemLookupInfo<LiveTvProgramLookupInfo>();
|
||||
info.IsMovie = IsMovie;
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,6 +145,12 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value>
|
||||
public bool IsPremiere { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the production year.
|
||||
/// </summary>
|
||||
/// <value>The production year.</value>
|
||||
public int? ProductionYear { get; set; }
|
||||
|
||||
public ProgramInfo()
|
||||
{
|
||||
Genres = new List<string>();
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
||||
namespace MediaBrowser.Controller.LiveTv
|
||||
|
|
|
@ -99,11 +99,13 @@
|
|||
<Compile Include="Collections\CollectionCreationOptions.cs" />
|
||||
<Compile Include="Collections\CollectionEvents.cs" />
|
||||
<Compile Include="Collections\ICollectionManager.cs" />
|
||||
<Compile Include="Connect\ConnectSupporterSummary.cs" />
|
||||
<Compile Include="Connect\IConnectManager.cs" />
|
||||
<Compile Include="Connect\UserLinkResult.cs" />
|
||||
<Compile Include="Devices\CameraImageUploadInfo.cs" />
|
||||
<Compile Include="Devices\IDeviceManager.cs" />
|
||||
<Compile Include="Devices\IDeviceRepository.cs" />
|
||||
<Compile Include="Diagnostics\IProcessManager.cs" />
|
||||
<Compile Include="Dlna\ControlRequest.cs" />
|
||||
<Compile Include="Dlna\ControlResponse.cs" />
|
||||
<Compile Include="Dlna\EventSubscriptionResponse.cs" />
|
||||
|
@ -129,6 +131,7 @@
|
|||
<Compile Include="Entities\Game.cs" />
|
||||
<Compile Include="Entities\GameGenre.cs" />
|
||||
<Compile Include="Entities\GameSystem.cs" />
|
||||
<Compile Include="Entities\IArchivable.cs" />
|
||||
<Compile Include="Entities\IByReferenceItem.cs" />
|
||||
<Compile Include="Entities\IHasAspectRatio.cs" />
|
||||
<Compile Include="Entities\IHasBudget.cs" />
|
||||
|
@ -138,6 +141,7 @@
|
|||
<Compile Include="Entities\IHasKeywords.cs" />
|
||||
<Compile Include="Entities\IHasMediaSources.cs" />
|
||||
<Compile Include="Entities\IHasMetascore.cs" />
|
||||
<Compile Include="Entities\IHasOriginalTitle.cs" />
|
||||
<Compile Include="Entities\IHasPreferredMetadataLanguage.cs" />
|
||||
<Compile Include="Entities\IHasProductionLocations.cs" />
|
||||
<Compile Include="Entities\IHasScreenshots.cs" />
|
||||
|
@ -171,6 +175,7 @@
|
|||
<Compile Include="Library\DeleteOptions.cs" />
|
||||
<Compile Include="Library\ILibraryPostScanTask.cs" />
|
||||
<Compile Include="Library\IMediaSourceManager.cs" />
|
||||
<Compile Include="Library\IMediaSourceProvider.cs" />
|
||||
<Compile Include="Library\IMetadataFileSaver.cs" />
|
||||
<Compile Include="Library\IMetadataSaver.cs" />
|
||||
<Compile Include="Library\IMusicManager.cs" />
|
||||
|
@ -182,6 +187,7 @@
|
|||
<Compile Include="Library\MetadataConfigurationStore.cs" />
|
||||
<Compile Include="Library\PlaybackStopEventArgs.cs" />
|
||||
<Compile Include="Library\UserDataSaveEventArgs.cs" />
|
||||
<Compile Include="LiveTv\ILiveTvItem.cs" />
|
||||
<Compile Include="LiveTv\RecordingGroup.cs" />
|
||||
<Compile Include="LiveTv\RecordingStatusChangedEventArgs.cs" />
|
||||
<Compile Include="LiveTv\ILiveTvRecording.cs" />
|
||||
|
@ -243,22 +249,69 @@
|
|||
<Compile Include="Persistence\MediaStreamQuery.cs" />
|
||||
<Compile Include="Playlists\IPlaylistManager.cs" />
|
||||
<Compile Include="Playlists\Playlist.cs" />
|
||||
<Compile Include="Plugins\ILocalizablePlugin.cs" />
|
||||
<Compile Include="Providers\AlbumInfo.cs" />
|
||||
<Compile Include="Providers\ArtistInfo.cs" />
|
||||
<Compile Include="Providers\BookInfo.cs" />
|
||||
<Compile Include="Providers\BoxSetInfo.cs" />
|
||||
<Compile Include="Providers\ChannelItemLookupInfo.cs" />
|
||||
<Compile Include="Providers\DirectoryService.cs" />
|
||||
<Compile Include="Providers\DynamicImageInfo.cs" />
|
||||
<Compile Include="Providers\DynamicImageResponse.cs" />
|
||||
<Compile Include="Providers\EpisodeIdentity.cs" />
|
||||
<Compile Include="Providers\EpisodeInfo.cs" />
|
||||
<Compile Include="Providers\ExtraInfo.cs" />
|
||||
<Compile Include="Providers\ExtraSource.cs" />
|
||||
<Compile Include="Providers\GameInfo.cs" />
|
||||
<Compile Include="Providers\GameSystemInfo.cs" />
|
||||
<Compile Include="Providers\ICustomMetadataProvider.cs" />
|
||||
<Compile Include="Providers\IDirectoryService.cs" />
|
||||
<Compile Include="Providers\IDynamicImageProvider.cs" />
|
||||
<Compile Include="Providers\IExternalId.cs" />
|
||||
<Compile Include="Providers\IExtrasProvider.cs" />
|
||||
<Compile Include="Providers\IForcedProvider.cs" />
|
||||
<Compile Include="Providers\IHasChangeMonitor.cs" />
|
||||
<Compile Include="Entities\IHasMetadata.cs" />
|
||||
<Compile Include="Providers\IHasIdentities.cs" />
|
||||
<Compile Include="Providers\IHasItemChangeMonitor.cs" />
|
||||
<Compile Include="Providers\IHasLookupInfo.cs" />
|
||||
<Compile Include="Providers\IHasOrder.cs" />
|
||||
<Compile Include="Providers\IImageFileSaver.cs" />
|
||||
<Compile Include="Providers\IImageProvider.cs" />
|
||||
<Compile Include="Providers\IImageSaver.cs" />
|
||||
<Compile Include="Providers\IItemIdentity.cs" />
|
||||
<Compile Include="Providers\IItemIdentityConverter.cs" />
|
||||
<Compile Include="Providers\IItemIdentityProvider.cs" />
|
||||
<Compile Include="Providers\ILocalImageFileProvider.cs" />
|
||||
<Compile Include="Providers\ILocalMetadataProvider.cs" />
|
||||
<Compile Include="Providers\ImageRefreshMode.cs" />
|
||||
<Compile Include="Providers\ImageRefreshOptions.cs" />
|
||||
<Compile Include="Providers\IPreRefreshProvider.cs" />
|
||||
<Compile Include="Providers\IProviderRepository.cs" />
|
||||
<Compile Include="Providers\IRemoteImageProvider.cs" />
|
||||
<Compile Include="Providers\ILocalImageProvider.cs" />
|
||||
<Compile Include="Providers\IMetadataProvider.cs" />
|
||||
<Compile Include="Providers\IMetadataService.cs" />
|
||||
<Compile Include="Providers\IRemoteMetadataProvider.cs" />
|
||||
<Compile Include="Providers\IRemoteSearchProvider.cs" />
|
||||
<Compile Include="Providers\ISeriesOrderProvider.cs" />
|
||||
<Compile Include="Providers\ItemInfo.cs" />
|
||||
<Compile Include="Providers\LiveTvProgramLookupInfo.cs" />
|
||||
<Compile Include="Providers\LocalImageInfo.cs" />
|
||||
<Compile Include="Providers\LocalMetadataResult.cs" />
|
||||
<Compile Include="Providers\MetadataRefreshMode.cs" />
|
||||
<Compile Include="Providers\MetadataResult.cs" />
|
||||
<Compile Include="Providers\MovieInfo.cs" />
|
||||
<Compile Include="Providers\MusicVideoInfo.cs" />
|
||||
<Compile Include="Providers\PersonLookupInfo.cs" />
|
||||
<Compile Include="Providers\RemoteSearchQuery.cs" />
|
||||
<Compile Include="Providers\SeasonIdentity.cs" />
|
||||
<Compile Include="Providers\SeasonInfo.cs" />
|
||||
<Compile Include="Providers\SeriesIdentity.cs" />
|
||||
<Compile Include="Providers\SeriesInfo.cs" />
|
||||
<Compile Include="Providers\SeriesOrderTypes.cs" />
|
||||
<Compile Include="Providers\SongInfo.cs" />
|
||||
<Compile Include="Providers\TrailerInfo.cs" />
|
||||
<Compile Include="Providers\VideoContentType.cs" />
|
||||
<Compile Include="RelatedMedia\IRelatedMediaProvider.cs" />
|
||||
<Compile Include="Security\AuthenticationInfo.cs" />
|
||||
|
@ -341,11 +394,12 @@
|
|||
<Compile Include="Subtitles\SubtitleDownloadEventArgs.cs" />
|
||||
<Compile Include="Subtitles\SubtitleResponse.cs" />
|
||||
<Compile Include="Subtitles\SubtitleSearchRequest.cs" />
|
||||
<Compile Include="Sync\ICloudSyncProvider.cs" />
|
||||
<Compile Include="Sync\IServerSyncProvider.cs" />
|
||||
<Compile Include="Sync\ISyncDataProvider.cs" />
|
||||
<Compile Include="Sync\ISyncManager.cs" />
|
||||
<Compile Include="Sync\ISyncProvider.cs" />
|
||||
<Compile Include="Sync\ISyncRepository.cs" />
|
||||
<Compile Include="Sync\SendFileResult.cs" />
|
||||
<Compile Include="Themes\IAppThemeManager.cs" />
|
||||
<Compile Include="Themes\InternalThemeImage.cs" />
|
||||
<Compile Include="TV\ITVSeriesManager.cs" />
|
||||
|
|
|
@ -70,6 +70,11 @@ namespace MediaBrowser.Controller.Net
|
|||
/// <returns>Task.</returns>
|
||||
public Task ProcessMessage(WebSocketMessageInfo message)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
throw new ArgumentNullException("message");
|
||||
}
|
||||
|
||||
if (message.MessageType.Equals(Name + "Start", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Start(message);
|
||||
|
|
|
@ -44,6 +44,11 @@ namespace MediaBrowser.Controller.Net
|
|||
/// </summary>
|
||||
event EventHandler<WebSocketConnectEventArgs> WebSocketConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [web socket connecting].
|
||||
/// </summary>
|
||||
event EventHandler<WebSocketConnectingEventArgs> WebSocketConnecting;
|
||||
|
||||
/// <summary>
|
||||
/// Inits this instance.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
@ -58,5 +58,10 @@ namespace MediaBrowser.Controller.Net
|
|||
/// </summary>
|
||||
/// <value>The web socket connections.</value>
|
||||
IEnumerable<IWebSocketConnection> WebSocketConnections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [web socket connected].
|
||||
/// </summary>
|
||||
event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
|
||||
}
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
{
|
||||
public interface ISessionContext
|
||||
{
|
||||
SessionInfo GetSession(object requestContext);
|
||||
User GetUser(object requestContext);
|
||||
|
||||
SessionInfo GetSession(IServiceRequest requestContext);
|
||||
User GetUser(IServiceRequest requestContext);
|
||||
Task<SessionInfo> GetSession(object requestContext);
|
||||
Task<User> GetUser(object requestContext);
|
||||
|
||||
Task<SessionInfo> GetSession(IServiceRequest requestContext);
|
||||
Task<User> GetUser(IServiceRequest requestContext);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using MediaBrowser.Model.Net;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
@ -11,7 +12,7 @@ namespace MediaBrowser.Controller.Net
|
|||
/// Occurs when [closed].
|
||||
/// </summary>
|
||||
event EventHandler<EventArgs> Closed;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id.
|
||||
/// </summary>
|
||||
|
@ -24,6 +25,17 @@ namespace MediaBrowser.Controller.Net
|
|||
/// <value>The last activity date.</value>
|
||||
DateTime LastActivityDate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL.
|
||||
/// </summary>
|
||||
/// <value>The URL.</value>
|
||||
string Url { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the query string.
|
||||
/// </summary>
|
||||
/// <value>The query string.</value>
|
||||
NameValueCollection QueryString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive action.
|
||||
/// </summary>
|
||||
|
|
|
@ -18,11 +18,6 @@ namespace MediaBrowser.Controller.Net
|
|||
|
||||
public IDictionary<string, string> ResponseHeaders { get; set; }
|
||||
|
||||
public bool Throttle { get; set; }
|
||||
public long ThrottleLimit { get; set; }
|
||||
public long MinThrottlePosition { get; set; }
|
||||
public Func<long, long, long> ThrottleCallback { get; set; }
|
||||
|
||||
public Action OnComplete { get; set; }
|
||||
|
||||
public StaticResultOptions()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
{
|
||||
|
@ -7,6 +8,16 @@ namespace MediaBrowser.Controller.Net
|
|||
/// </summary>
|
||||
public class WebSocketConnectEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the URL.
|
||||
/// </summary>
|
||||
/// <value>The URL.</value>
|
||||
public string Url { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the query string.
|
||||
/// </summary>
|
||||
/// <value>The query string.</value>
|
||||
public NameValueCollection QueryString { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the web socket.
|
||||
/// </summary>
|
||||
|
@ -18,4 +29,35 @@ namespace MediaBrowser.Controller.Net
|
|||
/// <value>The endpoint.</value>
|
||||
public string Endpoint { get; set; }
|
||||
}
|
||||
|
||||
public class WebSocketConnectingEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the URL.
|
||||
/// </summary>
|
||||
/// <value>The URL.</value>
|
||||
public string Url { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the endpoint.
|
||||
/// </summary>
|
||||
/// <value>The endpoint.</value>
|
||||
public string Endpoint { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the query string.
|
||||
/// </summary>
|
||||
/// <value>The query string.</value>
|
||||
public NameValueCollection QueryString { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [allow connection].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [allow connection]; otherwise, <c>false</c>.</value>
|
||||
public bool AllowConnection { get; set; }
|
||||
|
||||
public WebSocketConnectingEventArgs()
|
||||
{
|
||||
QueryString = new NameValueCollection();
|
||||
AllowConnection = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ namespace MediaBrowser.Controller.Playlists
|
|||
Func<BaseItem, bool> filter = i =>
|
||||
{
|
||||
var audio = i as Audio;
|
||||
return audio != null && audio.HasArtist(musicArtist.Name);
|
||||
return audio != null && audio.HasAnyArtist(musicArtist.Name);
|
||||
};
|
||||
|
||||
var items = user == null
|
||||
|
|
20
MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs
Normal file
20
MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MediaBrowser.Controller.Plugins
|
||||
{
|
||||
public interface ILocalizablePlugin
|
||||
{
|
||||
Stream GetDictionary(string culture);
|
||||
}
|
||||
|
||||
public static class LocalizablePluginHelper
|
||||
{
|
||||
public static Stream GetDictionary(Assembly assembly, string manifestPrefix, string culture)
|
||||
{
|
||||
// Find all dictionaries using GetManifestResourceNames, start start with the prefix
|
||||
// Return the one for the culture if exists, otherwise return the default
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user