start pulling in EmbyTV

This commit is contained in:
Luke Pulverenti 2015-07-20 14:32:55 -04:00
parent 3178896004
commit 20b990dc9a
16 changed files with 1379 additions and 10 deletions

View File

@ -432,7 +432,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
var httpResponse = (HttpWebResponse)response; var httpResponse = (HttpWebResponse)response;
EnsureSuccessStatusCode(httpResponse, options); EnsureSuccessStatusCode(client, httpResponse, options);
options.CancellationToken.ThrowIfCancellationRequested(); options.CancellationToken.ThrowIfCancellationRequested();
@ -443,7 +443,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
{ {
var httpResponse = (HttpWebResponse)response; var httpResponse = (HttpWebResponse)response;
EnsureSuccessStatusCode(httpResponse, options); EnsureSuccessStatusCode(client, httpResponse, options);
options.CancellationToken.ThrowIfCancellationRequested(); options.CancellationToken.ThrowIfCancellationRequested();
@ -629,7 +629,8 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
{ {
var httpResponse = (HttpWebResponse)response; var httpResponse = (HttpWebResponse)response;
EnsureSuccessStatusCode(httpResponse, options); var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
EnsureSuccessStatusCode(client, httpResponse, options);
options.CancellationToken.ThrowIfCancellationRequested(); options.CancellationToken.ThrowIfCancellationRequested();
@ -803,13 +804,20 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
return exception; return exception;
} }
private void EnsureSuccessStatusCode(HttpWebResponse response, HttpRequestOptions options) private void EnsureSuccessStatusCode(HttpClientInfo client, HttpWebResponse response, HttpRequestOptions options)
{ {
var statusCode = response.StatusCode; var statusCode = response.StatusCode;
var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299; var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299;
if (!isSuccessful) if (!isSuccessful)
{ {
if ((int) statusCode == 429)
{
client.LastTimeout = DateTime.UtcNow;
}
if (statusCode == HttpStatusCode.RequestEntityTooLarge)
if (options.LogErrorResponseBody) if (options.LogErrorResponseBody)
{ {
try try

View File

@ -48,6 +48,10 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary> /// </summary>
/// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
public bool? HasImage { get; set; } public bool? HasImage { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is favorite.
/// </summary>
/// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value>
public bool? IsFavorite { get; set; }
} }
} }

View File

@ -0,0 +1,7 @@

namespace MediaBrowser.Controller.LiveTv
{
public interface IListingsProvider
{
}
}

View File

@ -0,0 +1,50 @@
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.LiveTv;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.LiveTv
{
public interface ITunerHost
{
/// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
string Name { get; }
/// <summary>
/// Gets the type.
/// </summary>
/// <value>The type.</value>
string Type { get; }
/// <summary>
/// Gets the tuner hosts.
/// </summary>
/// <returns>List&lt;TunerHostInfo&gt;.</returns>
List<TunerHostInfo> GetTunerHosts();
/// <summary>
/// Gets the channels.
/// </summary>
/// <param name="info">The information.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;IEnumerable&lt;ChannelInfo&gt;&gt;.</returns>
Task<IEnumerable<ChannelInfo>> GetChannels(TunerHostInfo info, CancellationToken cancellationToken);
/// <summary>
/// Gets the tuner infos.
/// </summary>
/// <param name="info">The information.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;List&lt;LiveTvTunerInfo&gt;&gt;.</returns>
Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken);
/// <summary>
/// Gets the channel stream.
/// </summary>
/// <param name="info">The information.</param>
/// <param name="channelId">The channel identifier.</param>
/// <param name="streamId">The stream identifier.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken);
}
}

View File

@ -198,7 +198,9 @@
<Compile Include="Library\NameExtensions.cs" /> <Compile Include="Library\NameExtensions.cs" />
<Compile Include="Library\PlaybackStopEventArgs.cs" /> <Compile Include="Library\PlaybackStopEventArgs.cs" />
<Compile Include="Library\UserDataSaveEventArgs.cs" /> <Compile Include="Library\UserDataSaveEventArgs.cs" />
<Compile Include="LiveTv\IListingsProvider.cs" />
<Compile Include="LiveTv\ILiveTvItem.cs" /> <Compile Include="LiveTv\ILiveTvItem.cs" />
<Compile Include="LiveTv\ITunerHost.cs" />
<Compile Include="LiveTv\RecordingGroup.cs" /> <Compile Include="LiveTv\RecordingGroup.cs" />
<Compile Include="LiveTv\RecordingStatusChangedEventArgs.cs" /> <Compile Include="LiveTv\RecordingStatusChangedEventArgs.cs" />
<Compile Include="LiveTv\ILiveTvRecording.cs" /> <Compile Include="LiveTv\ILiveTvRecording.cs" />

View File

@ -1,13 +1,24 @@
namespace MediaBrowser.Model.LiveTv using System.Collections.Generic;
namespace MediaBrowser.Model.LiveTv
{ {
public class LiveTvOptions public class LiveTvOptions
{ {
public int? GuideDays { get; set; } public int? GuideDays { get; set; }
public bool EnableMovieProviders { get; set; } public bool EnableMovieProviders { get; set; }
public List<TunerHostInfo> TunerHosts { get; set; }
public string RecordingPath { get; set; }
public LiveTvOptions() public LiveTvOptions()
{ {
EnableMovieProviders = true; EnableMovieProviders = true;
TunerHosts = new List<TunerHostInfo>();
} }
} }
public class TunerHostInfo
{
public string Url { get; set; }
public string Type { get; set; }
}
} }

View File

@ -1,4 +1,5 @@
using MediaBrowser.Common.Net; using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -17,12 +18,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private readonly ILiveTvManager _liveTvManager; private readonly ILiveTvManager _liveTvManager;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IApplicationHost _appHost;
public ChannelImageProvider(ILiveTvManager liveTvManager, IHttpClient httpClient, ILogger logger) public ChannelImageProvider(ILiveTvManager liveTvManager, IHttpClient httpClient, ILogger logger, IApplicationHost appHost)
{ {
_liveTvManager = liveTvManager; _liveTvManager = liveTvManager;
_httpClient = httpClient; _httpClient = httpClient;
_logger = logger; _logger = logger;
_appHost = appHost;
} }
public IEnumerable<ImageType> GetSupportedImages(IHasImages item) public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
@ -46,7 +49,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var options = new HttpRequestOptions var options = new HttpRequestOptions
{ {
CancellationToken = cancellationToken, CancellationToken = cancellationToken,
Url = liveTvItem.ProviderImageUrl Url = liveTvItem.ProviderImageUrl,
// Some image hosts require a user agent to be specified.
UserAgent = "Emby Server/" + _appHost.ApplicationVersion
}; };
var response = await _httpClient.GetResponse(options).ConfigureAwait(false); var response = await _httpClient.GetResponse(options).ConfigureAwait(false);

View File

@ -0,0 +1,507 @@
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
public class EmbyTV : ILiveTvService, IDisposable
{
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
private readonly IConfigurationManager _config;
private readonly IJsonSerializer _jsonSerializer;
private readonly List<ITunerHost> _tunerHosts = new List<ITunerHost>();
private readonly ItemDataProvider<RecordingInfo> _recordingProvider;
private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
private readonly TimerManager _timerProvider;
public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IConfigurationManager config)
{
_logger = logger;
_httpClient = httpClient;
_config = config;
_jsonSerializer = jsonSerializer;
_tunerHosts.AddRange(appHost.GetExports<ITunerHost>());
_recordingProvider = new ItemDataProvider<RecordingInfo>(jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase));
_seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
_timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers"));
_timerProvider.TimerFired += _timerProvider_TimerFired;
}
public event EventHandler DataSourceChanged;
public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
private readonly ConcurrentDictionary<string, CancellationTokenSource> _activeRecordings =
new ConcurrentDictionary<string, CancellationTokenSource>(StringComparer.OrdinalIgnoreCase);
public string Name
{
get { return "Emby"; }
}
public string DataPath
{
get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); }
}
public string HomePageUrl
{
get { return "http://emby.media"; }
}
public async Task<LiveTvServiceStatusInfo> GetStatusInfoAsync(CancellationToken cancellationToken)
{
var status = new LiveTvServiceStatusInfo();
var list = new List<LiveTvTunerInfo>();
foreach (var host in _tunerHosts)
{
foreach (var hostInstance in host.GetTunerHosts())
{
try
{
var tuners = await host.GetTunerInfos(hostInstance, cancellationToken).ConfigureAwait(false);
list.AddRange(tuners);
}
catch (Exception ex)
{
_logger.ErrorException("Error getting tuners", ex);
}
}
}
status.Tuners = list;
status.Status = LiveTvServiceStatus.Ok;
return status;
}
public async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
{
var list = new List<ChannelInfo>();
foreach (var host in _tunerHosts)
{
foreach (var hostInstance in host.GetTunerHosts())
{
try
{
var channels = await host.GetChannels(hostInstance, cancellationToken).ConfigureAwait(false);
list.AddRange(channels);
}
catch (Exception ex)
{
_logger.ErrorException("Error getting channels", ex);
}
}
}
return list;
}
public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
{
var remove = _seriesTimerProvider.GetAll().SingleOrDefault(r => r.Id == timerId);
if (remove != null)
{
_seriesTimerProvider.Delete(remove);
}
return Task.FromResult(true);
}
private void CancelTimerInternal(string timerId)
{
var remove = _timerProvider.GetAll().SingleOrDefault(r => r.Id == timerId);
if (remove != null)
{
_timerProvider.Delete(remove);
}
CancellationTokenSource cancellationTokenSource;
if (_activeRecordings.TryGetValue(timerId, out cancellationTokenSource))
{
cancellationTokenSource.Cancel();
}
}
public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
{
CancelTimerInternal(timerId);
return Task.FromResult(true);
}
public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
{
var remove = _recordingProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, recordingId, StringComparison.OrdinalIgnoreCase));
if (remove != null)
{
try
{
File.Delete(remove.Path);
}
catch (DirectoryNotFoundException)
{
}
catch (FileNotFoundException)
{
}
_recordingProvider.Delete(remove);
}
return Task.FromResult(true);
}
public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
{
info.Id = Guid.NewGuid().ToString("N");
_timerProvider.Add(info);
return Task.FromResult(0);
}
public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
{
info.Id = info.ProgramId.Substring(0, 10);
UpdateTimersForSeriesTimer(info);
_seriesTimerProvider.Add(info);
return Task.FromResult(true);
}
public Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
{
_seriesTimerProvider.Update(info);
UpdateTimersForSeriesTimer(info);
return Task.FromResult(true);
}
public Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
{
_timerProvider.Update(info);
return Task.FromResult(true);
}
public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<ImageStream> GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<ImageStream> GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
{
return Task.FromResult((IEnumerable<RecordingInfo>)_recordingProvider.GetAll());
}
public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
{
return Task.FromResult((IEnumerable<TimerInfo>)_timerProvider.GetAll());
}
public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
{
var defaults = new SeriesTimerInfo()
{
PostPaddingSeconds = 60,
PrePaddingSeconds = 60,
RecordAnyChannel = false,
RecordAnyTime = false,
RecordNewOnly = false
};
return Task.FromResult(defaults);
}
public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
{
return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
}
public Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<MediaSourceInfo> GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task CloseLiveStream(string id, CancellationToken cancellationToken)
{
return Task.FromResult(0);
}
public Task RecordLiveStream(string id, CancellationToken cancellationToken)
{
return Task.FromResult(0);
}
public Task ResetTuner(string id, CancellationToken cancellationToken)
{
return Task.FromResult(0);
}
async void _timerProvider_TimerFired(object sender, GenericEventArgs<TimerInfo> e)
{
try
{
var cancellationTokenSource = new CancellationTokenSource();
if (_activeRecordings.TryAdd(e.Argument.Id, cancellationTokenSource))
{
await RecordStream(e.Argument, cancellationTokenSource.Token).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
_logger.ErrorException("Error recording stream", ex);
}
}
private async Task RecordStream(TimerInfo timer, CancellationToken cancellationToken)
{
var mediaStreamInfo = await GetChannelStream(timer.ChannelId, "none", CancellationToken.None);
var duration = (timer.EndDate - RecordingHelper.GetStartTime(timer)).TotalSeconds + timer.PrePaddingSeconds;
HttpRequestOptions httpRequestOptions = new HttpRequestOptions()
{
Url = mediaStreamInfo.Path + "?duration=" + duration
};
var info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
var recordPath = RecordingPath;
if (info.IsMovie)
{
recordPath = Path.Combine(recordPath, "Movies", RecordingHelper.RemoveSpecialCharacters(info.Name));
}
else
{
recordPath = Path.Combine(recordPath, "TV", RecordingHelper.RemoveSpecialCharacters(info.Name));
}
recordPath = Path.Combine(recordPath, RecordingHelper.GetRecordingName(timer, info));
Directory.CreateDirectory(Path.GetDirectoryName(recordPath));
var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, info.Id, StringComparison.OrdinalIgnoreCase));
if (recording == null)
{
recording = new RecordingInfo()
{
ChannelId = info.ChannelId,
Id = info.Id,
StartDate = info.StartDate,
EndDate = info.EndDate,
Genres = info.Genres ?? null,
IsKids = info.IsKids,
IsLive = info.IsLive,
IsMovie = info.IsMovie,
IsHD = info.IsHD,
IsNews = info.IsNews,
IsPremiere = info.IsPremiere,
IsSeries = info.IsSeries,
IsSports = info.IsSports,
IsRepeat = !info.IsPremiere,
Name = info.Name,
EpisodeTitle = info.EpisodeTitle ?? "",
ProgramId = info.Id,
HasImage = info.HasImage ?? false,
ImagePath = info.ImagePath ?? null,
ImageUrl = info.ImageUrl,
OriginalAirDate = info.OriginalAirDate,
Status = RecordingStatus.Scheduled,
Overview = info.Overview,
SeriesTimerId = info.Id.Substring(0, 10)
};
_recordingProvider.Add(recording);
}
recording.Path = recordPath;
recording.Status = RecordingStatus.InProgress;
_recordingProvider.Update(recording);
try
{
httpRequestOptions.BufferContent = false;
httpRequestOptions.CancellationToken = cancellationToken;
_logger.Info("Writing file to path: " + recordPath);
using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET"))
{
using (var output = File.Open(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
await response.Content.CopyToAsync(output, 4096, cancellationToken);
}
}
recording.Status = RecordingStatus.Completed;
}
catch (OperationCanceledException)
{
recording.Status = RecordingStatus.Cancelled;
}
catch
{
recording.Status = RecordingStatus.Error;
}
_recordingProvider.Update(recording);
_timerProvider.Delete(timer);
_logger.Info("Recording was a success");
}
private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
{
var epgData = GetEpgDataForChannel(channelId);
if (epgData.Any())
{
return epgData.FirstOrDefault(p => p.Id == programId);
}
return null;
}
private string RecordingPath
{
get
{
var path = GetConfiguration().RecordingPath;
return string.IsNullOrWhiteSpace(path)
? Path.Combine(DataPath, "recordings")
: path;
}
}
private LiveTvOptions GetConfiguration()
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
private void UpdateTimersForSeriesTimer(SeriesTimerInfo seriesTimer)
{
List<ProgramInfo> epgData;
if (seriesTimer.RecordAnyChannel)
{
epgData = GetEpgDataForAllChannels();
}
else
{
epgData = GetEpgDataForChannel(seriesTimer.ChannelId);
}
var newTimers = RecordingHelper.GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll(), _logger);
var existingTimers = _timerProvider.GetAll()
.Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var timer in newTimers)
{
_timerProvider.AddOrUpdate(timer);
}
var newTimerIds = newTimers.Select(i => i.Id).ToList();
foreach (var timer in existingTimers)
{
if (!newTimerIds.Contains(timer.Id, StringComparer.OrdinalIgnoreCase))
{
CancelTimerInternal(timer.Id);
}
}
}
private string GetChannelEpgCachePath(string channelId)
{
return Path.Combine(DataPath, "epg", channelId + ".json");
}
private readonly object _epgLock = new object();
private void SaveEpgDataForChannel(string channelId, List<ProgramInfo> epgData)
{
var path = GetChannelEpgCachePath(channelId);
Directory.CreateDirectory(Path.GetDirectoryName(path));
lock (_epgLock)
{
_jsonSerializer.SerializeToFile(epgData, path);
}
}
private List<ProgramInfo> GetEpgDataForChannel(string channelId)
{
try
{
lock (_epgLock)
{
return _jsonSerializer.DeserializeFromFile<List<ProgramInfo>>(GetChannelEpgCachePath(channelId));
}
}
catch
{
return new List<ProgramInfo>();
}
}
private List<ProgramInfo> GetEpgDataForAllChannels()
{
List<ProgramInfo> channelEpg = new List<ProgramInfo>();
DirectoryInfo dir = new DirectoryInfo(Path.Combine(DataPath, "epg"));
List<string> channels = dir.GetFiles("*").Where(i => string.Equals(i.Extension, ".json", StringComparison.OrdinalIgnoreCase)).Select(f => f.Name).ToList();
foreach (var channel in channels)
{
channelEpg.AddRange(GetEpgDataForChannel(channel));
}
return channelEpg;
}
public void Dispose()
{
foreach (var pair in _activeRecordings.ToList())
{
pair.Value.Cancel();
}
}
}
}

