Merge pull request #1043 from MediaBrowser/dev

3.0.5557.0
This commit is contained in:
Luke 2015-03-19 13:21:35 -04:00
commit 9926be0d9d
426 changed files with 12029 additions and 5306 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>");
}
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -325,7 +325,7 @@ namespace MediaBrowser.Common.Plugins
AssemblyFileName = AssemblyFileName,
ConfigurationDateLastModified = ConfigurationDateLastModified,
Description = Description,
Id = Id.ToString("N"),
Id = Id.ToString(),
ConfigurationFileName = ConfigurationFileName
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

@ -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&lt;ConnectSupporterSummary&gt;.</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);
}
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@

namespace MediaBrowser.Controller.Entities
{
public interface IArchivable
{
bool IsArchive { get; }
}
}

View File

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

View File

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

View File

@ -0,0 +1,8 @@

namespace MediaBrowser.Controller.Entities
{
public interface IHasOriginalTitle
{
string OriginalTitle { get; set; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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&lt;UserView&gt;.</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&lt;UserView&gt;.</returns>
Task<UserView> GetNamedView(string name,
Task<UserView> GetNamedView(User user,
string name,
string viewType,
string sortName,
CancellationToken cancellationToken);

View File

@ -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&lt;MediaStream&gt;.</returns>
IEnumerable<MediaStream> GetMediaStreams(Guid itemId);
/// <summary>
/// Gets the media streams.
/// </summary>
/// <param name="mediaSourceId">The media source identifier.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
IEnumerable<MediaStream> GetMediaStreams(string mediaSourceId);
/// <summary>
/// Gets the media streams.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</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&lt;MediaSourceInfo&gt;.</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&lt;IEnumerable&lt;MediaSourceInfo&gt;&gt;.</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&lt;MediaSourceInfo&gt;.</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);
}
}

View 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&lt;IEnumerable&lt;MediaSourceInfo&gt;&gt;.</returns>
Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken);
}
}

View File

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

View File

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

View File

@ -0,0 +1,8 @@

namespace MediaBrowser.Controller.LiveTv
{
public interface ILiveTvItem
{
string ServiceName { get; set; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.LiveTv

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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