diff options
| author | Luke <luke.pulverenti@gmail.com> | 2015-09-02 11:50:00 -0400 |
|---|---|---|
| committer | Luke <luke.pulverenti@gmail.com> | 2015-09-02 11:50:00 -0400 |
| commit | f868dd81e856488280978006cbb67afc2677049d (patch) | |
| tree | 616ba8ae846efe9ec889abeb12f6b2702c6b8592 /MediaBrowser.Server.Implementations/LiveTv/EmbyTV | |
| parent | af89446c20fb302087b82c18c28da92076dbc5ac (diff) | |
| parent | e6d5901408ba7d8e344a27ea1f3b0046c40e56c1 (diff) | |
Merge pull request #1164 from MediaBrowser/dev
3.0.5724.1
Diffstat (limited to 'MediaBrowser.Server.Implementations/LiveTv/EmbyTV')
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); + } + } + } +} |