View File

@ -0,0 +1,115 @@
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
public class ItemDataProvider<T>
where T : class
{
private readonly object _fileDataLock = new object();
private List<T> _items;
private readonly IJsonSerializer _jsonSerializer;
protected readonly ILogger Logger;
private readonly string _dataPath;
protected readonly Func<T, T, bool> EqualityComparer;
public ItemDataProvider(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, Func<T, T, bool> equalityComparer)
{
Logger = logger;
_dataPath = dataPath;
EqualityComparer = equalityComparer;
_jsonSerializer = jsonSerializer;
}
public IReadOnlyList<T> GetAll()
{
if (_items == null)
{
lock (_fileDataLock)
{
if (_items == null)
{
_items = GetItemsFromFile(_dataPath);
}
}
}
return _items;
}
private List<T> GetItemsFromFile(string path)
{
var jsonFile = path + ".json";
try
{
return _jsonSerializer.DeserializeFromFile<List<T>>(jsonFile);
}
catch (FileNotFoundException)
{
}
catch (DirectoryNotFoundException ex)
{
}
catch (IOException ex)
{
Logger.ErrorException("Error deserializing {0}", ex, jsonFile);
throw;
}
catch (Exception ex)
{
Logger.ErrorException("Error deserializing {0}", ex, jsonFile);
}
return new List<T>();
}
private void UpdateList(List<T> newList)
{
lock (_fileDataLock)
{
_jsonSerializer.SerializeToFile(newList, _dataPath + ".json");
_items = newList;
}
}
public virtual void Update(T item)
{
var list = GetAll().ToList();
var index = list.FindIndex(i => EqualityComparer(i, item));
if (index == -1)
{
throw new ArgumentException("item not found");
}
list[index] = item;
UpdateList(list);
}
public virtual void Add(T item)
{
var list = GetAll().ToList();
if (list.Any(i => EqualityComparer(i, item)))
{
throw new ArgumentException("item already exists");
}
list.Add(item);
UpdateList(list);
}
public virtual void Delete(T item)
{
var list = GetAll().Where(i => !EqualityComparer(i, item)).ToList();
UpdateList(list);
}
}
}

