commit
a2fa7c4754
|
@ -56,7 +56,8 @@ namespace Emby.Drawing.ImageMagick
|
|||
"bmp",
|
||||
"erf",
|
||||
"raf",
|
||||
"rw2"
|
||||
"rw2",
|
||||
"nrw"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using ServiceStack;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Api.Library
|
||||
{
|
||||
[Route("/Providers/Chapters", "GET")]
|
||||
public class GetChapterProviders : IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class ChapterService : BaseApiService
|
||||
{
|
||||
private readonly IChapterManager _chapterManager;
|
||||
|
||||
public ChapterService(IChapterManager chapterManager)
|
||||
{
|
||||
_chapterManager = chapterManager;
|
||||
}
|
||||
|
||||
public object Get(GetChapterProviders request)
|
||||
{
|
||||
var result = _chapterManager.GetProviders().ToList();
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -705,7 +705,7 @@ namespace MediaBrowser.Api.LiveTv
|
|||
|
||||
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
outputHeaders["Content-Type"] = MimeTypes.GetMimeType(filePath);
|
||||
outputHeaders["Content-Type"] = MediaBrowser.Model.Net.MimeTypes.GetMimeType(filePath);
|
||||
|
||||
long startPosition = 0;
|
||||
|
||||
|
|
|
@ -79,7 +79,6 @@
|
|||
<Compile Include="Dlna\DlnaService.cs" />
|
||||
<Compile Include="FilterService.cs" />
|
||||
<Compile Include="IHasDtoOptions.cs" />
|
||||
<Compile Include="Library\ChapterService.cs" />
|
||||
<Compile Include="Playback\MediaInfoService.cs" />
|
||||
<Compile Include="Playback\TranscodingThrottler.cs" />
|
||||
<Compile Include="PlaylistService.cs" />
|
||||
|
|
|
@ -1888,7 +1888,9 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
else
|
||||
{
|
||||
mediaSource = await MediaSourceManager.GetLiveStream(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
|
||||
var liveStreamInfo = await MediaSourceManager.GetLiveStreamWithDirectStreamProvider(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
|
||||
mediaSource = liveStreamInfo.Item1;
|
||||
state.DirectStreamProvider = liveStreamInfo.Item2;
|
||||
}
|
||||
|
||||
var videoRequest = request as VideoStreamRequest;
|
||||
|
|
|
@ -121,6 +121,25 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
|
||||
var responseHeaders = new Dictionary<string, string>();
|
||||
|
||||
if (request.Static && state.DirectStreamProvider != null)
|
||||
{
|
||||
AddDlnaHeaders(state, responseHeaders, true);
|
||||
|
||||
using (state)
|
||||
{
|
||||
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// TODO: Don't hardcode this
|
||||
outputHeaders["Content-Type"] = MediaBrowser.Model.Net.MimeTypes.GetMimeType("file.ts");
|
||||
|
||||
var streamSource = new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, CancellationToken.None)
|
||||
{
|
||||
AllowEndOfFile = false
|
||||
};
|
||||
return ResultFactory.GetAsyncStreamWriter(streamSource);
|
||||
}
|
||||
}
|
||||
|
||||
// Static remote stream
|
||||
if (request.Static && state.InputProtocol == MediaProtocol.Http)
|
||||
{
|
||||
|
@ -128,25 +147,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
|
||||
using (state)
|
||||
{
|
||||
if (state.MediaPath.IndexOf("/livestreamfiles/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
var parts = state.MediaPath.Split('/');
|
||||
var filename = parts[parts.Length - 2] + Path.GetExtension(parts[parts.Length - 1]);
|
||||
var filePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, filename);
|
||||
|
||||
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
outputHeaders["Content-Type"] = MimeTypes.GetMimeType(filePath);
|
||||
|
||||
var streamSource = new ProgressiveFileCopier(FileSystem, filePath, outputHeaders, null, Logger, CancellationToken.None)
|
||||
{
|
||||
AllowEndOfFile = false
|
||||
};
|
||||
return ResultFactory.GetAsyncStreamWriter(streamSource);
|
||||
}
|
||||
|
||||
return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource)
|
||||
.ConfigureAwait(false);
|
||||
return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ using CommonIO;
|
|||
using MediaBrowser.Controller.Net;
|
||||
using System.Collections.Generic;
|
||||
using ServiceStack.Web;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Progressive
|
||||
{
|
||||
|
@ -26,6 +27,8 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
public long StartPosition { get; set; }
|
||||
public bool AllowEndOfFile = true;
|
||||
|
||||
private IDirectStreamProvider _directStreamProvider;
|
||||
|
||||
public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
|
@ -36,6 +39,15 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
_cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
_directStreamProvider = directStreamProvider;
|
||||
_outputHeaders = outputHeaders;
|
||||
_job = job;
|
||||
_logger = logger;
|
||||
_cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public IDictionary<string, string> Options
|
||||
{
|
||||
get
|
||||
|
@ -44,22 +56,33 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
}
|
||||
}
|
||||
|
||||
private Stream GetInputStream()
|
||||
{
|
||||
return _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
|
||||
}
|
||||
|
||||
public async Task WriteToAsync(Stream outputStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_directStreamProvider != null)
|
||||
{
|
||||
await _directStreamProvider.CopyToAsync(outputStream, _cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var eofCount = 0;
|
||||
|
||||
using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
|
||||
using (var inputStream = GetInputStream())
|
||||
{
|
||||
if (StartPosition > 0)
|
||||
{
|
||||
fs.Position = StartPosition;
|
||||
inputStream.Position = StartPosition;
|
||||
}
|
||||
|
||||
while (eofCount < 15 || !AllowEndOfFile)
|
||||
{
|
||||
var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false);
|
||||
var bytesRead = await CopyToAsyncInternal(inputStream, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false);
|
||||
|
||||
//var position = fs.Position;
|
||||
//_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
|
||||
|
|
|
@ -37,6 +37,7 @@ namespace MediaBrowser.Api.Playback
|
|||
/// </summary>
|
||||
/// <value>The log file stream.</value>
|
||||
public Stream LogFileStream { get; set; }
|
||||
public IDirectStreamProvider DirectStreamProvider { get; set; }
|
||||
|
||||
public string InputContainer { get; set; }
|
||||
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
using MediaBrowser.Model.Chapters;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Chapters
|
||||
{
|
||||
public class ChapterResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the chapters.
|
||||
/// </summary>
|
||||
/// <value>The chapters.</value>
|
||||
public List<RemoteChapterInfo> Chapters { get; set; }
|
||||
|
||||
public ChapterResponse()
|
||||
{
|
||||
Chapters = new List<RemoteChapterInfo>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Chapters
|
||||
{
|
||||
public class ChapterSearchRequest : IHasProviderIds
|
||||
{
|
||||
public string Language { get; set; }
|
||||
|
||||
public VideoContentType ContentType { get; set; }
|
||||
|
||||
public string MediaPath { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int? IndexNumber { get; set; }
|
||||
public int? IndexNumberEnd { get; set; }
|
||||
public int? ParentIndexNumber { get; set; }
|
||||
public int? ProductionYear { get; set; }
|
||||
public long? RuntimeTicks { get; set; }
|
||||
public Dictionary<string, string> ProviderIds { get; set; }
|
||||
|
||||
public bool SearchAllProviders { get; set; }
|
||||
|
||||
public ChapterSearchRequest()
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Chapters;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
@ -13,12 +11,6 @@ namespace MediaBrowser.Controller.Chapters
|
|||
/// </summary>
|
||||
public interface IChapterManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the parts.
|
||||
/// </summary>
|
||||
/// <param name="chapterProviders">The chapter providers.</param>
|
||||
void AddParts(IEnumerable<IChapterProvider> chapterProviders);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chapters.
|
||||
/// </summary>
|
||||
|
@ -35,43 +27,6 @@ namespace MediaBrowser.Controller.Chapters
|
|||
/// <returns>Task.</returns>
|
||||
Task SaveChapters(string itemId, List<ChapterInfo> chapters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Searches the specified video.
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{IEnumerable{RemoteChapterResult}}.</returns>
|
||||
Task<IEnumerable<RemoteChapterResult>> Search(Video video, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Searches the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{IEnumerable{RemoteChapterResult}}.</returns>
|
||||
Task<IEnumerable<RemoteChapterResult>> Search(ChapterSearchRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chapters.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{ChapterResponse}.</returns>
|
||||
Task<ChapterResponse> GetChapters(string id, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the providers.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item identifier.</param>
|
||||
/// <returns>IEnumerable{ChapterProviderInfo}.</returns>
|
||||
IEnumerable<ChapterProviderInfo> GetProviders(string itemId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the providers.
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{ChapterProviderInfo}.</returns>
|
||||
IEnumerable<ChapterProviderInfo> GetProviders();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Chapters;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Chapters
|
||||
{
|
||||
public interface IChapterProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported media types.
|
||||
/// </summary>
|
||||
/// <value>The supported media types.</value>
|
||||
IEnumerable<VideoContentType> SupportedMediaTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Searches the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{IEnumerable{RemoteChapterResult}}.</returns>
|
||||
Task<IEnumerable<RemoteChapterResult>> Search(ChapterSearchRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chapters.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{ChapterResponse}.</returns>
|
||||
Task<ChapterResponse> GetChapters(string id, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
|
@ -79,6 +80,8 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<MediaSourceInfo>.</returns>
|
||||
Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken);
|
||||
|
||||
Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Pings the media source.
|
||||
|
@ -95,4 +98,9 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <returns>Task.</returns>
|
||||
Task CloseLiveStream(string id);
|
||||
}
|
||||
|
||||
public interface IDirectStreamProvider
|
||||
{
|
||||
Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ using MediaBrowser.Model.Dto;
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
|
@ -22,7 +23,7 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <param name="openToken">The open token.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<MediaSourceInfo>.</returns>
|
||||
Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken);
|
||||
Task<Tuple<MediaSourceInfo,IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the media source.
|
||||
|
|
|
@ -9,6 +9,7 @@ using System.Collections.Generic;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
namespace MediaBrowser.Controller.LiveTv
|
||||
{
|
||||
|
@ -156,7 +157,7 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <param name="mediaSourceId">The media source identifier.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{StreamResponseInfo}.</returns>
|
||||
Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken);
|
||||
Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the program.
|
||||
|
|
|
@ -4,6 +4,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
namespace MediaBrowser.Controller.LiveTv
|
||||
{
|
||||
|
@ -245,4 +246,9 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <returns>Task.</returns>
|
||||
Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface ISupportsDirectStreamProvider
|
||||
{
|
||||
Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,10 +91,7 @@
|
|||
<Compile Include="Channels\InternalChannelItemQuery.cs" />
|
||||
<Compile Include="Channels\IRequiresMediaInfoCallback.cs" />
|
||||
<Compile Include="Channels\ISearchableChannel.cs" />
|
||||
<Compile Include="Chapters\ChapterSearchRequest.cs" />
|
||||
<Compile Include="Chapters\IChapterManager.cs" />
|
||||
<Compile Include="Chapters\IChapterProvider.cs" />
|
||||
<Compile Include="Chapters\ChapterResponse.cs" />
|
||||
<Compile Include="Collections\CollectionCreationOptions.cs" />
|
||||
<Compile Include="Collections\CollectionEvents.cs" />
|
||||
<Compile Include="Collections\ICollectionManager.cs" />
|
||||
|
|
|
@ -160,15 +160,6 @@
|
|||
<Compile Include="..\MediaBrowser.Model\Channels\ChannelQuery.cs">
|
||||
<Link>Channels\ChannelQuery.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\MediaBrowser.Model\Chapters\ChapterProviderInfo.cs">
|
||||
<Link>Chapters\ChapterProviderInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\MediaBrowser.Model\Chapters\RemoteChapterInfo.cs">
|
||||
<Link>Chapters\RemoteChapterInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\MediaBrowser.Model\Chapters\RemoteChapterResult.cs">
|
||||
<Link>Chapters\RemoteChapterResult.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\MediaBrowser.Model\Collections\CollectionCreationResult.cs">
|
||||
<Link>Collections\CollectionCreationResult.cs</Link>
|
||||
</Compile>
|
||||
|
|
|
@ -132,15 +132,6 @@
|
|||
<Compile Include="..\MediaBrowser.Model\Channels\ChannelQuery.cs">
|
||||
<Link>Channels\ChannelQuery.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\MediaBrowser.Model\Chapters\ChapterProviderInfo.cs">
|
||||
<Link>Chapters\ChapterProviderInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\MediaBrowser.Model\Chapters\RemoteChapterInfo.cs">
|
||||
<Link>Chapters\RemoteChapterInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\MediaBrowser.Model\Chapters\RemoteChapterResult.cs">
|
||||
<Link>Chapters\RemoteChapterResult.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\MediaBrowser.Model\Collections\CollectionCreationResult.cs">
|
||||
<Link>Collections\CollectionCreationResult.cs</Link>
|
||||
</Compile>
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
namespace MediaBrowser.Model.Chapters
|
||||
{
|
||||
public class ChapterProviderInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
|
||||
namespace MediaBrowser.Model.Chapters
|
||||
{
|
||||
public class RemoteChapterInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the start position ticks.
|
||||
/// </summary>
|
||||
/// <value>The start position ticks.</value>
|
||||
public long StartPositionTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
|
||||
namespace MediaBrowser.Model.Chapters
|
||||
{
|
||||
public class RemoteChapterResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier.
|
||||
/// </summary>
|
||||
/// <value>The identifier.</value>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the run time ticks.
|
||||
/// </summary>
|
||||
/// <value>The run time ticks.</value>
|
||||
public long? RunTimeTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the provider.
|
||||
/// </summary>
|
||||
/// <value>The name of the provider.</value>
|
||||
public string ProviderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the community rating.
|
||||
/// </summary>
|
||||
/// <value>The community rating.</value>
|
||||
public float? CommunityRating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the chapter count.
|
||||
/// </summary>
|
||||
/// <value>The chapter count.</value>
|
||||
public int? ChapterCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the three letter iso language.
|
||||
/// </summary>
|
||||
/// <value>The name of the three letter iso language.</value>
|
||||
public string ThreeLetterISOLanguageName { get; set; }
|
||||
}
|
||||
}
|
|
@ -2,18 +2,10 @@
|
|||
{
|
||||
public class ChapterOptions
|
||||
{
|
||||
public bool DownloadMovieChapters { get; set; }
|
||||
public bool DownloadEpisodeChapters { get; set; }
|
||||
public bool EnableMovieChapterImageExtraction { get; set; }
|
||||
public bool EnableEpisodeChapterImageExtraction { get; set; }
|
||||
public bool EnableOtherVideoChapterImageExtraction { get; set; }
|
||||
|
||||
public string[] FetcherOrder { get; set; }
|
||||
public string[] DisabledFetchers { get; set; }
|
||||
|
||||
public ChapterOptions()
|
||||
{
|
||||
DownloadMovieChapters = true;
|
||||
|
||||
DisabledFetchers = new string[] { };
|
||||
FetcherOrder = new string[] { };
|
||||
}
|
||||
public bool ExtractDuringLibraryScan { get; set; }
|
||||
}
|
||||
}
|
|
@ -84,9 +84,6 @@
|
|||
<Compile Include="Channels\ChannelMediaContentType.cs" />
|
||||
<Compile Include="Channels\ChannelMediaType.cs" />
|
||||
<Compile Include="Channels\ChannelQuery.cs" />
|
||||
<Compile Include="Chapters\ChapterProviderInfo.cs" />
|
||||
<Compile Include="Chapters\RemoteChapterInfo.cs" />
|
||||
<Compile Include="Chapters\RemoteChapterResult.cs" />
|
||||
<Compile Include="Collections\CollectionCreationResult.cs" />
|
||||
<Compile Include="Configuration\AccessSchedule.cs" />
|
||||
<Compile Include="Configuration\ChannelOptions.cs" />
|
||||
|
|
|
@ -8,7 +8,6 @@ using MediaBrowser.Controller.Entities.TV;
|
|||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Chapters;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
@ -22,7 +21,6 @@ namespace MediaBrowser.Providers.Chapters
|
|||
{
|
||||
public class ChapterManager : IChapterManager
|
||||
{
|
||||
private IChapterProvider[] _providers;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
@ -36,224 +34,6 @@ namespace MediaBrowser.Providers.Chapters
|
|||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
public void AddParts(IEnumerable<IChapterProvider> chapterProviders)
|
||||
{
|
||||
_providers = chapterProviders.ToArray();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<RemoteChapterResult>> Search(Video video, CancellationToken cancellationToken)
|
||||
{
|
||||
VideoContentType mediaType;
|
||||
|
||||
if (video is Episode)
|
||||
{
|
||||
mediaType = VideoContentType.Episode;
|
||||
}
|
||||
else if (video is Movie)
|
||||
{
|
||||
mediaType = VideoContentType.Movie;
|
||||
}
|
||||
else
|
||||
{
|
||||
// These are the only supported types
|
||||
return Task.FromResult<IEnumerable<RemoteChapterResult>>(new List<RemoteChapterResult>());
|
||||
}
|
||||
|
||||
var request = new ChapterSearchRequest
|
||||
{
|
||||
ContentType = mediaType,
|
||||
IndexNumber = video.IndexNumber,
|
||||
Language = video.GetPreferredMetadataLanguage(),
|
||||
MediaPath = video.Path,
|
||||
Name = video.Name,
|
||||
ParentIndexNumber = video.ParentIndexNumber,
|
||||
ProductionYear = video.ProductionYear,
|
||||
ProviderIds = video.ProviderIds,
|
||||
RuntimeTicks = video.RunTimeTicks,
|
||||
SearchAllProviders = false
|
||||
};
|
||||
|
||||
var episode = video as Episode;
|
||||
|
||||
if (episode != null)
|
||||
{
|
||||
request.IndexNumberEnd = episode.IndexNumberEnd;
|
||||
request.SeriesName = episode.SeriesName;
|
||||
}
|
||||
|
||||
return Search(request, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RemoteChapterResult>> Search(ChapterSearchRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var contentType = request.ContentType;
|
||||
var providers = GetInternalProviders(false)
|
||||
.Where(i => i.SupportedMediaTypes.Contains(contentType))
|
||||
.ToList();
|
||||
|
||||
// If not searching all, search one at a time until something is found
|
||||
if (!request.SearchAllProviders)
|
||||
{
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentResults = await Search(request, provider, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (currentResults.Count > 0)
|
||||
{
|
||||
return currentResults;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error downloading subtitles from {0}", ex, provider.Name);
|
||||
}
|
||||
}
|
||||
return new List<RemoteChapterResult>();
|
||||
}
|
||||
|
||||
var tasks = providers.Select(async i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Search(request, i, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error downloading subtitles from {0}", ex, i.Name);
|
||||
return new List<RemoteChapterResult>();
|
||||
}
|
||||
});
|
||||
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
return results.SelectMany(i => i);
|
||||
}
|
||||
|
||||
private async Task<List<RemoteChapterResult>> Search(ChapterSearchRequest request,
|
||||
IChapterProvider provider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var searchResults = await provider.Search(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var list = searchResults.ToList();
|
||||
|
||||
foreach (var result in list)
|
||||
{
|
||||
result.Id = GetProviderId(provider.Name) + "_" + result.Id;
|
||||
result.ProviderName = provider.Name;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public Task<ChapterResponse> GetChapters(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
var parts = id.Split(new[] { '_' }, 2);
|
||||
|
||||
var provider = GetProvider(parts.First());
|
||||
id = parts.Last();
|
||||
|
||||
return provider.GetChapters(id, cancellationToken);
|
||||
}
|
||||
|
||||
public IEnumerable<ChapterProviderInfo> GetProviders(string itemId)
|
||||
{
|
||||
var video = _libraryManager.GetItemById(itemId) as Video;
|
||||
VideoContentType mediaType;
|
||||
|
||||
if (video is Episode)
|
||||
{
|
||||
mediaType = VideoContentType.Episode;
|
||||
}
|
||||
else if (video is Movie)
|
||||
{
|
||||
mediaType = VideoContentType.Movie;
|
||||
}
|
||||
else
|
||||
{
|
||||
// These are the only supported types
|
||||
return new List<ChapterProviderInfo>();
|
||||
}
|
||||
|
||||
var providers = GetInternalProviders(false)
|
||||
.Where(i => i.SupportedMediaTypes.Contains(mediaType));
|
||||
|
||||
return GetInfos(providers);
|
||||
}
|
||||
|
||||
public IEnumerable<ChapterProviderInfo> GetProviders()
|
||||
{
|
||||
return GetInfos(GetInternalProviders(true));
|
||||
}
|
||||
|
||||
private IEnumerable<IChapterProvider> GetInternalProviders(bool includeDisabledProviders)
|
||||
{
|
||||
var providers = _providers;
|
||||
|
||||
if (!includeDisabledProviders)
|
||||
{
|
||||
var options = GetConfiguration();
|
||||
|
||||
providers = providers
|
||||
.Where(i => !options.DisabledFetchers.Contains(i.Name))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
return providers
|
||||
.OrderBy(GetConfiguredOrder)
|
||||
.ThenBy(GetDefaultOrder)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private IEnumerable<ChapterProviderInfo> GetInfos(IEnumerable<IChapterProvider> providers)
|
||||
{
|
||||
return providers.Select(i => new ChapterProviderInfo
|
||||
{
|
||||
Name = i.Name,
|
||||
Id = GetProviderId(i.Name)
|
||||
});
|
||||
}
|
||||
|
||||
private string GetProviderId(string name)
|
||||
{
|
||||
return name.ToLower().GetMD5().ToString("N");
|
||||
}
|
||||
|
||||
private IChapterProvider GetProvider(string id)
|
||||
{
|
||||
return _providers.First(i => string.Equals(id, GetProviderId(i.Name)));
|
||||
}
|
||||
|
||||
private int GetConfiguredOrder(IChapterProvider provider)
|
||||
{
|
||||
var options = GetConfiguration();
|
||||
|
||||
// See if there's a user-defined order
|
||||
var index = Array.IndexOf(options.FetcherOrder, provider.Name);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
|
||||
// Not configured. Just return some high number to put it at the end.
|
||||
return 100;
|
||||
}
|
||||
|
||||
private int GetDefaultOrder(IChapterProvider provider)
|
||||
{
|
||||
var hasOrder = provider as IHasOrder;
|
||||
|
||||
if (hasOrder != null)
|
||||
{
|
||||
return hasOrder.Order;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public IEnumerable<ChapterInfo> GetChapters(string itemId)
|
||||
{
|
||||
return _itemRepo.GetChapters(new Guid(itemId));
|
||||
|
|
|
@ -238,22 +238,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
|
||||
options.MetadataRefreshMode == MetadataRefreshMode.Default)
|
||||
{
|
||||
var chapterOptions = _chapterManager.GetConfiguration();
|
||||
|
||||
try
|
||||
{
|
||||
var remoteChapters = await DownloadChapters(video, chapters, chapterOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (remoteChapters.Count > 0)
|
||||
{
|
||||
chapters = remoteChapters;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error downloading chapters", ex);
|
||||
}
|
||||
|
||||
if (chapters.Count == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
|
||||
{
|
||||
AddDummyChapters(video, chapters);
|
||||
|
@ -561,52 +545,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
currentStreams.AddRange(externalSubtitleStreams);
|
||||
}
|
||||
|
||||
private async Task<List<ChapterInfo>> DownloadChapters(Video video, List<ChapterInfo> currentChapters, ChapterOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
if ((options.DownloadEpisodeChapters &&
|
||||
video is Episode) ||
|
||||
(options.DownloadMovieChapters &&
|
||||
video is Movie))
|
||||
{
|
||||
var results = await _chapterManager.Search(video, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var result = results.FirstOrDefault();
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
var chapters = await _chapterManager.GetChapters(result.Id, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var chapterInfos = chapters.Chapters.Select(i => new ChapterInfo
|
||||
{
|
||||
Name = i.Name,
|
||||
StartPositionTicks = i.StartPositionTicks
|
||||
|
||||
}).ToList();
|
||||
|
||||
if (chapterInfos.All(i => i.StartPositionTicks == 0))
|
||||
{
|
||||
if (currentChapters.Count >= chapterInfos.Count)
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var info in chapterInfos)
|
||||
{
|
||||
info.StartPositionTicks = currentChapters[index].StartPositionTicks;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
chapterInfos.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
return chapterInfos;
|
||||
}
|
||||
}
|
||||
|
||||
return new List<ChapterInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The dummy chapter duration
|
||||
/// </summary>
|
||||
|
|
|
@ -14,6 +14,8 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace MediaBrowser.Providers.MediaInfo
|
||||
{
|
||||
|
@ -24,14 +26,16 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
private readonly ISubtitleManager _subtitleManager;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly ILogger _logger;
|
||||
private IJsonSerializer _json;
|
||||
|
||||
public SubtitleScheduledTask(ILibraryManager libraryManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, ILogger logger, IMediaSourceManager mediaSourceManager)
|
||||
public SubtitleScheduledTask(ILibraryManager libraryManager, IJsonSerializer json, IServerConfigurationManager config, ISubtitleManager subtitleManager, ILogger logger, IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_config = config;
|
||||
_subtitleManager = subtitleManager;
|
||||
_logger = logger;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_json = json;
|
||||
}
|
||||
|
||||
public string Name
|
||||
|
@ -58,38 +62,65 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
{
|
||||
var options = GetOptions();
|
||||
|
||||
var videos = _libraryManager.RootFolder
|
||||
.GetRecursiveChildren(i =>
|
||||
{
|
||||
if (!(i is Video))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var types = new List<string>();
|
||||
|
||||
if (i.LocationType == LocationType.Remote || i.LocationType == LocationType.Virtual)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (options.DownloadEpisodeSubtitles)
|
||||
{
|
||||
types.Add("Episode");
|
||||
}
|
||||
if (options.DownloadMovieSubtitles)
|
||||
{
|
||||
types.Add("Movie");
|
||||
}
|
||||
|
||||
return (options.DownloadEpisodeSubtitles &&
|
||||
i is Episode) ||
|
||||
(options.DownloadMovieSubtitles &&
|
||||
i is Movie);
|
||||
})
|
||||
.Cast<Video>()
|
||||
if (types.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var videos = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = new string[] { MediaType.Video },
|
||||
IsVirtualItem = false,
|
||||
ExcludeLocationTypes = new LocationType[] { LocationType.Remote, LocationType.Virtual },
|
||||
IncludeItemTypes = types.ToArray()
|
||||
|
||||
}).OfType<Video>()
|
||||
.ToList();
|
||||
|
||||
var failHistoryPath = Path.Combine(_config.ApplicationPaths.CachePath, "subtitlehistory.json");
|
||||
var history = GetHistory(failHistoryPath);
|
||||
|
||||
var numComplete = 0;
|
||||
|
||||
foreach (var video in videos)
|
||||
{
|
||||
DateTime lastAttempt;
|
||||
if (history.TryGetValue(video.Id.ToString("N"), out lastAttempt))
|
||||
{
|
||||
if ((DateTime.UtcNow - lastAttempt).TotalDays <= 7)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await DownloadSubtitles(video, options, cancellationToken).ConfigureAwait(false);
|
||||
var shouldRetry = await DownloadSubtitles(video, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (shouldRetry)
|
||||
{
|
||||
history[video.Id.ToString("N")] = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
history.Remove(video.Id.ToString("N"));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error downloading subtitles for {0}", ex, video.Path);
|
||||
history[video.Id.ToString("N")] = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Update progress
|
||||
|
@ -99,9 +130,23 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|
||||
progress.Report(100 * percent);
|
||||
}
|
||||
|
||||
_json.SerializeToFile(history, failHistoryPath);
|
||||
}
|
||||
|
||||
private async Task DownloadSubtitles(Video video, SubtitleOptions options, CancellationToken cancellationToken)
|
||||
private Dictionary<string,DateTime> GetHistory(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _json.DeserializeFromFile<Dictionary<string, DateTime>>(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new Dictionary<string, DateTime>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> DownloadSubtitles(Video video, SubtitleOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
if ((options.DownloadEpisodeSubtitles &&
|
||||
video is Episode) ||
|
||||
|
@ -124,8 +169,13 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
if (downloadedLanguages.Count > 0)
|
||||
{
|
||||
await video.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerable<ITaskTrigger> GetDefaultTriggers()
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace MediaBrowser.Server.Implementations.Channels
|
|||
return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>());
|
||||
}
|
||||
|
||||
public Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ using MediaBrowser.Server.Implementations.Library.Resolvers;
|
|||
using MoreLinq;
|
||||
using SortOrder = MediaBrowser.Model.Entities.SortOrder;
|
||||
using VideoResolver = MediaBrowser.Naming.Video.VideoResolver;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Library
|
||||
{
|
||||
|
@ -1900,6 +1901,24 @@ namespace MediaBrowser.Server.Implementations.Library
|
|||
options.EnableInternetProviders = ConfigurationManager.Configuration.EnableInternetProviders;
|
||||
}
|
||||
|
||||
if (options.SchemaVersion < 2)
|
||||
{
|
||||
var chapterOptions = ConfigurationManager.GetConfiguration<ChapterOptions>("chapters");
|
||||
options.ExtractChapterImagesDuringLibraryScan = chapterOptions.ExtractDuringLibraryScan;
|
||||
|
||||
if (collectionFolder != null)
|
||||
{
|
||||
if (string.Equals(collectionFolder.CollectionType, "movies", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
options.EnableChapterImageExtraction = chapterOptions.EnableMovieChapterImageExtraction;
|
||||
}
|
||||
else if (string.Equals(collectionFolder.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
options.EnableChapterImageExtraction = chapterOptions.EnableEpisodeChapterImageExtraction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
|
|
@ -367,7 +367,9 @@ namespace MediaBrowser.Server.Implementations.Library
|
|||
var tuple = GetProvider(request.OpenToken);
|
||||
var provider = tuple.Item1;
|
||||
|
||||
var mediaSource = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
|
||||
var mediaSourceTuple = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var mediaSource = mediaSourceTuple.Item1;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
|
||||
{
|
||||
|
@ -381,8 +383,10 @@ namespace MediaBrowser.Server.Implementations.Library
|
|||
Date = DateTime.UtcNow,
|
||||
EnableCloseTimer = enableAutoClose,
|
||||
Id = mediaSource.LiveStreamId,
|
||||
MediaSource = mediaSource
|
||||
MediaSource = mediaSource,
|
||||
DirectStreamProvider = mediaSourceTuple.Item2
|
||||
};
|
||||
|
||||
_openStreams[mediaSource.LiveStreamId] = info;
|
||||
|
||||
if (enableAutoClose)
|
||||
|
@ -414,7 +418,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
|
||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
|
@ -430,7 +434,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
|||
LiveStreamInfo info;
|
||||
if (_openStreams.TryGetValue(id, out info))
|
||||
{
|
||||
return info.MediaSource;
|
||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info.DirectStreamProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -443,6 +447,12 @@ namespace MediaBrowser.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await GetLiveStreamWithDirectStreamProvider(id, cancellationToken).ConfigureAwait(false);
|
||||
return result.Item1;
|
||||
}
|
||||
|
||||
public async Task PingLiveStream(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
@ -630,6 +640,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
|||
public string Id;
|
||||
public bool Closed;
|
||||
public MediaSourceInfo MediaSource;
|
||||
public IDirectStreamProvider DirectStreamProvider;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,7 +36,7 @@ using Microsoft.Win32;
|
|||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
public class EmbyTV : ILiveTvService, ISupportsNewTimerIds, IDisposable
|
||||
public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
|
||||
{
|
||||
private readonly IApplicationHost _appHpst;
|
||||
private readonly ILogger _logger;
|
||||
|
@ -828,10 +828,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
|||
private readonly Dictionary<string, LiveStream> _liveStreams = new Dictionary<string, LiveStream>();
|
||||
|
||||
public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await GetChannelStreamWithDirectStreamProvider(channelId, streamId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result.Item1;
|
||||
}
|
||||
|
||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result.Item2;
|
||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(result.Item2, result.Item1 as IDirectStreamProvider);
|
||||
}
|
||||
|
||||
private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, int consumerId, bool enableStreamSharing)
|
||||
|
|
|
@ -227,10 +227,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
|
||||
public async Task<MediaSourceInfo> GetRecordingStream(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
return await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false);
|
||||
var info = await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return info.Item1;
|
||||
}
|
||||
|
||||
public async Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
|
||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await GetLiveStream(id, mediaSourceId, true, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
@ -280,7 +282,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private async Task<MediaSourceInfo> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
|
||||
private async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
|
||||
{
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
@ -294,6 +296,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
MediaSourceInfo info;
|
||||
bool isVideo;
|
||||
ILiveTvService service;
|
||||
IDirectStreamProvider directStreamProvider = null;
|
||||
|
||||
if (isChannel)
|
||||
{
|
||||
|
@ -301,7 +304,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
isVideo = channel.ChannelType == ChannelType.TV;
|
||||
service = GetService(channel);
|
||||
_logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
|
||||
info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var supportsManagedStream = service as ISupportsDirectStreamProvider;
|
||||
if (supportsManagedStream != null)
|
||||
{
|
||||
var streamInfo = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
info = streamInfo.Item1;
|
||||
directStreamProvider = streamInfo.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
info.RequiresClosing = true;
|
||||
|
||||
if (info.RequiresClosing)
|
||||
|
@ -332,7 +346,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
_logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
|
||||
Normalize(info, service, isVideo);
|
||||
|
||||
return info;
|
||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info, directStreamProvider);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -544,11 +558,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
return item;
|
||||
}
|
||||
|
||||
private async Task<LiveTvProgram> GetProgram(ProgramInfo info, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
|
||||
private async Task<LiveTvProgram> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
|
||||
{
|
||||
var id = _tvDtoService.GetInternalProgramId(serviceName, info.Id);
|
||||
|
||||
var item = _libraryManager.GetItemById(id) as LiveTvProgram;
|
||||
LiveTvProgram item = null;
|
||||
allExistingPrograms.TryGetValue(id, out item);
|
||||
|
||||
var isNew = false;
|
||||
var forceUpdate = false;
|
||||
|
||||
|
@ -877,7 +893,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
|
||||
if (!string.IsNullOrWhiteSpace(query.SeriesTimerId))
|
||||
{
|
||||
var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery {}, cancellationToken).ConfigureAwait(false);
|
||||
var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false);
|
||||
var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.ServiceName, i.Id).ToString("N"), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase));
|
||||
if (seriesTimer != null)
|
||||
{
|
||||
|
@ -1265,9 +1281,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
|
||||
var channelPrograms = await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
||||
ChannelIds = new string[] { currentChannel.Id.ToString("N") }
|
||||
|
||||
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
||||
|
||||
foreach (var program in channelPrograms)
|
||||
{
|
||||
var programItem = await GetProgram(program, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken).ConfigureAwait(false);
|
||||
var programItem = await GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
programs.Add(programItem.Id);
|
||||
|
||||
|
@ -1881,11 +1905,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
{
|
||||
if (query.IsScheduled.Value)
|
||||
{
|
||||
timers = timers.Where(i => i.Item1.Status == RecordingStatus.New || i.Item1.Status == RecordingStatus.Scheduled);
|
||||
timers = timers.Where(i => i.Item1.Status == RecordingStatus.New);
|
||||
}
|
||||
else
|
||||
{
|
||||
timers = timers.Where(i => !(i.Item1.Status == RecordingStatus.New || i.Item1.Status == RecordingStatus.Scheduled));
|
||||
timers = timers.Where(i => !(i.Item1.Status == RecordingStatus.New));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -116,17 +116,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
return list;
|
||||
}
|
||||
|
||||
public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
{
|
||||
MediaSourceInfo stream;
|
||||
MediaSourceInfo stream = null;
|
||||
const bool isAudio = false;
|
||||
|
||||
var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
|
||||
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
|
||||
IDirectStreamProvider directStreamProvider = null;
|
||||
|
||||
if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
stream = info.Item1;
|
||||
directStreamProvider = info.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -142,7 +145,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
|||
_logger.ErrorException("Error probing live tv stream", ex);
|
||||
}
|
||||
|
||||
return stream;
|
||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(stream, directStreamProvider);
|
||||
}
|
||||
|
||||
private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
|
||||
|
|
|
@ -6,14 +6,16 @@ using CommonIO;
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public class HdHomerunLiveStream : LiveStream
|
||||
public class HdHomerunLiveStream : LiveStream, IDirectStreamProvider
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
@ -106,7 +108,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
{
|
||||
onStarted = () => ResolveWhenExists(openTaskCompletionSource, tempFilePath, cancellationToken);
|
||||
}
|
||||
await DirectRecorder.CopyUntilCancelled(response.Content, outputStream, onStarted, cancellationToken).ConfigureAwait(false);
|
||||
await CopyUntilCancelled(response.Content, outputStream, onStarted, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -137,6 +139,79 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private List<Tuple<Stream, CancellationToken, TaskCompletionSource<bool>>> _additionalStreams = new List<Tuple<Stream, CancellationToken, TaskCompletionSource<bool>>>();
|
||||
|
||||
public Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
_additionalStreams.Add(new Tuple<Stream, CancellationToken, TaskCompletionSource<bool>>(stream, cancellationToken, taskCompletionSource));
|
||||
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
private void PopAdditionalStream(Tuple<Stream, CancellationToken, TaskCompletionSource<bool>> stream, Exception exception)
|
||||
{
|
||||
if (_additionalStreams.Remove(stream))
|
||||
{
|
||||
stream.Item3.TrySetException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private const int BufferSize = 81920;
|
||||
private async Task CopyUntilCancelled(Stream source, Stream target, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var bytesRead = await CopyToAsyncInternal(source, target, BufferSize, onStarted, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
onStarted = null;
|
||||
|
||||
//var position = fs.Position;
|
||||
//_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int bytesRead;
|
||||
int totalBytesRead = 0;
|
||||
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var additionalStream in _additionalStreams)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
await additionalStream.Item1.WriteAsync(buffer, 0, bytesRead, additionalStream.Item2).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PopAdditionalStream(additionalStream, ex);
|
||||
}
|
||||
}
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
if (onStarted != null)
|
||||
{
|
||||
onStarted();
|
||||
}
|
||||
onStarted = null;
|
||||
}
|
||||
|
||||
return totalBytesRead;
|
||||
}
|
||||
|
||||
private async void ResolveWhenExists(TaskCompletionSource<bool> taskCompletionSource, string file, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!File.Exists(file) && !cancellationToken.IsCancellationRequested)
|
||||
|
|
|
@ -99,7 +99,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
|||
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
|
||||
private const string StreamIdDelimeterString = "_";
|
||||
|
||||
public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
{
|
||||
var openKeys = openToken.Split(new[] { StreamIdDelimeterString[0] }, 3);
|
||||
|
||||
|
@ -137,7 +137,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
|||
mediaSource.Protocol = dynamicInfo.Protocol;
|
||||
mediaSource.RequiredHttpHeaders = dynamicInfo.RequiredHttpHeaders;
|
||||
|
||||
return mediaSource;
|
||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(mediaSource, null);
|
||||
}
|
||||
|
||||
private void SetStaticMediaSourceInfo(LocalItem item, MediaSourceInfo mediaSource)
|
||||
|
|
|
@ -826,7 +826,6 @@ namespace MediaBrowser.Server.Startup.Common
|
|||
LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
|
||||
|
||||
SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
|
||||
ChapterManager.AddParts(GetExports<IChapterProvider>());
|
||||
|
||||
SessionManager.AddParts(GetExports<ISessionControllerFactory>());
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user