aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations/LiveTv/EmbyTV
diff options
context:
space:
mode:
authorLuke <luke.pulverenti@gmail.com>2015-09-02 11:50:00 -0400
committerLuke <luke.pulverenti@gmail.com>2015-09-02 11:50:00 -0400
commitf868dd81e856488280978006cbb67afc2677049d (patch)
tree616ba8ae846efe9ec889abeb12f6b2702c6b8592 /MediaBrowser.Server.Implementations/LiveTv/EmbyTV
parentaf89446c20fb302087b82c18c28da92076dbc5ac (diff)
parente6d5901408ba7d8e344a27ea1f3b0046c40e56c1 (diff)
Merge pull request #1164 from MediaBrowser/dev
3.0.5724.1
Diffstat (limited to 'MediaBrowser.Server.Implementations/LiveTv/EmbyTV')
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs802
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs16
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs118
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs78
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs25
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs138
6 files changed, 1177 insertions, 0 deletions
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
new file mode 100644
index 0000000000..4e0d6e8d49
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -0,0 +1,802 @@
+using MediaBrowser.Common;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Security;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.FileOrganization;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.FileOrganization;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Server.Implementations.FileOrganization;
+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, IHasRegistrationInfo, IDisposable
+ {
+ private readonly IApplicationHost _appHpst;
+ private readonly ILogger _logger;
+ private readonly IHttpClient _httpClient;
+ private readonly IServerConfigurationManager _config;
+ private readonly IJsonSerializer _jsonSerializer;
+
+ private readonly ItemDataProvider<RecordingInfo> _recordingProvider;
+ private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
+ private readonly TimerManager _timerProvider;
+
+ private readonly LiveTvManager _liveTvManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ISecurityManager _security;
+
+ private readonly ILibraryMonitor _libraryMonitor;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileOrganizationService _organizationService;
+
+ public static EmbyTV Current;
+
+ public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ISecurityManager security, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService)
+ {
+ Current = this;
+
+ _appHpst = appHost;
+ _logger = logger;
+ _httpClient = httpClient;
+ _config = config;
+ _fileSystem = fileSystem;
+ _security = security;
+ _libraryManager = libraryManager;
+ _libraryMonitor = libraryMonitor;
+ _providerManager = providerManager;
+ _organizationService = organizationService;
+ _liveTvManager = (LiveTvManager)liveTvManager;
+ _jsonSerializer = jsonSerializer;
+
+ _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 void Start()
+ {
+ _timerProvider.RestartTimers();
+ }
+
+ 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 hostInstance in _liveTvManager.TunerHosts)
+ {
+ try
+ {
+ var tuners = await hostInstance.GetTunerInfos(cancellationToken).ConfigureAwait(false);
+
+ list.AddRange(tuners);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting tuners", ex);
+ }
+ }
+
+ status.Tuners = list;
+ status.Status = LiveTvServiceStatus.Ok;
+ status.Version = _appHpst.ApplicationVersion.ToString();
+ status.IsVisible = false;
+ return status;
+ }
+
+ private List<ChannelInfo> _channelCache = null;
+ private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
+ {
+ if (enableCache && _channelCache != null)
+ {
+
+ return _channelCache.ToList();
+ }
+
+ var list = new List<ChannelInfo>();
+
+ foreach (var hostInstance in _liveTvManager.TunerHosts)
+ {
+ try
+ {
+ var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false);
+
+ list.AddRange(channels);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error getting channels", ex);
+ }
+ }
+
+ if (list.Count > 0)
+ {
+ foreach (var provider in GetListingProviders())
+ {
+ try
+ {
+ await provider.Item1.AddMetadata(provider.Item2, list, cancellationToken).ConfigureAwait(false);
+ }
+ catch (NotSupportedException)
+ {
+
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error adding metadata", ex);
+ }
+ }
+ }
+ _channelCache = list;
+ return list;
+ }
+
+ public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken)
+ {
+ return GetChannelsAsync(false, cancellationToken);
+ }
+
+ public Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken)
+ {
+ var timers = _timerProvider.GetAll().Where(i => string.Equals(i.SeriesTimerId, timerId, StringComparison.OrdinalIgnoreCase));
+ foreach (var timer in timers)
+ {
+ CancelTimerInternal(timer.Id);
+ }
+
+ var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
+ if (remove != null)
+ {
+ _seriesTimerProvider.Delete(remove);
+ }
+ return Task.FromResult(true);
+ }
+
+ private void CancelTimerInternal(string timerId)
+ {
+ var remove = _timerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
+ 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 async Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
+ {
+ var remove = _recordingProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, recordingId, StringComparison.OrdinalIgnoreCase));
+ if (remove != null)
+ {
+ if (!string.IsNullOrWhiteSpace(remove.TimerId))
+ {
+ var enableDelay = _activeRecordings.ContainsKey(remove.TimerId);
+
+ CancelTimerInternal(remove.TimerId);
+
+ if (enableDelay)
+ {
+ // A hack yes, but need to make sure the file is closed before attempting to delete it
+ await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ try
+ {
+ File.Delete(remove.Path);
+ }
+ catch (DirectoryNotFoundException)
+ {
+
+ }
+ catch (FileNotFoundException)
+ {
+
+ }
+ _recordingProvider.Delete(remove);
+ }
+ }
+
+ public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
+ {
+ info.Id = Guid.NewGuid().ToString("N");
+ _timerProvider.Add(info);
+ return Task.FromResult(0);
+ }
+
+ public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+ {
+ info.Id = Guid.NewGuid().ToString("N");
+
+ List<ProgramInfo> epgData;
+ if (info.RecordAnyChannel)
+ {
+ var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
+ var channelIds = channels.Select(i => i.Id).ToList();
+ epgData = GetEpgDataForChannels(channelIds);
+ }
+ else
+ {
+ epgData = GetEpgDataForChannel(info.ChannelId);
+ }
+
+ // populate info.seriesID
+ var program = epgData.FirstOrDefault(i => string.Equals(i.Id, info.ProgramId, StringComparison.OrdinalIgnoreCase));
+
+ if (program != null)
+ {
+ info.SeriesId = program.SeriesId;
+ }
+ else
+ {
+ throw new InvalidOperationException("SeriesId for program not found");
+ }
+
+ _seriesTimerProvider.Add(info);
+ await UpdateTimersForSeriesTimer(epgData, info).ConfigureAwait(false);
+ }
+
+ public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+ {
+ _seriesTimerProvider.Update(info);
+ List<ProgramInfo> epgData;
+ if (info.RecordAnyChannel)
+ {
+ var channels = await GetChannelsAsync(true, CancellationToken.None).ConfigureAwait(false);
+ var channelIds = channels.Select(i => i.Id).ToList();
+ epgData = GetEpgDataForChannels(channelIds);
+ }
+ else
+ {
+ epgData = GetEpgDataForChannel(info.ChannelId);
+ }
+
+ await UpdateTimersForSeriesTimer(epgData, info).ConfigureAwait(false);
+ }
+
+ 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 config = GetConfiguration();
+
+ var defaults = new SeriesTimerInfo()
+ {
+ PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0),
+ PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
+ RecordAnyChannel = false,
+ RecordAnyTime = false,
+ RecordNewOnly = false
+ };
+
+ if (program != null)
+ {
+ defaults.SeriesId = program.SeriesId;
+ defaults.ProgramId = program.Id;
+ }
+
+ return Task.FromResult(defaults);
+ }
+
+ public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll());
+ }
+
+ public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
+ {
+ var channels = await GetChannelsAsync(true, cancellationToken).ConfigureAwait(false);
+ var channel = channels.First(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
+
+ foreach (var provider in GetListingProviders())
+ {
+ var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, startDateUtc, endDateUtc, cancellationToken)
+ .ConfigureAwait(false);
+ var list = programs.ToList();
+
+ // Replace the value that came from the provider with a normalized value
+ foreach (var program in list)
+ {
+ program.ChannelId = channelId;
+ }
+
+ if (list.Count > 0)
+ {
+ SaveEpgDataForChannel(channelId, list);
+
+ return list;
+ }
+ }
+
+ return new List<ProgramInfo>();
+ }
+
+ private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
+ {
+ return GetConfiguration().ListingProviders
+ .Select(i =>
+ {
+ var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
+
+ return provider == null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
+ })
+ .Where(i => i != null)
+ .ToList();
+ }
+
+ public Task<MediaSourceInfo> GetRecordingStream(string recordingId, string streamId, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
+ {
+ _logger.Info("Streaming Channel " + channelId);
+
+ foreach (var hostInstance in _liveTvManager.TunerHosts)
+ {
+ MediaSourceInfo mediaSourceInfo = null;
+ try
+ {
+ mediaSourceInfo = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ _logger.ErrorException("Error getting channel stream", e);
+ }
+
+ if (mediaSourceInfo != null)
+ {
+ mediaSourceInfo.Id = Guid.NewGuid().ToString("N");
+ return mediaSourceInfo;
+ }
+ }
+
+ throw new ApplicationException("Tuner not found.");
+ }
+
+ public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
+ {
+ foreach (var hostInstance in _liveTvManager.TunerHosts)
+ {
+ try
+ {
+ var sources = await hostInstance.GetChannelStreamMediaSources(channelId, cancellationToken).ConfigureAwait(false);
+
+ if (sources.Count > 0)
+ {
+ return sources;
+ }
+ }
+ catch (NotImplementedException)
+ {
+
+ }
+ }
+
+ 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)
+ {
+ var timer = e.Argument;
+
+ _logger.Info("Recording timer fired.");
+
+ try
+ {
+ var cancellationTokenSource = new CancellationTokenSource();
+
+ if (_activeRecordings.TryAdd(timer.Id, cancellationTokenSource))
+ {
+ await RecordStream(timer, cancellationTokenSource.Token).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error recording stream", ex);
+
+ if (DateTime.UtcNow < timer.EndDate)
+ {
+ const int retryIntervalSeconds = 60;
+ _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds);
+
+ _timerProvider.StartTimer(timer, TimeSpan.FromSeconds(retryIntervalSeconds));
+ }
+ }
+ }
+
+ private async Task RecordStream(TimerInfo timer, CancellationToken cancellationToken)
+ {
+ if (timer == null)
+ {
+ throw new ArgumentNullException("timer");
+ }
+
+ var mediaStreamInfo = await GetChannelStream(timer.ChannelId, null, CancellationToken.None);
+ var duration = (timer.EndDate - DateTime.UtcNow).Add(TimeSpan.FromSeconds(timer.PostPaddingSeconds));
+
+ HttpRequestOptions httpRequestOptions = new HttpRequestOptions()
+ {
+ Url = mediaStreamInfo.Path
+ };
+
+ var info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
+ var recordPath = RecordingPath;
+
+ if (info.IsMovie)
+ {
+ recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name));
+ }
+ else if (info.IsSeries)
+ {
+ recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name));
+ }
+ else if (info.IsKids)
+ {
+ recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name));
+ }
+ else if (info.IsSports)
+ {
+ recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name));
+ }
+ else
+ {
+ recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name));
+ }
+
+ var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)) + ".ts";
+
+ recordPath = Path.Combine(recordPath, recordingFileName);
+ Directory.CreateDirectory(Path.GetDirectoryName(recordPath));
+
+ var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.ProgramId, info.Id, StringComparison.OrdinalIgnoreCase));
+
+ if (recording == null)
+ {
+ recording = new RecordingInfo
+ {
+ ChannelId = info.ChannelId,
+ Id = Guid.NewGuid().ToString("N"),
+ StartDate = info.StartDate,
+ EndDate = info.EndDate,
+ Genres = info.Genres,
+ 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,
+ ImagePath = info.ImagePath,
+ ImageUrl = info.ImageUrl,
+ OriginalAirDate = info.OriginalAirDate,
+ Status = RecordingStatus.Scheduled,
+ Overview = info.Overview,
+ SeriesTimerId = timer.SeriesTimerId,
+ TimerId = timer.Id,
+ ShowId = info.ShowId
+ };
+ _recordingProvider.Add(recording);
+ }
+
+ recording.Path = recordPath;
+ recording.Status = RecordingStatus.InProgress;
+ recording.DateLastUpdated = DateTime.UtcNow;
+ _recordingProvider.Update(recording);
+
+ _logger.Info("Beginning recording.");
+
+ try
+ {
+ httpRequestOptions.BufferContent = false;
+ var durationToken = new CancellationTokenSource(duration);
+ var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ httpRequestOptions.CancellationToken = linkedToken;
+ _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, StreamDefaults.DefaultCopyToBufferSize, linkedToken);
+ }
+ }
+
+ recording.Status = RecordingStatus.Completed;
+ _logger.Info("Recording completed");
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.Info("Recording stopped");
+ recording.Status = RecordingStatus.Completed;
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error recording", ex);
+ recording.Status = RecordingStatus.Error;
+ }
+
+ recording.DateLastUpdated = DateTime.UtcNow;
+ _recordingProvider.Update(recording);
+ _timerProvider.Delete(timer);
+ _logger.Info("Recording was a success");
+
+ if (recording.Status == RecordingStatus.Completed)
+ {
+ OnSuccessfulRecording(recording);
+ }
+ }
+
+ private async void OnSuccessfulRecording(RecordingInfo recording)
+ {
+ if (GetConfiguration().EnableAutoOrganize)
+ {
+ if (recording.IsSeries)
+ {
+ try
+ {
+ var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
+
+ var result = await organize.OrganizeEpisodeFile(recording.Path, CancellationToken.None).ConfigureAwait(false);
+
+ if (result.Status == FileSortingStatus.Success)
+ {
+ _recordingProvider.Delete(recording);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error processing new recording", ex);
+ }
+ }
+ }
+ }
+
+ private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
+ {
+ var epgData = GetEpgDataForChannel(channelId);
+ return epgData.FirstOrDefault(p => string.Equals(p.Id, programId, StringComparison.OrdinalIgnoreCase));
+ }
+
+ 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 async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer)
+ {
+ var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
+
+ if (registration.IsValid)
+ {
+ var newTimers = GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll()).ToList();
+
+ foreach (var timer in newTimers)
+ {
+ _timerProvider.AddOrUpdate(timer);
+ }
+ }
+ }
+
+ private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms, IReadOnlyList<RecordingInfo> currentRecordings)
+ {
+ // Exclude programs that have already ended
+ allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow);
+
+ allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
+
+ var recordingShowIds = currentRecordings.Select(i => i.ProgramId).Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+
+ allPrograms = allPrograms.Where(i => !recordingShowIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase));
+
+ return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
+ }
+
+ private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
+ {
+ if (!seriesTimer.RecordAnyTime)
+ {
+ allPrograms = allPrograms.Where(epg => (seriesTimer.StartDate.TimeOfDay == epg.StartDate.TimeOfDay));
+ }
+
+ if (seriesTimer.RecordNewOnly)
+ {
+ allPrograms = allPrograms.Where(epg => !epg.IsRepeat);
+ }
+
+ if (!seriesTimer.RecordAnyChannel)
+ {
+ allPrograms = allPrograms.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase));
+ }
+
+ allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek));
+
+ if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
+ {
+ _logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series");
+ return new List<ProgramInfo>();
+ }
+
+ return allPrograms.Where(i => string.Equals(i.SeriesId, seriesTimer.SeriesId, StringComparison.OrdinalIgnoreCase));
+ }
+
+ 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> GetEpgDataForChannels(List<string> channelIds)
+ {
+ return channelIds.SelectMany(GetEpgDataForChannel).ToList();
+ }
+
+ public void Dispose()
+ {
+ foreach (var pair in _activeRecordings.ToList())
+ {
+ pair.Value.Cancel();
+ }
+ }
+
+ public Task<MBRegistrationRecord> GetRegistrationInfo(string feature)
+ {
+ if (string.Equals(feature, "seriesrecordings", StringComparison.OrdinalIgnoreCase))
+ {
+ return _security.GetRegistrationStatus("embytvseriesrecordings");
+ }
+
+ return Task.FromResult(new MBRegistrationRecord
+ {
+ IsValid = true,
+ IsRegistered = true
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
new file mode 100644
index 0000000000..713cb9cd30
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
@@ -0,0 +1,16 @@
+using MediaBrowser.Controller.Plugins;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+ public class EntryPoint : IServerEntryPoint
+ {
+ public void Run()
+ {
+ EmbyTV.Current.Start();
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
new file mode 100644
index 0000000000..75dec5f970
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -0,0 +1,118 @@
+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)
+ {
+ var file = _dataPath + ".json";
+ Directory.CreateDirectory(Path.GetDirectoryName(file));
+
+ lock (_fileDataLock)
+ {
+ _jsonSerializer.SerializeToFile(newList, file);
+ _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);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
new file mode 100644
index 0000000000..5b83d63b17
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
@@ -0,0 +1,78 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.LiveTv;
+using System;
+using System.Globalization;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+ internal class RecordingHelper
+ {
+ public static DateTime GetStartTime(TimerInfo timer)
+ {
+ 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;
+ }
+
+ var name = info.Name;
+
+ if (info.IsSeries)
+ {
+ var addHyphen = true;
+
+ if (info.SeasonNumber.HasValue && info.EpisodeNumber.HasValue)
+ {
+ name += string.Format(" S{0}E{1}", info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture), info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
+ addHyphen = false;
+ }
+ else if (info.OriginalAirDate.HasValue)
+ {
+ name += " " + info.OriginalAirDate.Value.ToString("yyyy-MM-dd");
+ }
+
+ if (addHyphen)
+ {
+ name += " -";
+ }
+
+ if (!string.IsNullOrWhiteSpace(info.EpisodeTitle))
+ {
+ name += " " + info.EpisodeTitle;
+ }
+ }
+
+ else if (info.ProductionYear != null)
+ {
+ name += " (" + info.ProductionYear + ")";
+ }
+
+ return name;
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
new file mode 100644
index 0000000000..eab278eb4d
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
@@ -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);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
new file mode 100644
index 0000000000..3ae38f382e
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -0,0 +1,138 @@
+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();
+
+ foreach (var item in GetAll().ToList())
+ {
+ AddTimer(item);
+ }
+ }
+
+ public void StopTimers()
+ {
+ foreach (var pair in _timers.ToList())
+ {
+ pair.Value.Dispose();
+ }
+
+ _timers.Clear();
+ }
+
+ public override void Delete(TimerInfo item)
+ {
+ base.Delete(item);
+ StopTimer(item);
+ }
+
+ 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 startDate = RecordingHelper.GetStartTime(item);
+ var now = DateTime.UtcNow;
+
+ if (startDate < now)
+ {
+ EventHelper.FireEventIfNotNull(TimerFired, this, new GenericEventArgs<TimerInfo> { Argument = item }, Logger);
+ return;
+ }
+
+ var timerLength = startDate - now;
+ StartTimer(item, timerLength);
+ }
+
+ public void StartTimer(TimerInfo item, TimeSpan length)
+ {
+ StopTimer(item);
+
+ var timer = new Timer(TimerCallback, item.Id, length, TimeSpan.Zero);
+
+ if (!_timers.TryAdd(item.Id, timer))
+ {
+ timer.Dispose();
+ }
+ }
+
+ private void StopTimer(TimerInfo item)
+ {
+ Timer timer;
+ if (_timers.TryRemove(item.Id, out 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);
+ }
+ }
+ }
+}