View File

@ -0,0 +1,119 @@
using System.Text;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
internal class RecordingHelper
{
public static List<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> epgData, IReadOnlyList<RecordingInfo> currentRecordings, ILogger logger)
{
List<TimerInfo> timers = new List<TimerInfo>();
// Filtered Per Show
var filteredEpg = epgData.Where(epg => epg.Id.Substring(0, 10) == seriesTimer.Id);
if (!seriesTimer.RecordAnyTime)
{
filteredEpg = filteredEpg.Where(epg => (seriesTimer.StartDate.TimeOfDay == epg.StartDate.TimeOfDay));
}
if (seriesTimer.RecordNewOnly)
{
filteredEpg = filteredEpg.Where(epg => !epg.IsRepeat); //Filtered by New only
}
if (!seriesTimer.RecordAnyChannel)
{
filteredEpg = filteredEpg.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase));
}
filteredEpg = filteredEpg.Where(epg => seriesTimer.Days.Contains(epg.StartDate.DayOfWeek));
filteredEpg = filteredEpg.Where(epg => currentRecordings.All(r => r.Id.Substring(0, 14) != epg.Id.Substring(0, 14))); //filtered recordings already running
filteredEpg = filteredEpg.GroupBy(epg => epg.Id.Substring(0, 14)).Select(g => g.First()).ToList();
foreach (var epg in filteredEpg)
{
timers.Add(CreateTimer(epg, seriesTimer));
}
return timers;
}
public static DateTime GetStartTime(TimerInfo timer)
{
if (timer.StartDate.AddSeconds(-timer.PrePaddingSeconds + 1) < DateTime.UtcNow)
{
return DateTime.UtcNow.AddSeconds(1);
}
return timer.StartDate.AddSeconds(-timer.PrePaddingSeconds);
}
public static TimerInfo CreateTimer(ProgramInfo parent, SeriesTimerInfo series)
{
var timer = new TimerInfo();
timer.ChannelId = parent.ChannelId;
timer.Id = (series.Id + parent.Id).GetMD5().ToString("N");
timer.StartDate = parent.StartDate;
timer.EndDate = parent.EndDate;
timer.ProgramId = parent.Id;
timer.PrePaddingSeconds = series.PrePaddingSeconds;
timer.PostPaddingSeconds = series.PostPaddingSeconds;
timer.IsPostPaddingRequired = series.IsPostPaddingRequired;
timer.IsPrePaddingRequired = series.IsPrePaddingRequired;
timer.Priority = series.Priority;
timer.Name = parent.Name;
timer.Overview = parent.Overview;
timer.SeriesTimerId = series.Id;
return timer;
}
public static string GetRecordingName(TimerInfo timer, ProgramInfo info)
{
if (info == null)
{
return (timer.ProgramId + ".ts");
}
var fancyName = info.Name;
if (info.ProductionYear != null)
{
fancyName += "_(" + info.ProductionYear + ")";
}
if (info.IsSeries)
{
fancyName += "_" + info.EpisodeTitle.Replace("Season: ", "S").Replace(" Episode: ", "E");
}
if (info.IsHD ?? false)
{
fancyName += "_HD";
}
if (info.OriginalAirDate != null)
{
fancyName += "_" + info.OriginalAirDate.Value.ToString("yyyy-MM-dd");
}
return RemoveSpecialCharacters(fancyName) + ".ts";
}
public static string RemoveSpecialCharacters(string str)
{
StringBuilder sb = new StringBuilder();
foreach (char c in str)
{
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '.' || c == '_' || c == '-' || c == ' ')
{
sb.Append(c);
}
}
return sb.ToString();
}
}
}

