using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using Emby.Server.Implementations.LiveTv.Listings;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
using MediaBrowser.Common.Security;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv
{
///
/// Class LiveTvManager
///
public class LiveTvManager : ILiveTvManager, IDisposable
{
private readonly IServerConfigurationManager _config;
private readonly ILogger _logger;
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager;
private readonly ISecurityManager _security;
private readonly Func _channelManager;
private readonly IDtoService _dtoService;
private readonly ILocalizationManager _localization;
private readonly LiveTvDtoService _tvDtoService;
private ILiveTvService[] _services = new ILiveTvService[] { };
private ITunerHost[] _tunerHosts = Array.Empty();
private IListingsProvider[] _listingProviders = Array.Empty();
private readonly IFileSystem _fileSystem;
public event EventHandler> SeriesTimerCancelled;
public event EventHandler> TimerCancelled;
public event EventHandler> TimerCreated;
public event EventHandler> SeriesTimerCreated;
public string GetEmbyTvActiveRecordingPath(string id)
{
return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
}
private IServerApplicationHost _appHost;
private IHttpClient _httpClient;
public LiveTvManager(IServerApplicationHost appHost, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, IJsonSerializer jsonSerializer, IProviderManager providerManager, IFileSystem fileSystem, ISecurityManager security, Func channelManager)
{
_appHost = appHost;
_config = config;
_logger = logger;
_itemRepo = itemRepo;
_userManager = userManager;
_libraryManager = libraryManager;
_taskManager = taskManager;
_localization = localization;
_jsonSerializer = jsonSerializer;
_providerManager = providerManager;
_fileSystem = fileSystem;
_security = security;
_dtoService = dtoService;
_userDataManager = userDataManager;
_channelManager = channelManager;
_httpClient = httpClient;
_tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, logger, appHost, _libraryManager);
}
///
/// Gets the services.
///
/// The services.
public IReadOnlyList Services => _services;
private LiveTvOptions GetConfiguration()
{
return _config.GetConfiguration("livetv");
}
///
/// Adds the parts.
///
/// The services.
/// The tuner hosts.
/// The listing providers.
public void AddParts(IEnumerable services, IEnumerable tunerHosts, IEnumerable listingProviders)
{
_services = services.ToArray();
_tunerHosts = tunerHosts.Where(i => i.IsSupported).ToArray();
_listingProviders = listingProviders.ToArray();
foreach (var service in _services)
{
service.DataSourceChanged += service_DataSourceChanged;
if (service is EmbyTV.EmbyTV embyTv)
{
embyTv.TimerCreated += EmbyTv_TimerCreated;
embyTv.TimerCancelled += EmbyTv_TimerCancelled;
}
}
}
private void EmbyTv_TimerCancelled(object sender, GenericEventArgs e)
{
var timerId = e.Argument;
TimerCancelled?.Invoke(this, new GenericEventArgs
{
Argument = new TimerEventInfo
{
Id = timerId
}
});
}
private void EmbyTv_TimerCreated(object sender, GenericEventArgs e)
{
var timer = e.Argument;
var service = sender as ILiveTvService;
TimerCreated?.Invoke(this, new GenericEventArgs
{
Argument = new TimerEventInfo
{
ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId),
Id = timer.Id
}
});
}
public ITunerHost[] TunerHosts => _tunerHosts;
public IListingsProvider[] ListingProviders => _listingProviders;
public List GetTunerHostTypes()
{
return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair
{
Name = i.Name,
Id = i.Type
}).ToList();
}
public Task> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
{
return EmbyTV.EmbyTV.Current.DiscoverTuners(newDevicesOnly, cancellationToken);
}
void service_DataSourceChanged(object sender, EventArgs e)
{
if (!_isDisposed)
{
_taskManager.CancelIfRunningAndQueue();
}
}
public QueryResult GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken)
{
var user = query.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(query.UserId);
var topFolder = GetInternalLiveTvFolder(cancellationToken);
var internalQuery = new InternalItemsQuery(user)
{
IsMovie = query.IsMovie,
IsNews = query.IsNews,
IsKids = query.IsKids,
IsSports = query.IsSports,
IsSeries = query.IsSeries,
IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
TopParentIds = new[] { topFolder.Id },
IsFavorite = query.IsFavorite,
IsLiked = query.IsLiked,
StartIndex = query.StartIndex,
Limit = query.Limit,
DtoOptions = dtoOptions
};
var orderBy = internalQuery.OrderBy.ToList();
orderBy.AddRange(query.SortBy.Select(i => new ValueTuple(i, query.SortOrder ?? SortOrder.Ascending)));
if (query.EnableFavoriteSorting)
{
orderBy.Insert(0, new ValueTuple(ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
}
if (!internalQuery.OrderBy.Any(i => string.Equals(i.Item1, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)))
{
orderBy.Add(new ValueTuple(ItemSortBy.SortName, SortOrder.Ascending));
}
internalQuery.OrderBy = orderBy.ToArray();
return _libraryManager.GetItemsResult(internalQuery);
}
public async Task> GetChannelStream(string id, string mediaSourceId, List currentLiveStreams, CancellationToken cancellationToken)
{
if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
{
mediaSourceId = null;
}
var channel = (LiveTvChannel)_libraryManager.GetItemById(id);
bool isVideo = channel.ChannelType == ChannelType.TV;
ILiveTvService service = GetService(channel);
_logger.LogInformation("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
MediaSourceInfo info;
ILiveStream liveStream;
if (service is ISupportsDirectStreamProvider supportsManagedStream)
{
liveStream = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
info = liveStream.MediaSource;
}
else
{
info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
var openedId = info.Id;
Func closeFn = () => service.CloseLiveStream(openedId, CancellationToken.None);
liveStream = new ExclusiveLiveStream(info, closeFn);
var startTime = DateTime.UtcNow;
await liveStream.Open(cancellationToken).ConfigureAwait(false);
var endTime = DateTime.UtcNow;
_logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds);
}
info.RequiresClosing = true;
var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
info.LiveStreamId = idPrefix + info.Id;
Normalize(info, service, isVideo);
return new Tuple(info, liveStream);
}
public async Task> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken)
{
var baseItem = (LiveTvChannel)item;
var service = GetService(baseItem);
var sources = await service.GetChannelStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false);
if (sources.Count == 0)
{
throw new NotImplementedException();
}
foreach (var source in sources)
{
Normalize(source, service, baseItem.ChannelType == ChannelType.TV);
}
return sources;
}
private ILiveTvService GetService(LiveTvChannel item)
{
var name = item.ServiceName;
return GetService(name);
}
private ILiveTvService GetService(LiveTvProgram item)
{
var channel = _libraryManager.GetItemById(item.ChannelId) as LiveTvChannel;
return GetService(channel);
}
private ILiveTvService GetService(string name)
{
return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
}
private static void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo)
{
// Not all of the plugins are setting this
mediaSource.IsInfiniteStream = true;
if (mediaSource.MediaStreams.Count == 0)
{
if (isVideo)
{
mediaSource.MediaStreams.AddRange(new List
{
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,
// Set to true if unknown to enable deinterlacing
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
}
});
}
else
{
mediaSource.MediaStreams.AddRange(new List
{
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
}
});
}
}
// Clean some bad data coming from providers
foreach (var stream in mediaSource.MediaStreams)
{
if (stream.BitRate.HasValue && stream.BitRate <= 0)
{
stream.BitRate = null;
}
if (stream.Channels.HasValue && stream.Channels <= 0)
{
stream.Channels = null;
}
if (stream.AverageFrameRate.HasValue && stream.AverageFrameRate <= 0)
{
stream.AverageFrameRate = null;
}
if (stream.RealFrameRate.HasValue && stream.RealFrameRate <= 0)
{
stream.RealFrameRate = null;
}
if (stream.Width.HasValue && stream.Width <= 0)
{
stream.Width = null;
}
if (stream.Height.HasValue && stream.Height <= 0)
{
stream.Height = null;
}
if (stream.SampleRate.HasValue && stream.SampleRate <= 0)
{
stream.SampleRate = null;
}
if (stream.Level.HasValue && stream.Level <= 0)
{
stream.Level = null;
}
}
var indexes = mediaSource.MediaStreams.Select(i => i.Index).Distinct().ToList();
// If there are duplicate stream indexes, set them all to unknown
if (indexes.Count != mediaSource.MediaStreams.Count)
{
foreach (var stream in mediaSource.MediaStreams)
{
stream.Index = -1;
}
}
// Set the total bitrate if not already supplied
mediaSource.InferTotalBitrate();
if (!(service is EmbyTV.EmbyTV))
{
// We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says
//mediaSource.SupportsDirectPlay = false;
//mediaSource.SupportsDirectStream = false;
mediaSource.SupportsTranscoding = true;
foreach (var stream in mediaSource.MediaStreams)
{
if (stream.Type == MediaStreamType.Video && string.IsNullOrWhiteSpace(stream.NalLengthSize))
{
stream.NalLengthSize = "0";
}
if (stream.Type == MediaStreamType.Video)
{
stream.IsInterlaced = true;
}
}
}
}
private const string ExternalServiceTag = "ExternalServiceId";
private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken)
{
var parentFolderId = parentFolder.Id;
var isNew = false;
var forceUpdate = false;
var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id);
var item = _libraryManager.GetItemById(id) as LiveTvChannel;
if (item == null)
{
item = new LiveTvChannel
{
Name = channelInfo.Name,
Id = id,
DateCreated = DateTime.UtcNow
};
isNew = true;
}
if (channelInfo.Tags != null)
{
if (!channelInfo.Tags.SequenceEqual(item.Tags, StringComparer.OrdinalIgnoreCase))
{
isNew = true;
}
item.Tags = channelInfo.Tags;
}
if (!item.ParentId.Equals(parentFolderId))
{
isNew = true;
}
item.ParentId = parentFolderId;
item.ChannelType = channelInfo.ChannelType;
item.ServiceName = serviceName;
if (!string.Equals(item.GetProviderId(ExternalServiceTag), serviceName, StringComparison.OrdinalIgnoreCase))
{
forceUpdate = true;
}
item.SetProviderId(ExternalServiceTag, serviceName);
if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal))
{
forceUpdate = true;
}
item.ExternalId = channelInfo.Id;
if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal))
{
forceUpdate = true;
}
item.Number = channelInfo.Number;
if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal))
{
forceUpdate = true;
}
item.Name = channelInfo.Name;
if (!item.HasImage(ImageType.Primary))
{
if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath))
{
item.SetImagePath(ImageType.Primary, channelInfo.ImagePath);
forceUpdate = true;
}
else if (!string.IsNullOrWhiteSpace(channelInfo.ImageUrl))
{
item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl);
forceUpdate = true;
}
}
if (isNew)
{
_libraryManager.CreateItem(item, parentFolder);
}
else if (forceUpdate)
{
_libraryManager.UpdateItem(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken);
}
return item;
}
private const string EtagKey = "ProgramEtag";
private Tuple GetProgram(ProgramInfo info, Dictionary allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
{
var id = _tvDtoService.GetInternalProgramId(info.Id);
var isNew = false;
var forceUpdate = false;
LiveTvProgram item;
if (!allExistingPrograms.TryGetValue(id, out item))
{
isNew = true;
item = new LiveTvProgram
{
Name = info.Name,
Id = id,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow
};
if (!string.IsNullOrEmpty(info.Etag))
{
item.SetProviderId(EtagKey, info.Etag);
}
}
if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase))
{
item.ShowId = info.ShowId;
forceUpdate = true;
}
var seriesId = info.SeriesId;
if (!item.ParentId.Equals(channel.Id))
{
forceUpdate = true;
}
item.ParentId = channel.Id;
//item.ChannelType = channelType;
item.Audio = info.Audio;
item.ChannelId = channel.Id;
item.CommunityRating = item.CommunityRating ?? info.CommunityRating;
if ((item.CommunityRating ?? 0).Equals(0))
{
item.CommunityRating = null;
}
item.EpisodeTitle = info.EpisodeTitle;
item.ExternalId = info.Id;
if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal))
{
forceUpdate = true;
}
item.ExternalSeriesId = seriesId;
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
{
item.SeriesName = info.Name;
}
var tags = new List();
if (info.IsLive)
{
tags.Add("Live");
}
if (info.IsPremiere)
{
tags.Add("Premiere");
}
if (info.IsNews)
{
tags.Add("News");
}
if (info.IsSports)
{
tags.Add("Sports");
}
if (info.IsKids)
{
tags.Add("Kids");
}
if (info.IsRepeat)
{
tags.Add("Repeat");
}
if (info.IsMovie)
{
tags.Add("Movie");
}
if (isSeries)
{
tags.Add("Series");
}
item.Tags = tags.ToArray();
item.Genres = info.Genres.ToArray();
if (info.IsHD ?? false)
{
item.Width = 1280;
item.Height = 720;
}
item.IsMovie = info.IsMovie;
item.IsRepeat = info.IsRepeat;
if (item.IsSeries != isSeries)
{
forceUpdate = true;
}
item.IsSeries = isSeries;
item.Name = info.Name;
item.OfficialRating = item.OfficialRating ?? info.OfficialRating;
item.Overview = item.Overview ?? info.Overview;
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
item.ProviderIds = info.ProviderIds;
foreach (var providerId in info.SeriesProviderIds)
{
info.ProviderIds["Series" + providerId.Key] = providerId.Value;
}
if (item.StartDate != info.StartDate)
{
forceUpdate = true;
}
item.StartDate = info.StartDate;
if (item.EndDate != info.EndDate)
{
forceUpdate = true;
}
item.EndDate = info.EndDate;
item.ProductionYear = info.ProductionYear;
if (!isSeries || info.IsRepeat)
{
item.PremiereDate = info.OriginalAirDate;
}
item.IndexNumber = info.EpisodeNumber;
item.ParentIndexNumber = info.SeasonNumber;
if (!item.HasImage(ImageType.Primary))
{
if (!string.IsNullOrWhiteSpace(info.ImagePath))
{
item.SetImage(new ItemImageInfo
{
Path = info.ImagePath,
Type = ImageType.Primary
}, 0);
}
else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
{
item.SetImage(new ItemImageInfo
{
Path = info.ImageUrl,
Type = ImageType.Primary
}, 0);
}
}
if (!item.HasImage(ImageType.Thumb))
{
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
{
item.SetImage(new ItemImageInfo
{
Path = info.ThumbImageUrl,
Type = ImageType.Thumb
}, 0);
}
}
if (!item.HasImage(ImageType.Logo))
{
if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
{
item.SetImage(new ItemImageInfo
{
Path = info.LogoImageUrl,
Type = ImageType.Logo
}, 0);
}
}
if (!item.HasImage(ImageType.Backdrop))
{
if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
{
item.SetImage(new ItemImageInfo
{
Path = info.BackdropImageUrl,
Type = ImageType.Backdrop
}, 0);
}
}
var isUpdated = false;
if (isNew)
{
}
else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
{
isUpdated = true;
}
else
{
var etag = info.Etag;
if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
{
item.SetProviderId(EtagKey, etag);
isUpdated = true;
}
}
if (isNew || isUpdated)
{
item.OnMetadataChanged();
}
return new Tuple(item, isNew, isUpdated);
}
public async Task GetProgram(string id, CancellationToken cancellationToken, User user = null)
{
var program = _libraryManager.GetItemById(id);
var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user);
var list = new List>() {
new Tuple(dto, program.ExternalId, program.ExternalSeriesId)
};
await AddRecordingInfo(list, cancellationToken).ConfigureAwait(false);
return dto;
}
public async Task> GetPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken)
{
var user = query.User;
var topFolder = GetInternalLiveTvFolder(cancellationToken);
if (query.OrderBy.Length == 0)
{
if (query.IsAiring ?? false)
{
// Unless something else was specified, order by start date to take advantage of a specialized index
query.OrderBy = new ValueTuple[]
{
new ValueTuple(ItemSortBy.StartDate, SortOrder.Ascending)
};
}
else
{
// Unless something else was specified, order by start date to take advantage of a specialized index
query.OrderBy = new ValueTuple[]
{
new ValueTuple(ItemSortBy.StartDate, SortOrder.Ascending)
};
}
}
RemoveFields(options);
var internalQuery = new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
MinEndDate = query.MinEndDate,
MinStartDate = query.MinStartDate,
MaxEndDate = query.MaxEndDate,
MaxStartDate = query.MaxStartDate,
ChannelIds = query.ChannelIds,
IsMovie = query.IsMovie,
IsSeries = query.IsSeries,
IsSports = query.IsSports,
IsKids = query.IsKids,
IsNews = query.IsNews,
Genres = query.Genres,
GenreIds = query.GenreIds,
StartIndex = query.StartIndex,
Limit = query.Limit,
OrderBy = query.OrderBy,
EnableTotalRecordCount = query.EnableTotalRecordCount,
TopParentIds = new[] { topFolder.Id },
Name = query.Name,
DtoOptions = options,
HasAired = query.HasAired,
IsAiring = query.IsAiring
};
if (!string.IsNullOrWhiteSpace(query.SeriesTimerId))
{
var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false);
var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N"), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase));
if (seriesTimer != null)
{
internalQuery.ExternalSeriesId = seriesTimer.SeriesId;
if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
{
// Better to return nothing than every program in the database
return new QueryResult();
}
}
else
{
// Better to return nothing than every program in the database
return new QueryResult();
}
}
var queryResult = _libraryManager.QueryItems(internalQuery);
var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user);
var result = new QueryResult
{
Items = returnArray,
TotalRecordCount = queryResult.TotalRecordCount
};
return result;
}
public QueryResult GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken)
{
var user = query.User;
var topFolder = GetInternalLiveTvFolder(cancellationToken);
var internalQuery = new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
IsAiring = query.IsAiring,
HasAired = query.HasAired,
IsNews = query.IsNews,
IsMovie = query.IsMovie,
IsSeries = query.IsSeries,
IsSports = query.IsSports,
IsKids = query.IsKids,
EnableTotalRecordCount = query.EnableTotalRecordCount,
OrderBy = new[] { new ValueTuple(ItemSortBy.StartDate, SortOrder.Ascending) },
TopParentIds = new[] { topFolder.Id },
DtoOptions = options,
GenreIds = query.GenreIds
};
if (query.Limit.HasValue)
{
internalQuery.Limit = Math.Max(query.Limit.Value * 4, 200);
}
var programList = _libraryManager.QueryItems(internalQuery).Items;
var totalCount = programList.Length;
IOrderedEnumerable orderedPrograms = programList.Cast().OrderBy(i => i.StartDate.Date);
if (query.IsAiring ?? false)
{
orderedPrograms = orderedPrograms
.ThenByDescending(i => GetRecommendationScore(i, user, true));
}
IEnumerable programs = orderedPrograms;
if (query.Limit.HasValue)
{
programs = programs.Take(query.Limit.Value);
}
return new QueryResult
{
Items = programs.ToArray(),
TotalRecordCount = totalCount
};
}
public QueryResult GetRecommendedPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken)
{
if (!(query.IsAiring ?? false))
{
return GetPrograms(query, options, cancellationToken).Result;
}
RemoveFields(options);
var internalResult = GetRecommendedProgramsInternal(query, options, cancellationToken);
return new QueryResult
{
Items = _dtoService.GetBaseItemDtos(internalResult.Items, options, query.User),
TotalRecordCount = internalResult.TotalRecordCount
};
}
private int GetRecommendationScore(LiveTvProgram program, User user, bool factorChannelWatchCount)
{
var score = 0;
if (program.IsLive)
{
score++;
}
if (program.IsSeries && !program.IsRepeat)
{
score++;
}
var channel = _libraryManager.GetItemById(program.ChannelId);
if (channel == null)
{
return score;
}
var channelUserdata = _userDataManager.GetUserData(user, channel);
if (channelUserdata.Likes.HasValue)
{
score += channelUserdata.Likes.Value ? 2 : -2;
}
if (channelUserdata.IsFavorite)
{
score += 3;
}
if (factorChannelWatchCount)
{
score += channelUserdata.PlayCount;
}
return score;
}
private async Task AddRecordingInfo(IEnumerable> programs, CancellationToken cancellationToken)
{
var timers = new Dictionary>();
var seriesTimers = new Dictionary>();
TimerInfo[] timerList = null;
SeriesTimerInfo[] seriesTimerList = null;
foreach (var programTuple in programs)
{
var program = programTuple.Item1;
var externalProgramId = programTuple.Item2;
string externalSeriesId = programTuple.Item3;
if (timerList == null)
{
timerList = (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
}
var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase));
var foundSeriesTimer = false;
if (timer != null)
{
if (timer.Status != RecordingStatus.Cancelled && timer.Status != RecordingStatus.Error)
{
program.TimerId = _tvDtoService.GetInternalTimerId(timer.Id);
program.Status = timer.Status.ToString();
}
if (!string.IsNullOrEmpty(timer.SeriesTimerId))
{
program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(timer.SeriesTimerId)
.ToString("N");
foundSeriesTimer = true;
}
}
if (foundSeriesTimer || string.IsNullOrWhiteSpace(externalSeriesId))
{
continue;
}
if (seriesTimerList == null)
{
seriesTimerList = (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
}
var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase));
if (seriesTimer != null)
{
program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(seriesTimer.Id)
.ToString("N");
}
}
}
internal Task RefreshChannels(IProgress progress, CancellationToken cancellationToken)
{
return RefreshChannelsInternal(progress, cancellationToken);
}
private async Task RefreshChannelsInternal(IProgress progress, CancellationToken cancellationToken)
{
await EmbyTV.EmbyTV.Current.CreateRecordingFolders().ConfigureAwait(false);
await EmbyTV.EmbyTV.Current.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
var numComplete = 0;
double progressPerService = _services.Length == 0
? 0
: 1.0 / _services.Length;
var newChannelIdList = new List();
var newProgramIdList = new List();
var cleanDatabase = true;
foreach (var service in _services)
{
cancellationToken.ThrowIfCancellationRequested();
_logger.LogDebug("Refreshing guide from {name}", service.Name);
try
{
var innerProgress = new ActionableProgress();
innerProgress.RegisterAction(p => progress.Report(p * progressPerService));
var idList = await RefreshChannelsInternal(service, innerProgress, cancellationToken).ConfigureAwait(false);
newChannelIdList.AddRange(idList.Item1);
newProgramIdList.AddRange(idList.Item2);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
cleanDatabase = false;
_logger.LogError(ex, "Error refreshing channels for service");
}
numComplete++;
double percent = numComplete;
percent /= _services.Length;
progress.Report(100 * percent);
}
if (cleanDatabase)
{
CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken);
CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken);
}
var coreService = _services.OfType().FirstOrDefault();
if (coreService != null)
{
await coreService.RefreshSeriesTimers(cancellationToken, new SimpleProgress()).ConfigureAwait(false);
await coreService.RefreshTimers(cancellationToken, new SimpleProgress()).ConfigureAwait(false);
}
// Load these now which will prefetch metadata
var dtoOptions = new DtoOptions();
var fields = dtoOptions.Fields.ToList();
fields.Remove(ItemFields.BasicSyncInfo);
dtoOptions.Fields = fields.ToArray();
progress.Report(100);
}
private async Task, List>> RefreshChannelsInternal(ILiveTvService service, IProgress progress, CancellationToken cancellationToken)
{
progress.Report(10);
var allChannelsList = (await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false))
.Select(i => new Tuple(service.Name, i))
.ToList();
var list = new List();
var numComplete = 0;
var parentFolder = GetInternalLiveTvFolder(cancellationToken);
foreach (var channelInfo in allChannelsList)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var item = GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken);
list.Add(item);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting channel information for {name}", channelInfo.Item2.Name);
}
numComplete++;
double percent = numComplete;
percent /= allChannelsList.Count;
progress.Report(5 * percent + 10);
}
progress.Report(15);
numComplete = 0;
var programs = new List();
var channels = new List();
var guideDays = GetGuideDays();
_logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
cancellationToken.ThrowIfCancellationRequested();
foreach (var currentChannel in list)
{
channels.Add(currentChannel.Id);
cancellationToken.ThrowIfCancellationRequested();
try
{
var start = DateTime.UtcNow.AddHours(-1);
var end = start.AddDays(guideDays);
var isMovie = false;
var isSports = false;
var isNews = false;
var isKids = false;
var iSSeries = false;
var channelPrograms = (await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false)).ToList();
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true)
}).Cast().ToDictionary(i => i.Id);
var newPrograms = new List();
var updatedPrograms = new List();
foreach (var program in channelPrograms)
{
var programTuple = GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken);
var programItem = programTuple.Item1;
if (programTuple.Item2)
{
newPrograms.Add(programItem);
}
else if (programTuple.Item3)
{
updatedPrograms.Add(programItem);
}
programs.Add(programItem.Id);
isMovie |= program.IsMovie;
iSSeries |= program.IsSeries;
isSports |= program.IsSports;
isNews |= program.IsNews;
isKids |= program.IsKids;
}
_logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
if (newPrograms.Count > 0)
{
_libraryManager.CreateItems(newPrograms, null, cancellationToken);
}
if (updatedPrograms.Count > 0)
{
_libraryManager.UpdateItems(updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken);
}
currentChannel.IsMovie = isMovie;
currentChannel.IsNews = isNews;
currentChannel.IsSports = isSports;
currentChannel.IsSeries = iSSeries;
if (isKids)
{
currentChannel.AddTag("Kids");
}
//currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken);
await currentChannel.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
{
ForceSave = true
}, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting programs for channel {name}", currentChannel.Name);
}
numComplete++;
double percent = numComplete / (double)allChannelsList.Count;
progress.Report(85 * percent + 15);
}
progress.Report(100);
return new Tuple, List>(channels, programs);
}
private void CleanDatabaseInternal(Guid[] currentIdList, string[] validTypes, IProgress progress, CancellationToken cancellationToken)
{
var list = _itemRepo.GetItemIdsList(new InternalItemsQuery
{
IncludeItemTypes = validTypes,
DtoOptions = new DtoOptions(false)
});
var numComplete = 0;
foreach (var itemId in list)
{
cancellationToken.ThrowIfCancellationRequested();
if (itemId.Equals(Guid.Empty))
{
// Somehow some invalid data got into the db. It probably predates the boundary checking
continue;
}
if (!currentIdList.Contains(itemId))
{
var item = _libraryManager.GetItemById(itemId);
if (item != null)
{
_libraryManager.DeleteItem(item, new DeleteOptions
{
DeleteFileLocation = false,
DeleteFromExternalProvider = false
}, false);
}
}
numComplete++;
double percent = numComplete / (double)list.Count;
progress.Report(100 * percent);
}
}
private const int MaxGuideDays = 14;
private double GetGuideDays()
{
var config = GetConfiguration();
if (config.GuideDays.HasValue)
{
return Math.Max(1, Math.Min(config.GuideDays.Value, MaxGuideDays));
}
return 7;
}
private QueryResult GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user)
{
if (user == null)
{
return new QueryResult();
}
var folderIds = GetRecordingFolders(user, true)
.Select(i => i.Id)
.ToList();
var excludeItemTypes = new List();
if (folderIds.Count == 0)
{
return new QueryResult();
}
var includeItemTypes = new List();
var genres = new List();
if (query.IsMovie.HasValue)
{
if (query.IsMovie.Value)
{
includeItemTypes.Add(typeof(Movie).Name);
}
else
{
excludeItemTypes.Add(typeof(Movie).Name);
}
}
if (query.IsSeries.HasValue)
{
if (query.IsSeries.Value)
{
includeItemTypes.Add(typeof(Episode).Name);
}
else
{
excludeItemTypes.Add(typeof(Episode).Name);
}
}
if (query.IsSports ?? false)
{
genres.Add("Sports");
}
if (query.IsKids ?? false)
{
genres.Add("Kids");
genres.Add("Children");
genres.Add("Family");
}
var limit = query.Limit;
if (query.IsInProgress ?? false)
{
// limit = (query.Limit ?? 10) * 2;
limit = null;
//var allActivePaths = EmbyTV.EmbyTV.Current.GetAllActiveRecordings().Select(i => i.Path).ToArray();
//var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray();
//return new QueryResult
//{
// Items = items,
// TotalRecordCount = items.Length
//};
dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray();
}
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{
MediaTypes = new[] { MediaType.Video },
Recursive = true,
AncestorIds = folderIds.ToArray(),
IsFolder = false,
IsVirtualItem = false,
Limit = limit,
StartIndex = query.StartIndex,
OrderBy = new[] { new ValueTuple(ItemSortBy.DateCreated, SortOrder.Descending) },
EnableTotalRecordCount = query.EnableTotalRecordCount,
IncludeItemTypes = includeItemTypes.ToArray(),
ExcludeItemTypes = excludeItemTypes.ToArray(),
Genres = genres.ToArray(),
DtoOptions = dtoOptions
});
if (query.IsInProgress ?? false)
{
//TODO Fix The co-variant conversion between Video[] and BaseItem[], this can generate runtime issues.
result.Items = result
.Items
.OfType