View File

@ -0,0 +1,25 @@
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Serialization;
using System;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo>
{
public SeriesTimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
: base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
}
public override void Add(SeriesTimerInfo item)
{
if (string.IsNullOrWhiteSpace(item.Id))
{
throw new ArgumentException("SeriesTimerInfo.Id cannot be null or empty.");
}
base.Add(item);
}
}
}

View File

@ -0,0 +1,114 @@
using MediaBrowser.Common.Events;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
public class TimerManager : ItemDataProvider<TimerInfo>
{
private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase);
public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
: base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
}
public void RestartTimers()
{
StopTimers();
}
public void StopTimers()
{
foreach (var pair in _timers.ToList())
{
pair.Value.Dispose();
}
_timers.Clear();
}
public override void Delete(TimerInfo item)
{
base.Delete(item);
Timer timer;
if (_timers.TryRemove(item.Id, out timer))
{
timer.Dispose();
}
}
public override void Update(TimerInfo item)
{
base.Update(item);
Timer timer;
if (_timers.TryGetValue(item.Id, out timer))
{
var timespan = RecordingHelper.GetStartTime(item) - DateTime.UtcNow;
timer.Change(timespan, TimeSpan.Zero);
}
else
{
AddTimer(item);
}
}
public override void Add(TimerInfo item)
{
if (string.IsNullOrWhiteSpace(item.Id))
{
throw new ArgumentException("TimerInfo.Id cannot be null or empty.");
}
base.Add(item);
AddTimer(item);
}
public void AddOrUpdate(TimerInfo item)
{
var list = GetAll().ToList();
if (!list.Any(i => EqualityComparer(i, item)))
{
Add(item);
}
else
{
Update(item);
}
}
private void AddTimer(TimerInfo item)
{
var timespan = RecordingHelper.GetStartTime(item) - DateTime.UtcNow;
var timer = new Timer(TimerCallback, item.Id, timespan, TimeSpan.Zero);
if (!_timers.TryAdd(item.Id, timer))
{
timer.Dispose();
}
}
private void TimerCallback(object state)
{
var timerId = (string)state;
var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
if (timer != null)
{
EventHelper.FireEventIfNotNull(TimerFired, this, new GenericEventArgs<TimerInfo> { Argument = timer }, Logger);
}
}
}
}

View File

@ -55,7 +55,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private readonly SemaphoreSlim _refreshRecordingsLock = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _refreshRecordingsLock = new SemaphoreSlim(1, 1);
private ConcurrentDictionary<Guid, Guid> _refreshedPrograms = new ConcurrentDictionary<Guid, Guid>(); private readonly ConcurrentDictionary<Guid, Guid> _refreshedPrograms = new ConcurrentDictionary<Guid, Guid>();
public LiveTvManager(IApplicationHost appHost, IServerConfigurationManager config, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, IJsonSerializer jsonSerializer, IProviderManager providerManager) public LiveTvManager(IApplicationHost appHost, IServerConfigurationManager config, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, IJsonSerializer jsonSerializer, IProviderManager providerManager)
{ {

View File

@ -0,0 +1,205 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{
public class HdHomerun : ITunerHost
{
private readonly IHttpClient _httpClient;
private readonly ILogger _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly IConfigurationManager _config;
public HdHomerun(IHttpClient httpClient, ILogger logger, IJsonSerializer jsonSerializer, IConfigurationManager config)
{
_httpClient = httpClient;
_logger = logger;
_jsonSerializer = jsonSerializer;
_config = config;
}
public string Name
{
get { return "HD Homerun"; }
}
public string Type
{
get { return "hdhomerun"; }
}
public async Task<IEnumerable<ChannelInfo>> GetChannels(TunerHostInfo info, CancellationToken cancellationToken)
{
var options = new HttpRequestOptions
{
Url = string.Format("{0}/lineup.json", GetApiUrl(info)),
CancellationToken = cancellationToken
};
using (var stream = await _httpClient.Get(options))
{
var root = _jsonSerializer.DeserializeFromStream<List<Channels>>(stream);
if (root != null)
{
return root.Select(i => new ChannelInfo
{
Name = i.GuideName,
Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture),
Id = i.GuideNumber.ToString(CultureInfo.InvariantCulture),
IsFavorite = i.Favorite
});
}
return new List<ChannelInfo>();
}
}
public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
{
var httpOptions = new HttpRequestOptions()
{
Url = string.Format("{0}/tuners.html", GetApiUrl(info)),
CancellationToken = cancellationToken
};
using (var stream = await _httpClient.Get(httpOptions))
{
var tuners = new List<LiveTvTunerInfo>();
using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
{
while (!sr.EndOfStream)
{
string line = StripXML(sr.ReadLine());
if (line.Contains("Channel"))
{
LiveTvTunerStatus status;
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
var name = line.Substring(0, index - 1);
var currentChannel = line.Substring(index + 7);
if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; }
tuners.Add(new LiveTvTunerInfo()
{
Name = name,
SourceType = Name,
ProgramName = currentChannel,
Status = status
});
}
}
}
return tuners;
}
}
public string GetApiUrl(TunerHostInfo info)
{
var url = info.Url;
if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
url = "http://" + url;
}
return url.TrimEnd('/');
}
private static string StripXML(string source)
{
char[] buffer = new char[source.Length];
int bufferIndex = 0;
bool inside = false;
for (int i = 0; i < source.Length; i++)
{
char let = source[i];
if (let == '<')
{
inside = true;
continue;
}
if (let == '>')
{
inside = false;
continue;
}
if (!inside)
{
buffer[bufferIndex] = let;
bufferIndex++;
}
}
return new string(buffer, 0, bufferIndex);
}
private class Channels
{
public string GuideNumber { get; set; }
public string GuideName { get; set; }
public string URL { get; set; }
public bool Favorite { get; set; }
public bool DRM { get; set; }
}
private LiveTvOptions GetConfiguration()
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
public List<TunerHostInfo> GetTunerHosts()
{
return GetConfiguration().TunerHosts.Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)).ToList();
}
public async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
{
var channels = await GetChannels(info, cancellationToken).ConfigureAwait(false);
var tuners = await GetTunerInfos(info, cancellationToken).ConfigureAwait(false);
var channel = channels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase));
if (channel != null)
{
if (tuners.FindIndex(t => t.Status == LiveTvTunerStatus.Available) >= 0)
{
return new MediaSourceInfo
{
Path = GetApiUrl(info) + "/auto/v" + channelId,
Protocol = MediaProtocol.Http,
MediaStreams = new List<MediaStream>
{
new MediaStream
{
Type = MediaStreamType.Video,
// Set the index to -1 because we don't know the exact index of the video stream within the container
Index = -1,
IsInterlaced = true
},
new MediaStream
{
Type = MediaStreamType.Audio,
// Set the index to -1 because we don't know the exact index of the audio stream within the container
Index = -1
}
}
};
}
throw new ApplicationException("No tuners avaliable.");
}
throw new ApplicationException("Channel not found.");
}
}
}

View File

@ -0,0 +1,189 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : ITunerHost
{
public string Type
{
get { return "m3u"; }
}
public string Name
{
get { return "M3U Tuner"; }
}
private readonly IConfigurationManager _config;
public M3UTunerHost(IConfigurationManager config)
{
_config = config;
}
public Task<IEnumerable<ChannelInfo>> GetChannels(TunerHostInfo info, CancellationToken cancellationToken)
{
int position = 0;
string line;
// Read the file and display it line by line.
var file = new StreamReader(info.Url);
var channels = new List<M3UChannel>();
while ((line = file.ReadLine()) != null)
{
line = line.Trim();
if (!String.IsNullOrWhiteSpace(line))
{
if (position == 0 && !line.StartsWith("#EXTM3U"))
{
throw new ApplicationException("wrong file");
}
if (position % 2 == 0)
{
if (position != 0)
{
channels.Last().Path = line;
}
else
{
line = line.Replace("#EXTM3U", "");
line = line.Trim();
var vars = line.Split(' ').ToList();
foreach (var variable in vars)
{
var list = variable.Replace('"', ' ').Split('=');
switch (list[0])
{
case ("id"):
//_id = list[1];
break;
}
}
}
}
else
{
if (!line.StartsWith("#EXTINF:")) { throw new ApplicationException("Bad file"); }
line = line.Replace("#EXTINF:", "");
var nameStart = line.LastIndexOf(',');
line = line.Substring(0, nameStart);
var vars = line.Split(' ').ToList();
vars.RemoveAt(0);
channels.Add(new M3UChannel());
foreach (var variable in vars)
{
var list = variable.Replace('"', ' ').Split('=');
switch (list[0])
{
case "tvg-id":
channels.Last().Id = list[1];
channels.Last().Number = list[1];
break;
case "tvg-name":
channels.Last().Name = list[1];
break;
}
}
}
position++;
}
}
file.Close();
return Task.FromResult((IEnumerable<ChannelInfo>)channels);
}
public Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
{
var list = new List<LiveTvTunerInfo>();
list.Add(new LiveTvTunerInfo()
{
Name = Name,
SourceType = Type,
Status = LiveTvTunerStatus.Available,
Id = info.Url.GetMD5().ToString("N"),
Url = info.Url
});
return Task.FromResult(list);
}
private LiveTvOptions GetConfiguration()
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
public List<TunerHostInfo> GetTunerHosts()
{
return GetConfiguration().TunerHosts.Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)).ToList();
}
public async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
{
var channels = await GetChannels(info, cancellationToken).ConfigureAwait(false);
var m3uchannels = channels.Cast<M3UChannel>();
var channel = m3uchannels.FirstOrDefault(c => c.Id == channelId);
if (channel != null)
{
var path = channel.Path;
MediaProtocol protocol = MediaProtocol.File;
if (path.StartsWith("http"))
{
protocol = MediaProtocol.Http;
}
else if (path.StartsWith("rtmp"))
{
protocol = MediaProtocol.Rtmp;
}
else if (path.StartsWith("rtsp"))
{
protocol = MediaProtocol.Rtsp;
}
return new MediaSourceInfo
{
Path = channel.Path,
Protocol = protocol,
MediaStreams = new List<MediaStream>
{
new MediaStream
{
Type = MediaStreamType.Video,
// Set the index to -1 because we don't know the exact index of the video stream within the container
Index = -1,
IsInterlaced = true
},
new MediaStream
{
Type = MediaStreamType.Audio,
// Set the index to -1 because we don't know the exact index of the audio stream within the container
Index = -1
}
}
};
}
throw new ApplicationException("Host doesnt provide this channel");
}
class M3UChannel : ChannelInfo
{
public string Path { get; set; }
public M3UChannel()
{
}
}
}
}

View File

@ -216,10 +216,17 @@
<Compile Include="Library\Validators\StudiosValidator.cs" /> <Compile Include="Library\Validators\StudiosValidator.cs" />
<Compile Include="Library\Validators\YearsPostScanTask.cs" /> <Compile Include="Library\Validators\YearsPostScanTask.cs" />
<Compile Include="LiveTv\ChannelImageProvider.cs" /> <Compile Include="LiveTv\ChannelImageProvider.cs" />
<Compile Include="LiveTv\EmbyTV\EmbyTV.cs" />
<Compile Include="LiveTv\EmbyTV\ItemDataProvider.cs" />
<Compile Include="LiveTv\EmbyTV\RecordingHelper.cs" />
<Compile Include="LiveTv\EmbyTV\SeriesTimerManager.cs" />
<Compile Include="LiveTv\EmbyTV\TimerManager.cs" />
<Compile Include="LiveTv\LiveTvConfigurationFactory.cs" /> <Compile Include="LiveTv\LiveTvConfigurationFactory.cs" />
<Compile Include="LiveTv\LiveTvDtoService.cs" /> <Compile Include="LiveTv\LiveTvDtoService.cs" />
<Compile Include="LiveTv\LiveTvManager.cs" /> <Compile Include="LiveTv\LiveTvManager.cs" />
<Compile Include="LiveTv\LiveTvMediaSourceProvider.cs" /> <Compile Include="LiveTv\LiveTvMediaSourceProvider.cs" />
<Compile Include="LiveTv\TunerHosts\HdHomerun.cs" />
<Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" />
<Compile Include="LiveTv\ProgramImageProvider.cs" /> <Compile Include="LiveTv\ProgramImageProvider.cs" />
<Compile Include="LiveTv\RecordingImageProvider.cs" /> <Compile Include="LiveTv\RecordingImageProvider.cs" />
<Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" /> <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" />